Merge pull request #52 from modelcontextprotocol/justin/tab-specific-errors

Separate error states per tab, clear errors when clicking around
This commit is contained in:
Justin Spahr-Summers
2024-11-12 15:44:15 +00:00
committed by GitHub
5 changed files with 91 additions and 54 deletions

View File

@@ -33,6 +33,7 @@
"lucide-react": "^0.447.0", "lucide-react": "^0.447.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-toastify": "^10.0.6",
"serve-handler": "^6.1.6", "serve-handler": "^6.1.6",
"tailwind-merge": "^2.5.3", "tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",

View File

@@ -1,8 +1,10 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js"; import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import { import {
CompatibilityCallToolResultSchema, ClientNotification,
ClientRequest, ClientRequest,
CompatibilityCallToolResult,
CompatibilityCallToolResultSchema,
CreateMessageRequestSchema, CreateMessageRequestSchema,
CreateMessageResult, CreateMessageResult,
EmptyResultSchema, EmptyResultSchema,
@@ -19,8 +21,6 @@ import {
Root, Root,
ServerNotification, ServerNotification,
Tool, Tool,
CompatibilityCallToolResult,
ClientNotification,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import { useCallback, useEffect, useRef, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
// Add dark mode class based on system preference // Add dark mode class based on system preference
@@ -32,21 +32,21 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { import {
Bell, Bell,
Files, Files,
FolderTree,
Hammer, Hammer,
Hash, Hash,
MessageSquare, MessageSquare,
Send, Send,
Terminal, Terminal,
FolderTree,
} from "lucide-react"; } from "lucide-react";
import { toast } from "react-toastify";
import { ZodType } from "zod"; import { 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";
import PingTab from "./components/PingTab"; import PingTab from "./components/PingTab";
import PromptsTab, { Prompt } from "./components/PromptsTab"; import PromptsTab, { Prompt } from "./components/PromptsTab";
import RequestsTab from "./components/RequestsTabs";
import ResourcesTab from "./components/ResourcesTab"; import ResourcesTab from "./components/ResourcesTab";
import RootsTab from "./components/RootsTab"; import RootsTab from "./components/RootsTab";
import SamplingTab, { PendingRequest } from "./components/SamplingTab"; import SamplingTab, { PendingRequest } from "./components/SamplingTab";
@@ -67,7 +67,11 @@ const App = () => {
const [tools, setTools] = useState<Tool[]>([]); const [tools, setTools] = useState<Tool[]>([]);
const [toolResult, setToolResult] = const [toolResult, setToolResult] =
useState<CompatibilityCallToolResult | null>(null); useState<CompatibilityCallToolResult | null>(null);
const [error, setError] = useState<string | null>(null); const [errors, setErrors] = useState<Record<string, string | null>>({
resources: null,
prompts: null,
tools: null,
});
const [command, setCommand] = useState<string>(() => { const [command, setCommand] = useState<string>(() => {
return localStorage.getItem("lastCommand") || "mcp-server-everything"; return localStorage.getItem("lastCommand") || "mcp-server-everything";
}); });
@@ -202,9 +206,14 @@ const App = () => {
]); ]);
}; };
const clearError = (tabKey: keyof typeof errors) => {
setErrors((prev) => ({ ...prev, [tabKey]: null }));
};
const makeRequest = async <T extends ZodType<object>>( const makeRequest = async <T extends ZodType<object>>(
request: ClientRequest, request: ClientRequest,
schema: T, schema: T,
tabKey?: keyof typeof errors,
) => { ) => {
if (!mcpClient) { if (!mcpClient) {
throw new Error("MCP client not connected"); throw new Error("MCP client not connected");
@@ -213,9 +222,19 @@ const App = () => {
try { try {
const response = await mcpClient.request(request, schema); const response = await mcpClient.request(request, schema);
pushHistory(request, response); pushHistory(request, response);
if (tabKey !== undefined) {
clearError(tabKey);
}
return response; return response;
} catch (e: unknown) { } catch (e: unknown) {
setError((e as Error).message); if (tabKey === undefined) {
toast.error((e as Error).message);
} else {
setErrors((prev) => ({ ...prev, [tabKey]: (e as Error).message }));
}
throw e; throw e;
} }
}; };
@@ -229,7 +248,7 @@ const App = () => {
await mcpClient.notification(notification); await mcpClient.notification(notification);
pushHistory(notification); pushHistory(notification);
} catch (e: unknown) { } catch (e: unknown) {
setError((e as Error).message); toast.error((e as Error).message);
throw e; throw e;
} }
}; };
@@ -241,6 +260,7 @@ const App = () => {
params: nextResourceCursor ? { cursor: nextResourceCursor } : {}, params: nextResourceCursor ? { cursor: nextResourceCursor } : {},
}, },
ListResourcesResultSchema, ListResourcesResultSchema,
"resources",
); );
setResources(resources.concat(response.resources ?? [])); setResources(resources.concat(response.resources ?? []));
setNextResourceCursor(response.nextCursor); setNextResourceCursor(response.nextCursor);
@@ -255,6 +275,7 @@ const App = () => {
: {}, : {},
}, },
ListResourceTemplatesResultSchema, ListResourceTemplatesResultSchema,
"resources",
); );
setResourceTemplates( setResourceTemplates(
resourceTemplates.concat(response.resourceTemplates ?? []), resourceTemplates.concat(response.resourceTemplates ?? []),
@@ -269,6 +290,7 @@ const App = () => {
params: { uri }, params: { uri },
}, },
ReadResourceResultSchema, ReadResourceResultSchema,
"resources",
); );
setResourceContent(JSON.stringify(response, null, 2)); setResourceContent(JSON.stringify(response, null, 2));
}; };
@@ -280,6 +302,7 @@ const App = () => {
params: nextPromptCursor ? { cursor: nextPromptCursor } : {}, params: nextPromptCursor ? { cursor: nextPromptCursor } : {},
}, },
ListPromptsResultSchema, ListPromptsResultSchema,
"prompts",
); );
setPrompts(response.prompts); setPrompts(response.prompts);
setNextPromptCursor(response.nextCursor); setNextPromptCursor(response.nextCursor);
@@ -292,6 +315,7 @@ const App = () => {
params: { name, arguments: args }, params: { name, arguments: args },
}, },
GetPromptResultSchema, GetPromptResultSchema,
"prompts",
); );
setPromptContent(JSON.stringify(response, null, 2)); setPromptContent(JSON.stringify(response, null, 2));
}; };
@@ -303,6 +327,7 @@ const App = () => {
params: nextToolCursor ? { cursor: nextToolCursor } : {}, params: nextToolCursor ? { cursor: nextToolCursor } : {},
}, },
ListToolsResultSchema, ListToolsResultSchema,
"tools",
); );
setTools(response.tools); setTools(response.tools);
setNextToolCursor(response.nextCursor); setNextToolCursor(response.nextCursor);
@@ -321,6 +346,7 @@ const App = () => {
}, },
}, },
CompatibilityCallToolResultSchema, CompatibilityCallToolResultSchema,
"tools",
); );
setToolResult(response); setToolResult(response);
}; };
@@ -445,39 +471,66 @@ const App = () => {
<ResourcesTab <ResourcesTab
resources={resources} resources={resources}
resourceTemplates={resourceTemplates} resourceTemplates={resourceTemplates}
listResources={listResources} listResources={() => {
listResourceTemplates={listResourceTemplates} clearError("resources");
readResource={readResource} listResources();
}}
listResourceTemplates={() => {
clearError("resources");
listResourceTemplates();
}}
readResource={(uri) => {
clearError("resources");
readResource(uri);
}}
selectedResource={selectedResource} selectedResource={selectedResource}
setSelectedResource={setSelectedResource} setSelectedResource={(resource) => {
clearError("resources");
setSelectedResource(resource);
}}
resourceContent={resourceContent} resourceContent={resourceContent}
nextCursor={nextResourceCursor} nextCursor={nextResourceCursor}
nextTemplateCursor={nextResourceTemplateCursor} nextTemplateCursor={nextResourceTemplateCursor}
error={error} error={errors.resources}
/> />
<PromptsTab <PromptsTab
prompts={prompts} prompts={prompts}
listPrompts={listPrompts} listPrompts={() => {
getPrompt={getPrompt} clearError("prompts");
listPrompts();
}}
getPrompt={(name, args) => {
clearError("prompts");
getPrompt(name, args);
}}
selectedPrompt={selectedPrompt} selectedPrompt={selectedPrompt}
setSelectedPrompt={setSelectedPrompt} setSelectedPrompt={(prompt) => {
clearError("prompts");
setSelectedPrompt(prompt);
}}
promptContent={promptContent} promptContent={promptContent}
nextCursor={nextPromptCursor} nextCursor={nextPromptCursor}
error={error} error={errors.prompts}
/> />
<RequestsTab />
<ToolsTab <ToolsTab
tools={tools} tools={tools}
listTools={listTools} listTools={() => {
callTool={callTool} clearError("tools");
listTools();
}}
callTool={(name, params) => {
clearError("tools");
callTool(name, params);
}}
selectedTool={selectedTool} selectedTool={selectedTool}
setSelectedTool={(tool) => { setSelectedTool={(tool) => {
clearError("tools");
setSelectedTool(tool); setSelectedTool(tool);
setToolResult(null); setToolResult(null);
}} }}
toolResult={toolResult} toolResult={toolResult}
nextCursor={nextToolCursor} nextCursor={nextToolCursor}
error={error} error={errors.tools}
/> />
<ConsoleTab /> <ConsoleTab />
<PingTab <PingTab

View File

@@ -1,33 +0,0 @@
import { TabsContent } from "@/components/ui/tabs";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Send } from "lucide-react";
const RequestsTab = () => (
<TabsContent value="requests" className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div className="space-y-4">
<div className="flex space-x-2">
<Input placeholder="Method name" />
<Button>
<Send className="w-4 h-4 mr-2" />
Send
</Button>
</div>
<Textarea
placeholder="Request parameters (JSON)"
className="h-64 font-mono"
/>
</div>
<div>
<div className="bg-gray-50 p-4 rounded-lg h-96 font-mono text-sm overflow-auto">
<div className="text-gray-500 mb-2">Response:</div>
{/* Response content would go here */}
</div>
</div>
</div>
</TabsContent>
);
export default RequestsTab;

View File

@@ -1,10 +1,13 @@
import { StrictMode } from "react"; import { StrictMode } from "react";
import { createRoot } from "react-dom/client"; import { createRoot } from "react-dom/client";
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import App from "./App.tsx"; import App from "./App.tsx";
import "./index.css"; import "./index.css";
createRoot(document.getElementById("root")!).render( createRoot(document.getElementById("root")!).render(
<StrictMode> <StrictMode>
<App /> <App />
<ToastContainer />
</StrictMode>, </StrictMode>,
); );

13
package-lock.json generated
View File

@@ -39,6 +39,7 @@
"lucide-react": "^0.447.0", "lucide-react": "^0.447.0",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-toastify": "^10.0.6",
"serve-handler": "^6.1.6", "serve-handler": "^6.1.6",
"tailwind-merge": "^2.5.3", "tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
@@ -4676,6 +4677,18 @@
} }
} }
}, },
"node_modules/react-toastify": {
"version": "10.0.6",
"resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.6.tgz",
"integrity": "sha512-yYjp+omCDf9lhZcrZHKbSq7YMuK0zcYkDFTzfRFgTXkTFHZ1ToxwAonzA4JI5CxA91JpjFLmwEsZEgfYfOqI1A==",
"dependencies": {
"clsx": "^2.1.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/read-cache": { "node_modules/read-cache": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",