beth-copilot 1.0.17 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (265) hide show
  1. package/CHANGELOG.md +41 -28
  2. package/README.md +87 -247
  3. package/bin/cli.js +115 -7
  4. package/dist/__tests__/smoke.test.d.ts +8 -0
  5. package/dist/__tests__/smoke.test.d.ts.map +1 -0
  6. package/dist/__tests__/smoke.test.js +49 -0
  7. package/dist/__tests__/smoke.test.js.map +1 -0
  8. package/dist/cli/commands/beads.e2e.test.d.ts +13 -0
  9. package/dist/cli/commands/beads.e2e.test.d.ts.map +1 -0
  10. package/dist/cli/commands/beads.e2e.test.js +526 -0
  11. package/dist/cli/commands/beads.e2e.test.js.map +1 -0
  12. package/dist/cli/commands/cli-edge-cases.e2e.test.d.ts +32 -0
  13. package/dist/cli/commands/cli-edge-cases.e2e.test.d.ts.map +1 -0
  14. package/dist/cli/commands/cli-edge-cases.e2e.test.js +162 -0
  15. package/dist/cli/commands/cli-edge-cases.e2e.test.js.map +1 -0
  16. package/dist/cli/commands/close.d.ts +89 -0
  17. package/dist/cli/commands/close.d.ts.map +1 -0
  18. package/dist/cli/commands/close.e2e.test.d.ts +27 -0
  19. package/dist/cli/commands/close.e2e.test.d.ts.map +1 -0
  20. package/dist/cli/commands/close.e2e.test.js +252 -0
  21. package/dist/cli/commands/close.e2e.test.js.map +1 -0
  22. package/dist/cli/commands/close.js +309 -0
  23. package/dist/cli/commands/close.js.map +1 -0
  24. package/dist/cli/commands/close.test.d.ts +15 -0
  25. package/dist/cli/commands/close.test.d.ts.map +1 -0
  26. package/dist/cli/commands/close.test.js +634 -0
  27. package/dist/cli/commands/close.test.js.map +1 -0
  28. package/dist/cli/commands/doctor.d.ts +23 -0
  29. package/dist/cli/commands/doctor.d.ts.map +1 -1
  30. package/dist/cli/commands/doctor.js +93 -0
  31. package/dist/cli/commands/doctor.js.map +1 -1
  32. package/dist/cli/commands/doctor.test.js +209 -0
  33. package/dist/cli/commands/doctor.test.js.map +1 -1
  34. package/dist/cli/commands/framework-isolation.test.d.ts +30 -0
  35. package/dist/cli/commands/framework-isolation.test.d.ts.map +1 -0
  36. package/dist/cli/commands/framework-isolation.test.js +119 -0
  37. package/dist/cli/commands/framework-isolation.test.js.map +1 -0
  38. package/dist/cli/commands/init-logic.e2e.test.d.ts +37 -0
  39. package/dist/cli/commands/init-logic.e2e.test.d.ts.map +1 -0
  40. package/dist/cli/commands/init-logic.e2e.test.js +305 -0
  41. package/dist/cli/commands/init-logic.e2e.test.js.map +1 -0
  42. package/dist/cli/commands/land.d.ts +142 -0
  43. package/dist/cli/commands/land.d.ts.map +1 -0
  44. package/dist/cli/commands/land.js +647 -0
  45. package/dist/cli/commands/land.js.map +1 -0
  46. package/dist/cli/commands/land.test.d.ts +20 -0
  47. package/dist/cli/commands/land.test.d.ts.map +1 -0
  48. package/dist/cli/commands/land.test.js +622 -0
  49. package/dist/cli/commands/land.test.js.map +1 -0
  50. package/dist/cli/commands/pipeline.e2e.test.js +1 -1
  51. package/dist/cli/commands/pipeline.e2e.test.js.map +1 -1
  52. package/dist/cli/commands/pre-push-guard.d.ts +84 -0
  53. package/dist/cli/commands/pre-push-guard.d.ts.map +1 -0
  54. package/dist/cli/commands/pre-push-guard.e2e.test.d.ts +24 -0
  55. package/dist/cli/commands/pre-push-guard.e2e.test.d.ts.map +1 -0
  56. package/dist/cli/commands/pre-push-guard.e2e.test.js +171 -0
  57. package/dist/cli/commands/pre-push-guard.e2e.test.js.map +1 -0
  58. package/dist/cli/commands/pre-push-guard.js +257 -0
  59. package/dist/cli/commands/pre-push-guard.js.map +1 -0
  60. package/dist/cli/commands/pre-push-guard.test.d.ts +15 -0
  61. package/dist/cli/commands/pre-push-guard.test.d.ts.map +1 -0
  62. package/dist/cli/commands/pre-push-guard.test.js +397 -0
  63. package/dist/cli/commands/pre-push-guard.test.js.map +1 -0
  64. package/dist/cli/commands/quickstart-expanded.e2e.test.d.ts +23 -0
  65. package/dist/cli/commands/quickstart-expanded.e2e.test.d.ts.map +1 -0
  66. package/dist/cli/commands/quickstart-expanded.e2e.test.js +179 -0
  67. package/dist/cli/commands/quickstart-expanded.e2e.test.js.map +1 -0
  68. package/dist/cli/commands/quickstart.test.js +40 -2
  69. package/dist/cli/commands/quickstart.test.js.map +1 -1
  70. package/dist/core/agents/suite.test.js +4 -2
  71. package/dist/core/agents/suite.test.js.map +1 -1
  72. package/dist/core/agents/tools.test.js +5 -1
  73. package/dist/core/agents/tools.test.js.map +1 -1
  74. package/dist/index.d.ts +3 -10
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +5 -10
  77. package/dist/index.js.map +1 -1
  78. package/package.json +15 -9
  79. package/sbom.json +2011 -819
  80. package/templates/.github/agents/beth.agent.md +222 -45
  81. package/templates/.github/agents/developer.agent.md +37 -67
  82. package/templates/.github/agents/product-manager.agent.md +15 -57
  83. package/templates/.github/agents/researcher.agent.md +20 -60
  84. package/templates/.github/agents/security-reviewer.agent.md +29 -70
  85. package/templates/.github/agents/tester.agent.md +40 -58
  86. package/templates/.github/agents/ux-designer.agent.md +20 -63
  87. package/templates/.github/copilot-instructions.md +217 -204
  88. package/templates/AGENTS.md +108 -20
  89. package/dist/core/context.d.ts +0 -171
  90. package/dist/core/context.d.ts.map +0 -1
  91. package/dist/core/context.js +0 -353
  92. package/dist/core/context.js.map +0 -1
  93. package/dist/core/context.test.d.ts +0 -8
  94. package/dist/core/context.test.d.ts.map +0 -1
  95. package/dist/core/context.test.js +0 -253
  96. package/dist/core/context.test.js.map +0 -1
  97. package/dist/core/handoffs.d.ts +0 -151
  98. package/dist/core/handoffs.d.ts.map +0 -1
  99. package/dist/core/handoffs.js +0 -220
  100. package/dist/core/handoffs.js.map +0 -1
  101. package/dist/core/handoffs.test.d.ts +0 -8
  102. package/dist/core/handoffs.test.d.ts.map +0 -1
  103. package/dist/core/handoffs.test.js +0 -231
  104. package/dist/core/handoffs.test.js.map +0 -1
  105. package/dist/core/orchestrator.d.ts +0 -246
  106. package/dist/core/orchestrator.d.ts.map +0 -1
  107. package/dist/core/orchestrator.js +0 -514
  108. package/dist/core/orchestrator.js.map +0 -1
  109. package/dist/core/orchestrator.test.d.ts +0 -8
  110. package/dist/core/orchestrator.test.d.ts.map +0 -1
  111. package/dist/core/orchestrator.test.js +0 -517
  112. package/dist/core/orchestrator.test.js.map +0 -1
  113. package/dist/core/router.d.ts +0 -102
  114. package/dist/core/router.d.ts.map +0 -1
  115. package/dist/core/router.js +0 -178
  116. package/dist/core/router.js.map +0 -1
  117. package/dist/core/router.test.d.ts +0 -8
  118. package/dist/core/router.test.d.ts.map +0 -1
  119. package/dist/core/router.test.js +0 -215
  120. package/dist/core/router.test.js.map +0 -1
  121. package/dist/init.test.js +0 -288
  122. package/dist/providers/azure.d.ts +0 -147
  123. package/dist/providers/azure.d.ts.map +0 -1
  124. package/dist/providers/azure.js +0 -491
  125. package/dist/providers/azure.js.map +0 -1
  126. package/dist/providers/azure.test.d.ts +0 -11
  127. package/dist/providers/azure.test.d.ts.map +0 -1
  128. package/dist/providers/azure.test.js +0 -330
  129. package/dist/providers/azure.test.js.map +0 -1
  130. package/dist/providers/config.d.ts +0 -87
  131. package/dist/providers/config.d.ts.map +0 -1
  132. package/dist/providers/config.js +0 -193
  133. package/dist/providers/config.js.map +0 -1
  134. package/dist/providers/config.test.d.ts +0 -7
  135. package/dist/providers/config.test.d.ts.map +0 -1
  136. package/dist/providers/config.test.js +0 -370
  137. package/dist/providers/config.test.js.map +0 -1
  138. package/dist/providers/index.d.ts +0 -18
  139. package/dist/providers/index.d.ts.map +0 -1
  140. package/dist/providers/index.js +0 -14
  141. package/dist/providers/index.js.map +0 -1
  142. package/dist/providers/interface.d.ts +0 -191
  143. package/dist/providers/interface.d.ts.map +0 -1
  144. package/dist/providers/interface.js +0 -94
  145. package/dist/providers/interface.js.map +0 -1
  146. package/dist/providers/retry.d.ts +0 -128
  147. package/dist/providers/retry.d.ts.map +0 -1
  148. package/dist/providers/retry.js +0 -205
  149. package/dist/providers/retry.js.map +0 -1
  150. package/dist/providers/retry.test.d.ts +0 -7
  151. package/dist/providers/retry.test.d.ts.map +0 -1
  152. package/dist/providers/retry.test.js +0 -439
  153. package/dist/providers/retry.test.js.map +0 -1
  154. package/dist/providers/streaming.d.ts +0 -157
  155. package/dist/providers/streaming.d.ts.map +0 -1
  156. package/dist/providers/streaming.js +0 -233
  157. package/dist/providers/streaming.js.map +0 -1
  158. package/dist/providers/streaming.test.d.ts +0 -7
  159. package/dist/providers/streaming.test.d.ts.map +0 -1
  160. package/dist/providers/streaming.test.js +0 -372
  161. package/dist/providers/streaming.test.js.map +0 -1
  162. package/dist/providers/types.d.ts +0 -209
  163. package/dist/providers/types.d.ts.map +0 -1
  164. package/dist/providers/types.js +0 -53
  165. package/dist/providers/types.js.map +0 -1
  166. package/dist/providers/types.test.d.ts +0 -7
  167. package/dist/providers/types.test.d.ts.map +0 -1
  168. package/dist/providers/types.test.js +0 -141
  169. package/dist/providers/types.test.js.map +0 -1
  170. package/dist/tools/cli/beads.d.ts +0 -27
  171. package/dist/tools/cli/beads.d.ts.map +0 -1
  172. package/dist/tools/cli/beads.js +0 -172
  173. package/dist/tools/cli/beads.js.map +0 -1
  174. package/dist/tools/cli/beads.test.d.ts +0 -8
  175. package/dist/tools/cli/beads.test.d.ts.map +0 -1
  176. package/dist/tools/cli/beads.test.js +0 -264
  177. package/dist/tools/cli/beads.test.js.map +0 -1
  178. package/dist/tools/cli/editFile.d.ts +0 -17
  179. package/dist/tools/cli/editFile.d.ts.map +0 -1
  180. package/dist/tools/cli/editFile.js +0 -125
  181. package/dist/tools/cli/editFile.js.map +0 -1
  182. package/dist/tools/cli/editFile.test.d.ts +0 -8
  183. package/dist/tools/cli/editFile.test.d.ts.map +0 -1
  184. package/dist/tools/cli/editFile.test.js +0 -177
  185. package/dist/tools/cli/editFile.test.js.map +0 -1
  186. package/dist/tools/cli/readFile.d.ts +0 -25
  187. package/dist/tools/cli/readFile.d.ts.map +0 -1
  188. package/dist/tools/cli/readFile.js +0 -118
  189. package/dist/tools/cli/readFile.js.map +0 -1
  190. package/dist/tools/cli/readFile.test.d.ts +0 -8
  191. package/dist/tools/cli/readFile.test.d.ts.map +0 -1
  192. package/dist/tools/cli/readFile.test.js +0 -194
  193. package/dist/tools/cli/readFile.test.js.map +0 -1
  194. package/dist/tools/cli/search.d.ts +0 -16
  195. package/dist/tools/cli/search.d.ts.map +0 -1
  196. package/dist/tools/cli/search.js +0 -261
  197. package/dist/tools/cli/search.js.map +0 -1
  198. package/dist/tools/cli/search.test.d.ts +0 -8
  199. package/dist/tools/cli/search.test.d.ts.map +0 -1
  200. package/dist/tools/cli/search.test.js +0 -172
  201. package/dist/tools/cli/search.test.js.map +0 -1
  202. package/dist/tools/cli/subagent.d.ts +0 -43
  203. package/dist/tools/cli/subagent.d.ts.map +0 -1
  204. package/dist/tools/cli/subagent.js +0 -99
  205. package/dist/tools/cli/subagent.js.map +0 -1
  206. package/dist/tools/cli/subagent.test.d.ts +0 -8
  207. package/dist/tools/cli/subagent.test.d.ts.map +0 -1
  208. package/dist/tools/cli/subagent.test.js +0 -190
  209. package/dist/tools/cli/subagent.test.js.map +0 -1
  210. package/dist/tools/cli/terminal.d.ts +0 -19
  211. package/dist/tools/cli/terminal.d.ts.map +0 -1
  212. package/dist/tools/cli/terminal.js +0 -164
  213. package/dist/tools/cli/terminal.js.map +0 -1
  214. package/dist/tools/cli/terminal.test.d.ts +0 -8
  215. package/dist/tools/cli/terminal.test.d.ts.map +0 -1
  216. package/dist/tools/cli/terminal.test.js +0 -161
  217. package/dist/tools/cli/terminal.test.js.map +0 -1
  218. package/dist/tools/index.d.ts +0 -25
  219. package/dist/tools/index.d.ts.map +0 -1
  220. package/dist/tools/index.js +0 -41
  221. package/dist/tools/index.js.map +0 -1
  222. package/dist/tools/interface.d.ts +0 -64
  223. package/dist/tools/interface.d.ts.map +0 -1
  224. package/dist/tools/interface.js +0 -37
  225. package/dist/tools/interface.js.map +0 -1
  226. package/dist/tools/interface.test.d.ts +0 -7
  227. package/dist/tools/interface.test.d.ts.map +0 -1
  228. package/dist/tools/interface.test.js +0 -179
  229. package/dist/tools/interface.test.js.map +0 -1
  230. package/dist/tools/mcp/bridge.d.ts +0 -48
  231. package/dist/tools/mcp/bridge.d.ts.map +0 -1
  232. package/dist/tools/mcp/bridge.js +0 -128
  233. package/dist/tools/mcp/bridge.js.map +0 -1
  234. package/dist/tools/mcp/bridge.test.d.ts +0 -8
  235. package/dist/tools/mcp/bridge.test.d.ts.map +0 -1
  236. package/dist/tools/mcp/bridge.test.js +0 -300
  237. package/dist/tools/mcp/bridge.test.js.map +0 -1
  238. package/dist/tools/mcp/client.d.ts +0 -135
  239. package/dist/tools/mcp/client.d.ts.map +0 -1
  240. package/dist/tools/mcp/client.js +0 -263
  241. package/dist/tools/mcp/client.js.map +0 -1
  242. package/dist/tools/mcp/client.test.d.ts +0 -8
  243. package/dist/tools/mcp/client.test.d.ts.map +0 -1
  244. package/dist/tools/mcp/client.test.js +0 -390
  245. package/dist/tools/mcp/client.test.js.map +0 -1
  246. package/dist/tools/registry.d.ts +0 -82
  247. package/dist/tools/registry.d.ts.map +0 -1
  248. package/dist/tools/registry.js +0 -99
  249. package/dist/tools/registry.js.map +0 -1
  250. package/dist/tools/registry.test.d.ts +0 -7
  251. package/dist/tools/registry.test.d.ts.map +0 -1
  252. package/dist/tools/registry.test.js +0 -199
  253. package/dist/tools/registry.test.js.map +0 -1
  254. package/dist/tools/suite.test.d.ts +0 -11
  255. package/dist/tools/suite.test.d.ts.map +0 -1
  256. package/dist/tools/suite.test.js +0 -119
  257. package/dist/tools/suite.test.js.map +0 -1
  258. package/dist/tools/types.d.ts +0 -75
  259. package/dist/tools/types.d.ts.map +0 -1
  260. package/dist/tools/types.js +0 -30
  261. package/dist/tools/types.js.map +0 -1
  262. package/dist/tools/types.test.d.ts +0 -7
  263. package/dist/tools/types.test.d.ts.map +0 -1
  264. package/dist/tools/types.test.js +0 -178
  265. package/dist/tools/types.test.js.map +0 -1
