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:
@@ -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">
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
12
client/src/lib/contexts.ts
Normal file
12
client/src/lib/contexts.ts
Normal 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];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user