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:
@@ -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>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -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<{
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
77
client/src/components/RootsTab.tsx
Normal file
77
client/src/components/RootsTab.tsx
Normal 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;
|
||||||
@@ -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
19
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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}`);
|
||||||
|
|||||||
Reference in New Issue
Block a user