Skip to content

Conversation

@cliffhall
Copy link
Member

@cliffhall cliffhall commented Jan 15, 2026

Summary

Added client support for tasks.

  • 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

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Documentation update
  • Refactoring (no functional changes)
  • Test updates
  • Build/CI improvements

Changes Made

client/src/App.tsx

  • 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<Task[]>([]);
    • Extended errors state to include a tasks key: tasks: null,
    • const [selectedTask, setSelectedTask] = useState<Task | null>(null);
    • const [isPollingTask, setIsPollingTask] = useState(false);
    • const [nextTaskCursor, setNextTaskCursor] = useState<string | undefined>();
  • 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 <ListTodo /> icon, disabled unless server supports tasks.
    • Added <TasksTab /> 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<void> (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 onCheckedChangesetRunAsTask(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 <ToolResults />.

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).

Related Issues

Fixes #931

Testing

  • Tested in UI mode
  • Tested in CLI mode
  • Tested with STDIO transport
  • Tested with SSE transport
  • Tested with Streamable HTTP transport
  • Added/updated automated tests
  • Manual testing performed

Test Results and/or Instructions

Testing against the draft PR for adding Tasks to Everything server. Specifically using the simulate-research-query tool.

Running Tool as a Task

run-tool-as-task.mov

Tasks Tab - Completed Task

Screenshot 2026-01-15 at 3 31 42 PM

Tasks Tab - Running Task

running-task

Tasks Tab - Input Required

Screenshot 2026-01-15 at 4 06 48 PM

Tasks Tab - Cancelled Task

cancelled-task

Tools Tab - Cancelled Task

Screenshot 2026-01-15 at 3 40 27 PM

Tools Tab - Running Task

Screenshot 2026-01-15 at 3 41 43 PM

Tools Tab - Task Complete

Screenshot 2026-01-15 at 3 41 37 PM

Checklist

  • Code follows the style guidelines (ran npm run prettier-fix)
  • Self-review completed
  • Code is commented where necessary
  • Documentation updated (README, comments, etc.)

Breaking Changes

Nope.

Additional Context

- 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<Task[]>([]);`
  - Extended `errors` state to include a `tasks` key: `tasks: null,`
  - `const [selectedTask, setSelectedTask] = useState<Task | null>(null);`
  - `const [isPollingTask, setIsPollingTask] = useState(false);`
  - `const [nextTaskCursor, setNextTaskCursor] = useState<string | undefined>();`

- 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 `<ListTodo />` icon, disabled unless server supports tasks.
  - Added `<TasksTab />` 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<void>` (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 `<ToolResults />`.

---

### 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
…completes, leaving the cancel task button available.
@cliffhall cliffhall requested a review from olaservo January 15, 2026 21:02
@He-Pin
Copy link

He-Pin commented Jan 16, 2026

  1. Nice, the Button of Polling Task should contain the pollInterval information and polling times
  2. We need to explicitly show a flowchart to illustrate how a Task's status transitions, especially when it involves the input_required state.
  3. We need to display a notification that the task has been completed. For example, during the polling process, the server might proactively send a notification that the task is complete. At this point, we need to immediately trigger a tasks/result request to retrieve the results.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Support for SEP-1686: Tasks to Inspector

2 participants