Merge branch 'main' into justin/tab-specific-errors
This commit is contained in:
@@ -22,34 +22,22 @@ import {
|
||||
ServerNotification,
|
||||
Tool,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
// Add dark mode class based on system preference
|
||||
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
document.documentElement.classList.add("dark");
|
||||
}
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import {
|
||||
Bell,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Files,
|
||||
FolderTree,
|
||||
Hammer,
|
||||
Hash,
|
||||
MessageSquare,
|
||||
Play,
|
||||
Send,
|
||||
Terminal,
|
||||
Terminal
|
||||
} from "lucide-react";
|
||||
|
||||
import { toast } from "react-toastify";
|
||||
@@ -103,7 +91,6 @@ const App = () => {
|
||||
const [notifications, setNotifications] = useState<ServerNotification[]>([]);
|
||||
const [roots, setRoots] = useState<Root[]>([]);
|
||||
const [env, setEnv] = useState<Record<string, string>>({});
|
||||
const [showEnvVars, setShowEnvVars] = useState(false);
|
||||
|
||||
const [pendingSampleRequests, setPendingSampleRequests] = useState<
|
||||
Array<
|
||||
@@ -148,6 +135,49 @@ 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";
|
||||
},
|
||||
[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);
|
||||
},
|
||||
[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]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem("lastCommand", command);
|
||||
@@ -379,258 +409,180 @@ const App = () => {
|
||||
|
||||
return (
|
||||
<div className="flex h-screen bg-background">
|
||||
<Sidebar connectionStatus={connectionStatus} />
|
||||
<Sidebar
|
||||
connectionStatus={connectionStatus}
|
||||
transportType={transportType}
|
||||
setTransportType={setTransportType}
|
||||
command={command}
|
||||
setCommand={setCommand}
|
||||
args={args}
|
||||
setArgs={setArgs}
|
||||
url={url}
|
||||
setUrl={setUrl}
|
||||
env={env}
|
||||
setEnv={setEnv}
|
||||
onConnect={connectMcpServer}
|
||||
/>
|
||||
<div className="flex-1 flex flex-col overflow-hidden">
|
||||
<h1 className="text-2xl font-bold p-4">MCP Inspector</h1>
|
||||
<div className="flex-1 overflow-auto flex">
|
||||
<div className="flex-1">
|
||||
<div className="p-4 bg-card shadow-md m-4 rounded-md">
|
||||
<h2 className="text-lg font-semibold mb-2">Connect MCP Server</h2>
|
||||
<div className="flex space-x-2 mb-2">
|
||||
<Select
|
||||
value={transportType}
|
||||
onValueChange={(value: "stdio" | "sse") =>
|
||||
setTransportType(value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Select transport type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="stdio">STDIO</SelectItem>
|
||||
<SelectItem value="sse">SSE</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{transportType === "stdio" ? (
|
||||
<>
|
||||
<Input
|
||||
placeholder="Command"
|
||||
value={command}
|
||||
onChange={(e) => setCommand(e.target.value)}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Arguments (space-separated)"
|
||||
value={args}
|
||||
onChange={(e) => setArgs(e.target.value)}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<Input
|
||||
placeholder="URL"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
)}
|
||||
<Button onClick={connectMcpServer}>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
Connect
|
||||
</Button>
|
||||
</div>
|
||||
{transportType === "stdio" && (
|
||||
<div className="mt-4">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowEnvVars(!showEnvVars)}
|
||||
className="flex items-center"
|
||||
>
|
||||
{showEnvVars ? (
|
||||
<ChevronDown className="w-4 h-4 mr-2" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
Environment Variables
|
||||
</Button>
|
||||
{showEnvVars && (
|
||||
<div className="mt-2">
|
||||
{Object.entries(env).map(([key, value]) => (
|
||||
<div key={key} className="flex space-x-2 mb-2">
|
||||
<Input
|
||||
placeholder="Key"
|
||||
value={key}
|
||||
onChange={(e) =>
|
||||
setEnv((prev) => ({
|
||||
...prev,
|
||||
[e.target.value]: value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Value"
|
||||
value={value}
|
||||
onChange={(e) =>
|
||||
setEnv((prev) => ({
|
||||
...prev,
|
||||
[key]: e.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
onClick={() =>
|
||||
setEnv((prev) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { [key]: _, ...rest } = prev;
|
||||
return rest;
|
||||
})
|
||||
}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
onClick={() => setEnv((prev) => ({ ...prev, "": "" }))}
|
||||
>
|
||||
Add Environment Variable
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
{mcpClient ? (
|
||||
<Tabs defaultValue="resources" className="w-full p-4">
|
||||
<TabsList className="mb-4 p-0">
|
||||
<TabsTrigger value="resources">
|
||||
<Files className="w-4 h-4 mr-2" />
|
||||
Resources
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="prompts">
|
||||
<MessageSquare className="w-4 h-4 mr-2" />
|
||||
Prompts
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="requests" disabled>
|
||||
<Send className="w-4 h-4 mr-2" />
|
||||
Requests
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="tools">
|
||||
<Hammer className="w-4 h-4 mr-2" />
|
||||
Tools
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="console" disabled>
|
||||
<Terminal className="w-4 h-4 mr-2" />
|
||||
Console
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="ping">
|
||||
<Bell className="w-4 h-4 mr-2" />
|
||||
Ping
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="sampling" className="relative">
|
||||
<Hash className="w-4 h-4 mr-2" />
|
||||
Sampling
|
||||
{pendingSampleRequests.length > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
|
||||
{pendingSampleRequests.length}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{mcpClient ? (
|
||||
<Tabs defaultValue="resources" className="w-full p-4">
|
||||
<TabsList className="mb-4 p-0">
|
||||
<TabsTrigger value="resources">
|
||||
<Files className="w-4 h-4 mr-2" />
|
||||
Resources
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="prompts">
|
||||
<MessageSquare className="w-4 h-4 mr-2" />
|
||||
Prompts
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="requests" disabled>
|
||||
<Send className="w-4 h-4 mr-2" />
|
||||
Requests
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="tools">
|
||||
<Hammer className="w-4 h-4 mr-2" />
|
||||
Tools
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="console" disabled>
|
||||
<Terminal className="w-4 h-4 mr-2" />
|
||||
Console
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="ping">
|
||||
<Bell className="w-4 h-4 mr-2" />
|
||||
Ping
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="sampling" className="relative">
|
||||
<Hash className="w-4 h-4 mr-2" />
|
||||
Sampling
|
||||
{pendingSampleRequests.length > 0 && (
|
||||
<span className="absolute -top-1 -right-1 bg-red-500 text-white text-xs rounded-full h-4 w-4 flex items-center justify-center">
|
||||
{pendingSampleRequests.length}
|
||||
</span>
|
||||
)}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="roots">
|
||||
<FolderTree className="w-4 h-4 mr-2" />
|
||||
Roots
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="roots">
|
||||
<FolderTree className="w-4 h-4 mr-2" />
|
||||
Roots
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="w-full">
|
||||
<ResourcesTab
|
||||
resources={resources}
|
||||
resourceTemplates={resourceTemplates}
|
||||
listResources={() => {
|
||||
clearError("resources");
|
||||
listResources();
|
||||
}}
|
||||
listResourceTemplates={() => {
|
||||
clearError("resources");
|
||||
listResourceTemplates();
|
||||
}}
|
||||
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();
|
||||
}}
|
||||
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();
|
||||
}}
|
||||
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>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<p className="text-lg text-gray-500">
|
||||
Connect to an MCP server to start inspecting
|
||||
</p>
|
||||
<div className="w-full">
|
||||
<ResourcesTab
|
||||
resources={resources}
|
||||
resourceTemplates={resourceTemplates}
|
||||
listResources={() => {
|
||||
clearError("resources");
|
||||
listResources();
|
||||
}}
|
||||
listResourceTemplates={() => {
|
||||
clearError("resources");
|
||||
listResourceTemplates();
|
||||
}}
|
||||
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();
|
||||
}}
|
||||
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();
|
||||
}}
|
||||
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>
|
||||
) : (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<p className="text-lg text-gray-500">
|
||||
Connect to an MCP server to start inspecting
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
className="relative border-t border-border"
|
||||
style={{
|
||||
height: `${historyPaneHeight}px`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="absolute w-full h-4 -top-2 cursor-row-resize flex items-center justify-center hover:bg-accent/50"
|
||||
onMouseDown={handleDragStart}
|
||||
>
|
||||
<div className="w-8 h-1 rounded-full bg-border" />
|
||||
</div>
|
||||
<div className="h-full overflow-auto">
|
||||
<HistoryAndNotifications
|
||||
requestHistory={requestHistory}
|
||||
serverNotifications={notifications}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HistoryAndNotifications
|
||||
requestHistory={requestHistory}
|
||||
serverNotifications={notifications}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -29,8 +29,8 @@ const HistoryAndNotifications = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-64 bg-card shadow-md p-4 overflow-hidden flex flex-col h-full">
|
||||
<div className="flex-1 overflow-y-auto mb-4 border-b pb-4">
|
||||
<div className="bg-card overflow-hidden flex h-full">
|
||||
<div className="flex-1 overflow-y-auto p-4 border-r">
|
||||
<h2 className="text-lg font-semibold mb-4">History</h2>
|
||||
{requestHistory.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 italic">No history yet</p>
|
||||
@@ -107,7 +107,7 @@ const HistoryAndNotifications = ({
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
<div className="flex-1 overflow-y-auto p-4">
|
||||
<h2 className="text-lg font-semibold mb-4">Server Notifications</h2>
|
||||
{serverNotifications.length === 0 ? (
|
||||
<p className="text-sm text-gray-500 italic">No notifications yet</p>
|
||||
|
||||
@@ -1,39 +1,196 @@
|
||||
import { Menu, Settings } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { Play, ChevronDown, ChevronRight, Settings } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/components/ui/select";
|
||||
|
||||
const Sidebar = ({ connectionStatus }: { connectionStatus: string }) => (
|
||||
<div className="w-64 bg-card border-r border-border">
|
||||
<div className="flex items-center p-4 border-b border-gray-200">
|
||||
<Menu className="w-6 h-6 text-gray-500" />
|
||||
<h1 className="ml-2 text-lg font-semibold">MCP Inspector</h1>
|
||||
</div>
|
||||
interface SidebarProps {
|
||||
connectionStatus: "disconnected" | "connected" | "error";
|
||||
transportType: "stdio" | "sse";
|
||||
setTransportType: (type: "stdio" | "sse") => void;
|
||||
command: string;
|
||||
setCommand: (command: string) => void;
|
||||
args: string;
|
||||
setArgs: (args: string) => void;
|
||||
url: string;
|
||||
setUrl: (url: string) => void;
|
||||
env: Record<string, string>;
|
||||
setEnv: (env: Record<string, string>) => void;
|
||||
onConnect: () => void;
|
||||
}
|
||||
|
||||
<div className="p-4">
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
connectionStatus === "connected"
|
||||
? "bg-green-500"
|
||||
: connectionStatus === "error"
|
||||
? "bg-red-500"
|
||||
: "bg-gray-500"
|
||||
}`}
|
||||
/>
|
||||
<span className="text-sm text-gray-600">
|
||||
{connectionStatus === "connected"
|
||||
? "Connected"
|
||||
: connectionStatus === "error"
|
||||
? "Connection Error"
|
||||
: "Disconnected"}
|
||||
</span>
|
||||
const Sidebar = ({
|
||||
connectionStatus,
|
||||
transportType,
|
||||
setTransportType,
|
||||
command,
|
||||
setCommand,
|
||||
args,
|
||||
setArgs,
|
||||
url,
|
||||
setUrl,
|
||||
env,
|
||||
setEnv,
|
||||
onConnect,
|
||||
}: SidebarProps) => {
|
||||
const [showEnvVars, setShowEnvVars] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="w-80 bg-card border-r border-border flex flex-col h-full">
|
||||
<div className="flex items-center p-4 border-b border-gray-200">
|
||||
<Settings className="w-6 h-6 text-gray-500" />
|
||||
<h1 className="ml-2 text-lg font-semibold">MCP Inspector</h1>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" className="w-full justify-start">
|
||||
<Settings className="w-4 h-4 mr-2" />
|
||||
Connection Settings
|
||||
</Button>
|
||||
<div className="p-4 flex-1 overflow-auto">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Transport Type</label>
|
||||
<Select
|
||||
value={transportType}
|
||||
onValueChange={(value: "stdio" | "sse") =>
|
||||
setTransportType(value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="Select transport type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="stdio">STDIO</SelectItem>
|
||||
<SelectItem value="sse">SSE</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{transportType === "stdio" ? (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Command</label>
|
||||
<Input
|
||||
placeholder="Command"
|
||||
value={command}
|
||||
onChange={(e) => setCommand(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Arguments</label>
|
||||
<Input
|
||||
placeholder="Arguments (space-separated)"
|
||||
value={args}
|
||||
onChange={(e) => setArgs(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">URL</label>
|
||||
<Input
|
||||
placeholder="URL"
|
||||
value={url}
|
||||
onChange={(e) => setUrl(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{transportType === "stdio" && (
|
||||
<div className="space-y-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setShowEnvVars(!showEnvVars)}
|
||||
className="flex items-center w-full"
|
||||
>
|
||||
{showEnvVars ? (
|
||||
<ChevronDown className="w-4 h-4 mr-2" />
|
||||
) : (
|
||||
<ChevronRight className="w-4 h-4 mr-2" />
|
||||
)}
|
||||
Environment Variables
|
||||
</Button>
|
||||
{showEnvVars && (
|
||||
<div className="space-y-2">
|
||||
{Object.entries(env).map(([key, value]) => (
|
||||
<div key={key} className="grid grid-cols-[1fr,auto] gap-2">
|
||||
<div className="space-y-1">
|
||||
<Input
|
||||
placeholder="Key"
|
||||
value={key}
|
||||
onChange={(e) => {
|
||||
const newEnv = { ...env };
|
||||
newEnv[e.target.value] = value;
|
||||
setEnv(newEnv);
|
||||
}}
|
||||
/>
|
||||
<Input
|
||||
placeholder="Value"
|
||||
value={value}
|
||||
onChange={(e) => {
|
||||
const newEnv = { ...env };
|
||||
newEnv[key] = e.target.value;
|
||||
setEnv(newEnv);
|
||||
}}
|
||||
/>
|
||||
</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"
|
||||
onClick={() => {
|
||||
const newEnv = { ...env };
|
||||
newEnv[""] = "";
|
||||
setEnv(newEnv);
|
||||
}}
|
||||
>
|
||||
Add Environment Variable
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<Button className="w-full" onClick={onConnect}>
|
||||
<Play className="w-4 h-4 mr-2" />
|
||||
Connect
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center justify-center space-x-2 mb-4">
|
||||
<div
|
||||
className={`w-2 h-2 rounded-full ${
|
||||
connectionStatus === "connected"
|
||||
? "bg-green-500"
|
||||
: connectionStatus === "error"
|
||||
? "bg-red-500"
|
||||
: "bg-gray-500"
|
||||
}`}
|
||||
/>
|
||||
<span className="text-sm text-gray-600">
|
||||
{connectionStatus === "connected"
|
||||
? "Connected"
|
||||
: connectionStatus === "error"
|
||||
? "Connection Error"
|
||||
: "Disconnected"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
export default Sidebar;
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { TabsContent } from "@/components/ui/tabs";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
CallToolResult,
|
||||
ListToolsResult,
|
||||
@@ -127,24 +128,44 @@ const ToolsTab = ({
|
||||
>
|
||||
{key}
|
||||
</Label>
|
||||
<Input
|
||||
// @ts-expect-error value type is currently unknown
|
||||
type={value.type === "number" ? "number" : "text"}
|
||||
id={key}
|
||||
name={key}
|
||||
// @ts-expect-error value type is currently unknown
|
||||
placeholder={value.description}
|
||||
onChange={(e) =>
|
||||
setParams({
|
||||
...params,
|
||||
[key]:
|
||||
// @ts-expect-error value type is currently unknown
|
||||
value.type === "number"
|
||||
? Number(e.target.value)
|
||||
: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
{
|
||||
/* @ts-expect-error value type is currently unknown */
|
||||
value.type === "string" ? (
|
||||
<Textarea
|
||||
id={key}
|
||||
name={key}
|
||||
// @ts-expect-error value type is currently unknown
|
||||
placeholder={value.description}
|
||||
onChange={(e) =>
|
||||
setParams({
|
||||
...params,
|
||||
[key]: e.target.value,
|
||||
})
|
||||
}
|
||||
className="mt-1"
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
// @ts-expect-error value type is currently unknown
|
||||
type={value.type === "number" ? "number" : "text"}
|
||||
id={key}
|
||||
name={key}
|
||||
// @ts-expect-error value type is currently unknown
|
||||
placeholder={value.description}
|
||||
onChange={(e) =>
|
||||
setParams({
|
||||
...params,
|
||||
[key]:
|
||||
// @ts-expect-error value type is currently unknown
|
||||
value.type === "number"
|
||||
? Number(e.target.value)
|
||||
: e.target.value,
|
||||
})
|
||||
}
|
||||
className="mt-1"
|
||||
/>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
),
|
||||
)}
|
||||
|
||||
@@ -51,44 +51,59 @@ const createTransport = async (query: express.Request["query"]) => {
|
||||
};
|
||||
|
||||
app.get("/sse", async (req, res) => {
|
||||
console.log("New SSE connection");
|
||||
try {
|
||||
console.log("New SSE connection");
|
||||
|
||||
const backingServerTransport = await createTransport(req.query);
|
||||
const backingServerTransport = await createTransport(req.query);
|
||||
|
||||
console.log("Connected MCP client to backing server transport");
|
||||
console.log("Connected MCP client to backing server transport");
|
||||
|
||||
const webAppTransport = new SSEServerTransport("/message", res);
|
||||
console.log("Created web app transport");
|
||||
const webAppTransport = new SSEServerTransport("/message", res);
|
||||
console.log("Created web app transport");
|
||||
|
||||
webAppTransports.push(webAppTransport);
|
||||
console.log("Created web app transport");
|
||||
webAppTransports.push(webAppTransport);
|
||||
console.log("Created web app transport");
|
||||
|
||||
await webAppTransport.start();
|
||||
await webAppTransport.start();
|
||||
|
||||
mcpProxy({
|
||||
transportToClient: webAppTransport,
|
||||
transportToServer: backingServerTransport,
|
||||
onerror: (error) => {
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
console.log("Set up MCP proxy");
|
||||
mcpProxy({
|
||||
transportToClient: webAppTransport,
|
||||
transportToServer: backingServerTransport,
|
||||
onerror: (error) => {
|
||||
console.error(error);
|
||||
},
|
||||
});
|
||||
console.log("Set up MCP proxy");
|
||||
} catch (error) {
|
||||
console.error("Error in /sse route:", error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.post("/message", async (req, res) => {
|
||||
const sessionId = req.query.sessionId;
|
||||
console.log(`Received message for sessionId ${sessionId}`);
|
||||
try {
|
||||
const sessionId = req.query.sessionId;
|
||||
console.log(`Received message for sessionId ${sessionId}`);
|
||||
|
||||
const transport = webAppTransports.find((t) => t.sessionId === sessionId);
|
||||
if (!transport) {
|
||||
res.status(404).send("Session not found");
|
||||
return;
|
||||
const transport = webAppTransports.find((t) => t.sessionId === sessionId);
|
||||
if (!transport) {
|
||||
res.status(404).end("Session not found");
|
||||
return;
|
||||
}
|
||||
await transport.handlePostMessage(req, res);
|
||||
} catch (error) {
|
||||
console.error("Error in /message route:", error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
await transport.handlePostMessage(req, res);
|
||||
});
|
||||
|
||||
app.get("/default-environment", (req, res) => {
|
||||
res.json(getDefaultEnvironment());
|
||||
try {
|
||||
res.json(getDefaultEnvironment());
|
||||
} catch (error) {
|
||||
console.error("Error in /default-environment route:", error);
|
||||
res.status(500).json(error);
|
||||
}
|
||||
});
|
||||
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
Reference in New Issue
Block a user