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,17 +498,18 @@ 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 ? (
<CapabilityContext.Provider value={serverCapabilities}>
<Tabs defaultValue="resources" className="w-full p-4"> <Tabs defaultValue="resources" className="w-full p-4">
<TabsList className="mb-4 p-0"> <TabsList className="mb-4 p-0">
<TabsTrigger value="resources"> <TabsTrigger value="resources" disabled={!serverCapabilities?.resources}>
<Files className="w-4 h-4 mr-2" /> <Files className="w-4 h-4 mr-2" />
Resources Resources
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="prompts"> <TabsTrigger value="prompts" disabled={!serverCapabilities?.prompts}>
<MessageSquare className="w-4 h-4 mr-2" /> <MessageSquare className="w-4 h-4 mr-2" />
Prompts Prompts
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="tools"> <TabsTrigger value="tools" disabled={!serverCapabilities?.tools}>
<Hammer className="w-4 h-4 mr-2" /> <Hammer className="w-4 h-4 mr-2" />
Tools Tools
</TabsTrigger> </TabsTrigger>
@@ -622,6 +636,7 @@ const App = () => {
/> />
</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,6 +56,16 @@ const PromptsTab = ({
return ( return (
<TabsContent value="prompts" className="grid grid-cols-2 gap-4"> <TabsContent value="prompts" className="grid grid-cols-2 gap-4">
{!capabilities?.prompts ? (
<Alert variant="destructive" className="col-span-2">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Feature Not Available</AlertTitle>
<AlertDescription>
The connected server does not support prompts.
</AlertDescription>
</Alert>
) : (
<>
<ListPane <ListPane
items={prompts} items={prompts}
listItems={listPrompts} listItems={listPrompts}
@@ -134,6 +146,8 @@ const PromptsTab = ({
)} )}
</div> </div>
</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,13 +64,22 @@ 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">
{!capabilities?.resources ? (
<Alert variant="destructive" className="col-span-3">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Feature Not Available</AlertTitle>
<AlertDescription>
The connected server does not support resources.
</AlertDescription>
</Alert>
) : (
<>
<ListPane <ListPane
items={resources} items={resources}
listItems={listResources} listItems={listResources}
@@ -198,6 +209,8 @@ const ResourcesTab = ({
)} )}
</div> </div>
</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,6 +112,16 @@ const ToolsTab = ({
return ( return (
<TabsContent value="tools" className="grid grid-cols-2 gap-4"> <TabsContent value="tools" className="grid grid-cols-2 gap-4">
{!capabilities?.tools ? (
<Alert variant="destructive" className="col-span-2">
<AlertCircle className="h-4 w-4" />
<AlertTitle>Feature Not Available</AlertTitle>
<AlertDescription>
The connected server does not support tools.
</AlertDescription>
</Alert>
) : (
<>
<ListPane <ListPane
items={tools} items={tools}
listItems={listTools} listItems={listTools}
@@ -214,6 +226,8 @@ const ToolsTab = ({
)} )}
</div> </div>
</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];
}