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:
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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
13
package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user