aboutsummaryrefslogtreecommitdiffstats
path: root/src/main/mcp.ts
blob: cfd3d266b85ab53d565532a39fc3322a9ff5c5ae (plain)
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();
    }
  }
}