Merge branch 'main' into bugfix/issue-114
This commit is contained in:
@@ -1,37 +1,26 @@
|
|||||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
import { useDraggablePane } from "./lib/hooks/useDraggablePane";
|
||||||
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
import { useConnection } from "./lib/hooks/useConnection";
|
||||||
import {
|
import {
|
||||||
ClientNotification,
|
|
||||||
ClientRequest,
|
ClientRequest,
|
||||||
CompatibilityCallToolResult,
|
CompatibilityCallToolResult,
|
||||||
CompatibilityCallToolResultSchema,
|
CompatibilityCallToolResultSchema,
|
||||||
CreateMessageRequestSchema,
|
|
||||||
CreateMessageResult,
|
CreateMessageResult,
|
||||||
EmptyResultSchema,
|
EmptyResultSchema,
|
||||||
GetPromptResultSchema,
|
GetPromptResultSchema,
|
||||||
ListPromptsResultSchema,
|
ListPromptsResultSchema,
|
||||||
ListResourcesResultSchema,
|
ListResourcesResultSchema,
|
||||||
ListResourceTemplatesResultSchema,
|
ListResourceTemplatesResultSchema,
|
||||||
ListRootsRequestSchema,
|
|
||||||
ListToolsResultSchema,
|
|
||||||
ProgressNotificationSchema,
|
|
||||||
ReadResourceResultSchema,
|
ReadResourceResultSchema,
|
||||||
Request,
|
ListToolsResultSchema,
|
||||||
Resource,
|
Resource,
|
||||||
ResourceTemplate,
|
ResourceTemplate,
|
||||||
Root,
|
Root,
|
||||||
ServerNotification,
|
ServerNotification,
|
||||||
Tool,
|
Tool
|
||||||
ServerCapabilitiesSchema,
|
|
||||||
Result,
|
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
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 {
|
||||||
@@ -43,8 +32,7 @@ import {
|
|||||||
MessageSquare,
|
MessageSquare,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { toast } from "react-toastify";
|
import { z } from "zod";
|
||||||
import { z, type ZodType } 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";
|
||||||
@@ -56,21 +44,11 @@ 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<
|
|
||||||
"disconnected" | "connected" | "error"
|
|
||||||
>("disconnected");
|
|
||||||
const [serverCapabilities, setServerCapabilities] = useState<ServerCapabilities | null>(null);
|
|
||||||
const [resources, setResources] = useState<Resource[]>([]);
|
const [resources, setResources] = useState<Resource[]>([]);
|
||||||
const [resourceTemplates, setResourceTemplates] = useState<
|
const [resourceTemplates, setResourceTemplates] = useState<
|
||||||
ResourceTemplate[]
|
ResourceTemplate[]
|
||||||
@@ -95,10 +73,6 @@ const App = () => {
|
|||||||
|
|
||||||
const [sseUrl, setSseUrl] = useState<string>("http://localhost:3001/sse");
|
const [sseUrl, setSseUrl] = useState<string>("http://localhost:3001/sse");
|
||||||
const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio");
|
const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio");
|
||||||
const [requestHistory, setRequestHistory] = useState<
|
|
||||||
{ request: string; response?: string }[]
|
|
||||||
>([]);
|
|
||||||
const [mcpClient, setMcpClient] = useState<Client | null>(null);
|
|
||||||
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
|
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
|
||||||
const [stdErrNotifications, setStdErrNotifications] = useState<
|
const [stdErrNotifications, setStdErrNotifications] = useState<
|
||||||
StdErrNotification[]
|
StdErrNotification[]
|
||||||
@@ -149,49 +123,64 @@ 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 {
|
||||||
(e: React.MouseEvent) => {
|
height: historyPaneHeight,
|
||||||
setIsDragging(true);
|
handleDragStart
|
||||||
dragStartY.current = e.clientY;
|
} = useDraggablePane(300);
|
||||||
dragStartHeight.current = historyPaneHeight;
|
|
||||||
document.body.style.userSelect = "none";
|
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],
|
onStdErrNotification: (notification) => {
|
||||||
);
|
setStdErrNotifications(prev => [...prev, notification as StdErrNotification]);
|
||||||
|
|
||||||
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);
|
|
||||||
},
|
},
|
||||||
[isDragging],
|
onPendingRequest: (request, resolve, reject) => {
|
||||||
);
|
setPendingSampleRequests(prev => [
|
||||||
|
...prev,
|
||||||
|
{ id: nextRequestId.current++, request, resolve, reject }
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
getRoots: () => rootsRef.current
|
||||||
|
});
|
||||||
|
|
||||||
const handleDragEnd = useCallback(() => {
|
const makeRequest = async <T extends z.ZodType>(
|
||||||
setIsDragging(false);
|
request: ClientRequest,
|
||||||
document.body.style.userSelect = "";
|
schema: T,
|
||||||
}, []);
|
tabKey?: keyof typeof errors,
|
||||||
|
) => {
|
||||||
useEffect(() => {
|
try {
|
||||||
if (isDragging) {
|
const response = await makeConnectionRequest(request, schema);
|
||||||
window.addEventListener("mousemove", handleDragMove);
|
if (tabKey !== undefined) {
|
||||||
window.addEventListener("mouseup", handleDragEnd);
|
clearError(tabKey);
|
||||||
return () => {
|
}
|
||||||
window.removeEventListener("mousemove", handleDragMove);
|
return response;
|
||||||
window.removeEventListener("mouseup", handleDragEnd);
|
} catch (e) {
|
||||||
};
|
const errorString = (e as Error).message ?? String(e);
|
||||||
|
if (tabKey !== undefined) {
|
||||||
|
setErrors((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[tabKey]: errorString,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
}
|
}
|
||||||
}, [isDragging, handleDragMove, handleDragEnd]);
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem("lastCommand", command);
|
localStorage.setItem("lastCommand", command);
|
||||||
@@ -228,83 +217,10 @@ 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>>(
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (tabKey !== undefined) {
|
|
||||||
clearError(tabKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
} catch (e: unknown) {
|
|
||||||
const errorString = (e as Error).message ?? String(e);
|
|
||||||
if (tabKey === undefined) {
|
|
||||||
toast.error(errorString);
|
|
||||||
} else {
|
|
||||||
setErrors((prev) => ({
|
|
||||||
...prev,
|
|
||||||
[tabKey]: errorString,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
||||||
{
|
{
|
||||||
@@ -407,82 +323,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
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Play, ChevronDown, ChevronRight, CircleHelp, Bug } from "lucide-react";
|
import { Play, ChevronDown, ChevronRight, CircleHelp, Bug, Github } from "lucide-react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import {
|
import {
|
||||||
@@ -86,6 +86,7 @@ const Sidebar = ({
|
|||||||
placeholder="Command"
|
placeholder="Command"
|
||||||
value={command}
|
value={command}
|
||||||
onChange={(e) => setCommand(e.target.value)}
|
onChange={(e) => setCommand(e.target.value)}
|
||||||
|
className="font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -94,6 +95,7 @@ const Sidebar = ({
|
|||||||
placeholder="Arguments (space-separated)"
|
placeholder="Arguments (space-separated)"
|
||||||
value={args}
|
value={args}
|
||||||
onChange={(e) => setArgs(e.target.value)}
|
onChange={(e) => setArgs(e.target.value)}
|
||||||
|
className="font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -104,6 +106,7 @@ const Sidebar = ({
|
|||||||
placeholder="URL"
|
placeholder="URL"
|
||||||
value={sseUrl}
|
value={sseUrl}
|
||||||
onChange={(e) => setSseUrl(e.target.value)}
|
onChange={(e) => setSseUrl(e.target.value)}
|
||||||
|
className="font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -135,6 +138,7 @@ const Sidebar = ({
|
|||||||
newEnv[e.target.value] = value;
|
newEnv[e.target.value] = value;
|
||||||
setEnv(newEnv);
|
setEnv(newEnv);
|
||||||
}}
|
}}
|
||||||
|
className="font-mono"
|
||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Value"
|
placeholder="Value"
|
||||||
@@ -144,6 +148,7 @@ const Sidebar = ({
|
|||||||
newEnv[key] = e.target.value;
|
newEnv[key] = e.target.value;
|
||||||
setEnv(newEnv);
|
setEnv(newEnv);
|
||||||
}}
|
}}
|
||||||
|
className="font-mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
@@ -227,7 +232,7 @@ const Sidebar = ({
|
|||||||
setTheme(value as "system" | "light" | "dark")
|
setTheme(value as "system" | "light" | "dark")
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger className="w-[120px]" id="theme-select">
|
<SelectTrigger className="w-[100px]" id="theme-select">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -248,6 +253,11 @@ const Sidebar = ({
|
|||||||
<Bug className="w-4 h-4 text-gray-800" />
|
<Bug className="w-4 h-4 text-gray-800" />
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="https://github.com/modelcontextprotocol/inspector" target="_blank" rel="noopener noreferrer">
|
||||||
|
<Button variant="ghost" title="Report bugs or contribute on GitHub">
|
||||||
|
<Github className="w-4 h-4 text-gray-800" />
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
188
client/src/lib/hooks/useConnection.ts
Normal file
188
client/src/lib/hooks/useConnection.ts
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||||
|
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
|
||||||
|
import {
|
||||||
|
ClientNotification,
|
||||||
|
ClientRequest,
|
||||||
|
CreateMessageRequestSchema,
|
||||||
|
ListRootsRequestSchema,
|
||||||
|
ProgressNotificationSchema,
|
||||||
|
Request,
|
||||||
|
Result,
|
||||||
|
ServerCapabilities,
|
||||||
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { toast } from "react-toastify";
|
||||||
|
import { Notification, StdErrNotificationSchema } from "../notificationTypes";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000;
|
||||||
|
|
||||||
|
interface UseConnectionOptions {
|
||||||
|
transportType: "stdio" | "sse";
|
||||||
|
command: string;
|
||||||
|
args: string;
|
||||||
|
sseUrl: string;
|
||||||
|
env: Record<string, string>;
|
||||||
|
proxyServerUrl: string;
|
||||||
|
requestTimeout?: number;
|
||||||
|
onNotification?: (notification: Notification) => void;
|
||||||
|
onStdErrNotification?: (notification: Notification) => void;
|
||||||
|
onPendingRequest?: (request: any, resolve: any, reject: any) => void;
|
||||||
|
getRoots?: () => any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useConnection({
|
||||||
|
transportType,
|
||||||
|
command,
|
||||||
|
args,
|
||||||
|
sseUrl,
|
||||||
|
env,
|
||||||
|
proxyServerUrl,
|
||||||
|
requestTimeout = DEFAULT_REQUEST_TIMEOUT_MSEC,
|
||||||
|
onNotification,
|
||||||
|
onStdErrNotification,
|
||||||
|
onPendingRequest,
|
||||||
|
getRoots,
|
||||||
|
}: UseConnectionOptions) {
|
||||||
|
const [connectionStatus, setConnectionStatus] = useState<"disconnected" | "connected" | "error">("disconnected");
|
||||||
|
const [serverCapabilities, setServerCapabilities] = useState<ServerCapabilities | null>(null);
|
||||||
|
const [mcpClient, setMcpClient] = useState<Client | null>(null);
|
||||||
|
const [requestHistory, setRequestHistory] = useState<{ request: string; response?: string }[]>([]);
|
||||||
|
|
||||||
|
const pushHistory = (request: object, response?: object) => {
|
||||||
|
setRequestHistory((prev) => [
|
||||||
|
...prev,
|
||||||
|
{
|
||||||
|
request: JSON.stringify(request),
|
||||||
|
response: response !== undefined ? JSON.stringify(response) : undefined,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const makeRequest = async <T extends z.ZodType>(
|
||||||
|
request: ClientRequest,
|
||||||
|
schema: T
|
||||||
|
) => {
|
||||||
|
if (!mcpClient) {
|
||||||
|
throw new Error("MCP client not connected");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const abortController = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
abortController.abort("Request timed out");
|
||||||
|
}, requestTimeout);
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (e: unknown) {
|
||||||
|
const errorString = (e as Error).message ?? String(e);
|
||||||
|
toast.error(errorString);
|
||||||
|
|
||||||
|
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 connect = async () => {
|
||||||
|
try {
|
||||||
|
const client = new Client<Request, Notification, Result>(
|
||||||
|
{
|
||||||
|
name: "mcp-inspector",
|
||||||
|
version: "0.0.1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
capabilities: {
|
||||||
|
sampling: {},
|
||||||
|
roots: {
|
||||||
|
listChanged: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const backendUrl = new URL(`${proxyServerUrl}/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);
|
||||||
|
|
||||||
|
if (onNotification) {
|
||||||
|
client.setNotificationHandler(ProgressNotificationSchema, onNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onStdErrNotification) {
|
||||||
|
client.setNotificationHandler(StdErrNotificationSchema, onStdErrNotification);
|
||||||
|
}
|
||||||
|
|
||||||
|
await client.connect(clientTransport);
|
||||||
|
|
||||||
|
const capabilities = client.getServerCapabilities();
|
||||||
|
setServerCapabilities(capabilities ?? null);
|
||||||
|
|
||||||
|
if (onPendingRequest) {
|
||||||
|
client.setRequestHandler(CreateMessageRequestSchema, (request) => {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
onPendingRequest(request, resolve, reject);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (getRoots) {
|
||||||
|
client.setRequestHandler(ListRootsRequestSchema, async () => {
|
||||||
|
return { roots: getRoots() };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setMcpClient(client);
|
||||||
|
setConnectionStatus("connected");
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
setConnectionStatus("error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
connectionStatus,
|
||||||
|
serverCapabilities,
|
||||||
|
mcpClient,
|
||||||
|
requestHistory,
|
||||||
|
makeRequest,
|
||||||
|
sendNotification,
|
||||||
|
connect
|
||||||
|
};
|
||||||
|
}
|
||||||
44
client/src/lib/hooks/useDraggablePane.ts
Normal file
44
client/src/lib/hooks/useDraggablePane.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
|
export function useDraggablePane(initialHeight: number) {
|
||||||
|
const [height, setHeight] = useState(initialHeight);
|
||||||
|
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 = height;
|
||||||
|
document.body.style.userSelect = "none";
|
||||||
|
}, [height]);
|
||||||
|
|
||||||
|
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));
|
||||||
|
setHeight(newHeight);
|
||||||
|
}, [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]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
height,
|
||||||
|
isDragging,
|
||||||
|
handleDragStart
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user