update completions branch

This commit is contained in:
Gavin Aboulhosn
2025-02-12 11:41:46 -05:00
parent 9b624e8c87
commit c66feff37d
2 changed files with 106 additions and 229 deletions

View File

@@ -1,39 +1,29 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { import {
ClientNotification,
ClientRequest, ClientRequest,
CompatibilityCallToolResult, CompatibilityCallToolResult,
CompatibilityCallToolResultSchema, CompatibilityCallToolResultSchema,
CreateMessageRequestSchema,
CreateMessageResult, CreateMessageResult,
EmptyResultSchema, EmptyResultSchema,
GetPromptResultSchema, GetPromptResultSchema,
ListPromptsResultSchema, ListPromptsResultSchema,
ListResourcesResultSchema, ListResourcesResultSchema,
ListResourceTemplatesResultSchema, ListResourceTemplatesResultSchema,
ListRootsRequestSchema,
ListToolsResultSchema, ListToolsResultSchema,
ProgressNotificationSchema,
ReadResourceResultSchema, ReadResourceResultSchema,
Request,
Resource, Resource,
ResourceTemplate, ResourceTemplate,
Root, Root,
ServerNotification, ServerNotification,
Tool, Tool,
ServerCapabilitiesSchema,
Result,
PromptReference, PromptReference,
ResourceReference, ResourceReference,
CompleteResultSchema,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { useCallback, useEffect, useRef, useState } from "react"; import React, { Suspense, useEffect, useRef, useState } from "react";
import { useConnection } from "./lib/hooks/useConnection";
import { useDraggablePane } from "./lib/hooks/useDraggablePane";
import { import { StdErrNotification } from "./lib/notificationTypes";
Notification,
StdErrNotification,
StdErrNotificationSchema,
} from "./lib/notificationTypes";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { import {
@@ -46,7 +36,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { z, type ZodType } from "zod"; import { z } from "zod";
import "./App.css"; import "./App.css";
import ConsoleTab from "./components/ConsoleTab"; import ConsoleTab from "./components/ConsoleTab";
import HistoryAndNotifications from "./components/History"; import HistoryAndNotifications from "./components/History";
@@ -58,22 +48,22 @@ import SamplingTab, { PendingRequest } from "./components/SamplingTab";
import Sidebar from "./components/Sidebar"; import Sidebar from "./components/Sidebar";
import ToolsTab from "./components/ToolsTab"; import ToolsTab from "./components/ToolsTab";
type ServerCapabilities = z.infer<typeof ServerCapabilitiesSchema>;
const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000;
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const PROXY_PORT = params.get("proxyPort") ?? "3000"; const PROXY_PORT = params.get("proxyPort") ?? "3000";
const REQUEST_TIMEOUT =
parseInt(params.get("timeout") ?? "") || DEFAULT_REQUEST_TIMEOUT_MSEC;
const PROXY_SERVER_URL = `http://localhost:${PROXY_PORT}`; const PROXY_SERVER_URL = `http://localhost:${PROXY_PORT}`;
const App = () => { const App = () => {
const [connectionStatus, setConnectionStatus] = useState< // Handle OAuth callback route
"disconnected" | "connected" | "error" if (window.location.pathname === "/oauth/callback") {
>("disconnected"); const OAuthCallback = React.lazy(
const [serverCapabilities, setServerCapabilities] = () => import("./components/OAuthCallback"),
useState<ServerCapabilities | null>(null); );
return (
<Suspense fallback={<div>Loading...</div>}>
<OAuthCallback />
</Suspense>
);
}
const [resources, setResources] = useState<Resource[]>([]); const [resources, setResources] = useState<Resource[]>([]);
const [resourceTemplates, setResourceTemplates] = useState< const [resourceTemplates, setResourceTemplates] = useState<
ResourceTemplate[] ResourceTemplate[]
@@ -96,12 +86,14 @@ const App = () => {
return localStorage.getItem("lastArgs") || ""; return localStorage.getItem("lastArgs") || "";
}); });
const [sseUrl, setSseUrl] = useState<string>("http://localhost:3001/sse"); const [sseUrl, setSseUrl] = useState<string>(() => {
const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio"); return localStorage.getItem("lastSseUrl") || "http://localhost:3001/sse";
const [requestHistory, setRequestHistory] = useState< });
{ request: string; response?: string }[] const [transportType, setTransportType] = useState<"stdio" | "sse">(() => {
>([]); return (
const [mcpClient, setMcpClient] = useState<Client | null>(null); (localStorage.getItem("lastTransportType") as "stdio" | "sse") || "stdio"
);
});
const [notifications, setNotifications] = useState<ServerNotification[]>([]); const [notifications, setNotifications] = useState<ServerNotification[]>([]);
const [stdErrNotifications, setStdErrNotifications] = useState< const [stdErrNotifications, setStdErrNotifications] = useState<
StdErrNotification[] StdErrNotification[]
@@ -152,49 +144,41 @@ const App = () => {
>(); >();
const [nextToolCursor, setNextToolCursor] = useState<string | undefined>(); const [nextToolCursor, setNextToolCursor] = useState<string | undefined>();
const progressTokenRef = useRef(0); const progressTokenRef = useRef(0);
const [historyPaneHeight, setHistoryPaneHeight] = useState<number>(300);
const [isDragging, setIsDragging] = useState(false);
const dragStartY = useRef<number>(0);
const dragStartHeight = useRef<number>(0);
const handleDragStart = useCallback( const { height: historyPaneHeight, handleDragStart } = useDraggablePane(300);
(e: React.MouseEvent) => {
setIsDragging(true); const {
dragStartY.current = e.clientY; connectionStatus,
dragStartHeight.current = historyPaneHeight; serverCapabilities,
document.body.style.userSelect = "none"; mcpClient,
requestHistory,
makeRequest: makeConnectionRequest,
sendNotification,
connect: connectMcpServer,
} = useConnection({
transportType,
command,
args,
sseUrl,
env,
proxyServerUrl: PROXY_SERVER_URL,
onNotification: (notification) => {
setNotifications((prev) => [...prev, notification as ServerNotification]);
}, },
[historyPaneHeight], onStdErrNotification: (notification) => {
); setStdErrNotifications((prev) => [
...prev,
const handleDragMove = useCallback( notification as StdErrNotification,
(e: MouseEvent) => { ]);
if (!isDragging) return;
const deltaY = dragStartY.current - e.clientY;
const newHeight = Math.max(
100,
Math.min(800, dragStartHeight.current + deltaY),
);
setHistoryPaneHeight(newHeight);
}, },
[isDragging], onPendingRequest: (request, resolve, reject) => {
); setPendingSampleRequests((prev) => [
...prev,
const handleDragEnd = useCallback(() => { { id: nextRequestId.current++, request, resolve, reject },
setIsDragging(false); ]);
document.body.style.userSelect = ""; },
}, []); getRoots: () => rootsRef.current,
});
useEffect(() => {
if (isDragging) {
window.addEventListener("mousemove", handleDragMove);
window.addEventListener("mouseup", handleDragEnd);
return () => {
window.removeEventListener("mousemove", handleDragMove);
window.removeEventListener("mouseup", handleDragEnd);
};
}
}, [isDragging, handleDragMove, handleDragEnd]);
useEffect(() => { useEffect(() => {
localStorage.setItem("lastCommand", command); localStorage.setItem("lastCommand", command);
@@ -204,6 +188,31 @@ const App = () => {
localStorage.setItem("lastArgs", args); localStorage.setItem("lastArgs", args);
}, [args]); }, [args]);
useEffect(() => {
localStorage.setItem("lastSseUrl", sseUrl);
}, [sseUrl]);
useEffect(() => {
localStorage.setItem("lastTransportType", transportType);
}, [transportType]);
// Auto-connect if serverUrl is provided in URL params (e.g. after OAuth callback)
useEffect(() => {
const serverUrl = params.get("serverUrl");
if (serverUrl) {
setSseUrl(serverUrl);
setTransportType("sse");
// Remove serverUrl from URL without reloading the page
const newUrl = new URL(window.location.href);
newUrl.searchParams.delete("serverUrl");
window.history.replaceState({}, "", newUrl.toString());
// Show success toast for OAuth
toast.success("Successfully authenticated with OAuth");
// Connect to the server
connectMcpServer();
}
}, []);
useEffect(() => { useEffect(() => {
fetch(`${PROXY_SERVER_URL}/config`) fetch(`${PROXY_SERVER_URL}/config`)
.then((response) => response.json()) .then((response) => response.json())
@@ -231,66 +240,29 @@ const App = () => {
} }
}, []); }, []);
const pushHistory = (request: object, response?: object) => {
setRequestHistory((prev) => [
...prev,
{
request: JSON.stringify(request),
response: response !== undefined ? JSON.stringify(response) : undefined,
},
]);
};
const clearError = (tabKey: keyof typeof errors) => { const clearError = (tabKey: keyof typeof errors) => {
setErrors((prev) => ({ ...prev, [tabKey]: null })); setErrors((prev) => ({ ...prev, [tabKey]: null }));
}; };
const makeRequest = async <T extends ZodType<object>>( const makeRequest = async <T extends z.ZodType>(
request: ClientRequest, request: ClientRequest,
schema: T, schema: T,
tabKey?: keyof typeof errors, tabKey?: keyof typeof errors,
) => { ) => {
if (!mcpClient) {
throw new Error("MCP client not connected");
}
try { try {
const abortController = new AbortController(); const response = await makeConnectionRequest(request, schema);
const timeoutId = setTimeout(() => {
abortController.abort("Request timed out");
}, REQUEST_TIMEOUT);
let response;
try {
response = await mcpClient.request(request, schema, {
signal: abortController.signal,
});
pushHistory(request, response);
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : String(error);
pushHistory(request, { error: errorMessage });
throw error;
} finally {
clearTimeout(timeoutId);
}
if (tabKey !== undefined) { if (tabKey !== undefined) {
clearError(tabKey); clearError(tabKey);
} }
return response; return response;
} catch (e: unknown) { } catch (e) {
const errorString = (e as Error).message ?? String(e); const errorString = (e as Error).message ?? String(e);
if (tabKey === undefined) { if (tabKey !== undefined) {
toast.error(errorString);
} else {
setErrors((prev) => ({ setErrors((prev) => ({
...prev, ...prev,
[tabKey]: errorString, [tabKey]: errorString,
})); }));
} }
throw e; throw e;
} }
}; };
@@ -317,35 +289,16 @@ const App = () => {
}; };
try { try {
const response = await mcpClient.complete(request.params, { const result = await makeRequest(request, CompleteResultSchema);
signal, return result.completion.values;
});
pushHistory(request, response);
return response?.completion.values || [];
} catch (e: unknown) { } catch (e: unknown) {
const errorMessage = e instanceof Error ? e.message : String(e); const errorMessage = e instanceof Error ? e.message : String(e);
pushHistory(request, { error: errorMessage });
toast.error(errorMessage); toast.error(errorMessage);
throw e; throw e;
} }
}; };
const sendNotification = async (notification: ClientNotification) => {
if (!mcpClient) {
throw new Error("MCP client not connected");
}
try {
await mcpClient.notification(notification);
pushHistory(notification);
} catch (e: unknown) {
toast.error((e as Error).message ?? String(e));
throw e;
}
};
const listResources = async () => { const listResources = async () => {
const response = await makeRequest( const response = await makeRequest(
{ {
@@ -448,82 +401,6 @@ const App = () => {
await sendNotification({ method: "notifications/roots/list_changed" }); await sendNotification({ method: "notifications/roots/list_changed" });
}; };
const connectMcpServer = async () => {
try {
const client = new Client<Request, Notification, Result>(
{
name: "mcp-inspector",
version: "0.0.1",
},
{
capabilities: {
// Support all client capabilities since we're an inspector tool
sampling: {},
roots: {
listChanged: true,
},
},
},
);
const backendUrl = new URL(`${PROXY_SERVER_URL}/sse`);
backendUrl.searchParams.append("transportType", transportType);
if (transportType === "stdio") {
backendUrl.searchParams.append("command", command);
backendUrl.searchParams.append("args", args);
backendUrl.searchParams.append("env", JSON.stringify(env));
} else {
backendUrl.searchParams.append("url", sseUrl);
}
const clientTransport = new SSEClientTransport(backendUrl);
client.setNotificationHandler(
ProgressNotificationSchema,
(notification) => {
setNotifications((prevNotifications) => [
...prevNotifications,
notification,
]);
},
);
client.setNotificationHandler(
StdErrNotificationSchema,
(notification) => {
setStdErrNotifications((prevErrorNotifications) => [
...prevErrorNotifications,
notification,
]);
},
);
await client.connect(clientTransport);
const capabilities = client.getServerCapabilities();
setServerCapabilities(capabilities ?? null);
client.setRequestHandler(CreateMessageRequestSchema, (request) => {
return new Promise<CreateMessageResult>((resolve, reject) => {
setPendingSampleRequests((prev) => [
...prev,
{ id: nextRequestId.current++, request, resolve, reject },
]);
});
});
client.setRequestHandler(ListRootsRequestSchema, async () => {
return { roots: rootsRef.current };
});
setMcpClient(client);
setConnectionStatus("connected");
} catch (e) {
console.error(e);
setConnectionStatus("error");
}
};
return ( return (
<div className="flex h-screen bg-background"> <div className="flex h-screen bg-background">
<Sidebar <Sidebar

36
package-lock.json generated
View File

@@ -1224,6 +1224,18 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/@modelcontextprotocol/sdk/node_modules/eventsource": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.2.tgz",
"integrity": "sha512-YolzkJNxsTL3tCJMWFxpxtG2sCjbZ4LQUBUrkdaJK0ub0p6lmJt+2+1SwhKjLc652lpH9L/79Ptez972H9tphw==",
"license": "MIT",
"dependencies": {
"eventsource-parser": "^3.0.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -4138,18 +4150,6 @@
"node": ">= 0.6" "node": ">= 0.6"
} }
}, },
"node_modules/eventsource": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.5.tgz",
"integrity": "sha512-LT/5J605bx5SNyE+ITBDiM3FxffBiq9un7Vx0EwMDM3vg8sWKx/tO2zC+LMqZ+smAM0F2hblaDZUVZF0te2pSw==",
"license": "MIT",
"dependencies": {
"eventsource-parser": "^3.0.0"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/eventsource-parser": { "node_modules/eventsource-parser": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz",
@@ -6758,9 +6758,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.4.11", "version": "5.4.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.12.tgz",
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", "integrity": "sha512-KwUaKB27TvWwDJr1GjjWthLMATbGEbeWYZIbGZ5qFIsgPP3vWzLu4cVooqhm5/Z2SPDUMjyPVjTztm5tYKwQxA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -7406,9 +7406,9 @@
} }
}, },
"node_modules/zod": { "node_modules/zod": {
"version": "3.24.2", "version": "3.24.1",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==", "integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
"license": "MIT", "license": "MIT",
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"