From dbe39f7a6d5a8bc384e6db373f88b89f178d4f9c Mon Sep 17 00:00:00 2001
From: cliffhall
Date: Thu, 15 Jan 2026 15:02:05 -0500
Subject: [PATCH 1/3] ### client/src/App.tsx
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Imports:
- Added: `Task` and `GetTaskResultSchema` to the same import block.
- UI Icons: Added `ListTodo` from `lucide-react` to the icon imports.
- Components: Added `TasksTab` import beside `ToolsTab`.
- Config utils: Added `getMCPTaskTtl` to the config utils import block.
- State additions:
- `const [tasks, setTasks] = useState([]);`
- Extended `errors` state to include a `tasks` key: `tasks: null,`
- `const [selectedTask, setSelectedTask] = useState(null);`
- `const [isPollingTask, setIsPollingTask] = useState(false);`
- `const [nextTaskCursor, setNextTaskCursor] = useState();`
- Hook: `useConnection({...})` return value usage extended
- Added destructured functions: `cancelTask: cancelMcpTask` and `listTasks: listMcpTasks` from the custom hook.
- Notification handling:
- In `onNotification`, added:
- If `method === "notifications/tasks/list_changed"`, call `listTasks()` (voided).
- If `method === "notifications/tasks/status"`, treat `notification.params` as a `Task` and update `tasks` state: replace if exists by `taskId`, otherwise prepend. Also update `selectedTask` if it’s the same `taskId`.
- Tab routing:
- When computing valid tabs, added `tasks` when server declares capability: `...(serverCapabilities?.tasks ? ["tasks"] : []),`
- When choosing a default tab, added a branch to fall back to `"tasks"` if neither resources/prompts/tools are present but tasks are.
- Effect for Tasks tab:
- When `mcpClient` is connected and `activeTab === "tasks"`, invoke `listTasks()`.
- Tools → task-augmented calls integration in `callTool`:
- Parameter signature supports `runAsTask?: boolean` (already present in this file), but now:
- If `runAsTask` is true, augment the `tools/call` request’s `params` with a `task` object: `{ task: { ttl: getMCPTaskTtl(config) } }`.
- Use a permissive result schema for tool call: `sendMCPRequest(request, z.any(), "tools")` to avoid version-mismatch schema issues.
- Task reference detection introduced:
- `isTaskResult` helper checks for a nested `task` object with `taskId` (i.e., `response.task.taskId`).
- When task is detected:
- Set `isPollingTask(true)`.
- Immediately set a temporary `toolResult` that includes text content “Task created: … Polling for status…” and `_meta` with `"io.modelcontextprotocol/related-task": { taskId }`.
- Start a polling loop:
- Delay 1s between polls.
- Call `tasks/get` with `GetTaskResultSchema` for status.
- If status is `completed`: call `tasks/result` with `z.any()` to retrieve the final result and set it as `toolResult`; call `listTasks()`.
- If status is `failed` or `cancelled`: set an error `toolResult` content that includes the status + `statusMessage`; call `listTasks()`.
- Else (still running): update `toolResult` content with current `status`/`statusMessage` and preserve `_meta` related-task.
- After loop, set `isPollingTask(false)`.
- When not a task response, set `toolResult` directly from response (cast to `CompatibilityCallToolResult`).
- Tasks list + cancel helpers in App:
- `listTasks`: uses `listMcpTasks(nextTaskCursor)` from the hook, updates `tasks`, `nextTaskCursor`, and clears `errors.tasks`.
- `cancelTask`: calls `cancelMcpTask(taskId)`, updates `tasks` array by `taskId`, updates `selectedTask` if it matches, and clears `errors.tasks`.
- UI integration:
- Added a `TabsTrigger` for “Tasks” with `` icon, disabled unless server supports tasks.
- Added `` to the main `TabsContent` block, passing: `tasks`, `listTasks`, `clearTasks`, `cancelTask`, `selectedTask`, `setSelectedTask`, `error={errors.tasks}`, `nextCursor={nextTaskCursor}`.
- Passed `isPollingTask={isPollingTask}` and `toolResult` into `ToolsTab` so the Tools tab can show the live “Polling Task…” state and block reruns while polling.
Note: The raw diff is long; the key hunks align with the above bullet points (imports, state, notifications, tab wiring, request augmentation, polling loop, UI additions).
---
### client/src/components/ToolsTab.tsx
- Props shape changed:
- Added `isPollingTask?: boolean` prop in the destructured props and in the prop types.
- The `callTool` callback signature is now `(name, params, metadata?, runAsTask?) => Promise` (runAsTask added earlier; test updates elsewhere reflect this).
- Local state additions:
- `const [runAsTask, setRunAsTask] = useState(false);`
- Reset behavior:
- When switching tools (`useEffect` on `selectedTool`), reset `runAsTask(false)`.
- When clearing the list in `ListPane.clearItems`, also call `setRunAsTask(false)`.
- UI additions:
- New checkbox control block to toggle “Run as task”:
- Checkbox `id="run-as-task"`, bound to `runAsTask`, with `onCheckedChange` → `setRunAsTask(checked)`.
- Label “Run as task”.
- Run button disabling conditions expanded to include `isPollingTask`.
- Run button text shows spinner with conditional label:
- If `isToolRunning || isPollingTask` → show spinner and text `isPollingTask ? "Polling Task..." : "Running..."`.
- Call invocation change:
- When clicking “Run Tool”, the `callTool` is invoked with `(selectedTool.name, params, metadata?, runAsTask)`.
- ToolResults relay:
- Passes `isPollingTask` to ``.
---
### client/src/components/ToolResults.tsx
- Props shape changed:
- Added optional prop: `isPollingTask?: boolean`.
- Task-running banner logic:
- Extracts related task from the tool result’s `_meta["io.modelcontextprotocol/related-task"]` if present.
- Computes `isTaskRunning` as `isPollingTask ||` a text-heuristic against `structuredResult.content` entries that contain text like “Polling” or “Task status”.
- Header “Tool Result:” now conditionally shows:
- `Error` (red) if `isError` is true, else
- `Task Running` (yellow) if `isTaskRunning`, else
- `Success` (green).
No other changes to validation or rendering of content blocks.
---
### client/src/components/TasksTab.tsx (new file)
- A brand new tab to list and inspect tasks.
- Key elements:
- Imports `Task` type and multiple status icons.
- `TaskStatusIcon` component maps task `status` to an icon and color.
- Main `TasksTab` props: `tasks`, `listTasks`, `clearTasks`, `cancelTask`, `selectedTask`, `setSelectedTask`, `error`, `nextCursor`.
- Left column (`ListPane`): lists tasks, shows status icon, `taskId`, `status`, and last update time; button text changes to “List More Tasks” if `nextCursor` present; disables button if no cursor and list non-empty.
- Right column:
- Shows error `Alert` if `error` prop provided.
- If a task is selected: header with `Task Details`, a Cancel button when `status === "working"` (shows a spinner while cancelling), and a grid of task fields: Status (with colored label and icon), Last Updated, Created At, TTL (shows “Infinite” if `ttl === null`, otherwise shows numeric with `s` suffix), optional Status Message, and full task JSON via `JsonView`.
- If no task is selected: centered empty state with a “Refresh Tasks” button.
---
### client/src/lib/hooks/useConnection.ts
- Imports added from `@modelcontextprotocol/sdk/types.js`:
- `ListTasksResultSchema`, `CancelTaskResultSchema`, `TaskStatusNotificationSchema`.
- Client capabilities on `connect`:
- Added `tasks: { list: {}, cancel: {} }` into the `clientCapabilities` passed to `new Client(...)`.
- Notification handling setup:
- The hook’s notification schema registration now includes the `TaskStatusNotificationSchema` in the `setNotificationHandler` list so the app receives `notifications/tasks/status`.
- New hook functions:
- `cancelTask(taskId: string)` sends `tasks/cancel` with `CancelTaskResultSchema`.
- `listTasks(cursor?: string)` sends `tasks/list` with `ListTasksResultSchema`.
- Exports:
- Returned object now includes `cancelTask` and `listTasks`.
---
### client/src/utils/configUtils.ts
- Added a new getter:
- `export const getMCPTaskTtl = (config: InspectorConfig): number => { return config.MCP_TASK_TTL.value as number; };`
---
### client/src/lib/configurationTypes.ts
- `InspectorConfig` type extended with a new item:
- `MCP_TASK_TTL: ConfigItem;`
- Includes descriptive JSDoc about default TTL in milliseconds for newly created tasks.
---
### client/src/lib/constants.ts
- `DEFAULT_INSPECTOR_CONFIG` extended with a default for task TTL:
- Key: `MCP_TASK_TTL`
- Label: `"Task TTL"`
- Description: `"Default Time-to-Live (TTL) in milliseconds for newly created tasks"`
- Default `value: 60000`
- `is_session_item: false`
---
### client/src/components/__tests__/ToolsTab.test.tsx
- Expectations updated due to new `callTool` signature (4th arg `runAsTask`). Everywhere the test asserts a `callTool` invocation, an additional trailing `false` argument was added to reflect the default state when the box isn’t checked.
- Examples of added trailing `false` at various assertion points (line offsets from diff): after calls around prior lines 132, 157, 193, 236, 257, 279, 297, 818, 1082 (now passing 4 arguments: name, params, metadata-or-undefined, false).
---
### Additional notes
- The Tasks feature is wired end-to-end:
- Client capability declaration (list/cancel)
- Notification handler for `notifications/tasks/status`
- Tools call augmentation with `task` `{ ttl }`
- Polling loop using `tasks/get` and `tasks/result`
- UI feedback in Tools and dedicated Tasks tab
- Configurable TTL via new config item and getter
---
client/src/App.tsx | 269 +++++++++++++++++-
client/src/components/TasksTab.tsx | 222 +++++++++++++++
client/src/components/ToolResults.tsx | 17 ++
client/src/components/ToolsTab.tsx | 28 +-
.../components/__tests__/ToolsTab.test.tsx | 15 +-
client/src/lib/configurationTypes.ts | 5 +
client/src/lib/constants.ts | 7 +
client/src/lib/hooks/useConnection.ts | 30 ++
client/src/utils/configUtils.ts | 3 +
package-lock.json | 115 +++-----
10 files changed, 617 insertions(+), 94 deletions(-)
create mode 100644 client/src/components/TasksTab.tsx
diff --git a/client/src/App.tsx b/client/src/App.tsx
index a9f99686d..0f70886c7 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -16,6 +16,8 @@ import {
ServerNotification,
Tool,
LoggingLevel,
+ Task,
+ GetTaskResultSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { OAuthTokensSchema } from "@modelcontextprotocol/sdk/shared/auth.js";
import type {
@@ -55,6 +57,7 @@ import {
Hammer,
Hash,
Key,
+ ListTodo,
MessageSquare,
Settings,
} from "lucide-react";
@@ -71,6 +74,7 @@ import RootsTab from "./components/RootsTab";
import SamplingTab, { PendingRequest } from "./components/SamplingTab";
import Sidebar from "./components/Sidebar";
import ToolsTab from "./components/ToolsTab";
+import TasksTab from "./components/TasksTab";
import { InspectorConfig } from "./lib/configurationTypes";
import {
getMCPProxyAddress,
@@ -81,6 +85,7 @@ import {
getInitialArgs,
initializeInspectorConfig,
saveInspectorConfig,
+ getMCPTaskTtl,
} from "./utils/configUtils";
import ElicitationTab, {
PendingElicitationRequest,
@@ -124,12 +129,14 @@ const App = () => {
const [prompts, setPrompts] = useState([]);
const [promptContent, setPromptContent] = useState("");
const [tools, setTools] = useState([]);
+ const [tasks, setTasks] = useState([]);
const [toolResult, setToolResult] =
useState(null);
const [errors, setErrors] = useState>({
resources: null,
prompts: null,
tools: null,
+ tasks: null,
});
const [command, setCommand] = useState(getInitialCommand);
const [args, setArgs] = useState(getInitialArgs);
@@ -265,6 +272,8 @@ const App = () => {
const [selectedPrompt, setSelectedPrompt] = useState(null);
const [selectedTool, setSelectedTool] = useState(null);
+ const [selectedTask, setSelectedTask] = useState(null);
+ const [isPollingTask, setIsPollingTask] = useState(false);
const [nextResourceCursor, setNextResourceCursor] = useState<
string | undefined
>();
@@ -275,6 +284,7 @@ const App = () => {
string | undefined
>();
const [nextToolCursor, setNextToolCursor] = useState();
+ const [nextTaskCursor, setNextTaskCursor] = useState();
const progressTokenRef = useRef(0);
const [activeTab, setActiveTab] = useState(() => {
@@ -305,6 +315,8 @@ const App = () => {
requestHistory,
clearRequestHistory,
makeRequest,
+ cancelTask: cancelMcpTask,
+ listTasks: listMcpTasks,
sendNotification,
handleCompletion,
completionsSupported,
@@ -324,6 +336,25 @@ const App = () => {
connectionType,
onNotification: (notification) => {
setNotifications((prev) => [...prev, notification as ServerNotification]);
+
+ if (notification.method === "notifications/tasks/list_changed") {
+ void listTasks();
+ }
+
+ if (notification.method === "notifications/tasks/status") {
+ const task = notification.params as unknown as Task;
+ setTasks((prev) => {
+ const exists = prev.some((t) => t.taskId === task.taskId);
+ if (exists) {
+ return prev.map((t) => (t.taskId === task.taskId ? task : t));
+ } else {
+ return [task, ...prev];
+ }
+ });
+ if (selectedTask?.taskId === task.taskId) {
+ setSelectedTask(task);
+ }
+ }
},
onPendingRequest: (request, resolve, reject) => {
setPendingSampleRequests((prev) => [
@@ -367,6 +398,7 @@ const App = () => {
...(serverCapabilities?.resources ? ["resources"] : []),
...(serverCapabilities?.prompts ? ["prompts"] : []),
...(serverCapabilities?.tools ? ["tools"] : []),
+ ...(serverCapabilities?.tasks ? ["tasks"] : []),
"ping",
"sampling",
"elicitations",
@@ -383,7 +415,9 @@ const App = () => {
? "prompts"
: serverCapabilities?.tools
? "tools"
- : "ping";
+ : serverCapabilities?.tasks
+ ? "tasks"
+ : "ping";
setActiveTab(defaultTab);
window.location.hash = defaultTab;
@@ -391,6 +425,13 @@ const App = () => {
}
}, [serverCapabilities]);
+ useEffect(() => {
+ if (mcpClient && activeTab === "tasks") {
+ void listTasks();
+ }
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [mcpClient, activeTab]);
+
useEffect(() => {
localStorage.setItem("lastCommand", command);
}, [command]);
@@ -610,7 +651,9 @@ const App = () => {
? "prompts"
: serverCapabilities?.tools
? "tools"
- : "ping";
+ : serverCapabilities?.tasks
+ ? "tasks"
+ : "ping";
window.location.hash = defaultTab;
} else if (!mcpClient && window.location.hash) {
// Clear hash when disconnected - completely remove the fragment
@@ -666,6 +709,7 @@ const App = () => {
...(serverCapabilities?.resources ? ["resources"] : []),
...(serverCapabilities?.prompts ? ["prompts"] : []),
...(serverCapabilities?.tools ? ["tools"] : []),
+ ...(serverCapabilities?.tasks ? ["tasks"] : []),
"ping",
"sampling",
"elicitations",
@@ -841,6 +885,7 @@ const App = () => {
name: string,
params: Record,
toolMetadata?: Record,
+ runAsTask?: boolean,
) => {
lastToolCallOriginTabRef.current = currentTabRef.current;
@@ -859,20 +904,161 @@ const App = () => {
...toolMetadata, // Tool-specific metadata
};
- const response = await sendMCPRequest(
- {
- method: "tools/call" as const,
- params: {
- name,
- arguments: cleanedParams,
- _meta: mergedMetadata,
- },
+ const request: ClientRequest = {
+ method: "tools/call" as const,
+ params: {
+ name,
+ arguments: cleanedParams,
+ _meta: mergedMetadata,
},
+ };
+
+ if (runAsTask) {
+ request.params = {
+ ...request.params,
+ task: {
+ ttl: getMCPTaskTtl(config),
+ },
+ };
+ }
+
+ const response = await sendMCPRequest(
+ request,
CompatibilityCallToolResultSchema,
"tools",
);
- setToolResult(response);
+ // Check if this was a task-augmented request that returned a task reference
+ // The server returns { task: { taskId, status, ... } } when a task is created
+ const isTaskResult = (
+ res: unknown,
+ ): res is { task: { taskId: string; status: string } } =>
+ !!res &&
+ typeof res === "object" &&
+ "task" in res &&
+ !!res.task &&
+ typeof res.task === "object" &&
+ "taskId" in res.task;
+
+ if (runAsTask && isTaskResult(response)) {
+ const taskId = response.task.taskId;
+ // Set polling state BEFORE setting tool result for proper UI update
+ setIsPollingTask(true);
+ // Safely extract any _meta from the original response (if present)
+ const initialResponseMeta =
+ response &&
+ typeof response === "object" &&
+ "_meta" in (response as Record)
+ ? ((response as { _meta?: Record })._meta ?? {})
+ : undefined;
+ setToolResult({
+ content: [
+ {
+ type: "text",
+ text: `Task created: ${taskId}. Polling for status...`,
+ },
+ ],
+ _meta: {
+ ...(initialResponseMeta || {}),
+ "io.modelcontextprotocol/related-task": { taskId },
+ },
+ } as CompatibilityCallToolResult);
+
+ // Polling loop
+ let taskCompleted = false;
+ while (!taskCompleted) {
+ try {
+ // Wait for 1 second before polling
+ await new Promise((resolve) => setTimeout(resolve, 1000));
+
+ const taskStatus = await sendMCPRequest(
+ {
+ method: "tasks/get",
+ params: { taskId },
+ },
+ GetTaskResultSchema,
+ );
+
+ if (
+ taskStatus.status === "completed" ||
+ taskStatus.status === "failed" ||
+ taskStatus.status === "cancelled"
+ ) {
+ taskCompleted = true;
+ console.log(
+ `Polling complete for task ${taskId}: ${taskStatus.status}`,
+ );
+
+ if (taskStatus.status === "completed") {
+ console.log(`Fetching result for task ${taskId}`);
+ const result = await sendMCPRequest(
+ {
+ method: "tasks/result",
+ params: { taskId },
+ },
+ z.any(),
+ );
+ console.log(`Result received for task ${taskId}:`, result);
+ setToolResult(result as CompatibilityCallToolResult);
+
+ // Refresh tasks list to show completed state
+ void listTasks();
+ } else {
+ setToolResult({
+ content: [
+ {
+ type: "text",
+ text: `Task ${taskStatus.status}: ${taskStatus.statusMessage || "No additional information"}`,
+ },
+ ],
+ isError: true,
+ });
+ // Refresh tasks list to show failed/cancelled state
+ void listTasks();
+ }
+ } else {
+ // Update status message while polling
+ // Safely extract any _meta from the original response (if present)
+ const pollingResponseMeta =
+ response &&
+ typeof response === "object" &&
+ "_meta" in (response as Record)
+ ? ((response as { _meta?: Record })._meta ??
+ {})
+ : undefined;
+ setToolResult({
+ content: [
+ {
+ type: "text",
+ text: `Task status: ${taskStatus.status}${taskStatus.statusMessage ? ` - ${taskStatus.statusMessage}` : ""}. Polling...`,
+ },
+ ],
+ _meta: {
+ ...(pollingResponseMeta || {}),
+ "io.modelcontextprotocol/related-task": { taskId },
+ },
+ } as CompatibilityCallToolResult);
+ // Refresh tasks list to show progress
+ void listTasks();
+ }
+ } catch (pollingError) {
+ console.error("Error polling task status:", pollingError);
+ setToolResult({
+ content: [
+ {
+ type: "text",
+ text: `Error polling task status: ${pollingError instanceof Error ? pollingError.message : String(pollingError)}`,
+ },
+ ],
+ isError: true,
+ });
+ taskCompleted = true;
+ }
+ }
+ setIsPollingTask(false);
+ } else {
+ setToolResult(response as CompatibilityCallToolResult);
+ }
// Clear any validation errors since tool execution completed
setErrors((prev) => ({ ...prev, tools: null }));
} catch (e) {
@@ -891,6 +1077,37 @@ const App = () => {
}
};
+ const listTasks = useCallback(async () => {
+ try {
+ const response = await listMcpTasks(nextTaskCursor);
+ setTasks(response.tasks);
+ setNextTaskCursor(response.nextCursor);
+ // Inline error clear to avoid extra dependency on clearError
+ setErrors((prev) => ({ ...prev, tasks: null }));
+ } catch (e) {
+ setErrors((prev) => ({
+ ...prev,
+ tasks: (e as Error).message ?? String(e),
+ }));
+ }
+ }, [listMcpTasks, nextTaskCursor]);
+
+ const cancelTask = async (taskId: string) => {
+ try {
+ const response = await cancelMcpTask(taskId);
+ setTasks((prev) => prev.map((t) => (t.taskId === taskId ? response : t)));
+ if (selectedTask?.taskId === taskId) {
+ setSelectedTask(response);
+ }
+ clearError("tasks");
+ } catch (e) {
+ setErrors((prev) => ({
+ ...prev,
+ tasks: (e as Error).message ?? String(e),
+ }));
+ }
+ };
+
const handleRootsChange = async () => {
await sendNotification({ method: "notifications/roots/list_changed" });
};
@@ -1034,6 +1251,13 @@ const App = () => {
Tools
+
+
+ Tasks
+
Ping
@@ -1182,10 +1406,11 @@ const App = () => {
name: string,
params: Record,
metadata?: Record,
+ runAsTask?: boolean,
) => {
clearError("tools");
setToolResult(null);
- await callTool(name, params, metadata);
+ await callTool(name, params, metadata, runAsTask);
}}
selectedTool={selectedTool}
setSelectedTool={(tool) => {
@@ -1194,6 +1419,7 @@ const App = () => {
setToolResult(null);
}}
toolResult={toolResult}
+ isPollingTask={isPollingTask}
nextCursor={nextToolCursor}
error={errors.tools}
resourceContent={resourceContentMap}
@@ -1202,6 +1428,25 @@ const App = () => {
readResource(uri);
}}
/>
+ {
+ clearError("tasks");
+ listTasks();
+ }}
+ clearTasks={() => {
+ setTasks([]);
+ setNextTaskCursor(undefined);
+ }}
+ cancelTask={cancelTask}
+ selectedTask={selectedTask}
+ setSelectedTask={(task) => {
+ clearError("tasks");
+ setSelectedTask(task);
+ }}
+ error={errors.tasks}
+ nextCursor={nextTaskCursor}
+ />
{
diff --git a/client/src/components/TasksTab.tsx b/client/src/components/TasksTab.tsx
new file mode 100644
index 000000000..1e2db54fa
--- /dev/null
+++ b/client/src/components/TasksTab.tsx
@@ -0,0 +1,222 @@
+import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
+import { Button } from "@/components/ui/button";
+import { TabsContent } from "@/components/ui/tabs";
+import { Task } from "@modelcontextprotocol/sdk/types.js";
+import {
+ AlertCircle,
+ RefreshCw,
+ XCircle,
+ Clock,
+ CheckCircle2,
+ AlertTriangle,
+ PlayCircle,
+} from "lucide-react";
+import ListPane from "./ListPane";
+import { useState } from "react";
+import JsonView from "./JsonView";
+import { cn } from "@/lib/utils";
+
+const TaskStatusIcon = ({ status }: { status: Task["status"] }) => {
+ switch (status) {
+ case "working":
+ return ;
+ case "input_required":
+ return ;
+ case "completed":
+ return ;
+ case "failed":
+ return ;
+ case "cancelled":
+ return ;
+ default:
+ return ;
+ }
+};
+
+const TasksTab = ({
+ tasks,
+ listTasks,
+ clearTasks,
+ cancelTask,
+ selectedTask,
+ setSelectedTask,
+ error,
+ nextCursor,
+}: {
+ tasks: Task[];
+ listTasks: () => void;
+ clearTasks: () => void;
+ cancelTask: (taskId: string) => Promise;
+ selectedTask: Task | null;
+ setSelectedTask: (task: Task | null) => void;
+ error: string | null;
+ nextCursor?: string;
+}) => {
+ const [isCancelling, setIsCancelling] = useState(null);
+
+ const handleCancel = async (taskId: string) => {
+ setIsCancelling(taskId);
+ try {
+ await cancelTask(taskId);
+ } finally {
+ setIsCancelling(null);
+ }
+ };
+
+ return (
+
+
+
+
0}
+ renderItem={(task) => (
+
+
+
+ {task.taskId}
+
+ {task.status} -{" "}
+ {new Date(task.lastUpdatedAt).toLocaleString()}
+
+
+
+ )}
+ />
+
+
+
+ {error && (
+
+
+ Error
+ {error}
+
+ )}
+
+ {selectedTask ? (
+
+
+
+
+ Task Details
+
+
+ ID: {selectedTask.taskId}
+
+
+ {selectedTask.status === "working" && (
+
+ )}
+
+
+
+
+
+ Status
+
+
+
+
+ {selectedTask.status.replace("_", " ")}
+
+
+
+
+
+ Last Updated
+
+
+ {new Date(selectedTask.lastUpdatedAt).toLocaleString()}
+
+
+
+
+ Created At
+
+
+ {new Date(selectedTask.createdAt).toLocaleString()}
+
+
+
+
+ TTL
+
+
+ {selectedTask.ttl === null
+ ? "Infinite"
+ : `${selectedTask.ttl}s`}
+
+
+
+
+ {selectedTask.statusMessage && (
+
+
+ Status Message
+
+
+ {selectedTask.statusMessage}
+
+
+ )}
+
+
+
Full Task Object
+
+
+
+
+
+ ) : (
+
+
+
+
No Task Selected
+
Select a task from the list to view its details.
+
+
+
+ )}
+
+
+
+ );
+};
+
+export default TasksTab;
diff --git a/client/src/components/ToolResults.tsx b/client/src/components/ToolResults.tsx
index 8cdf38b96..38d1d0382 100644
--- a/client/src/components/ToolResults.tsx
+++ b/client/src/components/ToolResults.tsx
@@ -12,6 +12,7 @@ interface ToolResultsProps {
selectedTool: Tool | null;
resourceContent: Record;
onReadResource?: (uri: string) => void;
+ isPollingTask?: boolean;
}
const checkContentCompatibility = (
@@ -69,6 +70,7 @@ const ToolResults = ({
selectedTool,
resourceContent,
onReadResource,
+ isPollingTask,
}: ToolResultsProps) => {
if (!toolResult) return null;
@@ -89,6 +91,19 @@ const ToolResults = ({
const structuredResult = parsedResult.data;
const isError = structuredResult.isError ?? false;
+ // Check if this is a running task
+ const relatedTask = structuredResult._meta?.[
+ "io.modelcontextprotocol/related-task"
+ ] as { taskId: string } | undefined;
+ const isTaskRunning =
+ isPollingTask ||
+ (!!relatedTask &&
+ structuredResult.content.some(
+ (c) =>
+ c.type === "text" &&
+ (c.text?.includes("Polling") || c.text?.includes("Task status")),
+ ));
+
let validationResult = null;
const toolHasOutputSchema =
selectedTool && hasOutputSchema(selectedTool.name);
@@ -127,6 +142,8 @@ const ToolResults = ({
Tool Result:{" "}
{isError ? (
Error
+ ) : isTaskRunning ? (
+ Task Running
) : (
Success
)}
diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx
index 047d327e5..d872e6299 100644
--- a/client/src/components/ToolsTab.tsx
+++ b/client/src/components/ToolsTab.tsx
@@ -64,6 +64,7 @@ const ToolsTab = ({
selectedTool,
setSelectedTool,
toolResult,
+ isPollingTask,
nextCursor,
error,
resourceContent,
@@ -76,16 +77,19 @@ const ToolsTab = ({
name: string,
params: Record,
metadata?: Record,
+ runAsTask?: boolean,
) => Promise;
selectedTool: Tool | null;
setSelectedTool: (tool: Tool | null) => void;
toolResult: CompatibilityCallToolResult | null;
+ isPollingTask?: boolean;
nextCursor: ListToolsResult["nextCursor"];
error: string | null;
resourceContent: Record;
onReadResource?: (uri: string) => void;
}) => {
const [params, setParams] = useState>({});
+ const [runAsTask, setRunAsTask] = useState(false);
const [isToolRunning, setIsToolRunning] = useState(false);
const [isOutputSchemaExpanded, setIsOutputSchemaExpanded] = useState(false);
const [isMetadataExpanded, setIsMetadataExpanded] = useState(false);
@@ -125,6 +129,7 @@ const ToolsTab = ({
];
});
setParams(Object.fromEntries(params));
+ setRunAsTask(false);
// Reset validation errors when switching tools
setHasValidationErrors(false);
@@ -157,6 +162,7 @@ const ToolsTab = ({
clearItems={() => {
clearTools();
setSelectedTool(null);
+ setRunAsTask(false);
}}
setSelectedItem={setSelectedTool}
renderItem={(tool) => (
@@ -651,6 +657,21 @@ const ToolsTab = ({
)}
+
+
+ setRunAsTask(checked)
+ }
+ />
+
+
- {displayedTask.status === "working" && (
+ {(displayedTask.status === "working" ||
+ displayedTask.status === "input_required") && (