Skip to content

Commit eb70539

Browse files
authored
Merge pull request modelcontextprotocol#105 from devin-open-source/devin/1733551277-capability-negotiation
feat: implement capability negotiation for UI tabs
2 parents 2eae823 + 7878e17 commit eb70539

File tree

1 file changed

+131
-107
lines changed

1 file changed

+131
-107
lines changed

client/src/App.tsx

Lines changed: 131 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,11 @@ import {
1919
Request,
2020
Resource,
2121
ResourceTemplate,
22-
Result,
2322
Root,
2423
ServerNotification,
2524
Tool,
25+
ServerCapabilitiesSchema,
26+
Result,
2627
} from "@modelcontextprotocol/sdk/types.js";
2728
import { useCallback, useEffect, useRef, useState } from "react";
2829

@@ -43,7 +44,7 @@ import {
4344
} from "lucide-react";
4445

4546
import { toast } from "react-toastify";
46-
import { ZodType } from "zod";
47+
import { z, type ZodType } from "zod";
4748
import "./App.css";
4849
import ConsoleTab from "./components/ConsoleTab";
4950
import HistoryAndNotifications from "./components/History";
@@ -55,6 +56,8 @@ import SamplingTab, { PendingRequest } from "./components/SamplingTab";
5556
import Sidebar from "./components/Sidebar";
5657
import ToolsTab from "./components/ToolsTab";
5758

59+
type ServerCapabilities = z.infer<typeof ServerCapabilitiesSchema>;
60+
5861
const DEFAULT_REQUEST_TIMEOUT_MSEC = 10000;
5962

