Merge branch 'main' into claude/issue-1028-20260121-1235
This commit is contained in:
commit
edbe2f84f3
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