From e96b3be15941c36d51fcb5f1c88b430a1eb5fefb Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Sat, 7 Dec 2024 06:15:21 +0000 Subject: [PATCH] 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 --- client/src/App.tsx | 287 +++++++++++++------------ client/src/components/PromptsTab.tsx | 164 +++++++------- client/src/components/ResourcesTab.tsx | 267 ++++++++++++----------- client/src/components/ToolsTab.tsx | 220 ++++++++++--------- client/src/lib/contexts.ts | 12 ++ 5 files changed, 509 insertions(+), 441 deletions(-) create mode 100644 client/src/lib/contexts.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index 2c84377..2f36450 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -55,6 +55,8 @@ import SamplingTab, { PendingRequest } from "./components/SamplingTab"; import Sidebar from "./components/Sidebar"; import ToolsTab from "./components/ToolsTab"; +import { CapabilityContext, ServerCapabilities } from "@/lib/contexts"; + const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000; const params = new URLSearchParams(window.location.search); @@ -66,6 +68,7 @@ const App = () => { const [connectionStatus, setConnectionStatus] = useState< "disconnected" | "connected" | "error" >("disconnected"); + const [serverCapabilities, setServerCapabilities] = useState(null); const [resources, setResources] = useState([]); const [resourceTemplates, setResourceTemplates] = useState< ResourceTemplate[] @@ -217,6 +220,13 @@ const App = () => { rootsRef.current = roots; }, [roots]); + useEffect(() => { + if (mcpClient) { + const capabilities = mcpClient.getServerCapabilities(); + setServerCapabilities(capabilities ?? null); + } + }, [mcpClient]); + const pushHistory = (request: object, response?: object) => { setRequestHistory((prev) => [ ...prev, @@ -444,6 +454,9 @@ const App = () => { await client.connect(clientTransport); + const capabilities = client.getServerCapabilities(); + setServerCapabilities(capabilities ?? null); + client.setRequestHandler(CreateMessageRequestSchema, (request) => { return new Promise((resolve, reject) => { setPendingSampleRequests((prev) => [ @@ -485,143 +498,145 @@ const App = () => {
{mcpClient ? ( - - - - - Resources - - - - Prompts - - - - Tools - - - - Ping - - - - Sampling - {pendingSampleRequests.length > 0 && ( - - {pendingSampleRequests.length} - - )} - - - - Roots - - + + + + + + Resources + + + + Prompts + + + + Tools + + + + Ping + + + + Sampling + {pendingSampleRequests.length > 0 && ( + + {pendingSampleRequests.length} + + )} + + + + Roots + + -
- { - clearError("resources"); - listResources(); - }} - clearResources={() => { - setResources([]); - setNextResourceCursor(undefined); - }} - listResourceTemplates={() => { - clearError("resources"); - listResourceTemplates(); - }} - clearResourceTemplates={() => { - setResourceTemplates([]); - setNextResourceTemplateCursor(undefined); - }} - readResource={(uri) => { - clearError("resources"); - readResource(uri); - }} - selectedResource={selectedResource} - setSelectedResource={(resource) => { - clearError("resources"); - setSelectedResource(resource); - }} - resourceContent={resourceContent} - nextCursor={nextResourceCursor} - nextTemplateCursor={nextResourceTemplateCursor} - error={errors.resources} - /> - { - clearError("prompts"); - listPrompts(); - }} - clearPrompts={() => { - setPrompts([]); - setNextPromptCursor(undefined); - }} - getPrompt={(name, args) => { - clearError("prompts"); - getPrompt(name, args); - }} - selectedPrompt={selectedPrompt} - setSelectedPrompt={(prompt) => { - clearError("prompts"); - setSelectedPrompt(prompt); - }} - promptContent={promptContent} - nextCursor={nextPromptCursor} - error={errors.prompts} - /> - { - clearError("tools"); - listTools(); - }} - clearTools={() => { - setTools([]); - setNextToolCursor(undefined); - }} - callTool={(name, params) => { - clearError("tools"); - callTool(name, params); - }} - selectedTool={selectedTool} - setSelectedTool={(tool) => { - clearError("tools"); - setSelectedTool(tool); - setToolResult(null); - }} - toolResult={toolResult} - nextCursor={nextToolCursor} - error={errors.tools} - /> - - { - void makeRequest( - { - method: "ping" as const, - }, - EmptyResultSchema, - ); - }} - /> - - -
-
+
+ { + clearError("resources"); + listResources(); + }} + clearResources={() => { + setResources([]); + setNextResourceCursor(undefined); + }} + listResourceTemplates={() => { + clearError("resources"); + listResourceTemplates(); + }} + clearResourceTemplates={() => { + setResourceTemplates([]); + setNextResourceTemplateCursor(undefined); + }} + readResource={(uri) => { + clearError("resources"); + readResource(uri); + }} + selectedResource={selectedResource} + setSelectedResource={(resource) => { + clearError("resources"); + setSelectedResource(resource); + }} + resourceContent={resourceContent} + nextCursor={nextResourceCursor} + nextTemplateCursor={nextResourceTemplateCursor} + error={errors.resources} + /> + { + clearError("prompts"); + listPrompts(); + }} + clearPrompts={() => { + setPrompts([]); + setNextPromptCursor(undefined); + }} + getPrompt={(name, args) => { + clearError("prompts"); + getPrompt(name, args); + }} + selectedPrompt={selectedPrompt} + setSelectedPrompt={(prompt) => { + clearError("prompts"); + setSelectedPrompt(prompt); + }} + promptContent={promptContent} + nextCursor={nextPromptCursor} + error={errors.prompts} + /> + { + clearError("tools"); + listTools(); + }} + clearTools={() => { + setTools([]); + setNextToolCursor(undefined); + }} + callTool={(name, params) => { + clearError("tools"); + callTool(name, params); + }} + selectedTool={selectedTool} + setSelectedTool={(tool) => { + clearError("tools"); + setSelectedTool(tool); + setToolResult(null); + }} + toolResult={toolResult} + nextCursor={nextToolCursor} + error={errors.tools} + /> + + { + void makeRequest( + { + method: "ping" as const, + }, + EmptyResultSchema, + ); + }} + /> + + +
+
+ ) : (

diff --git a/client/src/components/PromptsTab.tsx b/client/src/components/PromptsTab.tsx index df8b8a5..5882e7e 100644 --- a/client/src/components/PromptsTab.tsx +++ b/client/src/components/PromptsTab.tsx @@ -6,8 +6,9 @@ import { TabsContent } from "@/components/ui/tabs"; import { Textarea } from "@/components/ui/textarea"; import { ListPromptsResult } from "@modelcontextprotocol/sdk/types.js"; import { AlertCircle } from "lucide-react"; -import { useState } from "react"; +import { useState, useContext } from "react"; import ListPane from "./ListPane"; +import { CapabilityContext } from "@/lib/contexts"; export type Prompt = { name: string; @@ -40,6 +41,7 @@ const PromptsTab = ({ nextCursor: ListPromptsResult["nextCursor"]; error: string | null; }) => { + const capabilities = useContext(CapabilityContext); const [promptArgs, setPromptArgs] = useState>({}); const handleInputChange = (argName: string, value: string) => { @@ -54,86 +56,98 @@ const PromptsTab = ({ return ( - { - setSelectedPrompt(prompt); - setPromptArgs({}); - }} - renderItem={(prompt) => ( - <> - {prompt.name} - {prompt.description} - - )} - title="Prompts" - buttonText={nextCursor ? "List More Prompts" : "List Prompts"} - isButtonDisabled={!nextCursor && prompts.length > 0} - /> + {!capabilities?.prompts ? ( + + + Feature Not Available + + The connected server does not support prompts. + + + ) : ( + <> + { + setSelectedPrompt(prompt); + setPromptArgs({}); + }} + renderItem={(prompt) => ( + <> + {prompt.name} + {prompt.description} + + )} + title="Prompts" + buttonText={nextCursor ? "List More Prompts" : "List Prompts"} + isButtonDisabled={!nextCursor && prompts.length > 0} + /> -

-
-

- {selectedPrompt ? selectedPrompt.name : "Select a prompt"} -

-
-
- {error ? ( - - - Error - {error} - - ) : selectedPrompt ? ( -
- {selectedPrompt.description && ( -

- {selectedPrompt.description} -

- )} - {selectedPrompt.arguments?.map((arg) => ( -
- - - handleInputChange(arg.name, e.target.value) - } - /> - {arg.description && ( -

- {arg.description} - {arg.required && ( - (Required) - )} +

+
+

+ {selectedPrompt ? selectedPrompt.name : "Select a prompt"} +

+
+
+ {error ? ( + + + Error + {error} + + ) : selectedPrompt ? ( +
+ {selectedPrompt.description && ( +

+ {selectedPrompt.description}

)} + {selectedPrompt.arguments?.map((arg) => ( +
+ + + handleInputChange(arg.name, e.target.value) + } + /> + {arg.description && ( +

+ {arg.description} + {arg.required && ( + (Required) + )} +

+ )} +
+ ))} + + {promptContent && ( +