From 6575697f25d11963fae541c9611797ad47446d93 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Wed, 9 Oct 2024 17:12:10 -0700 Subject: [PATCH 1/4] refactor to not use custom websocket protocol --- client/package.json | 1 + client/src/App.tsx | 214 ++++++++++++++++------------- client/src/components/History.tsx | 2 +- client/src/components/ToolsTab.tsx | 4 +- client/yarn.lock | 85 ++++++++++++ server/package.json | 5 +- server/src/client.ts | 21 +-- server/src/index.ts | 129 +++++++++-------- server/yarn.lock | 22 ++- 9 files changed, 313 insertions(+), 170 deletions(-) diff --git a/client/package.json b/client/package.json index b59deff..d54fd1a 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/src/App.tsx b/client/src/App.tsx index 84be122..4c0af5a 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -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(null); const [connectionStatus, setConnectionStatus] = useState< "disconnected" | "connected" | "error" >("disconnected"); @@ -39,87 +48,10 @@ const App = () => { const [args, setArgs] = useState( "/Users/ashwin/code/example-servers/build/everything/index.js", ); - const [mcpConnected, setMcpConnected] = useState(false); const [requestHistory, setRequestHistory] = useState< - Array<{ request: string; response: string | null }> + { request: string; response: string }[] >([]); - - 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"); - }; - - ws.onclose = () => { - setConnectionStatus("disconnected"); - setMcpConnected(false); - }; - - return () => ws.close(); - }, []); - - 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; - }); - }; - - 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 [mcpClient, setMcpClient] = useState(null); const [selectedResource, setSelectedResource] = useState( null, @@ -127,33 +59,121 @@ const App = () => { const [selectedPrompt, setSelectedPrompt] = useState(null); const [selectedTool, setSelectedTool] = useState(null); - const listResources = () => { - sendWebSocketMessage({ type: "listResources" }); + const pushHistory = (request: object, response: object) => { + setRequestHistory((prev) => [ + ...prev, + { request: JSON.stringify(request), response: JSON.stringify(response) }, + ]); }; - const readResource = (uri: string) => { - sendWebSocketMessage({ type: "readResource", uri }); + const makeRequest = async ( + request: Parameters[0], + schema: Parameters[1], + ): Promise> => { + if (!mcpClient) { + throw new Error("MCP client not connected"); + } + + try { + const response = await mcpClient.request(request, schema); + pushHistory(request, response); + return response; + } catch (e: unknown) { + setError((e as Error).message); + throw e; + } }; - const listPrompts = () => { - sendWebSocketMessage({ type: "listPrompts" }); + const listResources = async () => { + const response = await makeRequest( + { + method: "resources/list" as const, + }, + ListResourcesResultSchema, + ); + if (response.resources) { + setResources(response.resources); + } }; - const getPrompt = (name: string, args: Record = {}) => { - sendWebSocketMessage({ type: "getPrompt", name, args }); + const readResource = async (uri: string) => { + const response = await makeRequest( + { + method: "resources/read" as const, + params: { uri }, + }, + ReadResourceResultSchema, + ); + setResourceContent(JSON.stringify(response, null, 2)); }; - const listTools = () => { - sendWebSocketMessage({ type: "listTools" }); + const listPrompts = async () => { + const response = await makeRequest( + { + method: "prompts/list" as const, + }, + ListPromptsResultSchema, + ); + if (response.prompts) { + setPrompts(response.prompts); + } }; - const callTool = (name: string, params: Record) => { - sendWebSocketMessage({ type: "callTool", name, params }); + const getPrompt = async (name: string, args: Record = {}) => { + const response = await makeRequest( + { + method: "prompts/get" as const, + params: { name, arguments: args }, + }, + GetPromptResultSchema, + ); + setPromptContent(JSON.stringify(response, null, 2)); }; - const connectMcpServer = () => { - const argsArray = args.split(" ").filter((arg) => arg.trim() !== ""); - sendWebSocketMessage({ type: "connect", command, args: argsArray }); + const listTools = async () => { + const response = await makeRequest( + { + method: "tools/list" as const, + }, + ListToolsResultSchema, + ); + if (response.tools) { + setTools(response.tools); + } + }; + + const callTool = async (name: string, params: Record) => { + const response = await makeRequest( + { + method: "tools/call" as const, + params: { name, arguments: params }, + }, + CallToolResultSchema, + ); + setToolResult(JSON.stringify(response.toolResult, null, 2)); + }; + + 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); + + setMcpClient(client); + setConnectionStatus("connected"); + } catch (e) { + console.error(e); + setConnectionStatus("error"); + } }; return ( @@ -182,7 +202,7 @@ const App = () => { - {mcpConnected ? ( + {mcpClient ? ( diff --git a/client/src/components/History.tsx b/client/src/components/History.tsx index 8ccf3ab..c0ac2d1 100644 --- a/client/src/components/History.tsx +++ b/client/src/components/History.tsx @@ -38,7 +38,7 @@ const History = ({ > {requestHistory.length - index}.{" "} - {JSON.parse(request.request).type} + {JSON.parse(request.request).method} {expandedRequests[requestHistory.length - 1 - index] diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index 8e58dae..cd9e8f7 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -9,10 +9,10 @@ import ListPane from "./ListPane"; export type Tool = { name: string; - description: string; + description?: string | undefined; inputSchema: { type: string; - properties: Record; + properties?: Record; }; }; diff --git a/client/yarn.lock b/client/yarn.lock index 21ffba4..706d311 100644 --- a/client/yarn.lock +++ b/client/yarn.lock @@ -942,6 +942,11 @@ browserslist@^4.23.3, browserslist@^4.24.0: node-releases "^2.0.18" update-browserslist-db "^1.1.0" +bytes@3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5" + integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg== + callsites@^3.0.0: version "3.1.0" resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" @@ -1040,6 +1045,11 @@ concat-map@0.0.1: resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== +content-type@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918" + integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA== + convert-source-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" @@ -1076,6 +1086,11 @@ deep-is@^0.1.3: resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== +depd@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" + integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== + didyoumean@^1.2.2: version "1.2.2" resolved "https://registry.yarnpkg.com/didyoumean/-/didyoumean-1.2.2.tgz#989346ffe9e839b4555ecf5666edea0d3e8ad037" @@ -1411,6 +1426,24 @@ hasown@^2.0.2: dependencies: function-bind "^1.1.2" +http-errors@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3" + integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ== + dependencies: + depd "2.0.0" + inherits "2.0.4" + setprototypeof "1.2.0" + statuses "2.0.1" + toidentifier "1.0.1" + +iconv-lite@0.6.3: + version "0.6.3" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501" + integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + ignore@^5.2.0, ignore@^5.3.1: version "5.3.2" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.2.tgz#3cd40e729f3643fd87cb04e50bf0eb722bc596f5" @@ -1429,6 +1462,11 @@ imurmurhash@^0.1.4: resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== +inherits@2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + is-binary-path@~2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" @@ -1587,6 +1625,13 @@ lucide-react@^0.447.0: resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.447.0.tgz#1b2c4044c619517346306d9fae950265aafa76a5" integrity sha512-SZ//hQmvi+kDKrNepArVkYK7/jfeZ5uFNEnYmd45RKZcbGD78KLnrcNXmgeg6m+xNHFvTG+CblszXCy4n6DN4w== +"mcp-typescript@file:../packages/mcp-typescript": + version "0.1.0" + dependencies: + content-type "^1.0.5" + raw-body "^3.0.0" + zod "^3.23.8" + merge2@^1.3.0: version "1.4.1" resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" @@ -1817,6 +1862,16 @@ queue-microtask@^1.2.2: resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== +raw-body@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-3.0.0.tgz#25b3476f07a51600619dae3fe82ddc28a36e5e0f" + integrity sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g== + dependencies: + bytes "3.1.2" + http-errors "2.0.0" + iconv-lite "0.6.3" + unpipe "1.0.0" + react-dom@^18.3.1: version "18.3.1" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" @@ -1902,6 +1957,11 @@ run-parallel@^1.1.9: dependencies: queue-microtask "^1.2.2" +"safer-buffer@>= 2.1.2 < 3.0.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + scheduler@^0.23.2: version "0.23.2" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" @@ -1919,6 +1979,11 @@ semver@^7.6.0: resolved "https://registry.yarnpkg.com/semver/-/semver-7.6.3.tgz#980f7b5550bc175fb4dc09403085627f9eb33143" integrity sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A== +setprototypeof@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424" + integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw== + shebang-command@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" @@ -1941,6 +2006,11 @@ source-map-js@^1.2.1: resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== +statuses@2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63" + integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== + "string-width-cjs@npm:string-width@^4.2.0": version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" @@ -2095,6 +2165,11 @@ to-regex-range@^5.0.1: dependencies: is-number "^7.0.0" +toidentifier@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35" + integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA== + ts-api-utils@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.3.0.tgz#4b490e27129f1e8e686b45cc4ab63714dc60eea1" @@ -2131,6 +2206,11 @@ undici-types@~6.19.2: resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== +unpipe@1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ== + update-browserslist-db@^1.1.0: version "1.1.1" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz#80846fba1d79e82547fb661f8d141e0945755fe5" @@ -2206,3 +2286,8 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== + +zod@^3.23.8: + version "3.23.8" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d" + integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g== diff --git a/server/package.json b/server/package.json index 06fdbd1..3b11ccb 100644 --- a/server/package.json +++ b/server/package.json @@ -9,6 +9,7 @@ "dev": "tsx watch --clear-screen=false src/index.ts" }, "devDependencies": { + "@types/cors": "^2.8.17", "@types/eventsource": "^1.1.15", "@types/express": "^4.17.21", "@types/ws": "^8.5.12", @@ -17,8 +18,10 @@ "typescript": "^5.6.2" }, "dependencies": { + "cors": "^2.8.5", "express": "^4.21.0", "mcp-typescript": "file:../packages/mcp-typescript", - "ws": "^8.18.0" + "ws": "^8.18.0", + "zod": "^3.23.8" } } diff --git a/server/src/client.ts b/server/src/client.ts index b6e6cb9..f07b672 100644 --- a/server/src/client.ts +++ b/server/src/client.ts @@ -14,6 +14,9 @@ import { ListToolsResultSchema, CallToolResult, CallToolResultSchema, + GetPromptRequest, + ReadResourceRequest, + CallToolRequest, } from "mcp-typescript/types.js"; export class McpClient { @@ -60,11 +63,13 @@ export class McpClient { ); } - async readResource(uri: string): Promise { + async readResource( + params: ReadResourceRequest["params"], + ): Promise { return await this.client.request( { method: "resources/read", - params: { uri }, + params, }, ReadResourceResultSchema, ); @@ -81,13 +86,12 @@ export class McpClient { } async getPrompt( - name: string, - args?: Record, + params: GetPromptRequest["params"], ): Promise { return await this.client.request( { method: "prompts/get", - params: { name, arguments: args }, + params, }, GetPromptResultSchema, ); @@ -102,14 +106,11 @@ export class McpClient { ); } - async callTool( - name: string, - params: Record, - ): Promise { + async callTool(params: CallToolRequest["params"]): Promise { return await this.client.request( { method: "tools/call", - params: { name, arguments: params }, + params, }, CallToolResultSchema, ); diff --git a/server/src/index.ts b/server/src/index.ts index e6970b4..a97a5f8 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,71 +1,84 @@ import McpClient from "./client.js"; +import cors from "cors"; + +import { Server } from "mcp-typescript/server/index.js"; +import { SSEServerTransport } from "mcp-typescript/server/sse.js"; import express from "express"; -import http from "http"; -import { WebSocket, WebSocketServer } from "ws"; +import { + CallToolRequestSchema, + GetPromptRequestSchema, + ListPromptsRequestSchema, + ListResourcesRequestSchema, + ListToolsRequestSchema, + ReadResourceRequestSchema, +} from "mcp-typescript/types.js"; const app = express(); -const server = http.createServer(app); -const wss = new WebSocketServer({ server }); +app.use(cors()); -let mcpClient: McpClient | null = null; +let servers: Server[] = []; -wss.on("connection", (ws: WebSocket) => { - ws.on("message", async (message: string) => { - try { - const command = JSON.parse(message); +app.get("/sse", async (req, res) => { + console.log("New SSE connection"); + const command = decodeURIComponent(req.query.command as string); + const args = decodeURIComponent(req.query.args as string).split(","); + const mcpClient = new McpClient("MyApp", "1.0.0"); + await mcpClient.connectStdio(command, args); - if (command.type === "connect" && command.command && command.args) { - mcpClient = new McpClient("MyApp", "1.0.0"); - await mcpClient.connectStdio(command.command, command.args); - ws.send(JSON.stringify({ type: "connected" })); - } else if (!mcpClient) { - ws.send( - JSON.stringify({ - type: "error", - message: "Not connected to MCP server", - }), - ); - } else if (command.type === "listResources") { - const resources = await mcpClient.listResources(); - ws.send(JSON.stringify({ type: "resources", data: resources })); - } else if (command.type === "readResource" && command.uri) { - const resource = await mcpClient.readResource(command.uri); - ws.send(JSON.stringify({ type: "resource", data: resource })); - } else if (command.type === "listPrompts") { - const prompts = await mcpClient.listPrompts(); - ws.send(JSON.stringify({ type: "prompts", data: prompts })); - } else if (command.type === "getPrompt" && command.name) { - const prompt = await mcpClient.getPrompt(command.name, command.args); - ws.send(JSON.stringify({ type: "prompt", data: prompt })); - } else if (command.type === "listTools") { - const tools = await mcpClient.listTools(); - ws.send(JSON.stringify({ type: "tools", data: tools })); - } else if ( - command.type === "callTool" && - command.name && - command.params - ) { - const result = await mcpClient.callTool(command.name, command.params); - ws.send( - JSON.stringify({ type: "toolResult", data: result.toolResult }), - ); - } - } catch (error) { - console.error("Error:", error); - ws.send(JSON.stringify({ type: "error", message: String(error) })); - } + const transport = new SSEServerTransport("/message"); + const server = new Server({ + name: "mcp-server-inspector", + version: "0.0.1", }); + servers.push(server); + + server.onclose = async () => { + console.log("SSE connection closed"); + servers = servers.filter((s) => s !== server); + await mcpClient.close(); + }; + + server.setRequestHandler(ListResourcesRequestSchema, () => { + return mcpClient.listResources(); + }); + + server.setRequestHandler(ReadResourceRequestSchema, (params) => { + return mcpClient.readResource(params.params); + }); + + server.setRequestHandler(ListPromptsRequestSchema, () => { + return mcpClient.listPrompts(); + }); + + server.setRequestHandler(GetPromptRequestSchema, (params) => { + return mcpClient.getPrompt(params.params); + }); + + server.setRequestHandler(ListToolsRequestSchema, () => { + return mcpClient.listTools(); + }); + + server.setRequestHandler(CallToolRequestSchema, (params) => { + return mcpClient.callTool(params.params); + }); + await transport.connectSSE(req, res); + await server.connect(transport); +}); + +app.post("/message", async (req, res) => { + console.log("Received message"); + + const transport = servers + .map((s) => s.transport as SSEServerTransport) + .find((t) => true); + if (!transport) { + res.status(404).send("Session not found"); + return; + } + await transport.handlePostMessage(req, res); }); const PORT = process.env.PORT || 3000; -server.listen(PORT, () => { +app.listen(PORT, () => { console.log(`Server is running on port ${PORT}`); }); - -// Close the client when the server is shutting down -process.on("SIGINT", async () => { - if (mcpClient) { - await mcpClient.close(); - } - process.exit(); -}); diff --git a/server/yarn.lock b/server/yarn.lock index d6076b8..6163ec6 100644 --- a/server/yarn.lock +++ b/server/yarn.lock @@ -137,6 +137,13 @@ dependencies: "@types/node" "*" +"@types/cors@^2.8.17": + version "2.8.17" + resolved "https://registry.yarnpkg.com/@types/cors/-/cors-2.8.17.tgz#5d718a5e494a8166f569d986794e49c48b216b2b" + integrity sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA== + dependencies: + "@types/node" "*" + "@types/eventsource@^1.1.15": version "1.1.15" resolved "https://registry.yarnpkg.com/@types/eventsource/-/eventsource-1.1.15.tgz#949383d3482e20557cbecbf3b038368d94b6be27" @@ -282,6 +289,14 @@ cookie@0.6.0: resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051" integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw== +cors@^2.8.5: + version "2.8.5" + resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.5.tgz#eac11da51592dd86b9f06f6e7ac293b3df875d29" + integrity sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g== + dependencies: + object-assign "^4" + vary "^1" + debug@2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -588,6 +603,11 @@ negotiator@0.6.3: resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== +object-assign@^4: + version "4.1.1" + resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" + integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== + object-inspect@^1.13.1: version "1.13.2" resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.2.tgz#dea0088467fb991e67af4058147a24824a3043ff" @@ -769,7 +789,7 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== -vary@~1.1.2: +vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== From 4b82553046bee80144908a477eda36f058a82905 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Wed, 9 Oct 2024 17:18:47 -0700 Subject: [PATCH 2/4] remove client class --- server/src/client.ts | 128 ------------------------------------------- server/src/index.ts | 87 +++++++++++++++++++++-------- 2 files changed, 65 insertions(+), 150 deletions(-) delete mode 100644 server/src/client.ts diff --git a/server/src/client.ts b/server/src/client.ts deleted file mode 100644 index f07b672..0000000 --- a/server/src/client.ts +++ /dev/null @@ -1,128 +0,0 @@ -import { Client } from "mcp-typescript/client/index.js"; -import { SSEClientTransport } from "mcp-typescript/client/sse.js"; -import { StdioClientTransport } from "mcp-typescript/client/stdio.js"; -import { - ListResourcesResult, - ReadResourceResult, - ListResourcesResultSchema, - ReadResourceResultSchema, - ListPromptsResult, - ListPromptsResultSchema, - GetPromptResult, - GetPromptResultSchema, - ListToolsResult, - ListToolsResultSchema, - CallToolResult, - CallToolResultSchema, - GetPromptRequest, - ReadResourceRequest, - CallToolRequest, -} from "mcp-typescript/types.js"; - -export class McpClient { - private client: Client; - private transport?: SSEClientTransport | StdioClientTransport; - - constructor(name: string, version: string) { - this.client = new Client({ - name, - version, - }); - } - - async connect(url: string | URL) { - const urlObj = typeof url === "string" ? new URL(url) : url; - - if (urlObj.protocol === "http:" || urlObj.protocol === "https:") { - this.transport = new SSEClientTransport(); - } else { - throw new Error(`Unsupported protocol: ${urlObj.protocol}`); - } - - await this.transport.connect(urlObj); - await this.client.connect(this.transport); - } - - async connectStdio(command: string, args: string[] = []) { - this.transport = new StdioClientTransport(); - await this.transport.spawn({ command, args }); - await this.client.connect(this.transport); - } - - async close() { - await this.client.close(); - } - - // Resource Operations - async listResources(): Promise { - return await this.client.request( - { - method: "resources/list", - }, - ListResourcesResultSchema, - ); - } - - async readResource( - params: ReadResourceRequest["params"], - ): Promise { - return await this.client.request( - { - method: "resources/read", - params, - }, - ReadResourceResultSchema, - ); - } - - // Prompt Operations - async listPrompts(): Promise { - return await this.client.request( - { - method: "prompts/list", - }, - ListPromptsResultSchema, - ); - } - - async getPrompt( - params: GetPromptRequest["params"], - ): Promise { - return await this.client.request( - { - method: "prompts/get", - params, - }, - GetPromptResultSchema, - ); - } - // Tool Operations - async listTools(): Promise { - return await this.client.request( - { - method: "tools/list", - }, - ListToolsResultSchema, - ); - } - - async callTool(params: CallToolRequest["params"]): Promise { - return await this.client.request( - { - method: "tools/call", - params, - }, - CallToolResultSchema, - ); - } - - getServerCapabilities() { - return this.client.getServerCapabilities(); - } - - getServerVersion() { - return this.client.getServerVersion(); - } -} - -export default McpClient; diff --git a/server/src/index.ts b/server/src/index.ts index a97a5f8..547c483 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,4 +1,3 @@ -import McpClient from "./client.js"; import cors from "cors"; import { Server } from "mcp-typescript/server/index.js"; @@ -11,7 +10,15 @@ import { ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, + ListResourcesResultSchema, + ReadResourceResultSchema, + ListPromptsResultSchema, + GetPromptResultSchema, + ListToolsResultSchema, + CallToolResultSchema, } from "mcp-typescript/types.js"; +import { Client } from "mcp-typescript/client/index.js"; +import { StdioClientTransport } from "mcp-typescript/client/stdio.js"; const app = express(); app.use(cors()); @@ -22,10 +29,12 @@ app.get("/sse", async (req, res) => { console.log("New SSE connection"); const command = decodeURIComponent(req.query.command as string); const args = decodeURIComponent(req.query.args as string).split(","); - const mcpClient = new McpClient("MyApp", "1.0.0"); - await mcpClient.connectStdio(command, args); + const mcpClient = new Client({ name: "MyApp", version: "1.0.0" }); + const backingServerTransport = new StdioClientTransport(); + await backingServerTransport.spawn({ command, args }); + await mcpClient.connect(backingServerTransport); - const transport = new SSEServerTransport("/message"); + const webAppTransport = new SSEServerTransport("/message"); const server = new Server({ name: "mcp-server-inspector", version: "0.0.1", @@ -38,31 +47,65 @@ app.get("/sse", async (req, res) => { await mcpClient.close(); }; - server.setRequestHandler(ListResourcesRequestSchema, () => { - return mcpClient.listResources(); - }); + server.setRequestHandler(ListResourcesRequestSchema, () => + mcpClient.request( + { + method: "resources/list", + }, + ListResourcesResultSchema, + ), + ); - server.setRequestHandler(ReadResourceRequestSchema, (params) => { - return mcpClient.readResource(params.params); - }); + server.setRequestHandler(ReadResourceRequestSchema, (params) => + mcpClient.request( + { + method: "resources/read", + params: params.params, + }, + ReadResourceResultSchema, + ), + ); - server.setRequestHandler(ListPromptsRequestSchema, () => { - return mcpClient.listPrompts(); - }); + server.setRequestHandler(ListPromptsRequestSchema, () => + mcpClient.request( + { + method: "prompts/list", + }, + ListPromptsResultSchema, + ), + ); server.setRequestHandler(GetPromptRequestSchema, (params) => { - return mcpClient.getPrompt(params.params); + return mcpClient.request( + { + method: "prompts/get", + params: params.params, + }, + GetPromptResultSchema, + ); }); - server.setRequestHandler(ListToolsRequestSchema, () => { - return mcpClient.listTools(); - }); + server.setRequestHandler(ListToolsRequestSchema, () => + mcpClient.request( + { + method: "tools/list", + }, + ListToolsResultSchema, + ), + ); - server.setRequestHandler(CallToolRequestSchema, (params) => { - return mcpClient.callTool(params.params); - }); - await transport.connectSSE(req, res); - await server.connect(transport); + server.setRequestHandler(CallToolRequestSchema, (params) => + mcpClient.request( + { + method: "tools/call", + params: params.params, + }, + CallToolResultSchema, + ), + ); + + await webAppTransport.connectSSE(req, res); + await server.connect(webAppTransport); }); app.post("/message", async (req, res) => { From df8051b975778a17381bf32d093bc1f29bd47e85 Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Thu, 10 Oct 2024 09:13:19 -0700 Subject: [PATCH 3/4] use proxy function --- server/src/index.ts | 81 ++++++------------------------------------ server/src/mcpProxy.ts | 30 ++++++++++++++++ 2 files changed, 40 insertions(+), 71 deletions(-) create mode 100644 server/src/mcpProxy.ts diff --git a/server/src/index.ts b/server/src/index.ts index 547c483..902ee03 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -3,22 +3,9 @@ import cors from "cors"; import { Server } from "mcp-typescript/server/index.js"; import { SSEServerTransport } from "mcp-typescript/server/sse.js"; import express from "express"; -import { - CallToolRequestSchema, - GetPromptRequestSchema, - ListPromptsRequestSchema, - ListResourcesRequestSchema, - ListToolsRequestSchema, - ReadResourceRequestSchema, - ListResourcesResultSchema, - ReadResourceResultSchema, - ListPromptsResultSchema, - GetPromptResultSchema, - ListToolsResultSchema, - CallToolResultSchema, -} from "mcp-typescript/types.js"; import { Client } from "mcp-typescript/client/index.js"; import { StdioClientTransport } from "mcp-typescript/client/stdio.js"; +import mcpProxy from "./mcpProxy.js"; const app = express(); app.use(cors()); @@ -47,65 +34,17 @@ app.get("/sse", async (req, res) => { await mcpClient.close(); }; - server.setRequestHandler(ListResourcesRequestSchema, () => - mcpClient.request( - { - method: "resources/list", - }, - ListResourcesResultSchema, - ), - ); - - server.setRequestHandler(ReadResourceRequestSchema, (params) => - mcpClient.request( - { - method: "resources/read", - params: params.params, - }, - ReadResourceResultSchema, - ), - ); - - server.setRequestHandler(ListPromptsRequestSchema, () => - mcpClient.request( - { - method: "prompts/list", - }, - ListPromptsResultSchema, - ), - ); - - server.setRequestHandler(GetPromptRequestSchema, (params) => { - return mcpClient.request( - { - method: "prompts/get", - params: params.params, - }, - GetPromptResultSchema, - ); - }); - - server.setRequestHandler(ListToolsRequestSchema, () => - mcpClient.request( - { - method: "tools/list", - }, - ListToolsResultSchema, - ), - ); - - server.setRequestHandler(CallToolRequestSchema, (params) => - mcpClient.request( - { - method: "tools/call", - params: params.params, - }, - CallToolResultSchema, - ), - ); - await webAppTransport.connectSSE(req, res); await server.connect(webAppTransport); + + mcpProxy({ + transportToClient: webAppTransport, + transportToServer: backingServerTransport, + onerror: (error) => { + console.error(error); + server.close(); + }, + }); }); app.post("/message", async (req, res) => { diff --git a/server/src/mcpProxy.ts b/server/src/mcpProxy.ts new file mode 100644 index 0000000..e6c4124 --- /dev/null +++ b/server/src/mcpProxy.ts @@ -0,0 +1,30 @@ +import { Transport } from "mcp-typescript/shared/transport.js"; + +export default function mcpProxy({ + transportToClient, + transportToServer, + onerror, +}: { + transportToClient: Transport; + transportToServer: Transport; + onerror: (error: Error) => void; +}) { + transportToClient.onmessage = (message) => { + transportToServer.send(message).catch(onerror); + }; + + transportToServer.onmessage = (message) => { + transportToClient.send(message).catch(onerror); + }; + + transportToClient.onclose = () => { + transportToServer.close().catch(onerror); + }; + + transportToServer.onclose = () => { + transportToClient.close().catch(onerror); + }; + + transportToClient.onerror = onerror; + transportToServer.onerror = onerror; +} From 50f86ebbfe795090a69e4fa4679765e0367e1c7e Mon Sep 17 00:00:00 2001 From: Ashwin Bhat Date: Fri, 11 Oct 2024 08:34:28 -0700 Subject: [PATCH 4/4] fix types, eliminate client/server --- client/src/App.tsx | 25 ++++++++++++------------- client/src/components/ToolsTab.tsx | 10 +--------- server/src/index.ts | 24 +++--------------------- 3 files changed, 16 insertions(+), 43 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 4c0af5a..0193872 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -7,6 +7,8 @@ import { ReadResourceResultSchema, CallToolResultSchema, ListPromptsResultSchema, + Tool, + ClientRequest, } from "mcp-typescript/types.js"; import { useState } from "react"; import { @@ -28,8 +30,9 @@ import RequestsTab from "./components/RequestsTabs"; import ResourcesTab, { Resource } from "./components/ResourcesTab"; import NotificationsTab from "./components/NotificationsTab"; import PromptsTab, { Prompt } from "./components/PromptsTab"; -import ToolsTab, { Tool as ToolType } from "./components/ToolsTab"; +import ToolsTab from "./components/ToolsTab"; import History from "./components/History"; +import { AnyZodObject } from "node_modules/zod/lib"; const App = () => { const [connectionStatus, setConnectionStatus] = useState< @@ -39,7 +42,7 @@ const App = () => { const [resourceContent, setResourceContent] = useState(""); const [prompts, setPrompts] = useState([]); const [promptContent, setPromptContent] = useState(""); - const [tools, setTools] = useState([]); + const [tools, setTools] = useState([]); const [toolResult, setToolResult] = useState(""); const [error, setError] = useState(null); const [command, setCommand] = useState( @@ -57,7 +60,7 @@ const App = () => { null, ); const [selectedPrompt, setSelectedPrompt] = useState(null); - const [selectedTool, setSelectedTool] = useState(null); + const [selectedTool, setSelectedTool] = useState(null); const pushHistory = (request: object, response: object) => { setRequestHistory((prev) => [ @@ -66,10 +69,10 @@ const App = () => { ]); }; - const makeRequest = async ( - request: Parameters[0], - schema: Parameters[1], - ): Promise> => { + const makeRequest = async ( + request: ClientRequest, + schema: T, + ) => { if (!mcpClient) { throw new Error("MCP client not connected"); } @@ -114,9 +117,7 @@ const App = () => { }, ListPromptsResultSchema, ); - if (response.prompts) { - setPrompts(response.prompts); - } + setPrompts(response.prompts); }; const getPrompt = async (name: string, args: Record = {}) => { @@ -137,9 +138,7 @@ const App = () => { }, ListToolsResultSchema, ); - if (response.tools) { - setTools(response.tools); - } + setTools(response.tools); }; const callTool = async (name: string, params: Record) => { diff --git a/client/src/components/ToolsTab.tsx b/client/src/components/ToolsTab.tsx index cd9e8f7..d6da683 100644 --- a/client/src/components/ToolsTab.tsx +++ b/client/src/components/ToolsTab.tsx @@ -3,19 +3,11 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Send, AlertCircle } from "lucide-react"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Tool } from "mcp-typescript/types.js"; import { useState } from "react"; import { Label } from "@/components/ui/label"; import ListPane from "./ListPane"; -export type Tool = { - name: string; - description?: string | undefined; - inputSchema: { - type: string; - properties?: Record; - }; -}; - const ToolsTab = ({ tools, listTools, diff --git a/server/src/index.ts b/server/src/index.ts index 902ee03..240ef2c 100644 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -1,48 +1,32 @@ import cors from "cors"; -import { Server } from "mcp-typescript/server/index.js"; import { SSEServerTransport } from "mcp-typescript/server/sse.js"; import express from "express"; -import { Client } from "mcp-typescript/client/index.js"; import { StdioClientTransport } from "mcp-typescript/client/stdio.js"; import mcpProxy from "./mcpProxy.js"; const app = express(); app.use(cors()); -let servers: Server[] = []; +let transports: SSEServerTransport[] = []; app.get("/sse", async (req, res) => { console.log("New SSE connection"); const command = decodeURIComponent(req.query.command as string); const args = decodeURIComponent(req.query.args as string).split(","); - const mcpClient = new Client({ name: "MyApp", version: "1.0.0" }); const backingServerTransport = new StdioClientTransport(); await backingServerTransport.spawn({ command, args }); - await mcpClient.connect(backingServerTransport); const webAppTransport = new SSEServerTransport("/message"); - const server = new Server({ - name: "mcp-server-inspector", - version: "0.0.1", - }); - servers.push(server); - - server.onclose = async () => { - console.log("SSE connection closed"); - servers = servers.filter((s) => s !== server); - await mcpClient.close(); - }; + transports.push(webAppTransport); await webAppTransport.connectSSE(req, res); - await server.connect(webAppTransport); mcpProxy({ transportToClient: webAppTransport, transportToServer: backingServerTransport, onerror: (error) => { console.error(error); - server.close(); }, }); }); @@ -50,9 +34,7 @@ app.get("/sse", async (req, res) => { app.post("/message", async (req, res) => { console.log("Received message"); - const transport = servers - .map((s) => s.transport as SSEServerTransport) - .find((t) => true); + const transport = transports.find((t) => true); if (!transport) { res.status(404).send("Session not found"); return;