-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Refactor to use MCP on client rather than custom protocol #13
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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, | ||
|
|
@@ -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"); | ||
|
|
@@ -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"]>> => { | ||
| 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) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't this always true?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
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 ( | ||
|
|
@@ -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"> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 }>; | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can you use the Tool type directly from the SDK? |
||
| }; | ||
| }; | ||
|
|
||
|
|
||
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately it all becomes
anysThere was a problem hiding this comment.
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: