feat: implement capability negotiation for UI tabs
- Add CapabilityContext to manage server capabilities - Disable tabs when server doesn't support feature - Show error message in tab content when capability missing - Implements #85
This commit is contained in:
@@ -55,6 +55,8 @@ 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";
|
||||||
|
|
||||||
|
import { CapabilityContext, ServerCapabilities } from "@/lib/contexts";
|
||||||
|
|
||||||
const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000;
|
const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000;
|
||||||
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
@@ -66,6 +68,7 @@ const App = () => {
|
|||||||
const [connectionStatus, setConnectionStatus] = useState<
|
const [connectionStatus, setConnectionStatus] = useState<
|
||||||
"disconnected" | "connected" | "error"
|
"disconnected" | "connected" | "error"
|
||||||
>("disconnected");
|
>("disconnected");
|
||||||
|
const [serverCapabilities, setServerCapabilities] = useState<ServerCapabilities | null>(null);
|
||||||
const [resources, setResources] = useState<Resource[]>([]);
|
const [resources, setResources] = useState<Resource[]>([]);
|
||||||
const [resourceTemplates, setResourceTemplates] = useState<
|
const [resourceTemplates, setResourceTemplates] = useState<
|
||||||
ResourceTemplate[]
|
ResourceTemplate[]
|
||||||
@@ -217,6 +220,13 @@ const App = () => {
|
|||||||
rootsRef.current = roots;
|
rootsRef.current = roots;
|
||||||
}, [roots]);
|
}, [roots]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mcpClient) {
|
||||||
|
const capabilities = mcpClient.getServerCapabilities();
|
||||||
|
setServerCapabilities(capabilities ?? null);
|
||||||
|
}
|
||||||
|
}, [mcpClient]);
|
||||||
|
|
||||||
const pushHistory = (request: object, response?: object) => {
|
const pushHistory = (request: object, response?: object) => {
|
||||||
setRequestHistory((prev) => [
|
setRequestHistory((prev) => [
|
||||||
...prev,
|
...prev,
|
||||||
@@ -444,6 +454,9 @@ const App = () => {
|
|||||||
|
|
||||||
await client.connect(clientTransport);
|
await client.connect(clientTransport);
|
||||||
|
|
||||||
|
const capabilities = client.getServerCapabilities();
|
||||||
|
setServerCapabilities(capabilities ?? null);
|
||||||
|
|
||||||
client.setRequestHandler(CreateMessageRequestSchema, (request) => {
|
client.setRequestHandler(CreateMessageRequestSchema, (request) => {
|
||||||
return new Promise<CreateMessageResult>((resolve, reject) => {
|
return new Promise<CreateMessageResult>((resolve, reject) => {
|
||||||
setPendingSampleRequests((prev) => [
|
setPendingSampleRequests((prev) => [
|
||||||
@@ -485,143 +498,145 @@ const App = () => {
|
|||||||
<div className="flex-1 flex flex-col overflow-hidden">
|
<div className="flex-1 flex flex-col overflow-hidden">
|
||||||
<div className="flex-1 overflow-auto">
|
<div className="flex-1 overflow-auto">
|
||||||
{mcpClient ? (
|
{mcpClient ? (
|
||||||
<Tabs defaultValue="resources" className="w-full p-4">
|
<CapabilityContext.Provider value={serverCapabilities}>
|
||||||
<TabsList className="mb-4 p-0">
|
<Tabs defaultValue="resources" className="w-full p-4">
|
||||||
<TabsTrigger value="resources">
|
<TabsList className="mb-4 p-0">
|
||||||
<Files className="w-4 h-4 mr-2" />
|
<TabsTrigger value="resources" disabled={!serverCapabilities?.resources}>
|
||||||
Resources
|
<Files className="w-4 h-4 mr-2" />
|
||||||
</TabsTrigger>
|
Resources
|
||||||
<TabsTrigger value="prompts">
|
</TabsTrigger>
|
||||||
<MessageSquare className="w-4 h-4 mr-2" />
|
<TabsTrigger value="prompts" disabled={!serverCapabilities?.prompts}>
|
||||||
Prompts
|
<MessageSquare className="w-4 h-4 mr-2" />
|
||||||
</TabsTrigger>
|
Prompts
|
||||||
<TabsTrigger value="tools">
|
</TabsTrigger>
|
||||||
<Hammer className="w-4 h-4 mr-2" />
|
<TabsTrigger value="tools" disabled={!serverCapabilities?.tools}>
|
||||||
Tools
|
<Hammer className="w-4 h-4 mr-2" />
|
||||||
</TabsTrigger>
|
Tools
|
||||||
<TabsTrigger value="ping">
|
</TabsTrigger>
|
||||||
<Bell className="w-4 h-4 mr-2" />
|
<TabsTrigger value="ping">
|
||||||
Ping
|
<Bell className="w-4 h-4 mr-2" />
|
||||||
</TabsTrigger>
|
Ping
|
||||||
<TabsTrigger value="sampling" className="relative">
|
</TabsTrigger>
|
||||||
<Hash className="w-4 h-4 mr-2" />
|
<TabsTrigger value="sampling" className="relative">
|
||||||
Sampling
|
<Hash className="w-4 h-4 mr-2" />
|
||||||
{pendingSampleRequests.length > 0 && (
|
Sampling
|
||||||
<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 > 0 && (
|
||||||
{pendingSampleRequests.length}
|
<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">
|
||||||
</span>
|
{pendingSampleRequests.length}
|
||||||
)}
|
</span>
|
||||||
</TabsTrigger>
|
)}
|
||||||
<TabsTrigger value="roots">
|
</TabsTrigger>
|
||||||
<FolderTree className="w-4 h-4 mr-2" />
|
<TabsTrigger value="roots">
|
||||||
Roots
|
<FolderTree className="w-4 h-4 mr-2" />
|
||||||
</TabsTrigger>
|
Roots
|
||||||
</TabsList>
|
</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<ResourcesTab
|
<ResourcesTab
|
||||||
resources={resources}
|
resources={resources}
|
||||||
resourceTemplates={resourceTemplates}
|
resourceTemplates={resourceTemplates}
|
||||||
listResources={() => {
|
listResources={() => {
|
||||||
clearError("resources");
|
clearError("resources");
|
||||||
listResources();
|
listResources();
|
||||||
}}
|
}}
|
||||||
clearResources={() => {
|
clearResources={() => {
|
||||||
setResources([]);
|
setResources([]);
|
||||||
setNextResourceCursor(undefined);
|
setNextResourceCursor(undefined);
|
||||||
}}
|
}}
|
||||||
listResourceTemplates={() => {
|
listResourceTemplates={() => {
|
||||||
clearError("resources");
|
clearError("resources");
|
||||||
listResourceTemplates();
|
listResourceTemplates();
|
||||||
}}
|
}}
|
||||||
clearResourceTemplates={() => {
|
clearResourceTemplates={() => {
|
||||||
setResourceTemplates([]);
|
setResourceTemplates([]);
|
||||||
setNextResourceTemplateCursor(undefined);
|
setNextResourceTemplateCursor(undefined);
|
||||||
}}
|
}}
|
||||||
readResource={(uri) => {
|
readResource={(uri) => {
|
||||||
clearError("resources");
|
clearError("resources");
|
||||||
readResource(uri);
|
readResource(uri);
|
||||||
}}
|
}}
|
||||||
selectedResource={selectedResource}
|
selectedResource={selectedResource}
|
||||||
setSelectedResource={(resource) => {
|
setSelectedResource={(resource) => {
|
||||||
clearError("resources");
|
clearError("resources");
|
||||||
setSelectedResource(resource);
|
setSelectedResource(resource);
|
||||||
}}
|
}}
|
||||||
resourceContent={resourceContent}
|
resourceContent={resourceContent}
|
||||||
nextCursor={nextResourceCursor}
|
nextCursor={nextResourceCursor}
|
||||||
nextTemplateCursor={nextResourceTemplateCursor}
|
nextTemplateCursor={nextResourceTemplateCursor}
|
||||||
error={errors.resources}
|
error={errors.resources}
|
||||||
/>
|
/>
|
||||||
<PromptsTab
|
<PromptsTab
|
||||||
prompts={prompts}
|
prompts={prompts}
|
||||||
listPrompts={() => {
|
listPrompts={() => {
|
||||||
clearError("prompts");
|
clearError("prompts");
|
||||||
listPrompts();
|
listPrompts();
|
||||||
}}
|
}}
|
||||||
clearPrompts={() => {
|
clearPrompts={() => {
|
||||||
setPrompts([]);
|
setPrompts([]);
|
||||||
setNextPromptCursor(undefined);
|
setNextPromptCursor(undefined);
|
||||||
}}
|
}}
|
||||||
getPrompt={(name, args) => {
|
getPrompt={(name, args) => {
|
||||||
clearError("prompts");
|
clearError("prompts");
|
||||||
getPrompt(name, args);
|
getPrompt(name, args);
|
||||||
}}
|
}}
|
||||||
selectedPrompt={selectedPrompt}
|
selectedPrompt={selectedPrompt}
|
||||||
setSelectedPrompt={(prompt) => {
|
setSelectedPrompt={(prompt) => {
|
||||||
clearError("prompts");
|
clearError("prompts");
|
||||||
setSelectedPrompt(prompt);
|
setSelectedPrompt(prompt);
|
||||||
}}
|
}}
|
||||||
promptContent={promptContent}
|
promptContent={promptContent}
|
||||||
nextCursor={nextPromptCursor}
|
nextCursor={nextPromptCursor}
|
||||||
error={errors.prompts}
|
error={errors.prompts}
|
||||||
/>
|
/>
|
||||||
<ToolsTab
|
<ToolsTab
|
||||||
tools={tools}
|
tools={tools}
|
||||||
listTools={() => {
|
listTools={() => {
|
||||||
clearError("tools");
|
clearError("tools");
|
||||||
listTools();
|
listTools();
|
||||||
}}
|
}}
|
||||||
clearTools={() => {
|
clearTools={() => {
|
||||||
setTools([]);
|
setTools([]);
|
||||||
setNextToolCursor(undefined);
|
setNextToolCursor(undefined);
|
||||||
}}
|
}}
|
||||||
callTool={(name, params) => {
|
callTool={(name, params) => {
|
||||||
clearError("tools");
|
clearError("tools");
|
||||||
callTool(name, params);
|
callTool(name, params);
|
||||||
}}
|
}}
|
||||||
selectedTool={selectedTool}
|
selectedTool={selectedTool}
|
||||||
setSelectedTool={(tool) => {
|
setSelectedTool={(tool) => {
|
||||||
clearError("tools");
|
clearError("tools");
|
||||||
setSelectedTool(tool);
|
setSelectedTool(tool);
|
||||||
setToolResult(null);
|
setToolResult(null);
|
||||||
}}
|
}}
|
||||||
toolResult={toolResult}
|
toolResult={toolResult}
|
||||||
nextCursor={nextToolCursor}
|
nextCursor={nextToolCursor}
|
||||||
error={errors.tools}
|
error={errors.tools}
|
||||||
/>
|
/>
|
||||||
<ConsoleTab />
|
<ConsoleTab />
|
||||||
<PingTab
|
<PingTab
|
||||||
onPingClick={() => {
|
onPingClick={() => {
|
||||||
void makeRequest(
|
void makeRequest(
|
||||||
{
|
{
|
||||||
method: "ping" as const,
|
method: "ping" as const,
|
||||||
},
|
},
|
||||||
EmptyResultSchema,
|
EmptyResultSchema,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<SamplingTab
|
<SamplingTab
|
||||||
pendingRequests={pendingSampleRequests}
|
pendingRequests={pendingSampleRequests}
|
||||||
onApprove={handleApproveSampling}
|
onApprove={handleApproveSampling}
|
||||||
onReject={handleRejectSampling}
|
onReject={handleRejectSampling}
|
||||||
/>
|
/>
|
||||||
<RootsTab
|
<RootsTab
|
||||||
roots={roots}
|
roots={roots}
|
||||||
setRoots={setRoots}
|
setRoots={setRoots}
|
||||||
onRootsChange={handleRootsChange}
|
onRootsChange={handleRootsChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
</CapabilityContext.Provider>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex items-center justify-center h-full">
|
<div className="flex items-center justify-center h-full">
|
||||||
<p className="text-lg text-gray-500">
|
<p className="text-lg text-gray-500">
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import { TabsContent } from "@/components/ui/tabs";
|
|||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { ListPromptsResult } from "@modelcontextprotocol/sdk/types.js";
|
import { ListPromptsResult } from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { AlertCircle } from "lucide-react";
|
import { AlertCircle } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState, useContext } from "react";
|
||||||
import ListPane from "./ListPane";
|
import ListPane from "./ListPane";
|
||||||
|
import { CapabilityContext } from "@/lib/contexts";
|
||||||
|
|
||||||
export type Prompt = {
|
export type Prompt = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -40,6 +41,7 @@ const PromptsTab = ({
|
|||||||
nextCursor: ListPromptsResult["nextCursor"];
|
nextCursor: ListPromptsResult["nextCursor"];
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
|
const capabilities = useContext(CapabilityContext);
|
||||||
const [promptArgs, setPromptArgs] = useState<Record<string, string>>({});
|
const [promptArgs, setPromptArgs] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
const handleInputChange = (argName: string, value: string) => {
|
const handleInputChange = (argName: string, value: string) => {
|
||||||
@@ -54,86 +56,98 @@ const PromptsTab = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent value="prompts" className="grid grid-cols-2 gap-4">
|
<TabsContent value="prompts" className="grid grid-cols-2 gap-4">
|
||||||
<ListPane
|
{!capabilities?.prompts ? (
|
||||||
items={prompts}
|
<Alert variant="destructive" className="col-span-2">
|
||||||
listItems={listPrompts}
|
<AlertCircle className="h-4 w-4" />
|
||||||
clearItems={clearPrompts}
|
<AlertTitle>Feature Not Available</AlertTitle>
|
||||||
setSelectedItem={(prompt) => {
|
<AlertDescription>
|
||||||
setSelectedPrompt(prompt);
|
The connected server does not support prompts.
|
||||||
setPromptArgs({});
|
</AlertDescription>
|
||||||
}}
|
</Alert>
|
||||||
renderItem={(prompt) => (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span className="flex-1">{prompt.name}</span>
|
<ListPane
|
||||||
<span className="text-sm text-gray-500">{prompt.description}</span>
|
items={prompts}
|
||||||
</>
|
listItems={listPrompts}
|
||||||
)}
|
clearItems={clearPrompts}
|
||||||
title="Prompts"
|
setSelectedItem={(prompt) => {
|
||||||
buttonText={nextCursor ? "List More Prompts" : "List Prompts"}
|
setSelectedPrompt(prompt);
|
||||||
isButtonDisabled={!nextCursor && prompts.length > 0}
|
setPromptArgs({});
|
||||||
/>
|
}}
|
||||||
|
renderItem={(prompt) => (
|
||||||
|
<>
|
||||||
|
<span className="flex-1">{prompt.name}</span>
|
||||||
|
<span className="text-sm text-gray-500">{prompt.description}</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
title="Prompts"
|
||||||
|
buttonText={nextCursor ? "List More Prompts" : "List Prompts"}
|
||||||
|
isButtonDisabled={!nextCursor && prompts.length > 0}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="bg-card rounded-lg shadow">
|
<div className="bg-card rounded-lg shadow">
|
||||||
<div className="p-4 border-b border-gray-200">
|
<div className="p-4 border-b border-gray-200">
|
||||||
<h3 className="font-semibold">
|
<h3 className="font-semibold">
|
||||||
{selectedPrompt ? selectedPrompt.name : "Select a prompt"}
|
{selectedPrompt ? selectedPrompt.name : "Select a prompt"}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
{error ? (
|
{error ? (
|
||||||
<Alert variant="destructive">
|
<Alert variant="destructive">
|
||||||
<AlertCircle className="h-4 w-4" />
|
<AlertCircle className="h-4 w-4" />
|
||||||
<AlertTitle>Error</AlertTitle>
|
<AlertTitle>Error</AlertTitle>
|
||||||
<AlertDescription>{error}</AlertDescription>
|
<AlertDescription>{error}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
) : selectedPrompt ? (
|
) : selectedPrompt ? (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{selectedPrompt.description && (
|
{selectedPrompt.description && (
|
||||||
<p className="text-sm text-gray-600">
|
<p className="text-sm text-gray-600">
|
||||||
{selectedPrompt.description}
|
{selectedPrompt.description}
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{selectedPrompt.arguments?.map((arg) => (
|
|
||||||
<div key={arg.name}>
|
|
||||||
<Label htmlFor={arg.name}>{arg.name}</Label>
|
|
||||||
<Input
|
|
||||||
id={arg.name}
|
|
||||||
placeholder={`Enter ${arg.name}`}
|
|
||||||
value={promptArgs[arg.name] || ""}
|
|
||||||
onChange={(e) =>
|
|
||||||
handleInputChange(arg.name, e.target.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{arg.description && (
|
|
||||||
<p className="text-xs text-gray-500 mt-1">
|
|
||||||
{arg.description}
|
|
||||||
{arg.required && (
|
|
||||||
<span className="text-xs mt-1 ml-1">(Required)</span>
|
|
||||||
)}
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{selectedPrompt.arguments?.map((arg) => (
|
||||||
|
<div key={arg.name}>
|
||||||
|
<Label htmlFor={arg.name}>{arg.name}</Label>
|
||||||
|
<Input
|
||||||
|
id={arg.name}
|
||||||
|
placeholder={`Enter ${arg.name}`}
|
||||||
|
value={promptArgs[arg.name] || ""}
|
||||||
|
onChange={(e) =>
|
||||||
|
handleInputChange(arg.name, e.target.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{arg.description && (
|
||||||
|
<p className="text-xs text-gray-500 mt-1">
|
||||||
|
{arg.description}
|
||||||
|
{arg.required && (
|
||||||
|
<span className="text-xs mt-1 ml-1">(Required)</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<Button onClick={handleGetPrompt} className="w-full">
|
||||||
|
Get Prompt
|
||||||
|
</Button>
|
||||||
|
{promptContent && (
|
||||||
|
<Textarea
|
||||||
|
value={promptContent}
|
||||||
|
readOnly
|
||||||
|
className="h-64 font-mono"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
))}
|
) : (
|
||||||
<Button onClick={handleGetPrompt} className="w-full">
|
<Alert>
|
||||||
Get Prompt
|
<AlertDescription>
|
||||||
</Button>
|
Select a prompt from the list to view and use it
|
||||||
{promptContent && (
|
</AlertDescription>
|
||||||
<Textarea
|
</Alert>
|
||||||
value={promptContent}
|
|
||||||
readOnly
|
|
||||||
className="h-64 font-mono"
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
</div>
|
||||||
<Alert>
|
</>
|
||||||
<AlertDescription>
|
)}
|
||||||
Select a prompt from the list to view and use it
|
|
||||||
</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
} 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";
|
import { useState, useContext } from "react";
|
||||||
|
import { CapabilityContext } from "@/lib/contexts";
|
||||||
|
|
||||||
const ResourcesTab = ({
|
const ResourcesTab = ({
|
||||||
resources,
|
resources,
|
||||||
@@ -41,6 +42,7 @@ const ResourcesTab = ({
|
|||||||
nextTemplateCursor: ListResourceTemplatesResult["nextCursor"];
|
nextTemplateCursor: ListResourceTemplatesResult["nextCursor"];
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
|
const capabilities = useContext(CapabilityContext);
|
||||||
const [selectedTemplate, setSelectedTemplate] =
|
const [selectedTemplate, setSelectedTemplate] =
|
||||||
useState<ResourceTemplate | null>(null);
|
useState<ResourceTemplate | null>(null);
|
||||||
const [templateValues, setTemplateValues] = useState<Record<string, string>>(
|
const [templateValues, setTemplateValues] = useState<Record<string, string>>(
|
||||||
@@ -62,142 +64,153 @@ const ResourcesTab = ({
|
|||||||
const uri = fillTemplate(selectedTemplate.uriTemplate, templateValues);
|
const uri = fillTemplate(selectedTemplate.uriTemplate, templateValues);
|
||||||
readResource(uri);
|
readResource(uri);
|
||||||
setSelectedTemplate(null);
|
setSelectedTemplate(null);
|
||||||
// We don't have the full Resource object here, so we create a partial one
|
|
||||||
setSelectedResource({ uri, name: uri } as Resource);
|
setSelectedResource({ uri, name: uri } as Resource);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent value="resources" className="grid grid-cols-3 gap-4">
|
<TabsContent value="resources" className="grid grid-cols-3 gap-4">
|
||||||
<ListPane
|
{!capabilities?.resources ? (
|
||||||
items={resources}
|
<Alert variant="destructive" className="col-span-3">
|
||||||
listItems={listResources}
|
<AlertCircle className="h-4 w-4" />
|
||||||
clearItems={clearResources}
|
<AlertTitle>Feature Not Available</AlertTitle>
|
||||||
setSelectedItem={(resource) => {
|
<AlertDescription>
|
||||||
setSelectedResource(resource);
|
The connected server does not support resources.
|
||||||
readResource(resource.uri);
|
</AlertDescription>
|
||||||
setSelectedTemplate(null);
|
</Alert>
|
||||||
}}
|
) : (
|
||||||
renderItem={(resource) => (
|
<>
|
||||||
<div className="flex items-center w-full">
|
<ListPane
|
||||||
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
|
items={resources}
|
||||||
<span className="flex-1 truncate" title={resource.uri.toString()}>
|
listItems={listResources}
|
||||||
{resource.name}
|
clearItems={clearResources}
|
||||||
</span>
|
setSelectedItem={(resource) => {
|
||||||
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
|
setSelectedResource(resource);
|
||||||
</div>
|
readResource(resource.uri);
|
||||||
)}
|
setSelectedTemplate(null);
|
||||||
title="Resources"
|
}}
|
||||||
buttonText={nextCursor ? "List More Resources" : "List Resources"}
|
renderItem={(resource) => (
|
||||||
isButtonDisabled={!nextCursor && resources.length > 0}
|
<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
|
<ListPane
|
||||||
items={resourceTemplates}
|
items={resourceTemplates}
|
||||||
listItems={listResourceTemplates}
|
listItems={listResourceTemplates}
|
||||||
clearItems={clearResourceTemplates}
|
clearItems={clearResourceTemplates}
|
||||||
setSelectedItem={(template) => {
|
setSelectedItem={(template) => {
|
||||||
setSelectedTemplate(template);
|
setSelectedTemplate(template);
|
||||||
setSelectedResource(null);
|
setSelectedResource(null);
|
||||||
setTemplateValues({});
|
setTemplateValues({});
|
||||||
}}
|
}}
|
||||||
renderItem={(template) => (
|
renderItem={(template) => (
|
||||||
<div className="flex items-center w-full">
|
<div className="flex items-center w-full">
|
||||||
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
|
<FileText className="w-4 h-4 mr-2 flex-shrink-0 text-gray-500" />
|
||||||
<span className="flex-1 truncate" title={template.uriTemplate}>
|
<span className="flex-1 truncate" title={template.uriTemplate}>
|
||||||
{template.name}
|
{template.name}
|
||||||
</span>
|
</span>
|
||||||
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
|
<ChevronRight className="w-4 h-4 flex-shrink-0 text-gray-400" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
title="Resource Templates"
|
title="Resource Templates"
|
||||||
buttonText={
|
buttonText={
|
||||||
nextTemplateCursor ? "List More Templates" : "List Templates"
|
nextTemplateCursor ? "List More Templates" : "List Templates"
|
||||||
}
|
}
|
||||||
isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0}
|
isButtonDisabled={!nextTemplateCursor && resourceTemplates.length > 0}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="bg-card rounded-lg shadow">
|
<div className="bg-card rounded-lg shadow">
|
||||||
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
|
<div className="p-4 border-b border-gray-200 flex justify-between items-center">
|
||||||
<h3
|
<h3
|
||||||
className="font-semibold truncate"
|
className="font-semibold truncate"
|
||||||
title={selectedResource?.name || selectedTemplate?.name}
|
title={selectedResource?.name || selectedTemplate?.name}
|
||||||
>
|
|
||||||
{selectedResource
|
|
||||||
? selectedResource.name
|
|
||||||
: 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 dark:bg-gray-800 p-4 rounded text-sm overflow-auto max-h-96 whitespace-pre-wrap break-words text-gray-900 dark:text-gray-100">
|
|
||||||
{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
|
{selectedResource
|
||||||
</Button>
|
? selectedResource.name
|
||||||
|
: 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>
|
||||||
) : (
|
<div className="p-4">
|
||||||
<Alert>
|
{error ? (
|
||||||
<AlertDescription>
|
<Alert variant="destructive">
|
||||||
Select a resource or template from the list to view its contents
|
<AlertCircle className="h-4 w-4" />
|
||||||
</AlertDescription>
|
<AlertTitle>Error</AlertTitle>
|
||||||
</Alert>
|
<AlertDescription>{error}</AlertDescription>
|
||||||
)}
|
</Alert>
|
||||||
</div>
|
) : selectedResource ? (
|
||||||
</div>
|
<pre className="bg-gray-50 dark:bg-gray-800 p-4 rounded text-sm overflow-auto max-h-96 whitespace-pre-wrap break-words text-gray-900 dark:text-gray-100">
|
||||||
|
{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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import {
|
|||||||
CallToolResultSchema,
|
CallToolResultSchema,
|
||||||
} from "@modelcontextprotocol/sdk/types.js";
|
} from "@modelcontextprotocol/sdk/types.js";
|
||||||
import { AlertCircle, Send } from "lucide-react";
|
import { AlertCircle, Send } from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useContext } from "react";
|
||||||
import ListPane from "./ListPane";
|
import ListPane from "./ListPane";
|
||||||
|
import { CapabilityContext } from "@/lib/contexts";
|
||||||
|
|
||||||
import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
import { CompatibilityCallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
|
||||||
@@ -36,6 +37,7 @@ const ToolsTab = ({
|
|||||||
nextCursor: ListToolsResult["nextCursor"];
|
nextCursor: ListToolsResult["nextCursor"];
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}) => {
|
}) => {
|
||||||
|
const capabilities = useContext(CapabilityContext);
|
||||||
const [params, setParams] = useState<Record<string, unknown>>({});
|
const [params, setParams] = useState<Record<string, unknown>>({});
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setParams({});
|
setParams({});
|
||||||
@@ -110,110 +112,122 @@ const ToolsTab = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<TabsContent value="tools" className="grid grid-cols-2 gap-4">
|
<TabsContent value="tools" className="grid grid-cols-2 gap-4">
|
||||||
<ListPane
|
{!capabilities?.tools ? (
|
||||||
items={tools}
|
<Alert variant="destructive" className="col-span-2">
|
||||||
listItems={listTools}
|
<AlertCircle className="h-4 w-4" />
|
||||||
clearItems={() => {
|
<AlertTitle>Feature Not Available</AlertTitle>
|
||||||
clearTools();
|
<AlertDescription>
|
||||||
setSelectedTool(null);
|
The connected server does not support tools.
|
||||||
}}
|
</AlertDescription>
|
||||||
setSelectedItem={setSelectedTool}
|
</Alert>
|
||||||
renderItem={(tool) => (
|
) : (
|
||||||
<>
|
<>
|
||||||
<span className="flex-1">{tool.name}</span>
|
<ListPane
|
||||||
<span className="text-sm text-gray-500 text-right">
|
items={tools}
|
||||||
{tool.description}
|
listItems={listTools}
|
||||||
</span>
|
clearItems={() => {
|
||||||
</>
|
clearTools();
|
||||||
)}
|
setSelectedTool(null);
|
||||||
title="Tools"
|
}}
|
||||||
buttonText={nextCursor ? "List More Tools" : "List Tools"}
|
setSelectedItem={setSelectedTool}
|
||||||
isButtonDisabled={!nextCursor && tools.length > 0}
|
renderItem={(tool) => (
|
||||||
/>
|
<>
|
||||||
|
<span className="flex-1">{tool.name}</span>
|
||||||
|
<span className="text-sm text-gray-500 text-right">
|
||||||
|
{tool.description}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
title="Tools"
|
||||||
|
buttonText={nextCursor ? "List More Tools" : "List Tools"}
|
||||||
|
isButtonDisabled={!nextCursor && tools.length > 0}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="bg-card rounded-lg shadow">
|
<div className="bg-card rounded-lg shadow">
|
||||||
<div className="p-4 border-b border-gray-200">
|
<div className="p-4 border-b border-gray-200">
|
||||||
<h3 className="font-semibold">
|
<h3 className="font-semibold">
|
||||||
{selectedTool ? selectedTool.name : "Select a tool"}
|
{selectedTool ? selectedTool.name : "Select a tool"}
|
||||||
</h3>
|
</h3>
|
||||||
</div>
|
|
||||||
<div className="p-4">
|
|
||||||
{error ? (
|
|
||||||
<Alert variant="destructive">
|
|
||||||
<AlertCircle className="h-4 w-4" />
|
|
||||||
<AlertTitle>Error</AlertTitle>
|
|
||||||
<AlertDescription>{error}</AlertDescription>
|
|
||||||
</Alert>
|
|
||||||
) : selectedTool ? (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<p className="text-sm text-gray-600">
|
|
||||||
{selectedTool.description}
|
|
||||||
</p>
|
|
||||||
{Object.entries(selectedTool.inputSchema.properties ?? []).map(
|
|
||||||
([key, value]) => (
|
|
||||||
<div key={key}>
|
|
||||||
<Label
|
|
||||||
htmlFor={key}
|
|
||||||
className="block text-sm font-medium text-gray-700"
|
|
||||||
>
|
|
||||||
{key}
|
|
||||||
</Label>
|
|
||||||
{
|
|
||||||
/* @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>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
<Button onClick={() => callTool(selectedTool.name, params)}>
|
|
||||||
<Send className="w-4 h-4 mr-2" />
|
|
||||||
Run Tool
|
|
||||||
</Button>
|
|
||||||
{toolResult && renderToolResult()}
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
<div className="p-4">
|
||||||
<Alert>
|
{error ? (
|
||||||
<AlertDescription>
|
<Alert variant="destructive">
|
||||||
Select a tool from the list to view its details and run it
|
<AlertCircle className="h-4 w-4" />
|
||||||
</AlertDescription>
|
<AlertTitle>Error</AlertTitle>
|
||||||
</Alert>
|
<AlertDescription>{error}</AlertDescription>
|
||||||
)}
|
</Alert>
|
||||||
</div>
|
) : selectedTool ? (
|
||||||
</div>
|
<div className="space-y-4">
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{selectedTool.description}
|
||||||
|
</p>
|
||||||
|
{Object.entries(selectedTool.inputSchema.properties ?? []).map(
|
||||||
|
([key, value]) => (
|
||||||
|
<div key={key}>
|
||||||
|
<Label
|
||||||
|
htmlFor={key}
|
||||||
|
className="block text-sm font-medium text-gray-700"
|
||||||
|
>
|
||||||
|
{key}
|
||||||
|
</Label>
|
||||||
|
{
|
||||||
|
/* @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>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
<Button onClick={() => callTool(selectedTool.name, params)}>
|
||||||
|
<Send className="w-4 h-4 mr-2" />
|
||||||
|
Run Tool
|
||||||
|
</Button>
|
||||||
|
{toolResult && renderToolResult()}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Alert>
|
||||||
|
<AlertDescription>
|
||||||
|
Select a tool from the list to view its details and run it
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
12
client/src/lib/contexts.ts
Normal file
12
client/src/lib/contexts.ts
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { createContext, useContext } from "react";
|
||||||
|
import { ServerCapabilitiesSchema } from "@modelcontextprotocol/sdk/types.js";
|
||||||
|
import type { z } from "zod";
|
||||||
|
|
||||||
|
export type ServerCapabilities = z.infer<typeof ServerCapabilitiesSchema>;
|
||||||
|
|
||||||
|
export const CapabilityContext = createContext<ServerCapabilities | null>(null);
|
||||||
|
|
||||||
|
export function useServerCapability(capability: keyof ServerCapabilities): boolean {
|
||||||
|
const capabilities = useContext(CapabilityContext);
|
||||||
|
return !!capabilities?.[capability];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user