Add Tasks support (#1013)
* ### 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 `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
* Fix issue where displayed task on task tab was not updated when task completes, leaving the cancel task button available.
* Handle button the same when input_requred or working state
* ### What changed
- Added **receiver-side (client-side) Tasks support** to the Inspector client for task-augmented **server → client** requests.
- When the server sends `sampling/createMessage` or `elicitation/create` with `params.task`, the client now:
- Creates a local task and immediately returns a `CreateTaskResult` (`{ task: ... }`).
- Runs the underlying user-driven flow asynchronously.
- Updates task state to `completed` / `failed` and makes the final payload available via `tasks/result`.
- Implemented server polling handlers backed by the local task store:
- `tasks/get`
- `tasks/result` (blocks until payload is ready)
- `tasks/list`
- `tasks/cancel`
- Advertises task-augmented request capabilities via `capabilities.tasks.requests.sampling.createMessage` and `capabilities.tasks.requests.elicitation.create` when the corresponding callbacks are provided.
- Emits best-effort `notifications/tasks/status` updates as local task status changes.
* Fix review requests.
* In the ToolsTab, when a tool call results in an elicitation request, the app automatically switches to the elicitation tab, and once submitted, switches back to the ToolsTab to view the result of the tool call.
However, when a tool call results in a sampling request, it does not switch automatically to the SamplingTab and when the sampling request form is submitted it does not automatically switch back to the ToolsTab to view the result of the tool call.
These changes make the sampling use case work like the elicitation one. Tests were added.
This commit is contained in:
parent
6dc5d1a0bd
commit
7ef0971c81
13 changed files with 1479 additions and 108 deletions
|
|
@ -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<Prompt[]>([]);
|
||||
const [promptContent, setPromptContent] = useState<string>("");
|
||||
const [tools, setTools] = useState<Tool[]>([]);
|
||||
const [tasks, setTasks] = useState<Task[]>([]);
|
||||
const [toolResult, setToolResult] =
|
||||
useState<CompatibilityCallToolResult | null>(null);
|
||||
const [errors, setErrors] = useState<Record<string, string | null>>({
|
||||
resources: null,
|
||||
prompts: null,
|
||||
tools: null,
|
||||
tasks: null,
|
||||
});
|
||||
const [command, setCommand] = useState<string>(getInitialCommand);
|
||||
const [args, setArgs] = useState<string>(getInitialArgs);
|
||||
|
|
@ -265,6 +272,8 @@ const App = () => {
|
|||
|
||||
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
|
||||
const [selectedTool, setSelectedTool] = useState<Tool | null>(null);
|
||||
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
|
||||
const [isPollingTask, setIsPollingTask] = useState(false);
|
||||
const [nextResourceCursor, setNextResourceCursor] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
|
|
@ -275,6 +284,7 @@ const App = () => {
|
|||
string | undefined
|
||||
>();
|
||||
const [nextToolCursor, setNextToolCursor] = useState<string | undefined>();
|
||||
const [nextTaskCursor, setNextTaskCursor] = useState<string | undefined>();
|
||||
const progressTokenRef = useRef(0);
|
||||
|
||||
const [activeTab, setActiveTab] = useState<string>(() => {
|
||||
|
|
@ -290,6 +300,32 @@ const App = () => {
|
|||
currentTabRef.current = activeTab;
|
||||
}, [activeTab]);
|
||||
|
||||
const navigateToOriginatingTab = (originatingTab?: string) => {
|
||||
if (!originatingTab) return;
|
||||
|
||||
const validTabs = [
|
||||
...(serverCapabilities?.resources ? ["resources"] : []),
|
||||
...(serverCapabilities?.prompts ? ["prompts"] : []),
|
||||
...(serverCapabilities?.tools ? ["tools"] : []),
|
||||
...(serverCapabilities?.tasks ? ["tasks"] : []),
|
||||
"ping",
|
||||
"sampling",
|
||||
"elicitations",
|
||||
"roots",
|
||||
"auth",
|
||||
];
|
||||
|
||||
if (!validTabs.includes(originatingTab)) return;
|
||||
|
||||
setActiveTab(originatingTab);
|
||||
window.location.hash = originatingTab;
|
||||
|
||||
setTimeout(() => {
|
||||
setActiveTab(originatingTab);
|
||||
window.location.hash = originatingTab;
|
||||
}, 100);
|
||||
};
|
||||
|
||||
const { height: historyPaneHeight, handleDragStart } = useDraggablePane(300);
|
||||
const {
|
||||
width: sidebarWidth,
|
||||
|
|
@ -297,6 +333,11 @@ const App = () => {
|
|||
handleDragStart: handleSidebarDragStart,
|
||||
} = useDraggableSidebar(320);
|
||||
|
||||
const selectedTaskRef = useRef<Task | null>(null);
|
||||
useEffect(() => {
|
||||
selectedTaskRef.current = selectedTask;
|
||||
}, [selectedTask]);
|
||||
|
||||
const {
|
||||
connectionStatus,
|
||||
serverCapabilities,
|
||||
|
|
@ -305,6 +346,8 @@ const App = () => {
|
|||
requestHistory,
|
||||
clearRequestHistory,
|
||||
makeRequest,
|
||||
cancelTask: cancelMcpTask,
|
||||
listTasks: listMcpTasks,
|
||||
sendNotification,
|
||||
handleCompletion,
|
||||
completionsSupported,
|
||||
|
|
@ -324,12 +367,41 @@ 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 (selectedTaskRef.current?.taskId === task.taskId) {
|
||||
setSelectedTask(task);
|
||||
}
|
||||
}
|
||||
},
|
||||
onPendingRequest: (request, resolve, reject) => {
|
||||
const currentTab = lastToolCallOriginTabRef.current;
|
||||
setPendingSampleRequests((prev) => [
|
||||
...prev,
|
||||
{ id: nextRequestId.current++, request, resolve, reject },
|
||||
{
|
||||
id: nextRequestId.current++,
|
||||
request,
|
||||
originatingTab: currentTab,
|
||||
resolve,
|
||||
reject,
|
||||
},
|
||||
]);
|
||||
|
||||
setActiveTab("sampling");
|
||||
window.location.hash = "sampling";
|
||||
},
|
||||
onElicitationRequest: (request, resolve) => {
|
||||
const currentTab = lastToolCallOriginTabRef.current;
|
||||
|
|
@ -367,6 +439,7 @@ const App = () => {
|
|||
...(serverCapabilities?.resources ? ["resources"] : []),
|
||||
...(serverCapabilities?.prompts ? ["prompts"] : []),
|
||||
...(serverCapabilities?.tools ? ["tools"] : []),
|
||||
...(serverCapabilities?.tasks ? ["tasks"] : []),
|
||||
"ping",
|
||||
"sampling",
|
||||
"elicitations",
|
||||
|
|
@ -383,7 +456,9 @@ const App = () => {
|
|||
? "prompts"
|
||||
: serverCapabilities?.tools
|
||||
? "tools"
|
||||
: "ping";
|
||||
: serverCapabilities?.tasks
|
||||
? "tasks"
|
||||
: "ping";
|
||||
|
||||
setActiveTab(defaultTab);
|
||||
window.location.hash = defaultTab;
|
||||
|
|
@ -391,6 +466,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 +692,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
|
||||
|
|
@ -638,6 +722,9 @@ const App = () => {
|
|||
setPendingSampleRequests((prev) => {
|
||||
const request = prev.find((r) => r.id === id);
|
||||
request?.resolve(result);
|
||||
|
||||
navigateToOriginatingTab(request?.originatingTab);
|
||||
|
||||
return prev.filter((r) => r.id !== id);
|
||||
});
|
||||
};
|
||||
|
|
@ -646,6 +733,9 @@ const App = () => {
|
|||
setPendingSampleRequests((prev) => {
|
||||
const request = prev.find((r) => r.id === id);
|
||||
request?.reject(new Error("Sampling request rejected"));
|
||||
|
||||
navigateToOriginatingTab(request?.originatingTab);
|
||||
|
||||
return prev.filter((r) => r.id !== id);
|
||||
});
|
||||
};
|
||||
|
|
@ -666,6 +756,7 @@ const App = () => {
|
|||
...(serverCapabilities?.resources ? ["resources"] : []),
|
||||
...(serverCapabilities?.prompts ? ["prompts"] : []),
|
||||
...(serverCapabilities?.tools ? ["tools"] : []),
|
||||
...(serverCapabilities?.tasks ? ["tasks"] : []),
|
||||
"ping",
|
||||
"sampling",
|
||||
"elicitations",
|
||||
|
|
@ -841,6 +932,7 @@ const App = () => {
|
|||
name: string,
|
||||
params: Record<string, unknown>,
|
||||
toolMetadata?: Record<string, unknown>,
|
||||
runAsTask?: boolean,
|
||||
) => {
|
||||
lastToolCallOriginTabRef.current = currentTabRef.current;
|
||||
|
||||
|
|
@ -859,20 +951,164 @@ 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; pollInterval: number };
|
||||
} =>
|
||||
!!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;
|
||||
const pollInterval = response.task.pollInterval;
|
||||
// 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<string, unknown>)
|
||||
? ((response as { _meta?: Record<string, unknown> })._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, pollInterval));
|
||||
|
||||
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 },
|
||||
},
|
||||
CompatibilityCallToolResultSchema,
|
||||
);
|
||||
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<string, unknown>)
|
||||
? ((response as { _meta?: Record<string, unknown> })._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 +1127,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 +1301,13 @@ const App = () => {
|
|||
<Hammer className="w-4 h-4 mr-2" />
|
||||
Tools
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="tasks"
|
||||
disabled={!serverCapabilities?.tasks}
|
||||
>
|
||||
<ListTodo className="w-4 h-4 mr-2" />
|
||||
Tasks
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="ping">
|
||||
<Bell className="w-4 h-4 mr-2" />
|
||||
Ping
|
||||
|
|
@ -1182,10 +1456,11 @@ const App = () => {
|
|||
name: string,
|
||||
params: Record<string, unknown>,
|
||||
metadata?: Record<string, unknown>,
|
||||
runAsTask?: boolean,
|
||||
) => {
|
||||
clearError("tools");
|
||||
setToolResult(null);
|
||||
await callTool(name, params, metadata);
|
||||
await callTool(name, params, metadata, runAsTask);
|
||||
}}
|
||||
selectedTool={selectedTool}
|
||||
setSelectedTool={(tool) => {
|
||||
|
|
@ -1194,6 +1469,7 @@ const App = () => {
|
|||
setToolResult(null);
|
||||
}}
|
||||
toolResult={toolResult}
|
||||
isPollingTask={isPollingTask}
|
||||
nextCursor={nextToolCursor}
|
||||
error={errors.tools}
|
||||
resourceContent={resourceContentMap}
|
||||
|
|
@ -1202,6 +1478,25 @@ const App = () => {
|
|||
readResource(uri);
|
||||
}}
|
||||
/>
|
||||
<TasksTab
|
||||
tasks={tasks}
|
||||
listTasks={() => {
|
||||
clearError("tasks");
|
||||
listTasks();
|
||||
}}
|
||||
clearTasks={() => {
|
||||
setTasks([]);
|
||||
setNextTaskCursor(undefined);
|
||||
}}
|
||||
cancelTask={cancelTask}
|
||||
selectedTask={selectedTask}
|
||||
setSelectedTask={(task) => {
|
||||
clearError("tasks");
|
||||
setSelectedTask(task);
|
||||
}}
|
||||
error={errors.tasks}
|
||||
nextCursor={nextTaskCursor}
|
||||
/>
|
||||
<ConsoleTab />
|
||||
<PingTab
|
||||
onPingClick={() => {
|
||||
|
|
|
|||
239
client/src/__tests__/App.samplingNavigation.test.tsx
Normal file
239
client/src/__tests__/App.samplingNavigation.test.tsx
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
import {
|
||||
act,
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import App from "../App";
|
||||
import { useConnection } from "../lib/hooks/useConnection";
|
||||
import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import type {
|
||||
CreateMessageRequest,
|
||||
CreateMessageResult,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
|
||||
type OnPendingRequestHandler = (
|
||||
request: CreateMessageRequest,
|
||||
resolve: (result: CreateMessageResult) => void,
|
||||
reject: (error: Error) => void,
|
||||
) => void;
|
||||
|
||||
type SamplingRequestMockProps = {
|
||||
request: { id: number };
|
||||
onApprove: (id: number, result: CreateMessageResult) => void;
|
||||
onReject: (id: number) => void;
|
||||
};
|
||||
|
||||
type UseConnectionReturn = ReturnType<typeof useConnection>;
|
||||
|
||||
// Mock auth dependencies first
|
||||
jest.mock("@modelcontextprotocol/sdk/client/auth.js", () => ({
|
||||
auth: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../lib/oauth-state-machine", () => ({
|
||||
OAuthStateMachine: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../lib/auth", () => ({
|
||||
InspectorOAuthClientProvider: jest.fn().mockImplementation(() => ({
|
||||
tokens: jest.fn().mockResolvedValue(null),
|
||||
clear: jest.fn(),
|
||||
})),
|
||||
DebugInspectorOAuthClientProvider: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../utils/configUtils", () => ({
|
||||
...jest.requireActual("../utils/configUtils"),
|
||||
getMCPProxyAddress: jest.fn(() => "http://localhost:6277"),
|
||||
getMCPProxyAuthToken: jest.fn(() => ({
|
||||
token: "",
|
||||
header: "X-MCP-Proxy-Auth",
|
||||
})),
|
||||
getInitialTransportType: jest.fn(() => "stdio"),
|
||||
getInitialSseUrl: jest.fn(() => "http://localhost:3001/sse"),
|
||||
getInitialCommand: jest.fn(() => "mcp-server-everything"),
|
||||
getInitialArgs: jest.fn(() => ""),
|
||||
initializeInspectorConfig: jest.fn(() => ({})),
|
||||
saveInspectorConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../lib/hooks/useDraggablePane", () => ({
|
||||
useDraggablePane: () => ({
|
||||
height: 300,
|
||||
handleDragStart: jest.fn(),
|
||||
}),
|
||||
useDraggableSidebar: () => ({
|
||||
width: 320,
|
||||
isDragging: false,
|
||||
handleDragStart: jest.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
jest.mock("../components/Sidebar", () => ({
|
||||
__esModule: true,
|
||||
default: () => <div>Sidebar</div>,
|
||||
}));
|
||||
|
||||
jest.mock("../lib/hooks/useToast", () => ({
|
||||
useToast: () => ({ toast: jest.fn() }),
|
||||
}));
|
||||
|
||||
// Keep the test focused on navigation; avoid DynamicJsonForm/schema complexity.
|
||||
jest.mock("../components/SamplingRequest", () => ({
|
||||
__esModule: true,
|
||||
default: ({ request, onApprove, onReject }: SamplingRequestMockProps) => (
|
||||
<div data-testid="sampling-request">
|
||||
<div>sampling-request-{request.id}</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
onApprove(request.id, {
|
||||
model: "stub-model",
|
||||
stopReason: "endTurn",
|
||||
role: "assistant",
|
||||
content: { type: "text", text: "" },
|
||||
})
|
||||
}
|
||||
>
|
||||
Approve
|
||||
</button>
|
||||
<button type="button" onClick={() => onReject(request.id)}>
|
||||
Reject
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
// Mock fetch
|
||||
global.fetch = jest.fn().mockResolvedValue({ json: () => Promise.resolve({}) });
|
||||
|
||||
jest.mock("../lib/hooks/useConnection", () => ({
|
||||
useConnection: jest.fn(),
|
||||
}));
|
||||
|
||||
describe("App - Sampling auto-navigation", () => {
|
||||
const mockUseConnection = jest.mocked(useConnection);
|
||||
|
||||
const baseConnectionState = {
|
||||
connectionStatus: "connected" as const,
|
||||
serverCapabilities: { tools: { listChanged: true, subscribe: true } },
|
||||
mcpClient: {
|
||||
request: jest.fn(),
|
||||
notification: jest.fn(),
|
||||
close: jest.fn(),
|
||||
} as unknown as Client,
|
||||
requestHistory: [],
|
||||
clearRequestHistory: jest.fn(),
|
||||
makeRequest: jest.fn(),
|
||||
sendNotification: jest.fn(),
|
||||
handleCompletion: jest.fn(),
|
||||
completionsSupported: false,
|
||||
connect: jest.fn(),
|
||||
disconnect: jest.fn(),
|
||||
serverImplementation: null,
|
||||
cancelTask: jest.fn(),
|
||||
listTasks: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
window.location.hash = "#tools";
|
||||
});
|
||||
|
||||
test("switches to #sampling when a sampling request arrives and switches back to #tools after approve", async () => {
|
||||
let capturedOnPendingRequest: OnPendingRequestHandler | undefined;
|
||||
|
||||
mockUseConnection.mockImplementation((options) => {
|
||||
capturedOnPendingRequest = (
|
||||
options as { onPendingRequest?: OnPendingRequestHandler }
|
||||
).onPendingRequest;
|
||||
return baseConnectionState as unknown as UseConnectionReturn;
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
// Ensure we start on tools.
|
||||
await waitFor(() => {
|
||||
expect(window.location.hash).toBe("#tools");
|
||||
});
|
||||
|
||||
const resolve = jest.fn();
|
||||
const reject = jest.fn();
|
||||
|
||||
act(() => {
|
||||
if (!capturedOnPendingRequest) {
|
||||
throw new Error("Expected onPendingRequest to be provided");
|
||||
}
|
||||
|
||||
capturedOnPendingRequest(
|
||||
{
|
||||
method: "sampling/createMessage",
|
||||
params: { messages: [], maxTokens: 1 },
|
||||
},
|
||||
resolve,
|
||||
reject,
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.hash).toBe("#sampling");
|
||||
expect(screen.getByTestId("sampling-request")).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("Approve"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(resolve).toHaveBeenCalled();
|
||||
expect(window.location.hash).toBe("#tools");
|
||||
});
|
||||
});
|
||||
|
||||
test("switches back to #tools after reject", async () => {
|
||||
let capturedOnPendingRequest: OnPendingRequestHandler | undefined;
|
||||
|
||||
mockUseConnection.mockImplementation((options) => {
|
||||
capturedOnPendingRequest = (
|
||||
options as { onPendingRequest?: OnPendingRequestHandler }
|
||||
).onPendingRequest;
|
||||
return baseConnectionState as unknown as UseConnectionReturn;
|
||||
});
|
||||
|
||||
render(<App />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.hash).toBe("#tools");
|
||||
});
|
||||
|
||||
const resolve = jest.fn();
|
||||
const reject = jest.fn();
|
||||
|
||||
act(() => {
|
||||
if (!capturedOnPendingRequest) {
|
||||
throw new Error("Expected onPendingRequest to be provided");
|
||||
}
|
||||
|
||||
capturedOnPendingRequest(
|
||||
{
|
||||
method: "sampling/createMessage",
|
||||
params: { messages: [], maxTokens: 1 },
|
||||
},
|
||||
resolve,
|
||||
reject,
|
||||
);
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(window.location.hash).toBe("#sampling");
|
||||
expect(screen.getByTestId("sampling-request")).toBeTruthy();
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText("Reject"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(reject).toHaveBeenCalled();
|
||||
expect(window.location.hash).toBe("#tools");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -9,6 +9,7 @@ import SamplingRequest from "./SamplingRequest";
|
|||
export type PendingRequest = {
|
||||
id: number;
|
||||
request: CreateMessageRequest;
|
||||
originatingTab?: string;
|
||||
};
|
||||
|
||||
export type Props = {
|
||||
|
|
|
|||
229
client/src/components/TasksTab.tsx
Normal file
229
client/src/components/TasksTab.tsx
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
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 <Clock className="h-4 w-4 animate-pulse text-blue-500" />;
|
||||
case "input_required":
|
||||
return <AlertTriangle className="h-4 w-4 text-yellow-500" />;
|
||||
case "completed":
|
||||
return <CheckCircle2 className="h-4 w-4 text-green-500" />;
|
||||
case "failed":
|
||||
return <XCircle className="h-4 w-4 text-red-500" />;
|
||||
case "cancelled":
|
||||
return <XCircle className="h-4 w-4 text-gray-500" />;
|
||||
default:
|
||||
return <PlayCircle className="h-4 w-4" />;
|
||||
}
|
||||
};
|
||||
|
||||
const TasksTab = ({
|
||||
tasks,
|
||||
listTasks,
|
||||
clearTasks,
|
||||
cancelTask,
|
||||
selectedTask,
|
||||
setSelectedTask,
|
||||
error,
|
||||
nextCursor,
|
||||
}: {
|
||||
tasks: Task[];
|
||||
listTasks: () => void;
|
||||
clearTasks: () => void;
|
||||
cancelTask: (taskId: string) => Promise<void>;
|
||||
selectedTask: Task | null;
|
||||
setSelectedTask: (task: Task | null) => void;
|
||||
error: string | null;
|
||||
nextCursor?: string;
|
||||
}) => {
|
||||
const [isCancelling, setIsCancelling] = useState<string | null>(null);
|
||||
|
||||
const displayedTask = selectedTask
|
||||
? tasks.find((t) => t.taskId === selectedTask.taskId) || selectedTask
|
||||
: null;
|
||||
|
||||
const handleCancel = async (taskId: string) => {
|
||||
setIsCancelling(taskId);
|
||||
try {
|
||||
await cancelTask(taskId);
|
||||
} finally {
|
||||
setIsCancelling(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TabsContent value="tasks" className="flex-1 overflow-hidden p-0 m-0">
|
||||
<div className="flex h-full overflow-hidden p-4 gap-4">
|
||||
<div className="w-1/3">
|
||||
<ListPane
|
||||
title="Tasks"
|
||||
items={tasks}
|
||||
setSelectedItem={setSelectedTask}
|
||||
listItems={listTasks}
|
||||
clearItems={clearTasks}
|
||||
buttonText={nextCursor ? "List More Tasks" : "List Tasks"}
|
||||
isButtonDisabled={!nextCursor && tasks.length > 0}
|
||||
renderItem={(task) => (
|
||||
<div className="flex items-center gap-2 overflow-hidden w-full">
|
||||
<TaskStatusIcon status={task.status} />
|
||||
<div className="flex flex-col overflow-hidden">
|
||||
<span className="truncate font-medium">{task.taskId}</span>
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
{task.status} -{" "}
|
||||
{new Date(task.lastUpdatedAt).toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 bg-background border border-border rounded-lg">
|
||||
{error && (
|
||||
<Alert variant="destructive" className="mb-4">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertTitle>Error</AlertTitle>
|
||||
<AlertDescription>{error}</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{displayedTask ? (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between border-b pb-4">
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold tracking-tight">
|
||||
Task Details
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
ID: {displayedTask.taskId}
|
||||
</p>
|
||||
</div>
|
||||
{(displayedTask.status === "working" ||
|
||||
displayedTask.status === "input_required") && (
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
aria-label={`Cancel task ${displayedTask.taskId}`}
|
||||
onClick={() => handleCancel(displayedTask.taskId)}
|
||||
disabled={isCancelling === displayedTask.taskId}
|
||||
>
|
||||
{isCancelling === displayedTask.taskId ? (
|
||||
<RefreshCw className="mr-2 h-4 w-4 animate-spin" />
|
||||
) : (
|
||||
<XCircle className="mr-2 h-4 w-4" />
|
||||
)}
|
||||
Cancel Task
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="rounded-lg border p-3">
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
Status
|
||||
</p>
|
||||
<div className="mt-1 flex items-center gap-2">
|
||||
<TaskStatusIcon status={displayedTask.status} />
|
||||
<span
|
||||
className={cn(
|
||||
"font-semibold capitalize",
|
||||
displayedTask.status === "working" && "text-blue-500",
|
||||
displayedTask.status === "completed" &&
|
||||
"text-green-500",
|
||||
displayedTask.status === "failed" && "text-red-500",
|
||||
displayedTask.status === "cancelled" && "text-gray-500",
|
||||
displayedTask.status === "input_required" &&
|
||||
"text-yellow-500",
|
||||
)}
|
||||
>
|
||||
{displayedTask.status.replace("_", " ")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="rounded-lg border p-3">
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
Last Updated
|
||||
</p>
|
||||
<p className="mt-1 font-medium">
|
||||
{new Date(displayedTask.lastUpdatedAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border p-3">
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
Created At
|
||||
</p>
|
||||
<p className="mt-1 font-medium">
|
||||
{new Date(displayedTask.createdAt).toLocaleString()}
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-lg border p-3">
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
TTL
|
||||
</p>
|
||||
<p className="mt-1 font-medium">
|
||||
{displayedTask.ttl === null
|
||||
? "Infinite"
|
||||
: `${displayedTask.ttl}ms`}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{displayedTask.statusMessage && (
|
||||
<div className="rounded-lg border p-3">
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
Status Message
|
||||
</p>
|
||||
<p className="mt-1 whitespace-pre-wrap">
|
||||
{displayedTask.statusMessage}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">Full Task Object</h3>
|
||||
<div className="rounded-md border">
|
||||
<JsonView data={displayedTask} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex h-full items-center justify-center text-muted-foreground">
|
||||
<div className="text-center">
|
||||
<Clock className="mx-auto mb-4 h-12 w-12 opacity-20" />
|
||||
<h3 className="text-lg font-medium">No Task Selected</h3>
|
||||
<p>Select a task from the list to view its details.</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="mt-4"
|
||||
onClick={listTasks}
|
||||
>
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Refresh Tasks
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
);
|
||||
};
|
||||
|
||||
export default TasksTab;
|
||||
|
|
@ -12,6 +12,7 @@ interface ToolResultsProps {
|
|||
selectedTool: Tool | null;
|
||||
resourceContent: Record<string, string>;
|
||||
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 ? (
|
||||
<span className="text-red-600 font-semibold">Error</span>
|
||||
) : isTaskRunning ? (
|
||||
<span className="text-yellow-600 font-semibold">Task Running</span>
|
||||
) : (
|
||||
<span className="text-green-600 font-semibold">Success</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ const ToolsTab = ({
|
|||
selectedTool,
|
||||
setSelectedTool,
|
||||
toolResult,
|
||||
isPollingTask,
|
||||
nextCursor,
|
||||
error,
|
||||
resourceContent,
|
||||
|
|
@ -76,16 +77,19 @@ const ToolsTab = ({
|
|||
name: string,
|
||||
params: Record<string, unknown>,
|
||||
metadata?: Record<string, unknown>,
|
||||
runAsTask?: boolean,
|
||||
) => Promise<void>;
|
||||
selectedTool: Tool | null;
|
||||
setSelectedTool: (tool: Tool | null) => void;
|
||||
toolResult: CompatibilityCallToolResult | null;
|
||||
isPollingTask?: boolean;
|
||||
nextCursor: ListToolsResult["nextCursor"];
|
||||
error: string | null;
|
||||
resourceContent: Record<string, string>;
|
||||
onReadResource?: (uri: string) => void;
|
||||
}) => {
|
||||
const [params, setParams] = useState<Record<string, unknown>>({});
|
||||
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 = ({
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id="run-as-task"
|
||||
checked={runAsTask}
|
||||
onCheckedChange={(checked: boolean) =>
|
||||
setRunAsTask(checked)
|
||||
}
|
||||
/>
|
||||
<Label
|
||||
htmlFor="run-as-task"
|
||||
className="text-sm font-medium text-gray-700 dark:text-gray-300 cursor-pointer"
|
||||
>
|
||||
Run as task
|
||||
</Label>
|
||||
</div>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
// Validate JSON inputs before calling tool
|
||||
|
|
@ -676,6 +697,7 @@ const ToolsTab = ({
|
|||
selectedTool.name,
|
||||
params,
|
||||
Object.keys(metadata).length ? metadata : undefined,
|
||||
runAsTask,
|
||||
);
|
||||
} finally {
|
||||
setIsToolRunning(false);
|
||||
|
|
@ -683,16 +705,17 @@ const ToolsTab = ({
|
|||
}}
|
||||
disabled={
|
||||
isToolRunning ||
|
||||
isPollingTask ||
|
||||
hasValidationErrors ||
|
||||
hasReservedMetadataEntry ||
|
||||
hasInvalidMetaPrefixEntry ||
|
||||
hasInvalidMetaNameEntry
|
||||
}
|
||||
>
|
||||
{isToolRunning ? (
|
||||
{isToolRunning || isPollingTask ? (
|
||||
<>
|
||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
||||
Running...
|
||||
{isPollingTask ? "Polling Task..." : "Running..."}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -731,6 +754,7 @@ const ToolsTab = ({
|
|||
selectedTool={selectedTool}
|
||||
resourceContent={resourceContent}
|
||||
onReadResource={onReadResource}
|
||||
isPollingTask={isPollingTask}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
|
|
|||
|
|
@ -3,14 +3,14 @@ import "@testing-library/jest-dom";
|
|||
import { describe, it, jest, beforeEach } from "@jest/globals";
|
||||
import ToolsTab from "../ToolsTab";
|
||||
import { Tool } from "@modelcontextprotocol/sdk/types.js";
|
||||
import { Tabs } from "@/components/ui/tabs";
|
||||
import { cacheToolOutputSchemas } from "@/utils/schemaUtils";
|
||||
import { Tabs } from "../ui/tabs";
|
||||
import { cacheToolOutputSchemas } from "../../utils/schemaUtils";
|
||||
import { within } from "@testing-library/react";
|
||||
import {
|
||||
META_NAME_RULES_MESSAGE,
|
||||
META_PREFIX_RULES_MESSAGE,
|
||||
RESERVED_NAMESPACE_MESSAGE,
|
||||
} from "@/utils/metaUtils";
|
||||
} from "../../utils/metaUtils";
|
||||
|
||||
describe("ToolsTab", () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -130,6 +130,7 @@ describe("ToolsTab", () => {
|
|||
count: 42,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -155,6 +156,7 @@ describe("ToolsTab", () => {
|
|||
num: -42,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -191,6 +193,7 @@ describe("ToolsTab", () => {
|
|||
num: null,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -234,6 +237,7 @@ describe("ToolsTab", () => {
|
|||
optionalBoolean: null,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
|
||||
// State 2: Uncheck null checkbox -> should set value to false and enable input
|
||||
|
|
@ -255,6 +259,7 @@ describe("ToolsTab", () => {
|
|||
optionalBoolean: false,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
|
||||
// State 3: Check boolean checkbox -> should set value to true
|
||||
|
|
@ -277,6 +282,7 @@ describe("ToolsTab", () => {
|
|||
optionalBoolean: true,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
|
||||
// State 4: Check null checkbox again -> should set value back to null and disable input
|
||||
|
|
@ -295,6 +301,7 @@ describe("ToolsTab", () => {
|
|||
optionalBoolean: null,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -816,6 +823,7 @@ describe("ToolsTab", () => {
|
|||
mockTools[0].name,
|
||||
expect.any(Object),
|
||||
{ requestId: "abc123" },
|
||||
false,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1080,6 +1088,7 @@ describe("ToolsTab", () => {
|
|||
count: 5,
|
||||
},
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -40,4 +40,9 @@ export type InspectorConfig = {
|
|||
* Session token for authenticating with the MCP Proxy Server. This token is displayed in the proxy server console on startup.
|
||||
*/
|
||||
MCP_PROXY_AUTH_TOKEN: ConfigItem;
|
||||
|
||||
/**
|
||||
* Default Time-to-Live (TTL) in milliseconds for newly created tasks.
|
||||
*/
|
||||
MCP_TASK_TTL: ConfigItem;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -76,4 +76,11 @@ export const DEFAULT_INSPECTOR_CONFIG: InspectorConfig = {
|
|||
value: "",
|
||||
is_session_item: true,
|
||||
},
|
||||
MCP_TASK_TTL: {
|
||||
label: "Task TTL",
|
||||
description:
|
||||
"Default Time-to-Live (TTL) in milliseconds for newly created tasks",
|
||||
value: 60000,
|
||||
is_session_item: false,
|
||||
},
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
import { renderHook, act } from "@testing-library/react";
|
||||
import { useConnection } from "../useConnection";
|
||||
import { z } from "zod";
|
||||
import { z } from "zod/v3";
|
||||
import {
|
||||
ClientRequest,
|
||||
CreateTaskResultSchema,
|
||||
JSONRPCMessage,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import type {
|
||||
AnySchema,
|
||||
SchemaOutput,
|
||||
} from "@modelcontextprotocol/sdk/server/zod-compat.js";
|
||||
import { DEFAULT_INSPECTOR_CONFIG, CLIENT_IDENTITY } from "../../constants";
|
||||
import {
|
||||
SSEClientTransportOptions,
|
||||
|
|
@ -128,7 +133,7 @@ const mockDiscoverScopes = discoverScopes as jest.MockedFunction<
|
|||
>;
|
||||
|
||||
describe("useConnection", () => {
|
||||
const defaultProps = {
|
||||
const defaultProps: Parameters<typeof useConnection>[0] = {
|
||||
transportType: "sse" as const,
|
||||
command: "",
|
||||
args: "",
|
||||
|
|
@ -164,8 +169,10 @@ describe("useConnection", () => {
|
|||
test: z.string(),
|
||||
});
|
||||
|
||||
const mockSchemaAny: AnySchema = mockSchema as unknown as AnySchema;
|
||||
|
||||
await act(async () => {
|
||||
await result.current.makeRequest(mockRequest, mockSchema);
|
||||
await result.current.makeRequest(mockRequest, mockSchemaAny);
|
||||
});
|
||||
|
||||
expect(mockClient.request).toHaveBeenCalledWith(
|
||||
|
|
@ -204,8 +211,10 @@ describe("useConnection", () => {
|
|||
test: z.string(),
|
||||
});
|
||||
|
||||
const mockSchemaAny: AnySchema = mockSchema as unknown as AnySchema;
|
||||
|
||||
await act(async () => {
|
||||
await result.current.makeRequest(mockRequest, mockSchema, {
|
||||
await result.current.makeRequest(mockRequest, mockSchemaAny, {
|
||||
timeout: 1000,
|
||||
maxTotalTimeout: 2000,
|
||||
resetTimeoutOnProgress: false,
|
||||
|
|
@ -224,8 +233,251 @@ describe("useConnection", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("Receiver-side Tasks (task-augmented incoming requests)", () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test("declares tasks.requests.sampling.createMessage when onPendingRequest is provided", async () => {
|
||||
const Client = jest.requireMock(
|
||||
"@modelcontextprotocol/sdk/client/index.js",
|
||||
).Client;
|
||||
|
||||
const propsWithPending = {
|
||||
...defaultProps,
|
||||
onPendingRequest: jest.fn(),
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useConnection(propsWithPending));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.connect();
|
||||
});
|
||||
|
||||
expect(Client).toHaveBeenCalledWith(
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
capabilities: expect.objectContaining({
|
||||
tasks: expect.objectContaining({
|
||||
requests: expect.objectContaining({
|
||||
sampling: expect.objectContaining({
|
||||
createMessage: {},
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test("task-augmented sampling/createMessage returns { task } and tasks/result blocks until resolved", async () => {
|
||||
let pendingResolve: ((value: unknown) => void) | undefined;
|
||||
let pendingReject: ((reason?: unknown) => void) | undefined;
|
||||
|
||||
const mockOnPendingRequest = jest.fn((_request, resolve, reject) => {
|
||||
pendingResolve = resolve;
|
||||
pendingReject = reject;
|
||||
});
|
||||
|
||||
const propsWithPending = {
|
||||
...defaultProps,
|
||||
onPendingRequest: mockOnPendingRequest,
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useConnection(propsWithPending));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.connect();
|
||||
});
|
||||
|
||||
const samplingRequest = {
|
||||
method: "sampling/createMessage",
|
||||
params: {
|
||||
task: { ttl: 0 },
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content: { type: "text", text: "hello" },
|
||||
},
|
||||
],
|
||||
maxTokens: 1,
|
||||
},
|
||||
};
|
||||
|
||||
// Locate the sampling/createMessage handler
|
||||
const samplingHandlerCall = mockClient.setRequestHandler.mock.calls.find(
|
||||
(call) => {
|
||||
try {
|
||||
const schema = call[0];
|
||||
const parseResult =
|
||||
schema.safeParse && schema.safeParse(samplingRequest);
|
||||
return parseResult?.success;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
expect(samplingHandlerCall).toBeDefined();
|
||||
const [, samplingHandler] = samplingHandlerCall;
|
||||
|
||||
// Invoke handler; should return a CreateTaskResult immediately
|
||||
let createTaskResult: SchemaOutput<typeof CreateTaskResultSchema>;
|
||||
await act(async () => {
|
||||
createTaskResult = await samplingHandler(samplingRequest);
|
||||
});
|
||||
|
||||
expect(createTaskResult).toHaveProperty("task");
|
||||
expect(createTaskResult.task).toEqual(
|
||||
expect.objectContaining({
|
||||
taskId: expect.any(String),
|
||||
status: "input_required",
|
||||
ttl: 0,
|
||||
createdAt: expect.any(String),
|
||||
lastUpdatedAt: expect.any(String),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(mockOnPendingRequest).toHaveBeenCalledTimes(1);
|
||||
expect(pendingResolve).toBeDefined();
|
||||
expect(pendingReject).toBeDefined();
|
||||
|
||||
const taskId = createTaskResult.task.taskId as string;
|
||||
|
||||
// Locate tasks/get and tasks/result handlers
|
||||
const taskGetRequest = { method: "tasks/get", params: { taskId } };
|
||||
const taskResultRequest = { method: "tasks/result", params: { taskId } };
|
||||
|
||||
const taskGetHandlerCall = mockClient.setRequestHandler.mock.calls.find(
|
||||
(call) => {
|
||||
try {
|
||||
const schema = call[0];
|
||||
const parseResult =
|
||||
schema.safeParse && schema.safeParse(taskGetRequest);
|
||||
return parseResult?.success;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
},
|
||||
);
|
||||
const taskResultHandlerCall =
|
||||
mockClient.setRequestHandler.mock.calls.find((call) => {
|
||||
try {
|
||||
const schema = call[0];
|
||||
const parseResult =
|
||||
schema.safeParse && schema.safeParse(taskResultRequest);
|
||||
return parseResult?.success;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
expect(taskGetHandlerCall).toBeDefined();
|
||||
expect(taskResultHandlerCall).toBeDefined();
|
||||
|
||||
const [, taskGetHandler] = taskGetHandlerCall;
|
||||
const [, taskResultHandler] = taskResultHandlerCall;
|
||||
|
||||
// Verify tasks/get sees the in-progress task
|
||||
const getBefore = await taskGetHandler(taskGetRequest);
|
||||
expect(getBefore.status).toBe("input_required");
|
||||
|
||||
// tasks/result should block until user flow resolves
|
||||
const payloadPromise = taskResultHandler(taskResultRequest);
|
||||
const race = await Promise.race([
|
||||
payloadPromise.then(() => "resolved"),
|
||||
new Promise((r) => setTimeout(() => r("timeout"), 10)),
|
||||
]);
|
||||
expect(race).toBe("timeout");
|
||||
|
||||
const mockPayload = {
|
||||
model: "test-model",
|
||||
role: "assistant",
|
||||
content: { type: "text", text: "ok" },
|
||||
};
|
||||
|
||||
await act(async () => {
|
||||
pendingResolve!(mockPayload);
|
||||
// Let the background updater run
|
||||
await new Promise((r) => setTimeout(r, 0));
|
||||
});
|
||||
|
||||
await expect(payloadPromise).resolves.toEqual(mockPayload);
|
||||
|
||||
const getAfter = await taskGetHandler(taskGetRequest);
|
||||
expect(getAfter.status).toBe("completed");
|
||||
});
|
||||
|
||||
test("task-augmented elicitation/create returns { task } immediately", async () => {
|
||||
const mockOnElicitationRequest = jest.fn();
|
||||
const propsWithElicitation = {
|
||||
...defaultProps,
|
||||
onElicitationRequest: mockOnElicitationRequest,
|
||||
};
|
||||
|
||||
const { result } = renderHook(() => useConnection(propsWithElicitation));
|
||||
|
||||
await act(async () => {
|
||||
await result.current.connect();
|
||||
});
|
||||
|
||||
const elicitationRequest = {
|
||||
method: "elicitation/create",
|
||||
params: {
|
||||
task: { ttl: 0 },
|
||||
message: "Please provide your name",
|
||||
requestedSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
name: { type: "string" },
|
||||
},
|
||||
required: ["name"],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const elicitRequestHandlerCall =
|
||||
mockClient.setRequestHandler.mock.calls.find((call) => {
|
||||
try {
|
||||
const schema = call[0];
|
||||
const parseResult =
|
||||
schema.safeParse && schema.safeParse(elicitationRequest);
|
||||
return parseResult?.success;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
expect(elicitRequestHandlerCall).toBeDefined();
|
||||
const [, handler] = elicitRequestHandlerCall;
|
||||
|
||||
mockOnElicitationRequest.mockImplementation((_request, resolve) => {
|
||||
resolve({ action: "accept", content: { name: "test" } });
|
||||
});
|
||||
|
||||
const resultValue = await handler(elicitationRequest);
|
||||
|
||||
expect(resultValue).toHaveProperty("task");
|
||||
expect(resultValue.task).toEqual(
|
||||
expect.objectContaining({
|
||||
taskId: expect.any(String),
|
||||
status: "input_required",
|
||||
ttl: 0,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
test("throws error when mcpClient is not connected", async () => {
|
||||
const { result } = renderHook(() => useConnection(defaultProps));
|
||||
const { result } = renderHook(() => {
|
||||
const { makeRequest } = useConnection(defaultProps) as unknown as {
|
||||
makeRequest: (
|
||||
request: ClientRequest,
|
||||
schema: AnySchema,
|
||||
) => Promise<unknown>;
|
||||
};
|
||||
return { makeRequest };
|
||||
});
|
||||
|
||||
const mockRequest: ClientRequest = {
|
||||
method: "ping",
|
||||
|
|
@ -236,8 +488,10 @@ describe("useConnection", () => {
|
|||
test: z.string(),
|
||||
});
|
||||
|
||||
const mockSchemaAny: AnySchema = mockSchema as unknown as AnySchema;
|
||||
|
||||
await expect(
|
||||
result.current.makeRequest(mockRequest, mockSchema),
|
||||
result.current.makeRequest(mockRequest, mockSchemaAny),
|
||||
).rejects.toThrow("MCP client not connected");
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import {
|
|||
import {
|
||||
ClientNotification,
|
||||
ClientRequest,
|
||||
ClientResult,
|
||||
CreateMessageRequestSchema,
|
||||
ListRootsRequestSchema,
|
||||
ResourceUpdatedNotificationSchema,
|
||||
|
|
@ -32,13 +33,22 @@ import {
|
|||
LoggingLevel,
|
||||
ElicitRequestSchema,
|
||||
Implementation,
|
||||
Task,
|
||||
CreateTaskResultSchema,
|
||||
GetTaskRequestSchema,
|
||||
GetTaskPayloadRequestSchema,
|
||||
ListTasksRequestSchema,
|
||||
CancelTaskRequestSchema,
|
||||
ListTasksResultSchema,
|
||||
CancelTaskResultSchema,
|
||||
TaskStatusNotificationSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import type {
|
||||
AnySchema,
|
||||
SchemaOutput,
|
||||
} from "@modelcontextprotocol/sdk/server/zod-compat.js";
|
||||
import { RequestOptions } from "@modelcontextprotocol/sdk/shared/protocol.js";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useToast } from "@/lib/hooks/useToast";
|
||||
import { ConnectionStatus, CLIENT_IDENTITY } from "../constants";
|
||||
import { Notification } from "../notificationTypes";
|
||||
|
|
@ -56,6 +66,7 @@ import {
|
|||
} from "../auth";
|
||||
import {
|
||||
getMCPProxyAddress,
|
||||
getMCPTaskTtl,
|
||||
getMCPServerRequestMaxTotalTimeout,
|
||||
resetRequestTimeoutOnProgress,
|
||||
getMCPProxyAuthToken,
|
||||
|
|
@ -131,6 +142,18 @@ export function useConnection({
|
|||
const [serverImplementation, setServerImplementation] =
|
||||
useState<Implementation | null>(null);
|
||||
|
||||
type ReceiverTaskRecord = {
|
||||
task: Task;
|
||||
payloadPromise: Promise<ClientResult>;
|
||||
resolvePayload: (payload: ClientResult) => void;
|
||||
rejectPayload: (reason?: unknown) => void;
|
||||
cleanupTimeoutId?: ReturnType<typeof setTimeout>;
|
||||
};
|
||||
|
||||
// Tasks created locally in response to *incoming* task-augmented requests
|
||||
// (e.g. `sampling/createMessage` and `elicitation/create` with `params.task`).
|
||||
const receiverTasksRef = useRef<Map<string, ReceiverTaskRecord>>(new Map());
|
||||
|
||||
useEffect(() => {
|
||||
if (!oauthClientId) {
|
||||
clearClientInformationFromSessionStorage({
|
||||
|
|
@ -433,6 +456,22 @@ export function useConnection({
|
|||
roots: {
|
||||
listChanged: true,
|
||||
},
|
||||
tasks: {
|
||||
list: {},
|
||||
cancel: {},
|
||||
...(onPendingRequest || onElicitationRequest
|
||||
? {
|
||||
requests: {
|
||||
...(onPendingRequest
|
||||
? { sampling: { createMessage: {} } }
|
||||
: undefined),
|
||||
...(onElicitationRequest
|
||||
? { elicitation: { create: {} } }
|
||||
: undefined),
|
||||
},
|
||||
}
|
||||
: undefined),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -714,6 +753,7 @@ export function useConnection({
|
|||
ResourceListChangedNotificationSchema,
|
||||
ToolListChangedNotificationSchema,
|
||||
PromptListChangedNotificationSchema,
|
||||
TaskStatusNotificationSchema,
|
||||
].forEach((notificationSchema) => {
|
||||
client.setNotificationHandler(notificationSchema, onNotification);
|
||||
});
|
||||
|
|
@ -793,11 +833,218 @@ export function useConnection({
|
|||
setServerCapabilities(capabilities ?? null);
|
||||
setCompletionsSupported(capabilities?.completions !== undefined);
|
||||
|
||||
const nowIso = () => new Date().toISOString();
|
||||
|
||||
const makeTaskId = () => {
|
||||
// Prefer UUID when available; otherwise fall back to a reasonably unique id.
|
||||
const cryptoAny = globalThis.crypto as unknown as
|
||||
| { randomUUID?: () => string }
|
||||
| undefined;
|
||||
return (
|
||||
cryptoAny?.randomUUID?.() ??
|
||||
`task_${Date.now()}_${Math.random().toString(16).slice(2)}`
|
||||
);
|
||||
};
|
||||
|
||||
const emitTaskStatus = async (task: Task) => {
|
||||
// Best-effort; task status notifications are optional.
|
||||
try {
|
||||
const notification: ClientNotification = {
|
||||
method: "notifications/tasks/status",
|
||||
params: task,
|
||||
} as unknown as ClientNotification;
|
||||
await client.notification(notification);
|
||||
pushHistory(notification);
|
||||
} catch (e) {
|
||||
console.warn("Failed to send notifications/tasks/status", e);
|
||||
}
|
||||
};
|
||||
|
||||
const upsertReceiverTask = async (task: Task) => {
|
||||
// Update task record and emit status notification.
|
||||
const record = receiverTasksRef.current.get(task.taskId);
|
||||
if (record) {
|
||||
receiverTasksRef.current.set(task.taskId, { ...record, task });
|
||||
}
|
||||
await emitTaskStatus(task);
|
||||
};
|
||||
|
||||
const createReceiverTask = (opts: {
|
||||
ttl?: number;
|
||||
initialStatus: Task["status"];
|
||||
statusMessage?: string;
|
||||
pollInterval?: number;
|
||||
}): ReceiverTaskRecord => {
|
||||
const taskId = makeTaskId();
|
||||
const createdAt = nowIso();
|
||||
const ttl = opts.ttl ?? getMCPTaskTtl(config);
|
||||
|
||||
let resolvePayload: (payload: ClientResult) => void = () => undefined;
|
||||
let rejectPayload: (reason?: unknown) => void = () => undefined;
|
||||
const payloadPromise = new Promise<ClientResult>((resolve, reject) => {
|
||||
resolvePayload = resolve;
|
||||
rejectPayload = reject;
|
||||
});
|
||||
|
||||
const task: Task = {
|
||||
taskId,
|
||||
status: opts.initialStatus,
|
||||
ttl,
|
||||
createdAt,
|
||||
lastUpdatedAt: createdAt,
|
||||
...(opts.pollInterval !== undefined
|
||||
? { pollInterval: opts.pollInterval }
|
||||
: undefined),
|
||||
...(opts.statusMessage ? { statusMessage: opts.statusMessage } : {}),
|
||||
};
|
||||
|
||||
const record: ReceiverTaskRecord = {
|
||||
task,
|
||||
payloadPromise,
|
||||
resolvePayload,
|
||||
rejectPayload,
|
||||
};
|
||||
|
||||
// Cleanup after TTL (best-effort).
|
||||
if (ttl !== null && ttl > 0) {
|
||||
record.cleanupTimeoutId = setTimeout(() => {
|
||||
receiverTasksRef.current.delete(taskId);
|
||||
}, ttl);
|
||||
}
|
||||
|
||||
receiverTasksRef.current.set(taskId, record);
|
||||
void emitTaskStatus(task);
|
||||
return record;
|
||||
};
|
||||
|
||||
// Server -> client Tasks handlers (receiver side)
|
||||
client.setRequestHandler(ListTasksRequestSchema, async () => {
|
||||
return {
|
||||
tasks: Array.from(receiverTasksRef.current.values()).map(
|
||||
(r) => r.task,
|
||||
),
|
||||
};
|
||||
});
|
||||
|
||||
client.setRequestHandler(GetTaskRequestSchema, async (request) => {
|
||||
const record = receiverTasksRef.current.get(request.params.taskId);
|
||||
if (!record) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
`Unknown taskId: ${request.params.taskId}`,
|
||||
);
|
||||
}
|
||||
return record.task;
|
||||
});
|
||||
|
||||
client.setRequestHandler(GetTaskPayloadRequestSchema, async (request) => {
|
||||
const record = receiverTasksRef.current.get(request.params.taskId);
|
||||
if (!record) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
`Unknown taskId: ${request.params.taskId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// Block until the task payload is ready.
|
||||
return await record.payloadPromise;
|
||||
});
|
||||
|
||||
client.setRequestHandler(CancelTaskRequestSchema, async (request) => {
|
||||
const record = receiverTasksRef.current.get(request.params.taskId);
|
||||
if (!record) {
|
||||
throw new McpError(
|
||||
ErrorCode.InvalidParams,
|
||||
`Unknown taskId: ${request.params.taskId}`,
|
||||
);
|
||||
}
|
||||
|
||||
const terminalStatuses: Task["status"][] = [
|
||||
"completed",
|
||||
"failed",
|
||||
"cancelled",
|
||||
];
|
||||
|
||||
if (!terminalStatuses.includes(record.task.status)) {
|
||||
const updated: Task = {
|
||||
...record.task,
|
||||
status: "cancelled",
|
||||
lastUpdatedAt: nowIso(),
|
||||
statusMessage: "Cancelled",
|
||||
};
|
||||
receiverTasksRef.current.set(request.params.taskId, {
|
||||
...record,
|
||||
task: updated,
|
||||
});
|
||||
|
||||
// Unblock any pending `tasks/result`.
|
||||
record.rejectPayload(
|
||||
new McpError(ErrorCode.InternalError, "Task was cancelled"),
|
||||
);
|
||||
|
||||
await emitTaskStatus(updated);
|
||||
}
|
||||
|
||||
return receiverTasksRef.current.get(request.params.taskId)!.task;
|
||||
});
|
||||
|
||||
if (onPendingRequest) {
|
||||
client.setRequestHandler(CreateMessageRequestSchema, (request) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
onPendingRequest(request, resolve, reject);
|
||||
const taskSpec = (request as { params?: { task?: { ttl?: number } } })
|
||||
.params?.task;
|
||||
|
||||
if (!taskSpec) {
|
||||
return new Promise((resolve, reject) => {
|
||||
onPendingRequest(request, resolve, reject);
|
||||
});
|
||||
}
|
||||
|
||||
// Task-augmented sampling request: return a task immediately and
|
||||
// allow the server to poll via `tasks/get` and `tasks/result`.
|
||||
const record = createReceiverTask({
|
||||
ttl: taskSpec.ttl,
|
||||
initialStatus: "input_required",
|
||||
statusMessage: "Awaiting user input",
|
||||
});
|
||||
|
||||
// Background runner to complete and resolve this specific task record.
|
||||
void (async () => {
|
||||
try {
|
||||
const payload = await new Promise((resolve, reject) => {
|
||||
onPendingRequest(request, resolve, reject);
|
||||
});
|
||||
record.resolvePayload(payload as ClientResult);
|
||||
const updated: Task = {
|
||||
...record.task,
|
||||
status: "completed",
|
||||
lastUpdatedAt: nowIso(),
|
||||
};
|
||||
receiverTasksRef.current.set(record.task.taskId, {
|
||||
...record,
|
||||
task: updated,
|
||||
});
|
||||
await upsertReceiverTask(updated);
|
||||
} catch (e) {
|
||||
record.rejectPayload(e);
|
||||
const updated: Task = {
|
||||
...record.task,
|
||||
status: "failed",
|
||||
lastUpdatedAt: nowIso(),
|
||||
statusMessage: e instanceof Error ? e.message : "Task failed",
|
||||
};
|
||||
receiverTasksRef.current.set(record.task.taskId, {
|
||||
...record,
|
||||
task: updated,
|
||||
});
|
||||
await upsertReceiverTask(updated);
|
||||
}
|
||||
})();
|
||||
|
||||
const createTaskResult: SchemaOutput<typeof CreateTaskResultSchema> =
|
||||
{
|
||||
task: record.task,
|
||||
};
|
||||
return createTaskResult;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -807,6 +1054,64 @@ export function useConnection({
|
|||
});
|
||||
}
|
||||
|
||||
if (onElicitationRequest) {
|
||||
client.setRequestHandler(ElicitRequestSchema, (request) => {
|
||||
const taskSpec = (request as { params?: { task?: { ttl?: number } } })
|
||||
.params?.task;
|
||||
|
||||
if (!taskSpec) {
|
||||
return new Promise((resolve) => {
|
||||
onElicitationRequest(request, resolve);
|
||||
});
|
||||
}
|
||||
|
||||
const record = createReceiverTask({
|
||||
ttl: taskSpec.ttl,
|
||||
initialStatus: "input_required",
|
||||
statusMessage: "Awaiting user input",
|
||||
});
|
||||
|
||||
// Run elicitation flow and resolve the task payload.
|
||||
void (async () => {
|
||||
try {
|
||||
const payload = await new Promise((resolve) => {
|
||||
onElicitationRequest(request, resolve);
|
||||
});
|
||||
record.resolvePayload(payload as ClientResult);
|
||||
const updated: Task = {
|
||||
...record.task,
|
||||
status: "completed",
|
||||
lastUpdatedAt: nowIso(),
|
||||
};
|
||||
receiverTasksRef.current.set(record.task.taskId, {
|
||||
...record,
|
||||
task: updated,
|
||||
});
|
||||
await upsertReceiverTask(updated);
|
||||
} catch (e) {
|
||||
record.rejectPayload(e);
|
||||
const updated: Task = {
|
||||
...record.task,
|
||||
status: "failed",
|
||||
lastUpdatedAt: nowIso(),
|
||||
statusMessage: e instanceof Error ? e.message : "Task failed",
|
||||
};
|
||||
receiverTasksRef.current.set(record.task.taskId, {
|
||||
...record,
|
||||
task: updated,
|
||||
});
|
||||
await upsertReceiverTask(updated);
|
||||
}
|
||||
})();
|
||||
|
||||
const createTaskResult: SchemaOutput<typeof CreateTaskResultSchema> =
|
||||
{
|
||||
task: record.task,
|
||||
};
|
||||
return createTaskResult;
|
||||
});
|
||||
}
|
||||
|
||||
if (capabilities?.logging && defaultLoggingLevel) {
|
||||
lastRequest = "logging/setLevel";
|
||||
await client.setLoggingLevel(defaultLoggingLevel);
|
||||
|
|
@ -822,14 +1127,6 @@ export function useConnection({
|
|||
lastRequest = "";
|
||||
}
|
||||
|
||||
if (onElicitationRequest) {
|
||||
client.setRequestHandler(ElicitRequestSchema, async (request) => {
|
||||
return new Promise((resolve) => {
|
||||
onElicitationRequest(request, resolve);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
setMcpClient(client);
|
||||
setConnectionStatus("connected");
|
||||
} catch (e) {
|
||||
|
|
@ -855,7 +1152,35 @@ export function useConnection({
|
|||
}
|
||||
};
|
||||
|
||||
const cancelTask = async (taskId: string) => {
|
||||
return makeRequest(
|
||||
{
|
||||
method: "tasks/cancel",
|
||||
params: { taskId },
|
||||
},
|
||||
CancelTaskResultSchema,
|
||||
);
|
||||
};
|
||||
|
||||
const listTasks = async (cursor?: string) => {
|
||||
return makeRequest(
|
||||
{
|
||||
method: "tasks/list",
|
||||
params: { cursor },
|
||||
},
|
||||
ListTasksResultSchema,
|
||||
);
|
||||
};
|
||||
|
||||
const disconnect = async () => {
|
||||
// Clear any receiver-side tasks + cleanup timers
|
||||
receiverTasksRef.current.forEach((record) => {
|
||||
if (record.cleanupTimeoutId) {
|
||||
clearTimeout(record.cleanupTimeoutId);
|
||||
}
|
||||
});
|
||||
receiverTasksRef.current.clear();
|
||||
|
||||
if (transportType === "streamable-http")
|
||||
await (
|
||||
clientTransport as StreamableHTTPClientTransport
|
||||
|
|
@ -885,6 +1210,8 @@ export function useConnection({
|
|||
requestHistory,
|
||||
clearRequestHistory,
|
||||
makeRequest,
|
||||
cancelTask,
|
||||
listTasks,
|
||||
sendNotification,
|
||||
handleCompletion,
|
||||
completionsSupported,
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ export const getMCPProxyAuthToken = (
|
|||
header: "X-MCP-Proxy-Auth",
|
||||
};
|
||||
};
|
||||
export const getMCPTaskTtl = (config: InspectorConfig): number => {
|
||||
return config.MCP_TASK_TTL.value as number;
|
||||
};
|
||||
|
||||
export const getInitialTransportType = ():
|
||||
| "stdio"
|
||||
|
|
|
|||
107
package-lock.json
generated
107
package-lock.json
generated
|
|
@ -227,7 +227,6 @@
|
|||
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^2.1.3",
|
||||
"@csstools/css-color-parser": "^3.0.9",
|
||||
|
|
@ -241,8 +240,7 @@
|
|||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.27.1",
|
||||
|
|
@ -275,6 +273,7 @@
|
|||
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@babel/generator": "^7.28.5",
|
||||
|
|
@ -820,7 +819,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -841,7 +839,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
|
|
@ -866,7 +863,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^5.1.0",
|
||||
"@csstools/css-calc": "^2.1.4"
|
||||
|
|
@ -1888,7 +1884,6 @@
|
|||
"integrity": "sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/environment": "30.2.0",
|
||||
"@jest/fake-timers": "30.2.0",
|
||||
|
|
@ -1917,7 +1912,6 @@
|
|||
"integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/fake-timers": "30.2.0",
|
||||
"@jest/types": "30.2.0",
|
||||
|
|
@ -1934,7 +1928,6 @@
|
|||
"integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/types": "30.2.0",
|
||||
"@sinonjs/fake-timers": "^13.0.0",
|
||||
|
|
@ -1953,7 +1946,6 @@
|
|||
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "^0.34.0"
|
||||
},
|
||||
|
|
@ -1967,7 +1959,6 @@
|
|||
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/pattern": "30.0.1",
|
||||
"@jest/schemas": "30.0.5",
|
||||
|
|
@ -1986,8 +1977,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
|
||||
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jest/environment-jsdom-abstract/node_modules/@sinonjs/fake-timers": {
|
||||
"version": "13.0.5",
|
||||
|
|
@ -1995,7 +1985,6 @@
|
|||
"integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": "^3.0.1"
|
||||
}
|
||||
|
|
@ -2006,7 +1995,6 @@
|
|||
"integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/tough-cookie": "*",
|
||||
|
|
@ -2019,7 +2007,6 @@
|
|||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
|
@ -2039,7 +2026,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
|
|
@ -2050,7 +2036,6 @@
|
|||
"integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@jest/types": "30.2.0",
|
||||
|
|
@ -2072,7 +2057,6 @@
|
|||
"integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/types": "30.2.0",
|
||||
"@types/node": "*",
|
||||
|
|
@ -2088,7 +2072,6 @@
|
|||
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/types": "30.2.0",
|
||||
"@types/node": "*",
|
||||
|
|
@ -2107,7 +2090,6 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -2121,7 +2103,6 @@
|
|||
"integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/schemas": "30.0.5",
|
||||
"ansi-styles": "^5.2.0",
|
||||
|
|
@ -2136,8 +2117,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jest/expect": {
|
||||
"version": "29.7.0",
|
||||
|
|
@ -2206,7 +2186,6 @@
|
|||
"integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"jest-regex-util": "30.0.1"
|
||||
|
|
@ -2221,7 +2200,6 @@
|
|||
"integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
|
||||
}
|
||||
|
|
@ -3967,8 +3945,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
"version": "7.20.5",
|
||||
|
|
@ -4218,6 +4195,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.2.tgz",
|
||||
"integrity": "sha512-LPM2G3Syo1GLzXLGJAKdqoU35XvrWzGJ21/7sgZTUpbkBaOasTj8tjwn6w+hCkqaa1TfJ/w67rJSwYItlJ2mYw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
}
|
||||
|
|
@ -4256,6 +4234,7 @@
|
|||
"integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
|
|
@ -4267,6 +4246,7 @@
|
|||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
|
|
@ -4407,6 +4387,7 @@
|
|||
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.49.0",
|
||||
"@typescript-eslint/types": "8.49.0",
|
||||
|
|
@ -4789,6 +4770,7 @@
|
|||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
|
|
@ -5261,6 +5243,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
|
|
@ -6100,8 +6083,7 @@
|
|||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/domexception": {
|
||||
"version": "4.0.0",
|
||||
|
|
@ -6359,6 +6341,7 @@
|
|||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
|
|
@ -6656,6 +6639,7 @@
|
|||
"resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
|
||||
"integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"accepts": "^2.0.0",
|
||||
"body-parser": "^2.2.1",
|
||||
|
|
@ -7749,6 +7733,7 @@
|
|||
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/core": "^29.7.0",
|
||||
"@jest/types": "^29.6.3",
|
||||
|
|
@ -8089,7 +8074,6 @@
|
|||
"integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/environment": "30.2.0",
|
||||
"@jest/environment-jsdom-abstract": "30.2.0",
|
||||
|
|
@ -8115,7 +8099,6 @@
|
|||
"integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/fake-timers": "30.2.0",
|
||||
"@jest/types": "30.2.0",
|
||||
|
|
@ -8132,7 +8115,6 @@
|
|||
"integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/types": "30.2.0",
|
||||
"@sinonjs/fake-timers": "^13.0.0",
|
||||
|
|
@ -8151,7 +8133,6 @@
|
|||
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@sinclair/typebox": "^0.34.0"
|
||||
},
|
||||
|
|
@ -8165,7 +8146,6 @@
|
|||
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/pattern": "30.0.1",
|
||||
"@jest/schemas": "30.0.5",
|
||||
|
|
@ -8184,8 +8164,7 @@
|
|||
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
|
||||
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jest-environment-jsdom/node_modules/@sinonjs/fake-timers": {
|
||||
"version": "13.0.5",
|
||||
|
|
@ -8193,7 +8172,6 @@
|
|||
"integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@sinonjs/commons": "^3.0.1"
|
||||
}
|
||||
|
|
@ -8204,7 +8182,6 @@
|
|||
"integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"@types/tough-cookie": "*",
|
||||
|
|
@ -8217,7 +8194,6 @@
|
|||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
|
|
@ -8228,7 +8204,6 @@
|
|||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
|
@ -8248,7 +8223,6 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
|
|
@ -8259,7 +8233,6 @@
|
|||
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^3.2.0",
|
||||
"rrweb-cssom": "^0.8.0"
|
||||
|
|
@ -8274,7 +8247,6 @@
|
|||
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^14.0.0"
|
||||
|
|
@ -8289,7 +8261,6 @@
|
|||
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"whatwg-encoding": "^3.1.1"
|
||||
},
|
||||
|
|
@ -8303,7 +8274,6 @@
|
|||
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
"debug": "^4.3.4"
|
||||
|
|
@ -8318,7 +8288,6 @@
|
|||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
|
|
@ -8333,7 +8302,6 @@
|
|||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
|
|
@ -8347,7 +8315,6 @@
|
|||
"integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.27.1",
|
||||
"@jest/types": "30.2.0",
|
||||
|
|
@ -8369,7 +8336,6 @@
|
|||
"integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/types": "30.2.0",
|
||||
"@types/node": "*",
|
||||
|
|
@ -8385,7 +8351,6 @@
|
|||
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/types": "30.2.0",
|
||||
"@types/node": "*",
|
||||
|
|
@ -8404,7 +8369,6 @@
|
|||
"integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.2.1",
|
||||
"data-urls": "^5.0.0",
|
||||
|
|
@ -8445,7 +8409,6 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -8459,7 +8422,6 @@
|
|||
"integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/schemas": "30.0.5",
|
||||
"ansi-styles": "^5.2.0",
|
||||
|
|
@ -8474,8 +8436,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jest-environment-jsdom/node_modules/tough-cookie": {
|
||||
"version": "5.1.2",
|
||||
|
|
@ -8483,7 +8444,6 @@
|
|||
"integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tldts": "^6.1.32"
|
||||
},
|
||||
|
|
@ -8497,7 +8457,6 @@
|
|||
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
|
|
@ -8511,7 +8470,6 @@
|
|||
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
|
|
@ -8525,7 +8483,6 @@
|
|||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3"
|
||||
},
|
||||
|
|
@ -8539,7 +8496,6 @@
|
|||
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -8550,7 +8506,6 @@
|
|||
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tr46": "^5.1.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
|
|
@ -8565,7 +8520,6 @@
|
|||
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
|
|
@ -9160,6 +9114,7 @@
|
|||
"integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
|
|
@ -9198,6 +9153,7 @@
|
|||
"integrity": "sha512-SYhBvTh89tTfCD/CRdSOm13mOBa42iTaTyfyEWBdKcGdPxPtLFBXuHR8XHb33YNYaP+lLbmSvBTsnoesCNJEsQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"abab": "^2.0.6",
|
||||
"acorn": "^8.8.1",
|
||||
|
|
@ -9536,7 +9492,6 @@
|
|||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
}
|
||||
|
|
@ -10414,6 +10369,7 @@
|
|||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
|
|
@ -10589,7 +10545,6 @@
|
|||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
"ansi-styles": "^5.0.0",
|
||||
|
|
@ -10605,7 +10560,6 @@
|
|||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
|
|
@ -10760,6 +10714,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
|
|
@ -10772,6 +10727,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
|
|
@ -10785,8 +10741,7 @@
|
|||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-refresh": {
|
||||
"version": "0.18.0",
|
||||
|
|
@ -11194,8 +11149,7 @@
|
|||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/run-applescript": {
|
||||
"version": "7.1.0",
|
||||
|
|
@ -11861,6 +11815,7 @@
|
|||
"integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@alloc/quick-lru": "^5.2.0",
|
||||
"arg": "^5.0.2",
|
||||
|
|
@ -11999,6 +11954,7 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -12022,7 +11978,6 @@
|
|||
"integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tldts-core": "^6.1.86"
|
||||
},
|
||||
|
|
@ -12035,8 +11990,7 @@
|
|||
"resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz",
|
||||
"integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tmpl": {
|
||||
"version": "1.0.5",
|
||||
|
|
@ -12209,6 +12163,7 @@
|
|||
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz",
|
||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cspotcode/source-map-support": "^0.8.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
|
|
@ -12265,6 +12220,7 @@
|
|||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
|
|
@ -12818,6 +12774,7 @@
|
|||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
@ -13026,6 +12983,7 @@
|
|||
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
|
@ -13119,6 +13077,7 @@
|
|||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
|
|
@ -13538,6 +13497,7 @@
|
|||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"yaml": "bin.mjs"
|
||||
},
|
||||
|
|
@ -13625,6 +13585,7 @@
|
|||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
|
||||
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue