Merge branch 'main' into feature/audio-rendering

This commit is contained in:
Jerome
2025-01-28 02:15:55 -05:00
committed by GitHub
14 changed files with 688 additions and 358 deletions

View File

@@ -1,36 +1,26 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { useDraggablePane } from "./lib/hooks/useDraggablePane";
import { useConnection } from "./lib/hooks/useConnection";
import {
ClientNotification,
ClientRequest,
CompatibilityCallToolResult,
CompatibilityCallToolResultSchema,
CreateMessageRequestSchema,
CreateMessageResult,
EmptyResultSchema,
GetPromptResultSchema,
ListPromptsResultSchema,
ListResourcesResultSchema,
ListResourceTemplatesResultSchema,
ListRootsRequestSchema,
ListToolsResultSchema,
ProgressNotificationSchema,
ReadResourceResultSchema,
Request,
ListToolsResultSchema,
Resource,
ResourceTemplate,
Result,
Root,
ServerNotification,
Tool,
} from "@modelcontextprotocol/sdk/types.js";
import { useCallback, useEffect, useRef, useState } from "react";
import { useEffect, useRef, useState } from "react";
import {
Notification,
StdErrNotification,
StdErrNotificationSchema,
} from "./lib/notificationTypes";
import { StdErrNotification } from "./lib/notificationTypes";
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
@@ -42,8 +32,7 @@ import {
MessageSquare,
} from "lucide-react";
import { toast } from "react-toastify";
import { ZodType } from "zod";
import { z } from "zod";
import "./App.css";
import ConsoleTab from "./components/ConsoleTab";
import HistoryAndNotifications from "./components/History";
@@ -55,17 +44,11 @@ import SamplingTab, { PendingRequest } from "./components/SamplingTab";
import Sidebar from "./components/Sidebar";
import ToolsTab from "./components/ToolsTab";
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 [resources, setResources] = useState<Resource[]>([]);
const [resourceTemplates, setResourceTemplates] = useState<
ResourceTemplate[]
@@ -90,10 +73,6 @@ const App = () => {
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 [notifications, setNotifications] = useState<ServerNotification[]>([]);
const [stdErrNotifications, setStdErrNotifications] = useState<
StdErrNotification[]
@@ -144,49 +123,64 @@ 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],
);
onPendingRequest: (request, resolve, reject) => {
setPendingSampleRequests((prev) => [
...prev,
{ id: nextRequestId.current++, request, resolve, reject },
]);
},
getRoots: () => rootsRef.current,
});
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);
};
const makeRequest = async <T extends z.ZodType>(
request: ClientRequest,
schema: T,
tabKey?: keyof typeof errors,
) => {
try {
const response = await makeConnectionRequest(request, schema);
if (tabKey !== undefined) {
clearError(tabKey);
}
return response;
} catch (e) {
const errorString = (e as Error).message ?? String(e);
if (tabKey !== undefined) {
setErrors((prev) => ({
...prev,
[tabKey]: errorString,
}));
}
throw e;
}
}, [isDragging, handleDragMove, handleDragEnd]);
};
useEffect(() => {
localStorage.setItem("lastCommand", command);
@@ -217,79 +211,16 @@ const App = () => {
rootsRef.current = roots;
}, [roots]);
const pushHistory = (request: object, response?: object) => {
setRequestHistory((prev) => [
...prev,
{
request: JSON.stringify(request),
response: response !== undefined ? JSON.stringify(response) : undefined,
},
]);
};
useEffect(() => {
if (!window.location.hash) {
window.location.hash = "resources";
}
}, []);
const clearError = (tabKey: keyof typeof errors) => {
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,
});
} finally {
clearTimeout(timeoutId);
}
pushHistory(request, response);
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 response = await makeRequest(
{
@@ -392,79 +323,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);
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
@@ -485,17 +343,42 @@ const App = () => {
<div className="flex-1 flex flex-col overflow-hidden">
<div className="flex-1 overflow-auto">
{mcpClient ? (
<Tabs defaultValue="resources" className="w-full p-4">
<Tabs
defaultValue={
Object.keys(serverCapabilities ?? {}).includes(
window.location.hash.slice(1),
)
? window.location.hash.slice(1)
: serverCapabilities?.resources
? "resources"
: serverCapabilities?.prompts
? "prompts"
: serverCapabilities?.tools
? "tools"
: "ping"
}
className="w-full p-4"
onValueChange={(value) => (window.location.hash = value)}
>
<TabsList className="mb-4 p-0">
<TabsTrigger value="resources">
<TabsTrigger
value="resources"
disabled={!serverCapabilities?.resources}
>
<Files className="w-4 h-4 mr-2" />
Resources
</TabsTrigger>
<TabsTrigger value="prompts">
<TabsTrigger
value="prompts"
disabled={!serverCapabilities?.prompts}
>
<MessageSquare className="w-4 h-4 mr-2" />
Prompts
</TabsTrigger>
<TabsTrigger value="tools">
<TabsTrigger
value="tools"
disabled={!serverCapabilities?.tools}
>
<Hammer className="w-4 h-4 mr-2" />
Tools
</TabsTrigger>
@@ -519,107 +402,119 @@ const App = () => {
</TabsList>
<div className="w-full">
<ResourcesTab
resources={resources}
resourceTemplates={resourceTemplates}
listResources={() => {
clearError("resources");
listResources();
}}
clearResources={() => {
setResources([]);
setNextResourceCursor(undefined);
}}
listResourceTemplates={() => {
clearError("resources");
listResourceTemplates();
}}
clearResourceTemplates={() => {
setResourceTemplates([]);
setNextResourceTemplateCursor(undefined);
}}
readResource={(uri) => {
clearError("resources");
readResource(uri);
}}
selectedResource={selectedResource}
setSelectedResource={(resource) => {
clearError("resources");
setSelectedResource(resource);
}}
resourceContent={resourceContent}
nextCursor={nextResourceCursor}
nextTemplateCursor={nextResourceTemplateCursor}
error={errors.resources}
/>
<PromptsTab
prompts={prompts}
listPrompts={() => {
clearError("prompts");
listPrompts();
}}
clearPrompts={() => {
setPrompts([]);
setNextPromptCursor(undefined);
}}
getPrompt={(name, args) => {
clearError("prompts");
getPrompt(name, args);
}}
selectedPrompt={selectedPrompt}
setSelectedPrompt={(prompt) => {
clearError("prompts");
setSelectedPrompt(prompt);
}}
promptContent={promptContent}
nextCursor={nextPromptCursor}
error={errors.prompts}
/>
<ToolsTab
tools={tools}
listTools={() => {
clearError("tools");
listTools();
}}
clearTools={() => {
setTools([]);
setNextToolCursor(undefined);
}}
callTool={(name, params) => {
clearError("tools");
callTool(name, params);
}}
selectedTool={selectedTool}
setSelectedTool={(tool) => {
clearError("tools");
setSelectedTool(tool);
setToolResult(null);
}}
toolResult={toolResult}
nextCursor={nextToolCursor}
error={errors.tools}
/>
<ConsoleTab />
<PingTab
onPingClick={() => {
void makeRequest(
{
method: "ping" as const,
},
EmptyResultSchema,
);
}}
/>
<SamplingTab
pendingRequests={pendingSampleRequests}
onApprove={handleApproveSampling}
onReject={handleRejectSampling}
/>
<RootsTab
roots={roots}
setRoots={setRoots}
onRootsChange={handleRootsChange}
/>
{!serverCapabilities?.resources &&
!serverCapabilities?.prompts &&
!serverCapabilities?.tools ? (
<div className="flex items-center justify-center p-4">
<p className="text-lg text-gray-500">
The connected server does not support any MCP capabilities
</p>
</div>
) : (
<>
<ResourcesTab
resources={resources}
resourceTemplates={resourceTemplates}
listResources={() => {
clearError("resources");
listResources();
}}
clearResources={() => {
setResources([]);
setNextResourceCursor(undefined);
}}
listResourceTemplates={() => {
clearError("resources");
listResourceTemplates();
}}
clearResourceTemplates={() => {
setResourceTemplates([]);
setNextResourceTemplateCursor(undefined);
}}
readResource={(uri) => {
clearError("resources");
readResource(uri);
}}
selectedResource={selectedResource}
setSelectedResource={(resource) => {
clearError("resources");
setSelectedResource(resource);
}}
resourceContent={resourceContent}
nextCursor={nextResourceCursor}
nextTemplateCursor={nextResourceTemplateCursor}
error={errors.resources}
/>
<PromptsTab
prompts={prompts}
listPrompts={() => {
clearError("prompts");
listPrompts();
}}
clearPrompts={() => {
setPrompts([]);
setNextPromptCursor(undefined);
}}
getPrompt={(name, args) => {
clearError("prompts");
getPrompt(name, args);
}}
selectedPrompt={selectedPrompt}
setSelectedPrompt={(prompt) => {
clearError("prompts");
setSelectedPrompt(prompt);
}}
promptContent={promptContent}
nextCursor={nextPromptCursor}
error={errors.prompts}
/>
<ToolsTab
tools={tools}
listTools={() => {
clearError("tools");
listTools();
}}
clearTools={() => {
setTools([]);
setNextToolCursor(undefined);
}}
callTool={(name, params) => {
clearError("tools");
callTool(name, params);
}}
selectedTool={selectedTool}
setSelectedTool={(tool) => {
clearError("tools");
setSelectedTool(tool);
setToolResult(null);
}}
toolResult={toolResult}
nextCursor={nextToolCursor}
error={errors.tools}
/>
<ConsoleTab />
<PingTab
onPingClick={() => {
void makeRequest(
{
method: "ping" as const,
},
EmptyResultSchema,
);
}}
/>
<SamplingTab
pendingRequests={pendingSampleRequests}
onApprove={handleApproveSampling}
onReject={handleRejectSampling}
/>
<RootsTab
roots={roots}
setRoots={setRoots}
onRootsChange={handleRootsChange}
/>
</>
)}
</div>
</Tabs>
) : (

View File

@@ -1,5 +1,14 @@
import { useState } from "react";
import { Play, ChevronDown, ChevronRight } from "lucide-react";
import {
Play,
ChevronDown,
ChevronRight,
CircleHelp,
Bug,
Github,
Eye,
EyeOff,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
@@ -47,6 +56,7 @@ const Sidebar = ({
}: SidebarProps) => {
const [theme, setTheme] = useTheme();
const [showEnvVars, setShowEnvVars] = useState(false);
const [shownEnvVars, setShownEnvVars] = useState<Set<string>>(new Set());
return (
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
@@ -86,6 +96,7 @@ const Sidebar = ({
placeholder="Command"
value={command}
onChange={(e) => setCommand(e.target.value)}
className="font-mono"
/>
</div>
<div className="space-y-2">
@@ -94,6 +105,7 @@ const Sidebar = ({
placeholder="Arguments (space-separated)"
value={args}
onChange={(e) => setArgs(e.target.value)}
className="font-mono"
/>
</div>
</>
@@ -104,6 +116,7 @@ const Sidebar = ({
placeholder="URL"
value={sseUrl}
onChange={(e) => setSseUrl(e.target.value)}
className="font-mono"
/>
</div>
)}
@@ -124,19 +137,44 @@ const Sidebar = ({
{showEnvVars && (
<div className="space-y-2">
{Object.entries(env).map(([key, value], idx) => (
<div key={idx} className="grid grid-cols-[1fr,auto] gap-2">
<div className="space-y-1">
<div key={idx} className="space-y-2 pb-4">
<div className="flex gap-2">
<Input
placeholder="Key"
value={key}
onChange={(e) => {
const newKey = e.target.value;
const newEnv = { ...env };
delete newEnv[key];
newEnv[e.target.value] = value;
newEnv[newKey] = value;
setEnv(newEnv);
setShownEnvVars((prev) => {
const next = new Set(prev);
if (next.has(key)) {
next.delete(key);
next.add(newKey);
}
return next;
});
}}
className="font-mono"
/>
<Button
variant="destructive"
size="icon"
className="h-9 w-9 p-0 shrink-0"
onClick={() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [key]: _removed, ...rest } = env;
setEnv(rest);
}}
>
×
</Button>
</div>
<div className="flex gap-2">
<Input
type={shownEnvVars.has(key) ? "text" : "password"}
placeholder="Value"
value={value}
onChange={(e) => {
@@ -144,25 +182,47 @@ const Sidebar = ({
newEnv[key] = e.target.value;
setEnv(newEnv);
}}
className="font-mono"
/>
<Button
variant="outline"
size="icon"
className="h-9 w-9 p-0 shrink-0"
onClick={() => {
setShownEnvVars((prev) => {
const next = new Set(prev);
if (next.has(key)) {
next.delete(key);
} else {
next.add(key);
}
return next;
});
}}
aria-label={
shownEnvVars.has(key) ? "Hide value" : "Show value"
}
aria-pressed={shownEnvVars.has(key)}
title={
shownEnvVars.has(key) ? "Hide value" : "Show value"
}
>
{shownEnvVars.has(key) ? (
<Eye className="h-4 w-4" aria-hidden="true" />
) : (
<EyeOff className="h-4 w-4" aria-hidden="true" />
)}
</Button>
</div>
<Button
variant="destructive"
onClick={() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { [key]: removed, ...rest } = env;
setEnv(rest);
}}
>
Remove
</Button>
</div>
))}
<Button
variant="outline"
className="w-full mt-2"
onClick={() => {
const key = "";
const newEnv = { ...env };
newEnv[""] = "";
newEnv[key] = "";
setEnv(newEnv);
}}
>
@@ -220,14 +280,14 @@ const Sidebar = ({
</div>
</div>
<div className="p-4 border-t">
<div className="flex items-center space-x-2">
<div className="flex items-center justify-between">
<Select
value={theme}
onValueChange={(value: string) =>
setTheme(value as "system" | "light" | "dark")
}
>
<SelectTrigger className="w-[120px]" id="theme-select">
<SelectTrigger className="w-[100px]" id="theme-select">
<SelectValue />
</SelectTrigger>
<SelectContent>
@@ -236,6 +296,39 @@ const Sidebar = ({
<SelectItem value="dark">Dark</SelectItem>
</SelectContent>
</Select>
<div className="flex items-center space-x-2">
<a
href="https://modelcontextprotocol.io/docs/tools/inspector"
target="_blank"
rel="noopener noreferrer"
>
<Button variant="ghost" title="Inspector Documentation">
<CircleHelp className="w-4 h-4 text-gray-800" />
</Button>
</a>
<a
href="https://modelcontextprotocol.io/docs/tools/debugging"
target="_blank"
rel="noopener noreferrer"
>
<Button variant="ghost" title="Debugging Guide">
<Bug className="w-4 h-4 text-gray-800" />
</Button>
</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>

View File

@@ -10,7 +10,7 @@ import {
CallToolResultSchema,
} from "@modelcontextprotocol/sdk/types.js";
import { AlertCircle, Send } from "lucide-react";
import { useState } from "react";
import { useEffect, useState } from "react";
import ListPane from "./ListPane";
import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js";
@@ -31,12 +31,15 @@ const ToolsTab = ({
clearTools: () => void;
callTool: (name: string, params: Record<string, unknown>) => void;
selectedTool: Tool | null;
setSelectedTool: (tool: Tool) => void;
setSelectedTool: (tool: Tool | null) => void;
toolResult: CompatibilityCallToolResult | null;
nextCursor: ListToolsResult["nextCursor"];
error: string | null;
}) => {
const [params, setParams] = useState<Record<string, unknown>>({});
useEffect(() => {
setParams({});
}, [selectedTool]);
const renderToolResult = () => {
if (!toolResult) return null;
@@ -107,7 +110,7 @@ const ToolsTab = ({
return (
<>
<h4 className="font-semibold mb-2">Tool Result (Legacy):</h4>
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-64">
<pre className="bg-gray-50 dark:bg-gray-800 dark:text-gray-100 p-4 rounded text-sm overflow-auto max-h-64">
{JSON.stringify(toolResult.toolResult, null, 2)}
</pre>
</>
@@ -120,7 +123,10 @@ const ToolsTab = ({
<ListPane
items={tools}
listItems={listTools}
clearItems={clearTools}
clearItems={() => {
clearTools();
setSelectedTool(null);
}}
setSelectedItem={setSelectedTool}
renderItem={(tool) => (
<>
@@ -178,6 +184,30 @@ const ToolsTab = ({
}
className="mt-1"
/>
) : /* @ts-expect-error value type is currently unknown */
value.type === "object" ? (
<Textarea
id={key}
name={key}
// @ts-expect-error value type is currently unknown
placeholder={value.description}
onChange={(e) => {
try {
const parsed = JSON.parse(e.target.value);
setParams({
...params,
[key]: parsed,
});
} catch (err) {
// If invalid JSON, store as string - will be validated on submit
setParams({
...params,
[key]: e.target.value,
});
}
}}
className="mt-1"
/>
) : (
<Input
// @ts-expect-error value type is currently unknown

View File

@@ -0,0 +1,199 @@
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,
};
}

View File

@@ -0,0 +1,53 @@
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,
};
}

View File

@@ -1,7 +1,7 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { ToastContainer } from "react-toastify";
import "react-toastify/dist/ReactToastify.css";
import App from "./App.tsx";
import "./index.css";

View File

@@ -14,8 +14,8 @@ export default defineConfig({
minify: false,
rollupOptions: {
output: {
manualChunks: undefined
}
}
}
manualChunks: undefined,
},
},
},
});