Merge branch 'main' into claude/issue-1028-20260121-1235

This commit is contained in:
Ola Hungerford 2026-01-23 08:03:27 -07:00 • committed by GitHub
commit edbe2f84f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 1479 additions and 108 deletions

View file

@ -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={() => {

View 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");
});
});
});

View file

@ -9,6 +9,7 @@ import SamplingRequest from "./SamplingRequest";
export type PendingRequest = {
id: number;
request: CreateMessageRequest;
originatingTab?: string;
};
export type Props = {

View 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;

View file

@ -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>
)}

View file

@ -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>
) : (

View file

@ -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,
);
});

View file

@ -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;
};

View file

@ -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;

View file

@ -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");
});

View file

@ -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,

View file

@ -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
View file

@ -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"
}