aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/mcp.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/mcp.ts')
-rw-r--r--src/main/mcp.ts151
1 files changed, 151 insertions, 0 deletions
diff --git a/src/main/mcp.ts b/src/main/mcp.ts
new file mode 100644
index 0000000..cfd3d26
--- /dev/null
+++ b/src/main/mcp.ts
@@ -0,0 +1,151 @@
1import { spawn, type ChildProcess } from "node:child_process";
2import { randomUUID } from "node:crypto";
3
4interface McpTool {
5 name: string;
6 description?: string;
7}
8
9interface McpServerConfig {
10 type: "stdio" | "sse" | "http";
11 command?: string;
12 args?: string[];
13 env?: Record<string, string>;
14 url?: string;
15}
16
17interface JsonRpcRequest {
18 jsonrpc: "2.0";
19 id: string | number;
20 method: string;
21 params?: Record<string, unknown>;
22}
23
24interface JsonRpcResponse {
25 jsonrpc: "2.0";
26 id: string | number;
27 result?: unknown;
28 error?: { code: number; message: string };
29}
30
31/**
32 * Connect to an MCP server and discover its available tools.
33 * Only supports stdio transport for now.
34 */
35export async function discoverMcpTools(
36 config: McpServerConfig
37): Promise<{ tools: McpTool[]; error?: string }> {
38 if (config.type !== "stdio") {
39 return { tools: [], error: "Only stdio transport is supported for tool discovery" };
40 }
41
42 if (!config.command) {
43 return { tools: [], error: "Command is required for stdio transport" };
44 }
45
46 let process: ChildProcess | null = null;
47 let buffer = "";
48
49 try {
50 // Spawn the MCP server process
51 process = spawn(config.command, config.args || [], {
52 env: { ...globalThis.process.env, ...config.env },
53 stdio: ["pipe", "pipe", "pipe"],
54 });
55
56 if (!process.stdin || !process.stdout) {
57 return { tools: [], error: "Failed to create process streams" };
58 }
59
60 const sendRequest = (request: JsonRpcRequest): void => {
61 const json = JSON.stringify(request);
62 process!.stdin!.write(json + "\n");
63 };
64
65 const waitForResponse = (id: string | number, timeoutMs = 10000): Promise<JsonRpcResponse> => {
66 return new Promise((resolve, reject) => {
67 const timeout = setTimeout(() => {
68 reject(new Error("Timeout waiting for MCP response"));
69 }, timeoutMs);
70
71 const onData = (chunk: Buffer) => {
72 buffer += chunk.toString();
73
74 // Try to parse complete JSON-RPC messages (newline-delimited)
75 const lines = buffer.split("\n");
76 buffer = lines.pop() || ""; // Keep incomplete line in buffer
77
78 for (const line of lines) {
79 if (!line.trim()) continue;
80 try {
81 const response = JSON.parse(line) as JsonRpcResponse;
82 if (response.id === id) {
83 clearTimeout(timeout);
84 process!.stdout!.off("data", onData);
85 resolve(response);
86 return;
87 }
88 } catch {
89 // Not valid JSON, skip
90 }
91 }
92 };
93
94 process!.stdout!.on("data", onData);
95 });
96 };
97
98 // Step 1: Initialize
99 const initId = randomUUID();
100 sendRequest({
101 jsonrpc: "2.0",
102 id: initId,
103 method: "initialize",
104 params: {
105 protocolVersion: "2024-11-05",
106 capabilities: {},
107 clientInfo: { name: "claude-flow", version: "1.0.0" },
108 },
109 });
110
111 const initResponse = await waitForResponse(initId);
112 if (initResponse.error) {
113 return { tools: [], error: `Initialize failed: ${initResponse.error.message}` };
114 }
115
116 // Send initialized notification (no response expected)
117 process.stdin.write(JSON.stringify({
118 jsonrpc: "2.0",
119 method: "notifications/initialized",
120 }) + "\n");
121
122 // Step 2: List tools
123 const toolsId = randomUUID();
124 sendRequest({
125 jsonrpc: "2.0",
126 id: toolsId,
127 method: "tools/list",
128 params: {},
129 });
130
131 const toolsResponse = await waitForResponse(toolsId);
132 if (toolsResponse.error) {
133 return { tools: [], error: `tools/list failed: ${toolsResponse.error.message}` };
134 }
135
136 const result = toolsResponse.result as { tools?: Array<{ name: string; description?: string }> };
137 const tools: McpTool[] = (result.tools || []).map((t) => ({
138 name: t.name,
139 description: t.description,
140 }));
141
142 return { tools };
143 } catch (err) {
144 return { tools: [], error: String(err) };
145 } finally {
146 // Clean up
147 if (process) {
148 process.kill();
149 }
150 }
151}