Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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 @@ -17,6 +17,7 @@
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"lucide-react": "^0.447.0",
"mcp-typescript": "file:../packages/mcp-typescript",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwind-merge": "^2.5.3",
Expand Down
212 changes: 116 additions & 96 deletions client/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
import { useState, useEffect } from "react";
import { Client } from "mcp-typescript/client/index.js";
import { SSEClientTransport } from "mcp-typescript/client/sse.js";
import {
ListResourcesResultSchema,
GetPromptResultSchema,
ListToolsResultSchema,
ReadResourceResultSchema,
CallToolResultSchema,
ListPromptsResultSchema,
} from "mcp-typescript/types.js";
import { useState } from "react";
import {
Send,
Bell,
Expand All @@ -22,7 +32,6 @@ import ToolsTab, { Tool as ToolType } from "./components/ToolsTab";
import History from "./components/History";

const App = () => {
const [socket, setSocket] = useState<WebSocket | null>(null);
const [connectionStatus, setConnectionStatus] = useState<
"disconnected" | "connected" | "error"
>("disconnected");
Expand All @@ -39,121 +48,132 @@ const App = () => {
const [args, setArgs] = useState<string>(
"/Users/ashwin/code/example-servers/build/everything/index.js",
);
const [mcpConnected, setMcpConnected] = useState<boolean>(false);
const [requestHistory, setRequestHistory] = useState<
Array<{ request: string; response: string | null }>
{ request: string; response: string }[]
>([]);
const [mcpClient, setMcpClient] = useState<Client | null>(null);

useEffect(() => {
const ws = new WebSocket("ws://localhost:3000");

ws.onopen = () => {
console.log("Connected to WebSocket server");
setConnectionStatus("connected");
setSocket(ws);
};

ws.onmessage = (event) => {
const message = JSON.parse(event.data);
console.log("Received message:", message);
if (message.type === "resources") {
setResources(message.data.resources);
setError(null);
} else if (message.type === "resource") {
setResourceContent(JSON.stringify(message.data, null, 2));
setError(null);
} else if (message.type === "prompts") {
setPrompts(message.data.prompts);
setError(null);
} else if (message.type === "prompt") {
setPromptContent(JSON.stringify(message.data, null, 2));
setError(null);
} else if (message.type === "tools") {
setTools(message.data.tools);
setError(null);
} else if (message.type === "toolResult") {
setToolResult(JSON.stringify(message.data, null, 2));
setError(null);
} else if (message.type === "error") {
setError(message.message);
} else if (message.type === "connected") {
setMcpConnected(true);
}

updateRequestHistory(message);
};

ws.onerror = () => {
setConnectionStatus("error");
};
const [selectedResource, setSelectedResource] = useState<Resource | null>(
null,
);
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
const [selectedTool, setSelectedTool] = useState<ToolType | null>(null);

ws.onclose = () => {
setConnectionStatus("disconnected");
setMcpConnected(false);
};
const pushHistory = (request: object, response: object) => {
setRequestHistory((prev) => [
...prev,
{ request: JSON.stringify(request), response: JSON.stringify(response) },
]);
};

return () => ws.close();
}, []);
const makeRequest = async (
request: Parameters<Client["request"]>[0],
schema: Parameters<Client["request"]>[1],
): Promise<ReturnType<Client["request"]>> => {

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I definitely don't have the types right here. It's passing, but the callers aren't inferring the responses correctly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Consider removing the annotations? I'm curious if it will infer good types.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Unfortunately it all becomes anys

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Okay, this seems to work:

async <T extends AnyZodObject>(
    request: ClientRequest,
    schema: T,
  ) => { ... }

if (!mcpClient) {
throw new Error("MCP client not connected");
}

const updateRequestHistory = (response: unknown) => {
setRequestHistory((prev) => {
const lastRequest = prev[prev.length - 1];
if (lastRequest && lastRequest.response === null) {
const updatedHistory = [...prev];
updatedHistory[updatedHistory.length - 1] = {
...lastRequest,
response: JSON.stringify(response),
};
return updatedHistory;
}
return prev;
});
try {
const response = await mcpClient.request(request, schema);
pushHistory(request, response);
return response;
} catch (e: unknown) {
setError((e as Error).message);
throw e;
}
};

const sendWebSocketMessage = (message: object) => {
if (socket) {
console.log("Sending WebSocket message:", message);
socket.send(JSON.stringify(message));
setRequestHistory((prev) => [
...prev,
{ request: JSON.stringify(message), response: null },
]);
const listResources = async () => {
const response = await makeRequest(
{
method: "resources/list" as const,
},
ListResourcesResultSchema,
);
if (response.resources) {
setResources(response.resources);
}
};

const [selectedResource, setSelectedResource] = useState<Resource | null>(
null,
);
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
const [selectedTool, setSelectedTool] = useState<ToolType | null>(null);

const listResources = () => {
sendWebSocketMessage({ type: "listResources" });
const readResource = async (uri: string) => {
const response = await makeRequest(
{
method: "resources/read" as const,
params: { uri },
},
ReadResourceResultSchema,
);
setResourceContent(JSON.stringify(response, null, 2));
};

const readResource = (uri: string) => {
sendWebSocketMessage({ type: "readResource", uri });
const listPrompts = async () => {
const response = await makeRequest(
{
method: "prompts/list" as const,
},
ListPromptsResultSchema,
);
if (response.prompts) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Isn't this always true?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I think this might have been needed because of the bad type annotation on my wrapper function, will check

setPrompts(response.prompts);
}
};

const listPrompts = () => {
sendWebSocketMessage({ type: "listPrompts" });
const getPrompt = async (name: string, args: Record<string, string> = {}) => {
const response = await makeRequest(
{
method: "prompts/get" as const,
params: { name, arguments: args },
},
GetPromptResultSchema,
);
setPromptContent(JSON.stringify(response, null, 2));
};

const getPrompt = (name: string, args: Record<string, unknown> = {}) => {
sendWebSocketMessage({ type: "getPrompt", name, args });
const listTools = async () => {
const response = await makeRequest(
{
method: "tools/list" as const,
},
ListToolsResultSchema,
);
if (response.tools) {
Comment thread
ashwin-ant marked this conversation as resolved.
Outdated
setTools(response.tools);
}
};

const listTools = () => {
sendWebSocketMessage({ type: "listTools" });
const callTool = async (name: string, params: Record<string, unknown>) => {
const response = await makeRequest(
{
method: "tools/call" as const,
params: { name, arguments: params },
},
CallToolResultSchema,
);
setToolResult(JSON.stringify(response.toolResult, null, 2));
};

const callTool = (name: string, params: Record<string, unknown>) => {
sendWebSocketMessage({ type: "callTool", name, params });
};
const connectMcpServer = async () => {
try {
const client = new Client({
name: "mcp-inspector",
version: "0.0.1",
});

const clientTransport = new SSEClientTransport();
const url = new URL("http://localhost:3000/sse");
url.searchParams.append("command", encodeURIComponent(command));
url.searchParams.append("args", encodeURIComponent(args));
await clientTransport.connect(url);

await client.connect(clientTransport);

const connectMcpServer = () => {
const argsArray = args.split(" ").filter((arg) => arg.trim() !== "");
sendWebSocketMessage({ type: "connect", command, args: argsArray });
setMcpClient(client);
setConnectionStatus("connected");
} catch (e) {
console.error(e);
setConnectionStatus("error");
}
};

return (
Expand Down Expand Up @@ -182,7 +202,7 @@ const App = () => {
</Button>
</div>
</div>
{mcpConnected ? (
{mcpClient ? (
<Tabs defaultValue="resources" className="w-full p-4">
<TabsList className="mb-4 p-0">
<TabsTrigger value="resources">
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/History.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ const History = ({
>
<span className="font-mono">
{requestHistory.length - index}.{" "}
{JSON.parse(request.request).type}
{JSON.parse(request.request).method}
</span>
<span>
{expandedRequests[requestHistory.length - 1 - index]
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/ToolsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import ListPane from "./ListPane";

export type Tool = {
name: string;
description: string;
description?: string | undefined;
inputSchema: {
type: string;
properties: Record<string, { type: string; description: string }>;
properties?: Record<string, { type: string; description: string }>;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I probably have the types wrong here, not sure what's going on exactly

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can you use the Tool type directly from the SDK?

};
};

Expand Down
Loading