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:
devin-ai-integration[bot]
2024-12-07 06:15:21 +00:00
parent 8c7b0c360e
commit e96b3be159
5 changed files with 509 additions and 441 deletions

View File

@@ -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">

View File

@@ -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>
); );
}; };

View File

@@ -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>
); );
}; };

View File

@@ -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>
); );
}; };

View 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];
}