@@ -1,263 +0,0 @@
1
- /**
2
- * MCP Client
3
- *
4
- * Minimal Model Context Protocol client that communicates with
5
- * MCP servers over stdio transport using JSON-RPC 2.0.
6
- */
7
- import { spawn } from 'node:child_process';
8
- import { createInterface } from 'node:readline';
9
- /** Default timeout for MCP requests in milliseconds */
10
- const DEFAULT_TIMEOUT_MS = 10_000;
11
- /** MCP protocol version */
12
- const PROTOCOL_VERSION = '2024-11-05';
13
- /** Client info sent during initialization */
14
- const CLIENT_INFO = { name: 'beth-cli', version: '1.0.15' };
15
- // =============================================================================
16
- // MCP Client
17
- // =============================================================================
18
- /**
19
- * MCP protocol client for stdio-based MCP servers.
20
- *
21
- * Handles the JSON-RPC 2.0 transport, initialization handshake,
22
- * tool listing, and tool invocation.
23
- *
24
- * @example
25
- * ```typescript
26
- * const client = new MCPClient('shadcn', {
27
- * command: 'npx',
28
- * args: ['shadcn@3.7.0', 'mcp'],
29
- * });
30
- * await client.connect();
31
- * const tools = await client.listTools();
32
- * const result = await client.callTool('search', { query: 'button' });
33
- * await client.disconnect();
34
- * ```
35
- */
36
- export class MCPClient {
37
- /** Server name (for logging/namespacing) */
38
- name;
39
- /** Server configuration */
40
- config;
41
- /** Spawn function (injectable for testing) */
42
- spawnFn;
43
- /** Request timeout in milliseconds */
44
- timeoutMs;
45
- /** The spawned server process */
46
- process = null;
47
- /** Readline interface for reading stdout line by line */
48
- reader = null;
49
- /** Incrementing request ID counter */
50
- nextId = 1;
51
- /** Whether the initialization handshake has completed */
52
- initialized = false;
53
- /** Pending request resolvers keyed by request ID */
54
- pending = new Map();
55
- constructor(name, config, options) {
56
- this.name = name;
57
- this.config = config;
58
- this.spawnFn = options?.spawnFn ?? spawn;
59
- this.timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
60
- }
61
- /**
62
- * Whether the client is connected and initialized.
63
- */
64
- get connected() {
65
- return this.initialized && this.process !== null;
66
- }
67
- /**
68
- * Start the MCP server process and perform the initialization handshake.
69
- *
70
- * @throws Error if the process fails to start or initialization times out
71
- */
72
- async connect() {
73
- if (this.process) {
74
- throw new Error(`MCP client "${this.name}" is already connected`);
75
- }
76
- const env = this.config.env
77
- ? { ...process.env, ...this.config.env }
78
- : process.env;
79
- this.process = this.spawnFn(this.config.command, this.config.args, {
80
- stdio: ['pipe', 'pipe', 'pipe'],
81
- env,
82
- });
83
- // Handle unexpected process exit
84
- this.process.on('error', (err) => {
85
- this.rejectAllPending(new Error(`MCP server "${this.name}" process error: ${err.message}`));
86
- this.cleanup();
87
- });
88
- this.process.on('close', () => {
89
- this.rejectAllPending(new Error(`MCP server "${this.name}" process exited unexpectedly`));
90
- this.cleanup();
91
- });
92
- // Set up line-delimited JSON reader on stdout
93
- if (!this.process.stdout) {
94
- throw new Error(`MCP server "${this.name}" has no stdout`);
95
- }
96
- this.reader = createInterface({ input: this.process.stdout });
97
- this.reader.on('line', (line) => this.handleLine(line));
98
- // Perform initialization handshake
99
- await this.sendRequest('initialize', {
100
- protocolVersion: PROTOCOL_VERSION,
101
- capabilities: {},
102
- clientInfo: CLIENT_INFO,
103
- });
104
- this.sendNotification('notifications/initialized');
105
- this.initialized = true;
106
- }
107
- /**
108
- * List available tools from the MCP server.
109
- *
110
- * @returns Array of tool information
111
- * @throws Error if not connected
112
- */
113
- async listTools() {
114
- this.ensureConnected();
115
- const result = await this.sendRequest('tools/list', {});
116
- const tools = result.tools;
117
- if (!Array.isArray(tools)) {
118
- return [];
119
- }
120
- return tools.map((t) => ({
121
- name: String(t.name ?? ''),
122
- description: String(t.description ?? ''),
123
- inputSchema: t.inputSchema ?? {},
124
- }));
125
- }
126
- /**
127
- * Call a tool on the MCP server.
128
- *
129
- * @param toolName - Name of the tool to call
130
- * @param args - Arguments to pass to the tool
131
- * @returns Tool execution result
132
- * @throws Error if not connected or the tool call fails
133
- */
134
- async callTool(toolName, args) {
135
- this.ensureConnected();
136
- return this.sendRequest('tools/call', {
137
- name: toolName,
138
- arguments: args,
139
- });
140
- }
141
- /**
142
- * Shut down the MCP server process.
143
- */
144
- async disconnect() {
145
- this.rejectAllPending(new Error(`MCP client "${this.name}" disconnecting`));
146
- this.cleanup();
147
- }
148
- // ===========================================================================
149
- // Internal: JSON-RPC transport
150
- // ===========================================================================
151
- /**
152
- * Send a JSON-RPC request and wait for the matching response.
153
- */
154
- sendRequest(method, params) {
155
- const id = this.nextId++;
156
- const message = {
157
- jsonrpc: '2.0',
158
- id,
159
- method,
160
- params,
161
- };
162
- return new Promise((resolve, reject) => {
163
- // Set up timeout
164
- const timer = setTimeout(() => {
165
- this.pending.delete(id);
166
- reject(new Error(`MCP request "${method}" timed out after ${this.timeoutMs}ms`));
167
- }, this.timeoutMs);
168
- this.pending.set(id, {
169
- resolve: (value) => {
170
- clearTimeout(timer);
171
- resolve(value);
172
- },
173
- reject: (reason) => {
174
- clearTimeout(timer);
175
- reject(reason);
176
- },
177
- });
178
- this.writeMessage(JSON.stringify(message));
179
- });
180
- }
181
- /**
182
- * Send a JSON-RPC notification (no response expected).
183
- */
184
- sendNotification(method, params) {
185
- const message = {
186
- jsonrpc: '2.0',
187
- method,
188
- ...(params !== undefined ? { params } : {}),
189
- };
190
- this.writeMessage(JSON.stringify(message));
191
- }
192
- /**
193
- * Write a line-delimited JSON message to the process stdin.
194
- */
195
- writeMessage(json) {
196
- if (!this.process?.stdin?.writable) {
197
- throw new Error(`MCP server "${this.name}" stdin is not writable`);
198
- }
199
- this.process.stdin.write(json + '\n');
200
- }
201
- /**
202
- * Handle a line of JSON from the server's stdout.
203
- */
204
- handleLine(line) {
205
- const trimmed = line.trim();
206
- if (!trimmed)
207
- return;
208
- let parsed;
209
- try {
210
- parsed = JSON.parse(trimmed);
211
- }
212
- catch {
213
- // Ignore malformed lines (e.g., server logging to stdout)
214
- return;
215
- }
216
- // Only handle responses with an id that we're waiting for
217
- if (parsed.id === undefined || parsed.id === null)
218
- return;
219
- const pending = this.pending.get(parsed.id);
220
- if (!pending)
221
- return;
222
- this.pending.delete(parsed.id);
223
- if (parsed.error) {
224
- pending.reject(new Error(`MCP error (${parsed.error.code}): ${parsed.error.message}`));
225
- }
226
- else {
227
- pending.resolve(parsed.result);
228
- }
229
- }
230
- /**
231
- * Assert that the client is connected.
232
- */
233
- ensureConnected() {
234
- if (!this.connected) {
235
- throw new Error(`MCP client "${this.name}" is not connected`);
236
- }
237
- }
238
- /**
239
- * Reject all pending requests.
240
- */
241
- rejectAllPending(error) {
242
- for (const [id, pending] of this.pending) {
243
- pending.reject(error);
244
- this.pending.delete(id);
245
- }
246
- }
247
- /**
248
- * Clean up the process and reader.
249
- */
250
- cleanup() {
251
- this.initialized = false;
252
- if (this.reader) {
253
- this.reader.close();
254
- this.reader = null;
255
- }
256
- if (this.process) {
257
- this.process.stdin?.end();
258
- this.process.kill();
259
- this.process = null;
260
- }
261
- }
262
- }
263
- //# sourceMappingURL=client.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"client.js","sourceRoot":"","sources":["../../../src/tools/mcp/client.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,KAAK,EAAqB,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,eAAe,EAAuC,MAAM,eAAe,CAAC;AAmErF,uDAAuD;AACvD,MAAM,kBAAkB,GAAG,MAAM,CAAC;AAElC,2BAA2B;AAC3B,MAAM,gBAAgB,GAAG,YAAY,CAAC;AAEtC,6CAA6C;AAC7C,MAAM,WAAW,GAAG,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC;AAY5D,gFAAgF;AAChF,aAAa;AACb,gFAAgF;AAEhF;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,OAAO,SAAS;IACpB,4CAA4C;IACnC,IAAI,CAAS;IAEtB,2BAA2B;IACV,MAAM,CAAkB;IAEzC,8CAA8C;IAC7B,OAAO,CAAU;IAElC,sCAAsC;IACrB,SAAS,CAAS;IAEnC,iCAAiC;IACzB,OAAO,GAAwB,IAAI,CAAC;IAE5C,yDAAyD;IACjD,MAAM,GAA6B,IAAI,CAAC;IAEhD,sCAAsC;IAC9B,MAAM,GAAG,CAAC,CAAC;IAEnB,yDAAyD;IACjD,WAAW,GAAG,KAAK,CAAC;IAE5B,oDAAoD;IACnC,OAAO,GAAG,IAAI,GAAG,EAG9B,CAAC;IAEL,YACE,IAAY,EACZ,MAAuB,EACvB,OAGC;QAED,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,OAAO,GAAG,OAAO,EAAE,OAAO,IAAI,KAAK,CAAC;QACzC,IAAI,CAAC,SAAS,GAAG,OAAO,EAAE,SAAS,IAAI,kBAAkB,CAAC;IAC5D,CAAC;IAED;;OAEG;IACH,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC,OAAO,KAAK,IAAI,CAAC;IACnD,CAAC;IAED;;;;OAIG;IACH,KAAK,CAAC,OAAO;QACX,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,MAAM,IAAI,KAAK,CAAC,eAAe,IAAI,CAAC,IAAI,wBAAwB,CAAC,CAAC;QACpE,CAAC;QAED,MAAM,GAAG,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG;YACzB,CAAC,CAAC,EAAE,GAAG,OAAO,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE;YACxC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC;QAEhB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE;YACjE,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC;YAC/B,GAAG;SACJ,CAAC,CAAC;QAEH,iCAAiC;QACjC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;YAC/B,IAAI,CAAC,gBAAgB,CAAC,IAAI,KAAK,CAAC,eAAe,IAAI,CAAC,IAAI,oBAAoB,GAAG,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC;YAC5F,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,CAAC,CAAC,CAAC;QAEH,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YAC5B,IAAI,CAAC,gBAAgB,CAAC,IAAI,KAAK,CAAC,eAAe,IAAI,CAAC,IAAI,+BAA+B,CAAC,CAAC,CAAC;YAC1F,IAAI,CAAC,OAAO,EAAE,CAAC;QACjB,CAAC,CAAC,CAAC;QAEH,8CAA8C;QAC9C,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC;YACzB,MAAM,IAAI,KAAK,CAAC,eAAe,IAAI,CAAC,IAAI,iBAAiB,CAAC,CAAC;QAC7D,CAAC;QAED,IAAI,CAAC,MAAM,GAAG,eAAe,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;QAC9D,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;QAExD,mCAAmC;QACnC,MAAM,IAAI,CAAC,WAAW,CAAC,YAAY,EAAE;YACnC,eAAe,EAAE,gBAAgB;YACjC,YAAY,EAAE,EAAE;YAChB,UAAU,EAAE,WAAW;SACxB,CAAC,CAAC;QAEH,IAAI,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,CAAC;QACnD,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC;IAC1B,CAAC;IAED;;;;;OAKG;IACH,KAAK,CAAC,SAAS;QACb,IAAI,CAAC,eAAe,EAAE,CAAC;QAEvB,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,YAAY,EAAE,EAAE,CAA8B,CAAC;QACrF,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;QAE3B,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAC1B,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACvB,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;YAC1B,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,IAAI,EAAE,CAAC;YACxC,WAAW,EAAG,CAAC,CAAC,WAAuC,IAAI,EAAE;SAC9D,CAAC,CAAC,CAAC;IACN,CAAC;IAED;;;;;;;OAOG;IACH,KAAK,CAAC,QAAQ,CAAC,QAAgB,EAAE,IAA6B;QAC5D,IAAI,CAAC,eAAe,EAAE,CAAC;QAEvB,OAAO,IAAI,CAAC,WAAW,CAAC,YAAY,EAAE;YACpC,IAAI,EAAE,QAAQ;YACd,SAAS,EAAE,IAAI;SAChB,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACH,KAAK,CAAC,UAAU;QACd,IAAI,CAAC,gBAAgB,CAAC,IAAI,KAAK,CAAC,eAAe,IAAI,CAAC,IAAI,iBAAiB,CAAC,CAAC,CAAC;QAC5E,IAAI,CAAC,OAAO,EAAE,CAAC;IACjB,CAAC;IAED,8EAA8E;IAC9E,+BAA+B;IAC/B,8EAA8E;IAE9E;;OAEG;IACK,WAAW,CAAC,MAAc,EAAE,MAA+B;QACjE,MAAM,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;QAEzB,MAAM,OAAO,GAAmB;YAC9B,OAAO,EAAE,KAAK;YACd,EAAE;YACF,MAAM;YACN,MAAM;SACP,CAAC;QAEF,OAAO,IAAI,OAAO,CAAU,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YAC9C,iBAAiB;YACjB,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;gBAC5B,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;gBACxB,MAAM,CAAC,IAAI,KAAK,CAAC,gBAAgB,MAAM,qBAAqB,IAAI,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC;YACnF,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC;YAEnB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,EAAE,EAAE;gBACnB,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;oBACjB,YAAY,CAAC,KAAK,CAAC,CAAC;oBACpB,OAAO,CAAC,KAAK,CAAC,CAAC;gBACjB,CAAC;gBACD,MAAM,EAAE,CAAC,MAAM,EAAE,EAAE;oBACjB,YAAY,CAAC,KAAK,CAAC,CAAC;oBACpB,MAAM,CAAC,MAAM,CAAC,CAAC;gBACjB,CAAC;aACF,CAAC,CAAC;YAEH,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;IACL,CAAC;IAED;;OAEG;IACK,gBAAgB,CAAC,MAAc,EAAE,MAAgC;QACvE,MAAM,OAAO,GAAwB;YACnC,OAAO,EAAE,KAAK;YACd,MAAM;YACN,GAAG,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC5C,CAAC;QAEF,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;IAC7C,CAAC;IAED;;OAEG;IACK,YAAY,CAAC,IAAY;QAC/B,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC;YACnC,MAAM,IAAI,KAAK,CAAC,eAAe,IAAI,CAAC,IAAI,yBAAyB,CAAC,CAAC;QACrE,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,GAAG,IAAI,CAAC,CAAC;IACxC,CAAC;IAED;;OAEG;IACK,UAAU,CAAC,IAAY;QAC7B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,OAAO;YAAE,OAAO;QAErB,IAAI,MAAuB,CAAC;QAC5B,IAAI,CAAC;YACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAoB,CAAC;QAClD,CAAC;QAAC,MAAM,CAAC;YACP,0DAA0D;YAC1D,OAAO;QACT,CAAC;QAED,0DAA0D;QAC1D,IAAI,MAAM,CAAC,EAAE,KAAK,SAAS,IAAI,MAAM,CAAC,EAAE,KAAK,IAAI;YAAE,OAAO;QAE1D,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC5C,IAAI,CAAC,OAAO;YAAE,OAAO;QAErB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAE/B,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;YACjB,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CACtB,cAAc,MAAM,CAAC,KAAK,CAAC,IAAI,MAAM,MAAM,CAAC,KAAK,CAAC,OAAO,EAAE,CAC5D,CAAC,CAAC;QACL,CAAC;aAAM,CAAC;YACN,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;IAED;;OAEG;IACK,eAAe;QACrB,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;YACpB,MAAM,IAAI,KAAK,CAAC,eAAe,IAAI,CAAC,IAAI,oBAAoB,CAAC,CAAC;QAChE,CAAC;IACH,CAAC;IAED;;OAEG;IACK,gBAAgB,CAAC,KAAY;QACnC,KAAK,MAAM,CAAC,EAAE,EAAE,OAAO,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACzC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACtB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC1B,CAAC;IACH,CAAC;IAED;;OAEG;IACK,OAAO;QACb,IAAI,CAAC,WAAW,GAAG,KAAK,CAAC;QAEzB,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YAChB,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;YACpB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACrB,CAAC;QAED,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACjB,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,EAAE,CAAC;YAC1B,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,CAAC;YACpB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACtB,CAAC;IACH,CAAC;CACF"}
@@ -1,8 +0,0 @@
1
- /**
2
- * MCP Client Tests
3
- *
4
- * Tests for the MCP protocol client including JSON-RPC message
5
- * formatting, request lifecycle, and timeout handling.
6
- */
7
- export {};
8
- //# sourceMappingURL=client.test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"client.test.d.ts","sourceRoot":"","sources":["../../../src/tools/mcp/client.test.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -1,390 +0,0 @@
1
- /**
2
- * MCP Client Tests
3
- *
4
- * Tests for the MCP protocol client including JSON-RPC message
5
- * formatting, request lifecycle, and timeout handling.
6
- */
7
- import { describe, it } from 'node:test';
8
- import assert from 'node:assert';
9
- import { EventEmitter } from 'node:events';
10
- import { PassThrough } from 'node:stream';
11
- import { MCPClient } from './client.js';
12
- /**
13
- * Create a mock child process with controllable stdin/stdout.
14
- */
15
- function createMockProcess() {
16
- const proc = new EventEmitter();
17
- proc.stdin = new PassThrough();
18
- proc.stdout = new PassThrough();
19
- proc.stderr = new PassThrough();
20
- proc.killed = false;
21
- proc.kill = () => { proc.killed = true; };
22
- return proc;
23
- }
24
- /**
25
- * Collect all messages written to a PassThrough stream as parsed JSON-RPC.
26
- */
27
- function collectMessages(stream) {
28
- const messages = [];
29
- let buffer = '';
30
- stream.on('data', (chunk) => {
31
- buffer += chunk.toString();
32
- const lines = buffer.split('\n');
33
- // Keep the last incomplete line in the buffer
34
- buffer = lines.pop() ?? '';
35
- for (const line of lines) {
36
- if (line.trim()) {
37
- messages.push(JSON.parse(line));
38
- }
39
- }
40
- });
41
- return messages;
42
- }
43
- /**
44
- * Create a spawn function that returns the given mock process.
45
- */
46
- function createMockSpawn(proc) {
47
- return (() => proc);
48
- }
49
- /**
50
- * Respond to pending requests on a mock process's stdout.
51
- * Reads from stdin, auto-responds with matching id.
52
- */
53
- function autoRespond(proc, handler) {
54
- let buffer = '';
55
- proc.stdin.on('data', (chunk) => {
56
- buffer += chunk.toString();
57
- const lines = buffer.split('\n');
58
- buffer = lines.pop() ?? '';
59
- for (const line of lines) {
60
- if (!line.trim())
61
- continue;
62
- const msg = JSON.parse(line);
63
- // Only respond to requests (those with an id), not notifications
64
- if (msg.id !== undefined) {
65
- const result = handler(msg);
66
- const response = JSON.stringify({ jsonrpc: '2.0', id: msg.id, result });
67
- proc.stdout.push(response + '\n');
68
- }
69
- }
70
- });
71
- }
72
- // =============================================================================
73
- // Tests
74
- // =============================================================================
75
- describe('MCPClient', () => {
76
- describe('construction', () => {
77
- it('should create a client with name and config', () => {
78
- const client = new MCPClient('test-server', {
79
- command: 'npx',
80
- args: ['-y', 'some-server'],
81
- });
82
- assert.strictEqual(client.name, 'test-server');
83
- assert.strictEqual(client.connected, false);
84
- });
85
- it('should accept custom timeout', () => {
86
- const client = new MCPClient('test', { command: 'node', args: [] }, {
87
- timeoutMs: 5000,
88
- });
89
- assert.strictEqual(client.name, 'test');
90
- });
91
- });
92
- describe('connect', () => {
93
- it('should spawn the server process and perform handshake', async () => {
94
- const proc = createMockProcess();
95
- let spawnCalled = false;
96
- let spawnCmd = '';
97
- let spawnArgs = [];
98
- const mockSpawn = ((cmd, args) => {
99
- spawnCalled = true;
100
- spawnCmd = cmd;
101
- spawnArgs = args;
102
- return proc;
103
- });
104
- autoRespond(proc, (msg) => {
105
- if (msg.method === 'initialize') {
106
- return { protocolVersion: '2024-11-05', capabilities: {} };
107
- }
108
- return {};
109
- });
110
- const client = new MCPClient('test', {
111
- command: 'npx',
112
- args: ['-y', 'test-server'],
113
- }, { spawnFn: mockSpawn });
114
- await client.connect();
115
- assert.strictEqual(spawnCalled, true);
116
- assert.strictEqual(spawnCmd, 'npx');
117
- assert.deepStrictEqual(spawnArgs, ['-y', 'test-server']);
118
- assert.strictEqual(client.connected, true);
119
- await client.disconnect();
120
- });
121
- it('should throw if already connected', async () => {
122
- const proc = createMockProcess();
123
- autoRespond(proc, () => ({ protocolVersion: '2024-11-05', capabilities: {} }));
124
- const client = new MCPClient('test', { command: 'node', args: [] }, {
125
- spawnFn: createMockSpawn(proc),
126
- });
127
- await client.connect();
128
- await assert.rejects(() => client.connect(), (err) => {
129
- assert.ok(err.message.includes('already connected'));
130
- return true;
131
- });
132
- await client.disconnect();
133
- });
134
- it('should send initialize request with correct params', async () => {
135
- const proc = createMockProcess();
136
- const messages = collectMessages(proc.stdin);
137
- autoRespond(proc, () => ({ protocolVersion: '2024-11-05', capabilities: {} }));
138
- const client = new MCPClient('test', { command: 'node', args: [] }, {
139
- spawnFn: createMockSpawn(proc),
140
- });
141
- await client.connect();
142
- // First message should be the initialize request
143
- const initMsg = messages[0];
144
- assert.strictEqual(initMsg.jsonrpc, '2.0');
145
- assert.strictEqual(initMsg.method, 'initialize');
146
- assert.strictEqual(initMsg.id, 1);
147
- const params = initMsg.params;
148
- assert.strictEqual(params.protocolVersion, '2024-11-05');
149
- assert.deepStrictEqual(params.clientInfo, { name: 'beth-cli', version: '1.0.15' });
150
- await client.disconnect();
151
- });
152
- it('should send initialized notification after handshake', async () => {
153
- const proc = createMockProcess();
154
- const messages = collectMessages(proc.stdin);
155
- autoRespond(proc, () => ({ protocolVersion: '2024-11-05', capabilities: {} }));
156
- const client = new MCPClient('test', { command: 'node', args: [] }, {
157
- spawnFn: createMockSpawn(proc),
158
- });
159
- await client.connect();
160
- // Second message should be the initialized notification (no id)
161
- const notifMsg = messages[1];
162
- assert.strictEqual(notifMsg.jsonrpc, '2.0');
163
- assert.strictEqual(notifMsg.method, 'notifications/initialized');
164
- assert.strictEqual(notifMsg.id, undefined);
165
- await client.disconnect();
166
- });
167
- });
168
- describe('request IDs', () => {
169
- it('should increment request IDs', async () => {
170
- const proc = createMockProcess();
171
- const messages = collectMessages(proc.stdin);
172
- autoRespond(proc, () => ({ protocolVersion: '2024-11-05', capabilities: {}, tools: [] }));
173
- const client = new MCPClient('test', { command: 'node', args: [] }, {
174
- spawnFn: createMockSpawn(proc),
175
- });
176
- await client.connect(); // id: 1 (initialize)
177
- await client.listTools(); // id: 2 (tools/list)
178
- const initMsg = messages[0];
179
- assert.strictEqual(initMsg.id, 1);
180
- // messages[1] is the notification (no id), messages[2] is tools/list
181
- const listMsg = messages[2];
182
- assert.strictEqual(listMsg.id, 2);
183
- await client.disconnect();
184
- });
185
- });
186
- describe('listTools', () => {
187
- it('should return tool info from server', async () => {
188
- const proc = createMockProcess();
189
- autoRespond(proc, (msg) => {
190
- if (msg.method === 'initialize') {
191
- return { protocolVersion: '2024-11-05', capabilities: {} };
192
- }
193
- if (msg.method === 'tools/list') {
194
- return {
195
- tools: [
196
- {
197
- name: 'search',
198
- description: 'Search for components',
199
- inputSchema: { type: 'object', properties: { query: { type: 'string' } } },
200
- },
201
- {
202
- name: 'install',
203
- description: 'Install a component',
204
- inputSchema: { type: 'object', properties: { name: { type: 'string' } } },
205
- },
206
- ],
207
- };
208
- }
209
- return {};
210
- });
211
- const client = new MCPClient('test', { command: 'node', args: [] }, {
212
- spawnFn: createMockSpawn(proc),
213
- });
214
- await client.connect();
215
- const tools = await client.listTools();
216
- assert.strictEqual(tools.length, 2);
217
- assert.strictEqual(tools[0].name, 'search');
218
- assert.strictEqual(tools[0].description, 'Search for components');
219
- assert.deepStrictEqual(tools[0].inputSchema, {
220
- type: 'object',
221
- properties: { query: { type: 'string' } },
222
- });
223
- assert.strictEqual(tools[1].name, 'install');
224
- await client.disconnect();
225
- });
226
- it('should return empty array when server returns no tools', async () => {
227
- const proc = createMockProcess();
228
- autoRespond(proc, (msg) => {
229
- if (msg.method === 'initialize') {
230
- return { protocolVersion: '2024-11-05', capabilities: {} };
231
- }
232
- return { tools: [] };
233
- });
234
- const client = new MCPClient('test', { command: 'node', args: [] }, {
235
- spawnFn: createMockSpawn(proc),
236
- });
237
- await client.connect();
238
- const tools = await client.listTools();
239
- assert.deepStrictEqual(tools, []);
240
- await client.disconnect();
241
- });
242
- it('should throw if not connected', async () => {
243
- const client = new MCPClient('test', { command: 'node', args: [] });
244
- await assert.rejects(() => client.listTools(), (err) => {
245
- assert.ok(err.message.includes('not connected'));
246
- return true;
247
- });
248
- });
249
- });
250
- describe('callTool', () => {
251
- it('should send tools/call request with name and arguments', async () => {
252
- const proc = createMockProcess();
253
- const messages = collectMessages(proc.stdin);
254
- autoRespond(proc, (msg) => {
255
- if (msg.method === 'initialize') {
256
- return { protocolVersion: '2024-11-05', capabilities: {} };
257
- }
258
- if (msg.method === 'tools/call') {
259
- return { content: [{ type: 'text', text: 'result data' }] };
260
- }
261
- return {};
262
- });
263
- const client = new MCPClient('test', { command: 'node', args: [] }, {
264
- spawnFn: createMockSpawn(proc),
265
- });
266
- await client.connect();
267
- const result = await client.callTool('search', { query: 'button' });
268
- // Verify the request format (messages[2] is the callTool request)
269
- const callMsg = messages[2];
270
- assert.strictEqual(callMsg.method, 'tools/call');
271
- const params = callMsg.params;
272
- assert.strictEqual(params.name, 'search');
273
- assert.deepStrictEqual(params.arguments, { query: 'button' });
274
- // Verify the result
275
- assert.deepStrictEqual(result, { content: [{ type: 'text', text: 'result data' }] });
276
- await client.disconnect();
277
- });
278
- it('should throw if not connected', async () => {
279
- const client = new MCPClient('test', { command: 'node', args: [] });
280
- await assert.rejects(() => client.callTool('search', {}), (err) => {
281
- assert.ok(err.message.includes('not connected'));
282
- return true;
283
- });
284
- });
285
- });
286
- describe('disconnect', () => {
287
- it('should set connected to false', async () => {
288
- const proc = createMockProcess();
289
- autoRespond(proc, () => ({ protocolVersion: '2024-11-05', capabilities: {} }));
290
- const client = new MCPClient('test', { command: 'node', args: [] }, {
291
- spawnFn: createMockSpawn(proc),
292
- });
293
- await client.connect();
294
- assert.strictEqual(client.connected, true);
295
- await client.disconnect();
296
- assert.strictEqual(client.connected, false);
297
- });
298
- it('should be safe to call when not connected', async () => {
299
- const client = new MCPClient('test', { command: 'node', args: [] });
300
- // Should not throw
301
- await client.disconnect();
302
- assert.strictEqual(client.connected, false);
303
- });
304
- });
305
- describe('timeout handling', () => {
306
- it('should reject request on timeout', async () => {
307
- const proc = createMockProcess();
308
- // Don't auto-respond to anything — let it time out
309
- const client = new MCPClient('test', { command: 'node', args: [] }, {
310
- spawnFn: createMockSpawn(proc),
311
- timeoutMs: 50, // Very short timeout for test
312
- });
313
- await assert.rejects(() => client.connect(), // initialize will time out
314
- (err) => {
315
- assert.ok(err.message.includes('timed out'));
316
- return true;
317
- });
318
- await client.disconnect();
319
- });
320
- });
321
- describe('error responses', () => {
322
- it('should reject on JSON-RPC error response', async () => {
323
- const proc = createMockProcess();
324
- let initDone = false;
325
- // Custom handler: respond to initialize, error on tools/list
326
- let buffer = '';
327
- proc.stdin.on('data', (chunk) => {
328
- buffer += chunk.toString();
329
- const lines = buffer.split('\n');
330
- buffer = lines.pop() ?? '';
331
- for (const line of lines) {
332
- if (!line.trim())
333
- continue;
334
- const msg = JSON.parse(line);
335
- if (msg.id === undefined)
336
- continue;
337
- if (msg.method === 'initialize') {
338
- const resp = JSON.stringify({
339
- jsonrpc: '2.0',
340
- id: msg.id,
341
- result: { protocolVersion: '2024-11-05', capabilities: {} },
342
- });
343
- proc.stdout.push(resp + '\n');
344
- initDone = true;
345
- }
346
- else if (msg.method === 'tools/list' && initDone) {
347
- const resp = JSON.stringify({
348
- jsonrpc: '2.0',
349
- id: msg.id,
350
- error: { code: -32600, message: 'Not supported' },
351
- });
352
- proc.stdout.push(resp + '\n');
353
- }
354
- }
355
- });
356
- const client = new MCPClient('test', { command: 'node', args: [] }, {
357
- spawnFn: createMockSpawn(proc),
358
- });
359
- await client.connect();
360
- await assert.rejects(() => client.listTools(), (err) => {
361
- assert.ok(err.message.includes('Not supported'));
362
- return true;
363
- });
364
- await client.disconnect();
365
- });
366
- });
367
- describe('malformed responses', () => {
368
- it('should ignore non-JSON lines from server', async () => {
369
- const proc = createMockProcess();
370
- // Send garbage before the real response
371
- autoRespond(proc, (msg) => {
372
- if (msg.method === 'initialize') {
373
- // Push garbage first
374
- proc.stdout.push('This is not JSON\n');
375
- proc.stdout.push('DEBUG: server starting\n');
376
- return { protocolVersion: '2024-11-05', capabilities: {} };
377
- }
378
- return {};
379
- });
380
- const client = new MCPClient('test', { command: 'node', args: [] }, {
381
- spawnFn: createMockSpawn(proc),
382
- });
383
- // Should not throw despite garbage lines
384
- await client.connect();
385
- assert.strictEqual(client.connected, true);
386
- await client.disconnect();
387
- });
388
- });
389
- });
390
- //# sourceMappingURL=client.test.js.map