Merge pull request #44 from modelcontextprotocol/justin/update-sdk

Update SDK; add UI for roots, resource templates, and env vars
This commit is contained in:
Justin Spahr-Summers
2024-11-08 12:06:06 +00:00
committed by GitHub
7 changed files with 465 additions and 99 deletions

View File

@@ -1,7 +1,7 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { import {
CallToolResultSchema, CompatibilityCallToolResultSchema,
ClientRequest, ClientRequest,
CreateMessageRequestSchema, CreateMessageRequestSchema,
CreateMessageResult, CreateMessageResult,
@@ -9,12 +9,18 @@ import {
GetPromptResultSchema, GetPromptResultSchema,
ListPromptsResultSchema, ListPromptsResultSchema,
ListResourcesResultSchema, ListResourcesResultSchema,
ListResourceTemplatesResultSchema,
ListRootsRequestSchema,
ListToolsResultSchema, ListToolsResultSchema,
ProgressNotificationSchema, ProgressNotificationSchema,
ReadResourceResultSchema, ReadResourceResultSchema,
Resource, Resource,
ResourceTemplate,
Root,
ServerNotification, ServerNotification,
Tool, Tool,
CompatibilityCallToolResult,
ClientNotification,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
@@ -37,9 +43,12 @@ import {
Play, Play,
Send, Send,
Terminal, Terminal,
FolderTree,
ChevronDown,
ChevronRight,
} from "lucide-react"; } from "lucide-react";
import { AnyZodObject } from "zod"; import { 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";
@@ -47,6 +56,7 @@ import PingTab from "./components/PingTab";
import PromptsTab, { Prompt } from "./components/PromptsTab"; import PromptsTab, { Prompt } from "./components/PromptsTab";
import RequestsTab from "./components/RequestsTabs"; import RequestsTab from "./components/RequestsTabs";
import ResourcesTab from "./components/ResourcesTab"; import ResourcesTab from "./components/ResourcesTab";
import RootsTab from "./components/RootsTab";
import SamplingTab, { PendingRequest } from "./components/SamplingTab"; 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";
@@ -56,11 +66,15 @@ const App = () => {
"disconnected" | "connected" | "error" "disconnected" | "connected" | "error"
>("disconnected"); >("disconnected");
const [resources, setResources] = useState<Resource[]>([]); const [resources, setResources] = useState<Resource[]>([]);
const [resourceTemplates, setResourceTemplates] = useState<
ResourceTemplate[]
>([]);
const [resourceContent, setResourceContent] = useState<string>(""); const [resourceContent, setResourceContent] = useState<string>("");
const [prompts, setPrompts] = useState<Prompt[]>([]); const [prompts, setPrompts] = useState<Prompt[]>([]);
const [promptContent, setPromptContent] = useState<string>(""); const [promptContent, setPromptContent] = useState<string>("");
const [tools, setTools] = useState<Tool[]>([]); const [tools, setTools] = useState<Tool[]>([]);
const [toolResult, setToolResult] = useState<string>(""); const [toolResult, setToolResult] =
useState<CompatibilityCallToolResult | null>(null);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [command, setCommand] = useState<string>(() => { const [command, setCommand] = useState<string>(() => {
return ( return (
@@ -77,10 +91,13 @@ const App = () => {
const [url, setUrl] = useState<string>("http://localhost:3001/sse"); const [url, setUrl] = useState<string>("http://localhost:3001/sse");
const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio"); const [transportType, setTransportType] = useState<"stdio" | "sse">("stdio");
const [requestHistory, setRequestHistory] = useState< const [requestHistory, setRequestHistory] = useState<
{ request: string; response: string }[] { request: string; response?: string }[]
>([]); >([]);
const [mcpClient, setMcpClient] = useState<Client | null>(null); const [mcpClient, setMcpClient] = useState<Client | null>(null);
const [notifications, setNotifications] = useState<ServerNotification[]>([]); 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< const [pendingSampleRequests, setPendingSampleRequests] = useState<
Array< Array<
@@ -116,6 +133,9 @@ const App = () => {
const [nextResourceCursor, setNextResourceCursor] = useState< const [nextResourceCursor, setNextResourceCursor] = useState<
string | undefined string | undefined
>(); >();
const [nextResourceTemplateCursor, setNextResourceTemplateCursor] = useState<
string | undefined
>();
const [nextPromptCursor, setNextPromptCursor] = useState< const [nextPromptCursor, setNextPromptCursor] = useState<
string | undefined string | undefined
>(); >();
@@ -130,14 +150,26 @@ const App = () => {
localStorage.setItem("lastArgs", args); localStorage.setItem("lastArgs", args);
}, [args]); }, [args]);
const pushHistory = (request: object, response: object) => { useEffect(() => {
fetch("http://localhost:3000/default-environment")
.then((response) => response.json())
.then((data) => setEnv(data))
.catch((error) =>
console.error("Error fetching default environment:", error),
);
}, []);
const pushHistory = (request: object, response?: object) => {
setRequestHistory((prev) => [ setRequestHistory((prev) => [
...prev, ...prev,
{ request: JSON.stringify(request), response: JSON.stringify(response) }, {
request: JSON.stringify(request),
response: response !== undefined ? JSON.stringify(response) : undefined,
},
]); ]);
}; };
const makeRequest = async <T extends AnyZodObject>( const makeRequest = async <T extends ZodType<object>>(
request: ClientRequest, request: ClientRequest,
schema: T, schema: T,
) => { ) => {
@@ -155,6 +187,20 @@ const App = () => {
} }
}; };
const sendNotification = async (notification: ClientNotification) => {
if (!mcpClient) {
throw new Error("MCP client not connected");
}
try {
await mcpClient.notification(notification);
pushHistory(notification);
} catch (e: unknown) {
setError((e as Error).message);
throw e;
}
};
const listResources = async () => { const listResources = async () => {
const response = await makeRequest( const response = await makeRequest(
{ {
@@ -167,6 +213,22 @@ const App = () => {
setNextResourceCursor(response.nextCursor); setNextResourceCursor(response.nextCursor);
}; };
const listResourceTemplates = async () => {
const response = await makeRequest(
{
method: "resources/templates/list" as const,
params: nextResourceTemplateCursor
? { cursor: nextResourceTemplateCursor }
: {},
},
ListResourceTemplatesResultSchema,
);
setResourceTemplates(
resourceTemplates.concat(response.resourceTemplates ?? []),
);
setNextResourceTemplateCursor(response.nextCursor);
};
const readResource = async (uri: string) => { const readResource = async (uri: string) => {
const response = await makeRequest( const response = await makeRequest(
{ {
@@ -225,9 +287,13 @@ const App = () => {
}, },
}, },
}, },
CallToolResultSchema, CompatibilityCallToolResultSchema,
); );
setToolResult(JSON.stringify(response.toolResult, null, 2)); setToolResult(response);
};
const handleRootsChange = async () => {
sendNotification({ method: "notifications/roots/list_changed" });
}; };
const connectMcpServer = async () => { const connectMcpServer = async () => {
@@ -243,6 +309,7 @@ const App = () => {
if (transportType === "stdio") { if (transportType === "stdio") {
backendUrl.searchParams.append("command", command); backendUrl.searchParams.append("command", command);
backendUrl.searchParams.append("args", args); backendUrl.searchParams.append("args", args);
backendUrl.searchParams.append("env", JSON.stringify(env));
} else { } else {
backendUrl.searchParams.append("url", url); backendUrl.searchParams.append("url", url);
} }
@@ -269,6 +336,10 @@ const App = () => {
}); });
}); });
client.setRequestHandler(ListRootsRequestSchema, async () => {
return { roots };
});
setMcpClient(client); setMcpClient(client);
setConnectionStatus("connected"); setConnectionStatus("connected");
} catch (e) { } catch (e) {
@@ -326,6 +397,66 @@ const App = () => {
Connect Connect
</Button> </Button>
</div> </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>
)}
</div> </div>
{mcpClient ? ( {mcpClient ? (
<Tabs defaultValue="resources" className="w-full p-4"> <Tabs defaultValue="resources" className="w-full p-4">
@@ -363,17 +494,24 @@ const App = () => {
</span> </span>
)} )}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="roots">
<FolderTree className="w-4 h-4 mr-2" />
Roots
</TabsTrigger>
</TabsList> </TabsList>
<div className="w-full"> <div className="w-full">
<ResourcesTab <ResourcesTab
resources={resources} resources={resources}
resourceTemplates={resourceTemplates}
listResources={listResources} listResources={listResources}
listResourceTemplates={listResourceTemplates}
readResource={readResource} readResource={readResource}
selectedResource={selectedResource} selectedResource={selectedResource}
setSelectedResource={setSelectedResource} setSelectedResource={setSelectedResource}
resourceContent={resourceContent} resourceContent={resourceContent}
nextCursor={nextResourceCursor} nextCursor={nextResourceCursor}
nextTemplateCursor={nextResourceTemplateCursor}
error={error} error={error}
/> />
<PromptsTab <PromptsTab
@@ -394,7 +532,7 @@ const App = () => {
selectedTool={selectedTool} selectedTool={selectedTool}
setSelectedTool={(tool) => { setSelectedTool={(tool) => {
setSelectedTool(tool); setSelectedTool(tool);
setToolResult(""); setToolResult(null);
}} }}
toolResult={toolResult} toolResult={toolResult}
nextCursor={nextToolCursor} nextCursor={nextToolCursor}
@@ -416,6 +554,11 @@ const App = () => {
onApprove={handleApproveSampling} onApprove={handleApproveSampling}
onReject={handleRejectSampling} onReject={handleRejectSampling}
/> />
<RootsTab
roots={roots}
setRoots={setRoots}
onRootsChange={handleRootsChange}
/>
</div> </div>
</Tabs> </Tabs>
) : ( ) : (

View File

@@ -6,7 +6,7 @@ const HistoryAndNotifications = ({
requestHistory, requestHistory,
serverNotifications, serverNotifications,
}: { }: {
requestHistory: Array<{ request: string; response: string | null }>; requestHistory: Array<{ request: string; response?: string }>;
serverNotifications: ServerNotification[]; serverNotifications: ServerNotification[];
}) => { }) => {
const [expandedRequests, setExpandedRequests] = useState<{ const [expandedRequests, setExpandedRequests] = useState<{

View File

@@ -1,91 +1,199 @@
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs"; import { TabsContent } from "@/components/ui/tabs";
import { import {
ListResourcesResult, ListResourcesResult,
Resource, Resource,
ResourceTemplate,
ListResourceTemplatesResult,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { AlertCircle, ChevronRight, FileText, RefreshCw } from "lucide-react"; import { AlertCircle, ChevronRight, FileText, RefreshCw } from "lucide-react";
import ListPane from "./ListPane"; import ListPane from "./ListPane";
import { useState } from "react";
const ResourcesTab = ({ const ResourcesTab = ({
resources, resources,
resourceTemplates,
listResources, listResources,
listResourceTemplates,
readResource, readResource,
selectedResource, selectedResource,
setSelectedResource, setSelectedResource,
resourceContent, resourceContent,
nextCursor, nextCursor,
nextTemplateCursor,
error, error,
}: { }: {
resources: Resource[]; resources: Resource[];
resourceTemplates: ResourceTemplate[];
listResources: () => void; listResources: () => void;
listResourceTemplates: () => void;
readResource: (uri: string) => void; readResource: (uri: string) => void;
selectedResource: Resource | null; selectedResource: Resource | null;
setSelectedResource: (resource: Resource) => void; setSelectedResource: (resource: Resource | null) => void;
resourceContent: string; resourceContent: string;
nextCursor: ListResourcesResult["nextCursor"]; nextCursor: ListResourcesResult["nextCursor"];
nextTemplateCursor: ListResourceTemplatesResult["nextCursor"];
error: string | null; error: string | null;
}) => ( }) => {
<TabsContent value="resources" className="grid grid-cols-2 gap-4"> const [selectedTemplate, setSelectedTemplate] =
<ListPane useState<ResourceTemplate | null>(null);
items={resources} const [templateValues, setTemplateValues] = useState<Record<string, string>>(
listItems={listResources} {},
setSelectedItem={(resource) => { );
setSelectedResource(resource);
readResource(resource.uri);
}}
renderItem={(resource) => (
<div className="flex items-center w-full">
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
<span className="flex-1 truncate" title={resource.uri.toString()}>
{resource.name}
</span>
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
</div>
)}
title="Resources"
buttonText={nextCursor ? "List More Resources" : "List Resources"}
isButtonDisabled={!nextCursor && resources.length > 0}
/>
<div className="bg-white rounded-lg shadow"> const fillTemplate = (
<div className="p-4 border-b border-gray-200 flex justify-between items-center"> template: string,
<h3 className="font-semibold truncate" title={selectedResource?.name}> values: Record<string, string>,
{selectedResource ? selectedResource.name : "Select a resource"} ): string => {
</h3> return template.replace(
{selectedResource && ( /{([^}]+)}/g,
<Button (_, key) => values[key] || `{${key}}`,
variant="outline" );
size="sm" };
onClick={() => readResource(selectedResource.uri)}
const handleReadTemplateResource = () => {
if (selectedTemplate) {
const uri = fillTemplate(selectedTemplate.uriTemplate, templateValues);
readResource(uri);
setSelectedTemplate(null);
// We don't have the full Resource object here, so we create a partial one
setSelectedResource({ uri, name: uri } as Resource);
}
};
return (
<TabsContent value="resources" className="grid grid-cols-3 gap-4">
<ListPane
items={resources}
listItems={listResources}
setSelectedItem={(resource) => {
setSelectedResource(resource);
readResource(resource.uri);
setSelectedTemplate(null);
}}
renderItem={(resource) => (
<div className="flex items-center w-full">
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
<span className="flex-1 truncate" title={resource.uri.toString()}>
{resource.name}
</span>
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
</div>
)}
title="Resources"
buttonText={nextCursor ? "List More Resources" : "List Resources"}
isButtonDisabled={!nextCursor && resources.length > 0}
/>
<ListPane
items={resourceTemplates}
listItems={listResourceTemplates}
setSelectedItem={(template) => {
setSelectedTemplate(template);
setSelectedResource(null);
setTemplateValues({});
}}
renderItem={(template) => (
<div className="flex items-center w-full">
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
<span className="flex-1 truncate" title={template.uriTemplate}>
{template.name}
</span>
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
</div>
)}
title="Resource Templates"
buttonText={
nextTemplateCursor ? "List More Templates" : "List Templates"
}
isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0}
/>
<div className="bg-white rounded-lg shadow">
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
<h3
className="font-semibold truncate"
title={selectedResource?.name || selectedTemplate?.name}
> >
<RefreshCw className="w-4 h-4 mr-2" /> {selectedResource
Refresh ? selectedResource.name
</Button> : selectedTemplate
)} ? selectedTemplate.name
: "Select a resource or template"}
</h3>
{selectedResource && (
<Button
variant="outline"
size="sm"
onClick={() => readResource(selectedResource.uri)}
>
<RefreshCw className="w-4 h-4 mr-2" />
Refresh
</Button>
)}
</div>
<div className="p-4">
{error ? (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
) : selectedResource ? (
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-96 whitespace-pre-wrap break-words">
{resourceContent}
</pre>
) : selectedTemplate ? (
<div className="space-y-4">
<p className="text-sm text-gray-600">
{selectedTemplate.description}
</p>
{selectedTemplate.uriTemplate
.match(/{([^}]+)}/g)
?.map((param) => {
const key = param.slice(1, -1);
return (
<div key={key}>
<label
htmlFor={key}
className="block text-sm font-medium text-gray-700"
>
{key}
</label>
<Input
id={key}
value={templateValues[key] || ""}
onChange={(e) =>
setTemplateValues({
...templateValues,
[key]: e.target.value,
})
}
className="mt-1"
/>
</div>
);
})}
<Button
onClick={handleReadTemplateResource}
disabled={Object.keys(templateValues).length === 0}
>
Read Resource
</Button>
</div>
) : (
<Alert>
<AlertDescription>
Select a resource or template from the list to view its contents
</AlertDescription>
</Alert>
)}
</div>
</div> </div>
<div className="p-4"> </TabsContent>
{error ? ( );
<Alert variant="destructive"> };
<AlertCircle className="h-4 w-4" />
<AlertTitle>Error</AlertTitle>
<AlertDescription>{error}</AlertDescription>
</Alert>
) : selectedResource ? (
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-96 whitespace-pre-wrap break-words">
{resourceContent}
</pre>
) : (
<Alert>
<AlertDescription>
Select a resource from the list to view its contents
</AlertDescription>
</Alert>
)}
</div>
</div>
</TabsContent>
);
export default ResourcesTab; export default ResourcesTab;

View File

@@ -0,0 +1,77 @@
import { Alert, AlertDescription } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { TabsContent } from "@/components/ui/tabs";
import { Root } from "@modelcontextprotocol/sdk/types.js";
import { Plus, Minus, Save } from "lucide-react";
const RootsTab = ({
roots,
setRoots,
onRootsChange,
}: {
roots: Root[];
setRoots: React.Dispatch<React.SetStateAction<Root[]>>;
onRootsChange: () => void;
}) => {
const addRoot = () => {
setRoots((currentRoots) => [...currentRoots, { uri: "file://", name: "" }]);
};
const removeRoot = (index: number) => {
setRoots((currentRoots) => currentRoots.filter((_, i) => i !== index));
};
const updateRoot = (index: number, field: keyof Root, value: string) => {
setRoots((currentRoots) =>
currentRoots.map((root, i) =>
i === index ? { ...root, [field]: value } : root,
),
);
};
const handleSave = () => {
onRootsChange();
};
return (
<TabsContent value="roots" className="space-y-4">
<Alert>
<AlertDescription>
Configure the root directories that the server can access
</AlertDescription>
</Alert>
{roots.map((root, index) => (
<div key={index} className="flex gap-2 items-center">
<Input
placeholder="file:// URI"
value={root.uri}
onChange={(e) => updateRoot(index, "uri", e.target.value)}
className="flex-1"
/>
<Button
variant="destructive"
size="sm"
onClick={() => removeRoot(index)}
>
<Minus className="h-4 w-4" />
</Button>
</div>
))}
<div className="flex gap-2">
<Button variant="outline" onClick={addRoot}>
<Plus className="h-4 w-4 mr-2" />
Add Root
</Button>
<Button onClick={handleSave}>
<Save className="h-4 w-4 mr-2" />
Save Changes
</Button>
</div>
</TabsContent>
);
};
export default RootsTab;

View File

@@ -3,11 +3,13 @@ import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input"; import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label"; import { Label } from "@/components/ui/label";
import { TabsContent } from "@/components/ui/tabs"; import { TabsContent } from "@/components/ui/tabs";
import { ListToolsResult, Tool } from "@modelcontextprotocol/sdk/types.js"; import { CallToolResult, ListToolsResult, Tool } from "@modelcontextprotocol/sdk/types.js";
import { AlertCircle, Send } from "lucide-react"; import { AlertCircle, Send } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
import ListPane from "./ListPane"; import ListPane from "./ListPane";
import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js";
const ToolsTab = ({ const ToolsTab = ({
tools, tools,
listTools, listTools,
@@ -23,12 +25,58 @@ const ToolsTab = ({
callTool: (name: string, params: Record<string, unknown>) => void; callTool: (name: string, params: Record<string, unknown>) => void;
selectedTool: Tool | null; selectedTool: Tool | null;
setSelectedTool: (tool: Tool) => void; setSelectedTool: (tool: Tool) => void;
toolResult: string; toolResult: CompatibilityCallToolResult | null;
nextCursor: ListToolsResult["nextCursor"]; nextCursor: ListToolsResult["nextCursor"];
error: string | null; error: string | null;
}) => { }) => {
const [params, setParams] = useState<Record<string, unknown>>({}); const [params, setParams] = useState<Record<string, unknown>>({});
const renderToolResult = () => {
if (!toolResult) return null;
if ("content" in toolResult) {
const structuredResult = toolResult as CallToolResult;
return (
<>
<h4 className="font-semibold mb-2">
Tool Result: {structuredResult.isError ? "Error" : "Success"}
</h4>
{structuredResult.content.map((item, index) => (
<div key={index} className="mb-2">
{item.type === "text" && (
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-64">
{item.text}
</pre>
)}
{item.type === "image" && (
<img
src={`data:${item.mimeType};base64,${item.data}`}
alt="Tool result image"
className="max-w-full h-auto"
/>
)}
{item.type === "resource" && (
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-64">
{JSON.stringify(item.resource, null, 2)}
</pre>
)}
</div>
))}
</>
);
} else if ("toolResult" in toolResult) {
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">
{JSON.stringify(toolResult.toolResult, null, 2)}
</pre>
</>
);
}
};
return ( return (
<TabsContent value="tools" className="grid grid-cols-2 gap-4"> <TabsContent value="tools" className="grid grid-cols-2 gap-4">
<ListPane <ListPane
@@ -100,14 +148,7 @@ const ToolsTab = ({
<Send className="w-4 h-4 mr-2" /> <Send className="w-4 h-4 mr-2" />
Run Tool Run Tool
</Button> </Button>
{toolResult && ( {toolResult && renderToolResult()}
<>
<h4 className="font-semibold mb-2">Tool Result:</h4>
<pre className="bg-gray-50 p-4 rounded text-sm overflow-auto max-h-64">
{toolResult}
</pre>
</>
)}
</div> </div>
) : ( ) : (
<Alert> <Alert>

19
package-lock.json generated
View File

@@ -841,9 +841,9 @@
} }
}, },
"node_modules/@modelcontextprotocol/sdk": { "node_modules/@modelcontextprotocol/sdk": {
"version": "0.1.0", "version": "0.3.2",
"resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-internal/@modelcontextprotocol/sdk/-/@modelcontextprotocol/sdk-0.1.0.tgz", "resolved": "https://artifactory.infra.ant.dev:443/artifactory/api/npm/npm-internal/@modelcontextprotocol/sdk/-/@modelcontextprotocol/sdk-0.3.2.tgz",
"integrity": "sha512-46/FTHNZWUWbdFspKsCIixhCKwi9Hub5+HWNXC1DRIL2TSV8cdx5sTsg+Jy6I4Uc8/rd7tLZcVQ6IasIY1g4zg==", "integrity": "sha512-7f4VYf43cH0VzewG1kdgoKZN56PA69VA2iix+/nlc0AOEJ6ClhebOpmeugK2ZvPctbzFuESaYW8Oh29u8Y09Jw==",
"dependencies": { "dependencies": {
"content-type": "^1.0.5", "content-type": "^1.0.5",
"raw-body": "^3.0.0", "raw-body": "^3.0.0",
@@ -4635,12 +4635,6 @@
} }
} }
}, },
"node_modules/react-remove-scroll/node_modules/tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
"license": "0BSD"
},
"node_modules/react-style-singleton": { "node_modules/react-style-singleton": {
"version": "2.2.1", "version": "2.2.1",
"resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz",
@@ -4811,13 +4805,6 @@
"tslib": "^2.1.0" "tslib": "^2.1.0"
} }
}, },
"node_modules/rxjs/node_modules/tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
"dev": true,
"license": "0BSD"
},
"node_modules/safe-buffer": { "node_modules/safe-buffer": {
"version": "5.2.1", "version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",

View File

@@ -2,7 +2,10 @@ import cors from "cors";
import EventSource from "eventsource"; import EventSource from "eventsource";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js"; import {
StdioClientTransport,
getDefaultEnvironment,
} from "@modelcontextprotocol/sdk/client/stdio.js";
import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js"; import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
import express from "express"; import express from "express";
import mcpProxy from "./mcpProxy.js"; import mcpProxy from "./mcpProxy.js";
@@ -24,8 +27,11 @@ const createTransport = async (query: express.Request["query"]) => {
if (transportType === "stdio") { if (transportType === "stdio") {
const command = query.command as string; const command = query.command as string;
const args = (query.args as string).split(/\s+/); const args = (query.args as string).split(/\s+/);
console.log(`Stdio transport: command=${command}, args=${args}`); const env = query.env ? JSON.parse(query.env as string) : undefined;
const transport = new StdioClientTransport({ command, args }); console.log(
`Stdio transport: command=${command}, args=${args}, env=${JSON.stringify(env)}`,
);
const transport = new StdioClientTransport({ command, args, env });
await transport.start(); await transport.start();
console.log("Spawned stdio transport"); console.log("Spawned stdio transport");
return transport; return transport;
@@ -79,6 +85,10 @@ app.post("/message", async (req, res) => {
await transport.handlePostMessage(req, res); await transport.handlePostMessage(req, res);
}); });
app.get("/default-environment", (req, res) => {
res.json(getDefaultEnvironment());
});
const PORT = process.env.PORT || 3000; const PORT = process.env.PORT || 3000;
app.listen(PORT, () => { app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`); console.log(`Server is running on port ${PORT}`);