Skip to content

Commit 64b4b61

Browse files
committed
Working (temp) version of Task tab
1 parent 1d4828d commit 64b4b61

9 files changed

Lines changed: 765 additions & 1 deletion

core/mcp/inspectorClient.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import {
8484
TaskStatusNotificationSchema,
8585
} from "@modelcontextprotocol/sdk/types.js";
8686
import type { ClientResult } from "@modelcontextprotocol/sdk/types.js";
87+
import { TasksListChangedNotificationSchema } from "./taskNotificationSchemas.js";
8788
import {
8889
type JsonValue,
8990
convertToolParameters,
@@ -1072,6 +1073,26 @@ export class InspectorClient extends InspectorClientEventTarget {
10721073
);
10731074
}
10741075

1076+
// Tasks list_changed and status handlers (when server advertises tasks capability)
1077+
if (this.capabilities?.tasks) {
1078+
this.client.setNotificationHandler(
1079+
TasksListChangedNotificationSchema,
1080+
async () => {
1081+
this.dispatchTypedEvent("tasksListChanged");
1082+
},
1083+
);
1084+
this.client.setNotificationHandler(
1085+
TaskStatusNotificationSchema,
1086+
async (notification) => {
1087+
const task = notification.params as Task;
1088+
this.dispatchTypedEvent("taskStatusChange", {
1089+
taskId: task.taskId,
1090+
task,
1091+
});
1092+
},
1093+
);
1094+
}
1095+
10751096
// Resource updated notification handler (only if server supports subscriptions)
10761097
if (this.capabilities?.resources?.subscribe === true) {
10771098
this.client.setNotificationHandler(

core/mcp/inspectorClientEventTarget.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export interface InspectorClientEventMap {
101101
toolsListChanged: void;
102102
resourcesListChanged: void;
103103
promptsListChanged: void;
104+
tasksListChanged: void;
104105
// OAuth events
105106
oauthAuthorizationRequired: {
106107
url: URL;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/**
2+
* Notification schema for notifications/tasks/list_changed (server → client).
3+
*
4+
* The SDK exports list_changed schemas for resources, prompts, tools, and roots, and
5+
* TaskStatusNotificationSchema for notifications/tasks/status, but no schema for
6+
* notifications/tasks/list_changed. Mainline (v1) does not register a schema for it
7+
* either: they use client.fallbackNotificationHandler so any unmatched notification
8+
* (including tasks/list_changed) is passed to onNotification, and App branches on
9+
* notification.method === "notifications/tasks/list_changed". We register specific
10+
* handlers only (no fallback), so we define this schema to handle the notification.
11+
*
12+
* List-changed notifications have no defined params (they are signals to refetch);
13+
* the SDK uses params: NotificationsParamsSchema.optional() for other list_changed
14+
* types (which is private, so we can't import it). We accept optional params only
15+
* so the notification parses; we do not use any params in the handler.
16+
*/
17+
import { NotificationSchema } from "@modelcontextprotocol/sdk/types.js";
18+
import { z } from "zod/v4";
19+
20+
export const TasksListChangedNotificationSchema = NotificationSchema.extend({
21+
method: z.literal("notifications/tasks/list_changed"),
22+
params: z.record(z.string(), z.unknown()).optional(),
23+
});

core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
"devDependencies": {
4848
"@hono/node-server": "^1.19.0",
4949
"@modelcontextprotocol/sdk": "^1.25.2",
50+
"zod": "^3.25 || ^4.0",
5051
"@types/react": "^18.3.23",
5152
"pino": "^9.6.0",
5253
"typescript": "^5.4.2",

docs/protocol-and-state-managers-architecture.md

Lines changed: 239 additions & 0 deletions
Large diffs are not rendered by default.

docs/web-tasks-tab-plan.md

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
# Web App: Tasks Tab Implementation Plan
2+
3+
Implement a **Tasks** tab in the inspector web app with parity to **inspector-main**, using `InspectorClient` and `useInspectorClient` instead of `useConnection`.
4+
5+
**Scope:** The Tasks tab is for **requestor tasks** only — tasks that we (the client) created on the server (e.g. via `tools/call` with task support), listed via `tasks/list` (`listRequestorTasks`). **Receiver tasks** (server-initiated, e.g. task-augmented `createMessage`/`elicit` where the server polls us) are a different flow and are not shown in this tab.
6+
7+
---
8+
9+
## 1. Mainline behavior (inspector-main)
10+
11+
### 1.1 TasksTab component
12+
13+
- **Path:** `inspector-main/client/src/components/TasksTab.tsx`
14+
- **Layout:** Two panels: left 1/3 list (via `ListPane`), right 2/3 task details.
15+
- **List:** `ListPane` with `items` = tasks, `listItems` = `listTasks`, `clearItems` = `clearTasks`, `buttonText` = `nextCursor ? "List More Tasks" : "List Tasks"`, `isButtonDisabled` = `!nextCursor && tasks.length > 0`, `renderItem` = status icon + taskId + status + lastUpdatedAt.
16+
- **Detail panel:** Empty state (“No Task Selected” + “Refresh Tasks”); when selected: “Task Details” (ID, Cancel when status is `working` or `input_required`), grid (Status, Last Updated, Created At, TTL), optional Status Message, “Full Task Object” `JsonView`.
17+
- **Status icons:** `TaskStatusIcon` for `working` (pulse), `input_required` (yellow), `completed` (green), `failed` (red), `cancelled` (gray), default (PlayCircle).
18+
- **Props:** `tasks`, `listTasks`, `clearTasks`, `cancelTask(taskId)`, `selectedTask`, `setSelectedTask`, `error`, `nextCursor`.
19+
20+
### 1.2 How tasks stay up to date in mainline
21+
22+
Tasks are updated by the **server** (status changes, completion, cancellation). The client does not update them locally except in response to server notifications:
23+
24+
1. **`notifications/tasks/list_changed`**
25+
App’s `onNotification` calls `listTasks()` to refetch the full list.
26+
27+
2. **`notifications/tasks/status`**
28+
App’s `onNotification` receives the updated task and:
29+
- Merges it into `tasks` (replace by `taskId` or prepend if new).
30+
- Updates `selectedTask` if it’s the same task.
31+
32+
So the list and selection stay in sync only by reacting to these two server notifications. Refetch on tab load is for initial load; ongoing updates depend on these handlers.
33+
34+
---
35+
36+
## 2. Our project: existing pieces
37+
38+
- **InspectorClient:**
39+
- `listRequestorTasks(cursor?)``Promise<{ tasks: Task[]; nextCursor?: string }>`
40+
- `cancelRequestorTask(taskId)``Promise<void>` (updates internal cache and dispatches `taskCancelled`; does not return the updated task).
41+
- **Events:**
42+
- `tasksChange: Task[]` — dispatched only after **we** call `listRequestorTasks` (our own list result).
43+
- `taskStatusChange: { taskId: string; task: Task }` — exists on the event map but is not currently dispatched when the **server** sends `notifications/tasks/status`.
44+
- No event or handler for **server** `notifications/tasks/list_changed`.
45+
- **useInspectorClient:** Does not expose tasks or task events. App can call `inspectorClient.listRequestorTasks()` and `inspectorClient.cancelRequestorTask()`.
46+
- **Web app:** Has `ListPane`, same UI stack, no Tasks tab, no `errors.tasks`, no `"tasks"` in `validTabsForNavigation`. `serverCapabilities?.tasks` is already used elsewhere (e.g. Tools “Run as task”).
47+
- **Icon:** Mainline uses `ListTodo` for the Tasks trigger; we can add it from `lucide-react`.
48+
49+
---
50+
51+
## 3. Implementation plan
52+
53+
### 3.1 TasksTab component
54+
55+
- **File:** `web/src/components/TasksTab.tsx`
56+
- Port mainline’s `TasksTab.tsx` with the same layout, props, and behavior:
57+
- Same `TabsContent value="tasks"`, left `ListPane`, right detail panel (empty state + selected task: header, Cancel button, grid, status message, full task `JsonView`).
58+
- Same `TaskStatusIcon` and status styling.
59+
- Props: `tasks`, `listTasks`, `clearTasks`, `cancelTask`, `selectedTask`, `setSelectedTask`, `error`, `nextCursor`.
60+
- **Cancel:** After `await cancelTask(taskId)`, the parent is responsible for refreshing the list (our `cancelRequestorTask` returns `void`), so parent will call `listTasks()` after a successful cancel.
61+
62+
### 3.2 App state and handlers
63+
64+
- **State in `App.tsx`:**
65+
- `tasks: Task[]`
66+
- `nextTaskCursor: string | undefined`
67+
- `selectedTask: Task | null`
68+
- Include `tasks: null` in initial `errors`; use `errors.tasks` for the tab.
69+
- **Handlers:**
70+
- **listTasks:** Call `inspectorClient.listRequestorTasks(nextTaskCursor)`; on success set `tasks`, `nextTaskCursor`, clear `errors.tasks`; on catch set `errors.tasks`.
71+
- **clearTasks:** `setTasks([])`, `setNextTaskCursor(undefined)`.
72+
- **cancelTask(taskId):** `await inspectorClient.cancelRequestorTask(taskId)`; on success clear `errors.tasks` and call `listTasks()` (or `listRequestorTasks(undefined)`) to refresh the list so the cancelled task shows updated status.
73+
- **Tab load:** When user switches to the Tasks tab (or on first load with hash `tasks`), call `listRequestorTasks()` once so the list is loaded (parity with mainline “load when selecting tab”).
74+
- **validTabsForNavigation:** Add `...(serverCapabilities?.tasks ? ["tasks"] : [])`.
75+
- **Tab trigger:** Add `TabsTrigger value="tasks"` disabled when `!serverCapabilities?.tasks`, with `ListTodo` icon.
76+
- **Render:** Add `<TasksTab ... />` with the same props as mainline, after ToolsTab (or same order as mainline).
77+
78+
### 3.3 Syncing when the server updates tasks (required for parity)
79+
80+
We handle tasks like other capabilities: when **`capabilities.tasks`** is present, subscribe to the relevant server notifications (same pattern as tools/resources/prompts and their list_changed handlers). No separate “open” design — use existing `capabilities.tasks`; no new config needed for parity (when the server supports tasks, we register the handlers).
81+
82+
1. **InspectorClient (core)**
83+
- When `capabilities?.tasks` is true, register notification handlers (same style as tools/resources/prompts list_changed):
84+
- **`notifications/tasks/list_changed`:** On receipt, either call `listRequestorTasks(undefined)` and dispatch `tasksChange` with the result, or dispatch a `tasksListChanged` event so the web App refetches. Follow the same choice as for tools/resources/prompts (e.g. dispatch event and let App refetch, or refetch inside client and dispatch `tasksChange`).
85+
- **`notifications/tasks/status`:** On receipt, dispatch `taskStatusChange: { taskId, task }` so the web App can merge the task and update `selectedTask` if it’s the same task.
86+
- Use the same capability gating as other list_changed handlers (register only when the server advertises the capability).
87+
88+
2. **Web App**
89+
- Subscribe to:
90+
- **tasks list_changed** (or `tasksChange` if client refetches and dispatches): set `tasks` and `nextTaskCursor` (e.g. by calling `listRequestorTasks(undefined)` if the client dispatched a “list changed” event).
91+
- **taskStatusChange:** merge the task into `tasks` by `taskId`; if `selectedTask?.taskId === taskId`, call `setSelectedTask(task)`.
92+
93+
### 3.4 Types and imports
94+
95+
- Use `Task` from `@modelcontextprotocol/sdk/types.js` in `TasksTab` and App.
96+
- Add `ListTodo` from `lucide-react` for the tab trigger.
97+
- Reuse existing imports for `ListPane`, `TabsContent`, `Alert`, `Button`, `JsonView`, `cn`, and other icons used in the ported `TasksTab`.
98+
99+
### 3.5 ListPane
100+
101+
- Use `buttonText={nextCursor ? "List More Tasks" : "List Tasks"}` and `isButtonDisabled={!nextCursor && tasks.length > 0}` so behavior matches mainline.
102+
103+
---
104+
105+
## 4. Summary checklist
106+
107+
| Item | Action |
108+
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
109+
| Add `TasksTab.tsx` | Port from mainline (same layout, props, `TaskStatusIcon`, detail panel, `JsonView`). |
110+
| App state | Add `tasks`, `nextTaskCursor`, `selectedTask`, `errors.tasks`. |
111+
| App handlers | `listTasks` (uses `listRequestorTasks`), `clearTasks`, `cancelTask` (calls `cancelRequestorTask` then `listTasks()`). |
112+
| Tab trigger | Add `TabsTrigger value="tasks"` with `ListTodo`, disabled when `!serverCapabilities?.tasks`. |
113+
| validTabsForNavigation | Include `"tasks"` when `serverCapabilities?.tasks`. |
114+
| Render TasksTab | Pass same props as mainline; get `inspectorClient` from existing hook/context. |
115+
| Load on tab select | When switching to Tasks tab, call `listRequestorTasks()` once. |
116+
| **Server notifications** | InspectorClient: when `capabilities.tasks`, register handlers for `notifications/tasks/list_changed` and `notifications/tasks/status` (same pattern as tools/resources/prompts). Dispatch events; App subscribes and refetches/merges. |
117+
118+
---
119+
120+
## 5. Implementation notes
121+
122+
- **No open design issues.** Design is: handle tasks like other capabilities; when `capabilities.tasks` is present, register the two notification handlers and dispatch events; App subscribes and updates state. Use existing `capabilities.tasks`; no new config.
123+
- **SDK schemas:** When implementing, use the same notification method names and schemas as mainline (`notifications/tasks/list_changed`, `notifications/tasks/status`). If the SDK exports a schema for incoming task status (e.g. same as outbound `TaskStatusNotificationSchema`), use it; otherwise match the payload shape mainline expects.
124+
- **Pagination after list_changed:** Refetch with `listRequestorTasks(undefined)` and replace the list with the first page (same as mainline).

package-lock.json

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)