update completions branch
This commit is contained in:
@@ -1,39 +1,29 @@
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
import {
|
||||
ClientNotification,
|
||||
ClientRequest,
|
||||
CompatibilityCallToolResult,
|
||||
CompatibilityCallToolResultSchema,
|
||||
CreateMessageRequestSchema,
|
||||
CreateMessageResult,
|
||||
EmptyResultSchema,
|
||||
GetPromptResultSchema,
|
||||
ListPromptsResultSchema,
|
||||
ListResourcesResultSchema,
|
||||
ListResourceTemplatesResultSchema,
|
||||
ListRootsRequestSchema,
|
||||
ListToolsResultSchema,
|
||||
ProgressNotificationSchema,
|
||||
ReadResourceResultSchema,
|
||||
Request,
|
||||
Resource,
|
||||
ResourceTemplate,
|
||||
Root,
|
||||
ServerNotification,
|
||||
Tool,
|
||||
ServerCapabilitiesSchema,
|
||||
Result,
|
||||
PromptReference,
|
||||
ResourceReference,
|
||||
CompleteResultSchema,
|
||||
} 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 {
|
||||
Notification,
|
||||
StdErrNotification,
|
||||
StdErrNotificationSchema,
|
||||
} from "./lib/notificationTypes";
|
||||
import { StdErrNotification } from "./lib/notificationTypes";
|
||||
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
@@ -46,7 +36,7 @@ import {
|
||||
} from "lucide-react";
|
||||
|
||||
import { toast } from "react-toastify";
|
||||
import { z, type ZodType } from "zod";
|
||||
import { z } from "zod";
|
||||
import "./App.css";
|
||||
import ConsoleTab from "./components/ConsoleTab";
|
||||
import HistoryAndNotifications from "./components/History";
|
||||
@@ -58,22 +48,22 @@ import SamplingTab, { PendingRequest } from "./components/SamplingTab";
|
||||
import Sidebar from "./components/Sidebar";
|
||||
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 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 App = () => {
|
||||
const [connectionStatus, setConnectionStatus] = useState<
|
||||
"disconnected" | "connected" | "error"
|
||||
>("disconnected");
|
||||
const [serverCapabilities, setServerCapabilities] =
|
||||
useState<ServerCapabilities | null>(null);
|
||||
// Handle OAuth callback route
|
||||
if (window.location.pathname === "/oauth/callback") {
|
||||
const OAuthCallback = React.lazy(
|
||||
() => import("./components/OAuthCallback"),
|
||||
);
|
||||
return (
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<OAuthCallback />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
const [resources, setResources] = useState<Resource[]>([]);
|
||||
const [resourceTemplates, setResourceTemplates] = useState<
|
||||
ResourceTemplate[]
|
||||
@@ -96,12 +86,14 @@ const App = () => {
|
||||
return localStorage.getItem("lastArgs") || "";
|
||||
});
|
||||
|
||||
const [sseUrl, setSseUrl] = useState<string>("http://localhost:3001/sse");
|
||||
const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio");
|
||||
const [requestHistory, setRequestHistory] = useState<
|
||||
{ request: string; response?: string }[]
|
||||
>([]);
|
||||
const [mcpClient, setMcpClient] = useState<Client | null>(null);
|
||||
const [sseUrl, setSseUrl] = useState<string>(() => {
|
||||
return localStorage.getItem("lastSseUrl") || "http://localhost:3001/sse";
|
||||
});
|
||||
const [transportType, setTransportType] = useState<"stdio" | "sse">(() => {
|
||||
return (
|
||||
(localStorage.getItem("lastTransportType") as "stdio" | "sse") || "stdio"
|
||||
);
|
||||
});
|
||||
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
|
||||
const [stdErrNotifications, setStdErrNotifications] = useState<
|
||||
StdErrNotification[]
|
||||
@@ -152,49 +144,41 @@ const App = () => {
|
||||
>();
|
||||
const [nextToolCursor, setNextToolCursor] = useState<string | undefined>();
|
||||
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(
|
||||
(e: React.MouseEvent) => {
|
||||
setIsDragging(true);
|
||||
dragStartY.current = e.clientY;
|
||||
dragStartHeight.current = historyPaneHeight;
|
||||
document.body.style.userSelect = "none";
|
||||
const { height: historyPaneHeight, handleDragStart } = useDraggablePane(300);
|
||||
|
||||
const {
|
||||
connectionStatus,
|
||||
serverCapabilities,
|
||||
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],
|
||||
);
|
||||
|
||||
const handleDragMove = useCallback(
|
||||
(e: MouseEvent) => {
|
||||
if (!isDragging) return;
|
||||
const deltaY = dragStartY.current - e.clientY;
|
||||
const newHeight = Math.max(
|
||||
100,
|
||||
Math.min(800, dragStartHeight.current + deltaY),
|
||||
);
|
||||
setHistoryPaneHeight(newHeight);
|
||||
onStdErrNotification: (notification) => {
|
||||
setStdErrNotifications((prev) => [
|
||||
...prev,
|
||||
notification as StdErrNotification,
|
||||
]);
|
||||
},
|
||||
[isDragging],
|
||||
);
|
||||
|
||||
const handleDragEnd = useCallback(() => {
|
||||
setIsDragging(false);
|
||||
document.body.style.userSelect = "";
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
window.addEventListener("mousemove", handleDragMove);
|
||||
window.addEventListener("mouseup", handleDragEnd);
|
||||
return () => {
|
||||
window.removeEventListener("mousemove", handleDragMove);
|
||||
window.removeEventListener("mouseup", handleDragEnd);
|
||||
};
|
||||
}
|
||||
}, [isDragging, handleDragMove, handleDragEnd]);
|
||||
onPendingRequest: (request, resolve, reject) => {
|
||||
setPendingSampleRequests((prev) => [
|
||||
...prev,
|
||||
{ id: nextRequestId.current++, request, resolve, reject },
|
||||
]);
|
||||
},
|
||||
getRoots: () => rootsRef.current,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("lastCommand", command);
|
||||
@@ -204,6 +188,31 @@ const App = () => {
|
||||
localStorage.setItem("lastArgs", 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(() => {
|
||||
fetch(`${PROXY_SERVER_URL}/config`)
|
||||
.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) => {
|
||||
setErrors((prev) => ({ ...prev, [tabKey]: null }));
|
||||
};
|
||||
|
||||
const makeRequest = async <T extends ZodType<object>>(
|
||||
const makeRequest = async <T extends z.ZodType>(
|
||||
request: ClientRequest,
|
||||
schema: T,
|
||||
tabKey?: keyof typeof errors,
|
||||
) => {
|
||||
if (!mcpClient) {
|
||||
throw new Error("MCP client not connected");
|
||||
}
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
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);
|
||||
}
|
||||
|
||||
const response = await makeConnectionRequest(request, schema);
|
||||
if (tabKey !== undefined) {
|
||||
clearError(tabKey);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (e: unknown) {
|
||||
} catch (e) {
|
||||
const errorString = (e as Error).message ?? String(e);
|
||||
if (tabKey === undefined) {
|
||||
toast.error(errorString);
|
||||
} else {
|
||||
if (tabKey !== undefined) {
|
||||
setErrors((prev) => ({
|
||||
...prev,
|
||||
[tabKey]: errorString,
|
||||
}));
|
||||
}
|
||||
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
@@ -317,35 +289,16 @@ const App = () => {
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await mcpClient.complete(request.params, {
|
||||
signal,
|
||||
});
|
||||
pushHistory(request, response);
|
||||
|
||||
return response?.completion.values || [];
|
||||
const result = await makeRequest(request, CompleteResultSchema);
|
||||
return result.completion.values;
|
||||
} catch (e: unknown) {
|
||||
const errorMessage = e instanceof Error ? e.message : String(e);
|
||||
pushHistory(request, { error: errorMessage });
|
||||
|
||||
toast.error(errorMessage);
|
||||
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 response = await makeRequest(
|
||||
{
|
||||
@@ -448,82 +401,6 @@ const App = () => {
|
||||
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 (
|
||||
<div className="flex h-screen bg-background">
|
||||
<Sidebar
|
||||
|
||||
36
package-lock.json
generated
36
package-lock.json
generated
@@ -1224,6 +1224,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": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
@@ -4138,18 +4150,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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.0.tgz",
|
||||
@@ -6758,9 +6758,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.4.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
|
||||
"integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
|
||||
"version": "5.4.12",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.12.tgz",
|
||||
"integrity": "sha512-KwUaKB27TvWwDJr1GjjWthLMATbGEbeWYZIbGZ5qFIsgPP3vWzLu4cVooqhm5/Z2SPDUMjyPVjTztm5tYKwQxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -7406,9 +7406,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.24.2",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
|
||||
"integrity": "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==",
|
||||
"version": "3.24.1",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.1.tgz",
|
||||
"integrity": "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
|
||||
Reference in New Issue
Block a user