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