Merge pull request #105 from devin-open-source/devin/1733551277-capability-negotiation

feat: implement capability negotiation for UI tabs
This commit is contained in:
Ashwin Bhat
2024-12-09 03:56:58 -08:00
committed by GitHub

View File

@@ -19,10 +19,11 @@ import {
Request, Request,
Resource, Resource,
ResourceTemplate, ResourceTemplate,
Result,
Root, Root,
ServerNotification, ServerNotification,
Tool, Tool,
ServerCapabilitiesSchema,
Result,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
@@ -43,7 +44,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { ZodType } from "zod"; import { z, type 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";
@@ -55,6 +56,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";
type ServerCapabilities = z.infer<typeof ServerCapabilitiesSchema>;
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);
@@ -67,6 +70,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[]
@@ -455,6 +459,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) => [
@@ -497,20 +504,27 @@ const App = () => {
<div className="flex-1 overflow-auto"> <div className="flex-1 overflow-auto">
{mcpClient ? ( {mcpClient ? (
<Tabs <Tabs
defaultValue={window.location.hash.slice(1) || "resources"} defaultValue={
Object.keys(serverCapabilities ?? {}).includes(window.location.hash.slice(1)) ?
window.location.hash.slice(1) :
serverCapabilities?.resources ? "resources" :
serverCapabilities?.prompts ? "prompts" :
serverCapabilities?.tools ? "tools" :
"ping"
}
className="w-full p-4" className="w-full p-4"
onValueChange={(value) => (window.location.hash = value)} onValueChange={(value) => (window.location.hash = value)}
> >
<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>
@@ -534,107 +548,117 @@ const App = () => {
</TabsList> </TabsList>
<div className="w-full"> <div className="w-full">
<ResourcesTab {!serverCapabilities?.resources && !serverCapabilities?.prompts && !serverCapabilities?.tools ? (
resources={resources} <div className="flex items-center justify-center p-4">
resourceTemplates={resourceTemplates} <p className="text-lg text-gray-500">
listResources={() => { The connected server does not support any MCP capabilities
clearError("resources"); </p>
listResources(); </div>
}} ) : (
clearResources={() => { <>
setResources([]); <ResourcesTab
setNextResourceCursor(undefined); resources={resources}
}} resourceTemplates={resourceTemplates}
listResourceTemplates={() => { listResources={() => {
clearError("resources"); clearError("resources");
listResourceTemplates(); listResources();
}} }}
clearResourceTemplates={() => { clearResources={() => {
setResourceTemplates([]); setResources([]);
setNextResourceTemplateCursor(undefined); setNextResourceCursor(undefined);
}} }}
readResource={(uri) => { listResourceTemplates={() => {
clearError("resources"); clearError("resources");
readResource(uri); listResourceTemplates();
}} }}
selectedResource={selectedResource} clearResourceTemplates={() => {
setSelectedResource={(resource) => { setResourceTemplates([]);
clearError("resources"); setNextResourceTemplateCursor(undefined);
setSelectedResource(resource); }}
}} readResource={(uri) => {
resourceContent={resourceContent} clearError("resources");
nextCursor={nextResourceCursor} readResource(uri);
nextTemplateCursor={nextResourceTemplateCursor} }}
error={errors.resources} selectedResource={selectedResource}
/> setSelectedResource={(resource) => {
<PromptsTab clearError("resources");
prompts={prompts} setSelectedResource(resource);
listPrompts={() => { }}
clearError("prompts"); resourceContent={resourceContent}
listPrompts(); nextCursor={nextResourceCursor}
}} nextTemplateCursor={nextResourceTemplateCursor}
clearPrompts={() => { error={errors.resources}
setPrompts([]); />
setNextPromptCursor(undefined); <PromptsTab
}} prompts={prompts}
getPrompt={(name, args) => { listPrompts={() => {
clearError("prompts"); clearError("prompts");
getPrompt(name, args); listPrompts();
}} }}
selectedPrompt={selectedPrompt} clearPrompts={() => {
setSelectedPrompt={(prompt) => { setPrompts([]);
clearError("prompts"); setNextPromptCursor(undefined);
setSelectedPrompt(prompt); }}
}} getPrompt={(name, args) => {
promptContent={promptContent} clearError("prompts");
nextCursor={nextPromptCursor} getPrompt(name, args);
error={errors.prompts} }}
/> selectedPrompt={selectedPrompt}
<ToolsTab setSelectedPrompt={(prompt) => {
tools={tools} clearError("prompts");
listTools={() => { setSelectedPrompt(prompt);
clearError("tools"); }}
listTools(); promptContent={promptContent}
}} nextCursor={nextPromptCursor}
clearTools={() => { error={errors.prompts}
setTools([]); />
setNextToolCursor(undefined); <ToolsTab
}} tools={tools}
callTool={(name, params) => { listTools={() => {
clearError("tools"); clearError("tools");
callTool(name, params); listTools();
}} }}
selectedTool={selectedTool} clearTools={() => {
setSelectedTool={(tool) => { setTools([]);
clearError("tools"); setNextToolCursor(undefined);
setSelectedTool(tool); }}
setToolResult(null); callTool={(name, params) => {
}} clearError("tools");
toolResult={toolResult} callTool(name, params);
nextCursor={nextToolCursor} }}
error={errors.tools} selectedTool={selectedTool}
/> setSelectedTool={(tool) => {
<ConsoleTab /> clearError("tools");
<PingTab setSelectedTool(tool);
onPingClick={() => { setToolResult(null);
void makeRequest( }}
{ toolResult={toolResult}
method: "ping" as const, nextCursor={nextToolCursor}
}, error={errors.tools}
EmptyResultSchema, />
); <ConsoleTab />
}} <PingTab
/> onPingClick={() => {
<SamplingTab void makeRequest(
pendingRequests={pendingSampleRequests} {
onApprove={handleApproveSampling} method: "ping" as const,
onReject={handleRejectSampling} },
/> EmptyResultSchema,
<RootsTab );
roots={roots} }}
setRoots={setRoots} />
onRootsChange={handleRootsChange} <SamplingTab
/> pendingRequests={pendingSampleRequests}
onApprove={handleApproveSampling}
onReject={handleRejectSampling}
/>
<RootsTab
roots={roots}
setRoots={setRoots}
onRootsChange={handleRootsChange}
/>
</>
)}
</div> </div>
</Tabs> </Tabs>
) : ( ) : (