Merge pull request #13 from modelcontextprotocol/ashwin/mcp

Refactor to use MCP on client rather than custom protocol
This commit is contained in:
ashwin-ant
2024-10-11 08:34:47 -07:00
committed by GitHub
10 changed files with 298 additions and 298 deletions

View File

@@ -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",

View File

@@ -1,4 +1,16 @@
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,
Tool,
ClientRequest,
} from "mcp-typescript/types.js";
import { useState } from "react";
import {
Send,
Bell,
@@ -18,11 +30,11 @@ 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 [socket, setSocket] = useState<WebSocket | null>(null);
const [connectionStatus, setConnectionStatus] = useState<
"disconnected" | "connected" | "error"
>("disconnected");
@@ -30,7 +42,7 @@ const App = () => {
const [resourceContent, setResourceContent] = useState<string>("");
const [prompts, setPrompts] = useState<Prompt[]>([]);
const [promptContent, setPromptContent] = useState<string>("");
const [tools, setTools] = useState<ToolType[]>([]);
const [tools, setTools] = useState<Tool[]>([]);
const [toolResult, setToolResult] = useState<string>("");
const [error, setError] = useState<string | null>(null);
const [command, setCommand] = useState<string>(
@@ -39,121 +51,128 @@ 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 }[]
>([]);
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<Client | null>(null);
const [selectedResource, setSelectedResource] = useState<Resource | null>(
null,
);
const [selectedPrompt, setSelectedPrompt] = useState<Prompt | null>(null);
const [selectedTool, setSelectedTool] = useState<ToolType | null>(null);
const [selectedTool, setSelectedTool] = useState<Tool | null>(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 <T extends AnyZodObject>(
request: ClientRequest,
schema: T,
) => {
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<string, unknown> = {}) => {
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,
);
setPrompts(response.prompts);
};
const callTool = (name: string, params: Record<string, unknown>) => {
sendWebSocketMessage({ type: "callTool", name, params });
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 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,
);
setTools(response.tools);
};
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 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 +201,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">

View File

@@ -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]

View File

@@ -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;
inputSchema: {
type: string;
properties: Record<string, { type: string; description: string }>;
};
};
const ToolsTab = ({
tools,
listTools,

85
client/yarn.lock generated
View File

@@ -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==

View File

@@ -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"
}
}

View File

@@ -1,127 +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,
} 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<ListResourcesResult> {
return await this.client.request(
{
method: "resources/list",
},
ListResourcesResultSchema,
);
}
async readResource(uri: string): Promise<ReadResourceResult> {
return await this.client.request(
{
method: "resources/read",
params: { uri },
},
ReadResourceResultSchema,
);
}
// Prompt Operations
async listPrompts(): Promise<ListPromptsResult> {
return await this.client.request(
{
method: "prompts/list",
},
ListPromptsResultSchema,
);
}
async getPrompt(
name: string,
args?: Record<string, string>,
): Promise<GetPromptResult> {
return await this.client.request(
{
method: "prompts/get",
params: { name, arguments: args },
},
GetPromptResultSchema,
);
}
// Tool Operations
async listTools(): Promise<ListToolsResult> {
return await this.client.request(
{
method: "tools/list",
},
ListToolsResultSchema,
);
}
async callTool(
name: string,
params: Record<string, unknown>,
): Promise<CallToolResult> {
return await this.client.request(
{
method: "tools/call",
params: { name, arguments: params },
},
CallToolResultSchema,
);
}
getServerCapabilities() {
return this.client.getServerCapabilities();
}
getServerVersion() {
return this.client.getServerVersion();
}
}
export default McpClient;

View File

@@ -1,71 +1,48 @@
import McpClient from "./client.js";
import cors from "cors";
import { SSEServerTransport } from "mcp-typescript/server/sse.js";
import express from "express";
import http from "http";
import { WebSocket, WebSocketServer } from "ws";
import { StdioClientTransport } from "mcp-typescript/client/stdio.js";
import mcpProxy from "./mcpProxy.js";
const app = express();
const server = http.createServer(app);
const wss = new WebSocketServer({ server });
app.use(cors());
let mcpClient: McpClient | null = null;
let transports: SSEServerTransport[] = [];
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 backingServerTransport = new StdioClientTransport();
await backingServerTransport.spawn({ 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 webAppTransport = new SSEServerTransport("/message");
transports.push(webAppTransport);
await webAppTransport.connectSSE(req, res);
mcpProxy({
transportToClient: webAppTransport,
transportToServer: backingServerTransport,
onerror: (error) => {
console.error(error);
},
});
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
app.post("/message", async (req, res) => {
console.log("Received message");
const transport = transports.find((t) => true);
if (!transport) {
res.status(404).send("Session not found");
return;
}
await transport.handlePostMessage(req, res);
});
// Close the client when the server is shutting down
process.on("SIGINT", async () => {
if (mcpClient) {
await mcpClient.close();
}
process.exit();
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});

30
server/src/mcpProxy.ts Normal file
View File

@@ -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;
}

22
server/yarn.lock generated
View File

@@ -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==