6063
const params = new URLSearchParams(window.location.search);
@@ -67,6 +70,7 @@ const App = () => {
6770
const [connectionStatus, setConnectionStatus] = useState<
6871
"disconnected" | "connected" | "error"
6972
>("disconnected");
73+
const [serverCapabilities, setServerCapabilities] = useState<ServerCapabilities | null>(null);
7074
const [resources, setResources] = useState<Resource[]>([]);
7175
const [resourceTemplates, setResourceTemplates] = useState<
7276
ResourceTemplate[]
@@ -455,6 +459,9 @@ const App = () => {
455459

456460
await client.connect(clientTransport);
457461

462+
const capabilities = client.getServerCapabilities();
463+
setServerCapabilities(capabilities ?? null);
464+
458465
client.setRequestHandler(CreateMessageRequestSchema, (request) => {
459466
return new Promise<CreateMessageResult>((resolve, reject) => {
460467
setPendingSampleRequests((prev) => [
@@ -497,20 +504,27 @@ const App = () => {
497504
<div className="flex-1 overflow-auto">
498505
{mcpClient ? (
499506
<Tabs
500-
defaultValue={window.location.hash.slice(1) || "resources"}
507+
defaultValue={
508+
Object.keys(serverCapabilities ?? {}).includes(window.location.hash.slice(1)) ?
509+
window.location.hash.slice(1) :
510+
serverCapabilities?.resources ? "resources" :
511+
serverCapabilities?.prompts ? "prompts" :
512+
serverCapabilities?.tools ? "tools" :
513+
"ping"
514+
}
501515
className="w-full p-4"
502516
onValueChange={(value) => (window.location.hash = value)}
503517
>
504518
<TabsList className="mb-4 p-0">
505-
<TabsTrigger value="resources">
519+
<TabsTrigger value="resources" disabled={!serverCapabilities?.resources}>
506520
<Files className="w-4 h-4 mr-2" />
507521
Resources
508522
</TabsTrigger>
509-
<TabsTrigger value="prompts">
523+
<TabsTrigger value="prompts" disabled={!serverCapabilities?.prompts}>
510524
<MessageSquare className="w-4 h-4 mr-2" />
511525
Prompts
512526
</TabsTrigger>
513-
<TabsTrigger value="tools">
527+
<TabsTrigger value="tools" disabled={!serverCapabilities?.tools}>
514528
<Hammer className="w-4 h-4 mr-2" />
515529
Tools
516530
</TabsTrigger>
@@ -534,107 +548,117 @@ const App = () => {
534548
</TabsList>
535549

536550
<div className="w-full">
537-
<ResourcesTab
538-
resources={resources}
539-
resourceTemplates={resourceTemplates}
540-
listResources={() => {
541-
clearError("resources");
542-
listResources();
543-
}}
544-
clearResources={() => {
545-
setResources([]);
546-
setNextResourceCursor(undefined);
547-
}}
548-
listResourceTemplates={() => {
549-
clearError("resources");
550-
listResourceTemplates();
551-
}}
552-
clearResourceTemplates={() => {
553-
setResourceTemplates([]);
554-
setNextResourceTemplateCursor(undefined);
555-
}}
556-
readResource={(uri) => {
557-
clearError("resources");
558-
readResource(uri);
559-
}}
560-
selectedResource={selectedResource}
561-
setSelectedResource={(resource) => {
562-
clearError("resources");
563-
setSelectedResource(resource);
564-
}}
565-
resourceContent={resourceContent}
566-
nextCursor={nextResourceCursor}
567-
nextTemplateCursor={nextResourceTemplateCursor}
568-
error={errors.resources}
569-
/>
570-
<PromptsTab
571-
prompts={prompts}
572-
listPrompts={() => {
573-
clearError("prompts");
574-
listPrompts();
575-
}}
576-
clearPrompts={() => {
577-
setPrompts([]);
578-
setNextPromptCursor(undefined);
579-
}}
580-
getPrompt={(name, args) => {
581-
clearError("prompts");
582-
getPrompt(name, args);
583-
}}
584-
selectedPrompt={selectedPrompt}
585-
setSelectedPrompt={(prompt) => {
586-
clearError("prompts");
587-
setSelectedPrompt(prompt);
588-
}}
589-
promptContent={promptContent}
590-
nextCursor={nextPromptCursor}
591-
error={errors.prompts}
592-
/>
593-
<ToolsTab
594-
tools={tools}
595-
listTools={() => {
596-
clearError("tools");
597-
listTools();
598-
}}
599-
clearTools={() => {
600-
setTools([]);
601-
setNextToolCursor(undefined);
602-
}}
603-
callTool={(name, params) => {
604-
clearError("tools");
605-
callTool(name, params);
606-
}}
607-
selectedTool={selectedTool}
608-
setSelectedTool={(tool) => {
609-
clearError("tools");
610-
setSelectedTool(tool);
611-
setToolResult(null);
612-
}}
613-
toolResult={toolResult}
614-
nextCursor={nextToolCursor}
615-
error={errors.tools}
616-
/>
617-
<ConsoleTab />
618-
<PingTab
619-
onPingClick={() => {
620-
void makeRequest(
621-
{
622-
method: "ping" as const,
623-
},
624-
EmptyResultSchema,
625-
);
626-
}}
627-
/>
628-
<SamplingTab
629-
pendingRequests={pendingSampleRequests}
630-
onApprove={handleApproveSampling}
631-
onReject={handleRejectSampling}
632-
/>
633-
<RootsTab
634-
roots={roots}
635-
setRoots={setRoots}
636-
onRootsChange={handleRootsChange}
637-
/>
551+
{!serverCapabilities?.resources && !serverCapabilities?.prompts && !serverCapabilities?.tools ? (
552+
<div className="flex items-center justify-center p-4">
553+
<p className="text-lg text-gray-500">
554+
The connected server does not support any MCP capabilities
555+
</p>
556+
</div>
557+
) : (
558+
<>
559+
<ResourcesTab
560+
resources={resources}
561+
resourceTemplates={resourceTemplates}
562+
listResources={() => {
563+
clearError("resources");
564+
listResources();
565+
}}
566+
clearResources={() => {
567+
setResources([]);
568+
setNextResourceCursor(undefined);
569+
}}
570+
listResourceTemplates={() => {
571+
clearError("resources");
572+
listResourceTemplates();
573+
}}
574+
clearResourceTemplates={() => {
575+
setResourceTemplates([]);
576+
setNextResourceTemplateCursor(undefined);
577+
}}
578+
readResource={(uri) => {
579+
clearError("resources");
580+
readResource(uri);
581+
}}
582+
selectedResource={selectedResource}
583+
setSelectedResource={(resource) => {
584+
clearError("resources");
585+
setSelectedResource(resource);
586+
}}
587+
resourceContent={resourceContent}
588+
nextCursor={nextResourceCursor}
589+
nextTemplateCursor={nextResourceTemplateCursor}
590+
error={errors.resources}
591+
/>
592+
<PromptsTab
593+
prompts={prompts}
594+
listPrompts={() => {
595+
clearError("prompts");
596+
listPrompts();
597+
}}
598+
clearPrompts={() => {
599+
setPrompts([]);
600+
setNextPromptCursor(undefined);
601+
}}
602+
getPrompt={(name, args) => {
603+
clearError("prompts");
604+
getPrompt(name, args);
605+
}}
606+
selectedPrompt={selectedPrompt}
607+
setSelectedPrompt={(prompt) => {
608+
clearError("prompts");
609+
setSelectedPrompt(prompt);
610+
}}
611+
promptContent={promptContent}
612+
nextCursor={nextPromptCursor}
613+
error={errors.prompts}
614+
/>
615+
<ToolsTab
616+
tools={tools}
617+
listTools={() => {
618+
clearError("tools");
619+
listTools();
620+
}}
621+
clearTools={() => {
622+
setTools([]);
623+
setNextToolCursor(undefined);
624+
}}
625+
callTool={(name, params) => {
626+
clearError("tools");
627+
callTool(name, params);
628+
}}
629+
selectedTool={selectedTool}
630+
setSelectedTool={(tool) => {
631+
clearError("tools");
632+
setSelectedTool(tool);
633+
setToolResult(null);
634+
}}
635+
toolResult={toolResult}
636+
nextCursor={nextToolCursor}
637+
error={errors.tools}
638+
/>
639+
<ConsoleTab />
640+
<PingTab
641+
onPingClick={() => {
642+
void makeRequest(
643+
{
644+
method: "ping" as const,
645+
},
646+
EmptyResultSchema,
647+
);
648+
}}
649+
/>
650+
<SamplingTab
651+
pendingRequests={pendingSampleRequests}
652+
onApprove={handleApproveSampling}
653+
onReject={handleRejectSampling}
654+
/>
655+
<RootsTab
656+
roots={roots}
657+
setRoots={setRoots}
658+
onRootsChange={handleRootsChange}
659+
/>
660+
</>
661+
)}
638662
</div>
639663
</Tabs>
640664
) : (

0 commit comments

Comments
 (0)