Merge pull request #13 from modelcontextprotocol/ashwin/mcp
Refactor to use MCP on client rather than custom protocol
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
85
client/yarn.lock
generated
@@ -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==
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
|
||||
30
server/src/mcpProxy.ts
Normal file
30
server/src/mcpProxy.ts
Normal 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
22
server/yarn.lock
generated
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user