Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"lucide-react": "^0.447.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-toastify": "^10.0.6",
"serve-handler": "^6.1.6",
"tailwind-merge": "^2.5.3",
"tailwindcss-animate": "^1.0.7",
Expand Down
95 changes: 74 additions & 21 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
import { SSEClientTransport } from "@modelcontextprotocol/sdk/client/sse.js";
import {
CompatibilityCallToolResultSchema,
ClientNotification,
ClientRequest,
CompatibilityCallToolResult,
CompatibilityCallToolResultSchema,
CreateMessageRequestSchema,
CreateMessageResult,
EmptyResultSchema,
Expand All @@ -19,8 +21,6 @@ import {
Root,
ServerNotification,
Tool,
CompatibilityCallToolResult,
ClientNotification,
} from "@modelcontextprotocol/sdk/types.js";
import { useCallback, useEffect, useRef, useState } from "react";
// Add dark mode class based on system preference
Expand All @@ -32,21 +32,21 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Bell,
Files,
FolderTree,
Hammer,
Hash,
MessageSquare,
Send,
Terminal,
FolderTree,
} from "lucide-react";

import { toast } from "react-toastify";
import { ZodType } from "zod";
import "./App.css";
import ConsoleTab from "./components/ConsoleTab";
import HistoryAndNotifications from "./components/History";
import PingTab from "./components/PingTab";
import PromptsTab, { Prompt } from "./components/PromptsTab";
import RequestsTab from "./components/RequestsTabs";
import ResourcesTab from "./components/ResourcesTab";
import RootsTab from "./components/RootsTab";
import SamplingTab, { PendingRequest } from "./components/SamplingTab";
Expand All @@ -67,7 +67,11 @@ const App = () => {
const [tools, setTools] = useState<Tool[]>([]);
const [toolResult, setToolResult] =
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>(() => {
return localStorage.getItem("lastCommand") || "mcp-server-everything";
});
Expand Down Expand Up @@ -202,9 +206,14 @@ const App = () => {
]);
};

const clearError = (tabKey: keyof typeof errors) => {
setErrors((prev) => ({ ...prev, [tabKey]: null }));
};

const makeRequest = async <T extends ZodType<object>>(
request: ClientRequest,
schema: T,
tabKey?: keyof typeof errors,
) => {
if (!mcpClient) {
throw new Error("MCP client not connected");
Expand All @@ -213,9 +222,19 @@ const App = () => {
try {
const response = await mcpClient.request(request, schema);
pushHistory(request, response);

if (tabKey !== undefined) {
clearError(tabKey);
}

return response;
} 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 }));
}
Comment on lines +234 to +236
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since we have a clearErrors would it make sense to have a setError that takes a specific tab key and sets the error?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would only be used in this and clearError, whereas the latter gets used in many more places. I'd prefer to keep it to this for now.


throw e;
}
};
Expand All @@ -229,7 +248,7 @@ const App = () => {
await mcpClient.notification(notification);
pushHistory(notification);
} catch (e: unknown) {
setError((e as Error).message);
toast.error((e as Error).message);
throw e;
}
};
Expand All @@ -241,6 +260,7 @@ const App = () => {
params: nextResourceCursor ? { cursor: nextResourceCursor } : {},
},
ListResourcesResultSchema,
"resources",
);
setResources(resources.concat(response.resources ?? []));
setNextResourceCursor(response.nextCursor);
Expand All @@ -255,6 +275,7 @@ const App = () => {
: {},
},
ListResourceTemplatesResultSchema,
"resources",
);
setResourceTemplates(
resourceTemplates.concat(response.resourceTemplates ?? []),
Expand All @@ -269,6 +290,7 @@ const App = () => {
params: { uri },
},
ReadResourceResultSchema,
"resources",
);
setResourceContent(JSON.stringify(response, null, 2));
};
Expand All @@ -280,6 +302,7 @@ const App = () => {
params: nextPromptCursor ? { cursor: nextPromptCursor } : {},
},
ListPromptsResultSchema,
"prompts",
);
setPrompts(response.prompts);
setNextPromptCursor(response.nextCursor);
Expand All @@ -292,6 +315,7 @@ const App = () => {
params: { name, arguments: args },
},
GetPromptResultSchema,
"prompts",
);
setPromptContent(JSON.stringify(response, null, 2));
};
Expand All @@ -303,6 +327,7 @@ const App = () => {
params: nextToolCursor ? { cursor: nextToolCursor } : {},
},
ListToolsResultSchema,
"tools",
);
setTools(response.tools);
setNextToolCursor(response.nextCursor);
Expand All @@ -321,6 +346,7 @@ const App = () => {
},
},
CompatibilityCallToolResultSchema,
"tools",
);
setToolResult(response);
};
Expand Down Expand Up @@ -445,39 +471,66 @@ const App = () => {
<ResourcesTab
resources={resources}
resourceTemplates={resourceTemplates}
listResources={listResources}
listResourceTemplates={listResourceTemplates}
readResource={readResource}
listResources={() => {
clearError("resources");
listResources();
}}
listResourceTemplates={() => {
clearError("resources");
listResourceTemplates();
}}
readResource={(uri) => {
clearError("resources");
readResource(uri);
}}
selectedResource={selectedResource}
setSelectedResource={setSelectedResource}
setSelectedResource={(resource) => {
clearError("resources");
setSelectedResource(resource);
}}
resourceContent={resourceContent}
nextCursor={nextResourceCursor}
nextTemplateCursor={nextResourceTemplateCursor}
error={error}
error={errors.resources}
/>
<PromptsTab
prompts={prompts}
listPrompts={listPrompts}
getPrompt={getPrompt}
listPrompts={() => {
clearError("prompts");
listPrompts();
}}
getPrompt={(name, args) => {
clearError("prompts");
getPrompt(name, args);
}}
selectedPrompt={selectedPrompt}
setSelectedPrompt={setSelectedPrompt}
setSelectedPrompt={(prompt) => {
clearError("prompts");
setSelectedPrompt(prompt);
}}
promptContent={promptContent}
nextCursor={nextPromptCursor}
error={error}
error={errors.prompts}
/>
<RequestsTab />
<ToolsTab
tools={tools}
listTools={listTools}
callTool={callTool}
listTools={() => {
clearError("tools");
listTools();
}}
callTool={(name, params) => {
clearError("tools");
callTool(name, params);
}}
selectedTool={selectedTool}
setSelectedTool={(tool) => {
clearError("tools");
setSelectedTool(tool);
setToolResult(null);
}}
toolResult={toolResult}
nextCursor={nextToolCursor}
error={error}
error={errors.tools}
/>
<ConsoleTab />
<PingTab
Expand Down
33 changes: 0 additions & 33 deletions client/src/components/RequestsTabs.tsx

This file was deleted.

3 changes: 3 additions & 0 deletions client/src/main.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import App from "./App.tsx";
import "./index.css";

createRoot(document.getElementById("root")!).render(
<StrictMode>
<App />
<ToastContainer />
</StrictMode>,
);
13 changes: 13 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.