@yuaone/core 0.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 (235) hide show
  1. package/LICENSE +663 -0
  2. package/README.md +15 -0
  3. package/dist/__tests__/context-manager.test.d.ts +6 -0
  4. package/dist/__tests__/context-manager.test.d.ts.map +1 -0
  5. package/dist/__tests__/context-manager.test.js +220 -0
  6. package/dist/__tests__/context-manager.test.js.map +1 -0
  7. package/dist/__tests__/governor.test.d.ts +6 -0
  8. package/dist/__tests__/governor.test.d.ts.map +1 -0
  9. package/dist/__tests__/governor.test.js +210 -0
  10. package/dist/__tests__/governor.test.js.map +1 -0
  11. package/dist/__tests__/model-router.test.d.ts +6 -0
  12. package/dist/__tests__/model-router.test.d.ts.map +1 -0
  13. package/dist/__tests__/model-router.test.js +329 -0
  14. package/dist/__tests__/model-router.test.js.map +1 -0
  15. package/dist/agent-logger.d.ts +384 -0
  16. package/dist/agent-logger.d.ts.map +1 -0
  17. package/dist/agent-logger.js +820 -0
  18. package/dist/agent-logger.js.map +1 -0
  19. package/dist/agent-loop.d.ts +163 -0
  20. package/dist/agent-loop.d.ts.map +1 -0
  21. package/dist/agent-loop.js +609 -0
  22. package/dist/agent-loop.js.map +1 -0
  23. package/dist/agent-modes.d.ts +85 -0
  24. package/dist/agent-modes.d.ts.map +1 -0
  25. package/dist/agent-modes.js +418 -0
  26. package/dist/agent-modes.js.map +1 -0
  27. package/dist/approval.d.ts +137 -0
  28. package/dist/approval.d.ts.map +1 -0
  29. package/dist/approval.js +299 -0
  30. package/dist/approval.js.map +1 -0
  31. package/dist/async-completion-queue.d.ts +56 -0
  32. package/dist/async-completion-queue.d.ts.map +1 -0
  33. package/dist/async-completion-queue.js +77 -0
  34. package/dist/async-completion-queue.js.map +1 -0
  35. package/dist/auto-fix.d.ts +174 -0
  36. package/dist/auto-fix.d.ts.map +1 -0
  37. package/dist/auto-fix.js +319 -0
  38. package/dist/auto-fix.js.map +1 -0
  39. package/dist/codebase-context.d.ts +396 -0
  40. package/dist/codebase-context.d.ts.map +1 -0
  41. package/dist/codebase-context.js +1260 -0
  42. package/dist/codebase-context.js.map +1 -0
  43. package/dist/conflict-resolver.d.ts +191 -0
  44. package/dist/conflict-resolver.d.ts.map +1 -0
  45. package/dist/conflict-resolver.js +524 -0
  46. package/dist/conflict-resolver.js.map +1 -0
  47. package/dist/constants.d.ts +52 -0
  48. package/dist/constants.d.ts.map +1 -0
  49. package/dist/constants.js +141 -0
  50. package/dist/constants.js.map +1 -0
  51. package/dist/context-budget.d.ts +435 -0
  52. package/dist/context-budget.d.ts.map +1 -0
  53. package/dist/context-budget.js +903 -0
  54. package/dist/context-budget.js.map +1 -0
  55. package/dist/context-compressor.d.ts +143 -0
  56. package/dist/context-compressor.d.ts.map +1 -0
  57. package/dist/context-compressor.js +511 -0
  58. package/dist/context-compressor.js.map +1 -0
  59. package/dist/context-manager.d.ts +112 -0
  60. package/dist/context-manager.d.ts.map +1 -0
  61. package/dist/context-manager.js +247 -0
  62. package/dist/context-manager.js.map +1 -0
  63. package/dist/continuous-reflection.d.ts +267 -0
  64. package/dist/continuous-reflection.d.ts.map +1 -0
  65. package/dist/continuous-reflection.js +338 -0
  66. package/dist/continuous-reflection.js.map +1 -0
  67. package/dist/cross-file-refactor.d.ts +352 -0
  68. package/dist/cross-file-refactor.d.ts.map +1 -0
  69. package/dist/cross-file-refactor.js +1544 -0
  70. package/dist/cross-file-refactor.js.map +1 -0
  71. package/dist/dag-orchestrator.d.ts +138 -0
  72. package/dist/dag-orchestrator.d.ts.map +1 -0
  73. package/dist/dag-orchestrator.js +379 -0
  74. package/dist/dag-orchestrator.js.map +1 -0
  75. package/dist/debate-orchestrator.d.ts +301 -0
  76. package/dist/debate-orchestrator.d.ts.map +1 -0
  77. package/dist/debate-orchestrator.js +719 -0
  78. package/dist/debate-orchestrator.js.map +1 -0
  79. package/dist/dependency-analyzer.d.ts +113 -0
  80. package/dist/dependency-analyzer.d.ts.map +1 -0
  81. package/dist/dependency-analyzer.js +444 -0
  82. package/dist/dependency-analyzer.js.map +1 -0
  83. package/dist/design-loop.d.ts +59 -0
  84. package/dist/design-loop.d.ts.map +1 -0
  85. package/dist/design-loop.js +344 -0
  86. package/dist/design-loop.js.map +1 -0
  87. package/dist/doc-intelligence.d.ts +383 -0
  88. package/dist/doc-intelligence.d.ts.map +1 -0
  89. package/dist/doc-intelligence.js +1307 -0
  90. package/dist/doc-intelligence.js.map +1 -0
  91. package/dist/dynamic-role-generator.d.ts +76 -0
  92. package/dist/dynamic-role-generator.d.ts.map +1 -0
  93. package/dist/dynamic-role-generator.js +194 -0
  94. package/dist/dynamic-role-generator.js.map +1 -0
  95. package/dist/errors.d.ts +69 -0
  96. package/dist/errors.d.ts.map +1 -0
  97. package/dist/errors.js +102 -0
  98. package/dist/errors.js.map +1 -0
  99. package/dist/event-bus.d.ts +159 -0
  100. package/dist/event-bus.d.ts.map +1 -0
  101. package/dist/event-bus.js +305 -0
  102. package/dist/event-bus.js.map +1 -0
  103. package/dist/execution-engine.d.ts +425 -0
  104. package/dist/execution-engine.d.ts.map +1 -0
  105. package/dist/execution-engine.js +1555 -0
  106. package/dist/execution-engine.js.map +1 -0
  107. package/dist/git-intelligence.d.ts +306 -0
  108. package/dist/git-intelligence.d.ts.map +1 -0
  109. package/dist/git-intelligence.js +1099 -0
  110. package/dist/git-intelligence.js.map +1 -0
  111. package/dist/governor.d.ts +77 -0
  112. package/dist/governor.d.ts.map +1 -0
  113. package/dist/governor.js +161 -0
  114. package/dist/governor.js.map +1 -0
  115. package/dist/hierarchical-planner.d.ts +313 -0
  116. package/dist/hierarchical-planner.d.ts.map +1 -0
  117. package/dist/hierarchical-planner.js +981 -0
  118. package/dist/hierarchical-planner.js.map +1 -0
  119. package/dist/index.d.ts +121 -0
  120. package/dist/index.d.ts.map +1 -0
  121. package/dist/index.js +123 -0
  122. package/dist/index.js.map +1 -0
  123. package/dist/intent-inference.d.ts +103 -0
  124. package/dist/intent-inference.d.ts.map +1 -0
  125. package/dist/intent-inference.js +605 -0
  126. package/dist/intent-inference.js.map +1 -0
  127. package/dist/interrupt-manager.d.ts +143 -0
  128. package/dist/interrupt-manager.d.ts.map +1 -0
  129. package/dist/interrupt-manager.js +196 -0
  130. package/dist/interrupt-manager.js.map +1 -0
  131. package/dist/kernel.d.ts +564 -0
  132. package/dist/kernel.d.ts.map +1 -0
  133. package/dist/kernel.js +1419 -0
  134. package/dist/kernel.js.map +1 -0
  135. package/dist/language-support.d.ts +232 -0
  136. package/dist/language-support.d.ts.map +1 -0
  137. package/dist/language-support.js +1134 -0
  138. package/dist/language-support.js.map +1 -0
  139. package/dist/llm-client.d.ts +82 -0
  140. package/dist/llm-client.d.ts.map +1 -0
  141. package/dist/llm-client.js +475 -0
  142. package/dist/llm-client.js.map +1 -0
  143. package/dist/mcp-client.d.ts +232 -0
  144. package/dist/mcp-client.d.ts.map +1 -0
  145. package/dist/mcp-client.js +718 -0
  146. package/dist/mcp-client.js.map +1 -0
  147. package/dist/memory-manager.d.ts +200 -0
  148. package/dist/memory-manager.d.ts.map +1 -0
  149. package/dist/memory-manager.js +568 -0
  150. package/dist/memory-manager.js.map +1 -0
  151. package/dist/memory.d.ts +87 -0
  152. package/dist/memory.d.ts.map +1 -0
  153. package/dist/memory.js +341 -0
  154. package/dist/memory.js.map +1 -0
  155. package/dist/model-router.d.ts +245 -0
  156. package/dist/model-router.d.ts.map +1 -0
  157. package/dist/model-router.js +632 -0
  158. package/dist/model-router.js.map +1 -0
  159. package/dist/parallel-executor.d.ts +125 -0
  160. package/dist/parallel-executor.d.ts.map +1 -0
  161. package/dist/parallel-executor.js +201 -0
  162. package/dist/parallel-executor.js.map +1 -0
  163. package/dist/perf-optimizer.d.ts +212 -0
  164. package/dist/perf-optimizer.d.ts.map +1 -0
  165. package/dist/perf-optimizer.js +721 -0
  166. package/dist/perf-optimizer.js.map +1 -0
  167. package/dist/persona.d.ts +305 -0
  168. package/dist/persona.d.ts.map +1 -0
  169. package/dist/persona.js +887 -0
  170. package/dist/persona.js.map +1 -0
  171. package/dist/planner.d.ts +70 -0
  172. package/dist/planner.d.ts.map +1 -0
  173. package/dist/planner.js +264 -0
  174. package/dist/planner.js.map +1 -0
  175. package/dist/qa-pipeline.d.ts +365 -0
  176. package/dist/qa-pipeline.d.ts.map +1 -0
  177. package/dist/qa-pipeline.js +1352 -0
  178. package/dist/qa-pipeline.js.map +1 -0
  179. package/dist/reasoning-adapter.d.ts +116 -0
  180. package/dist/reasoning-adapter.d.ts.map +1 -0
  181. package/dist/reasoning-adapter.js +187 -0
  182. package/dist/reasoning-adapter.js.map +1 -0
  183. package/dist/role-registry.d.ts +55 -0
  184. package/dist/role-registry.d.ts.map +1 -0
  185. package/dist/role-registry.js +192 -0
  186. package/dist/role-registry.js.map +1 -0
  187. package/dist/sandbox-tiers.d.ts +327 -0
  188. package/dist/sandbox-tiers.d.ts.map +1 -0
  189. package/dist/sandbox-tiers.js +928 -0
  190. package/dist/sandbox-tiers.js.map +1 -0
  191. package/dist/security-scanner.d.ts +222 -0
  192. package/dist/security-scanner.d.ts.map +1 -0
  193. package/dist/security-scanner.js +1129 -0
  194. package/dist/security-scanner.js.map +1 -0
  195. package/dist/security.d.ts +93 -0
  196. package/dist/security.d.ts.map +1 -0
  197. package/dist/security.js +393 -0
  198. package/dist/security.js.map +1 -0
  199. package/dist/self-reflection.d.ts +397 -0
  200. package/dist/self-reflection.d.ts.map +1 -0
  201. package/dist/self-reflection.js +908 -0
  202. package/dist/self-reflection.js.map +1 -0
  203. package/dist/session-persistence.d.ts +191 -0
  204. package/dist/session-persistence.d.ts.map +1 -0
  205. package/dist/session-persistence.js +395 -0
  206. package/dist/session-persistence.js.map +1 -0
  207. package/dist/speculative-executor.d.ts +210 -0
  208. package/dist/speculative-executor.d.ts.map +1 -0
  209. package/dist/speculative-executor.js +618 -0
  210. package/dist/speculative-executor.js.map +1 -0
  211. package/dist/state-machine.d.ts +289 -0
  212. package/dist/state-machine.d.ts.map +1 -0
  213. package/dist/state-machine.js +695 -0
  214. package/dist/state-machine.js.map +1 -0
  215. package/dist/sub-agent.d.ts +177 -0
  216. package/dist/sub-agent.d.ts.map +1 -0
  217. package/dist/sub-agent.js +303 -0
  218. package/dist/sub-agent.js.map +1 -0
  219. package/dist/system-prompt.d.ts +26 -0
  220. package/dist/system-prompt.d.ts.map +1 -0
  221. package/dist/system-prompt.js +84 -0
  222. package/dist/system-prompt.js.map +1 -0
  223. package/dist/test-intelligence.d.ts +439 -0
  224. package/dist/test-intelligence.d.ts.map +1 -0
  225. package/dist/test-intelligence.js +1165 -0
  226. package/dist/test-intelligence.js.map +1 -0
  227. package/dist/types.d.ts +632 -0
  228. package/dist/types.d.ts.map +1 -0
  229. package/dist/types.js +6 -0
  230. package/dist/types.js.map +1 -0
  231. package/dist/vector-index.d.ts +314 -0
  232. package/dist/vector-index.d.ts.map +1 -0
  233. package/dist/vector-index.js +618 -0
  234. package/dist/vector-index.js.map +1 -0
  235. package/package.json +41 -0
@@ -0,0 +1,718 @@
1
+ /**
2
+ * @module mcp-client
3
+ * @description MCP (Model Context Protocol) Client Bridge
4
+ *
5
+ * Connects to external MCP servers (GitHub, Postgres, Slack, etc.) via stdio
6
+ * transport, discovers their tools, and invokes them. This extends YUAN's
7
+ * tool ecosystem dynamically at runtime.
8
+ *
9
+ * Protocol: JSON-RPC 2.0 over newline-delimited stdio
10
+ * Spec: https://modelcontextprotocol.io/specification/2024-11-05
11
+ *
12
+ * Pure TypeScript — no @modelcontextprotocol/sdk dependency.
13
+ * Uses only Node.js builtins: child_process, events, readline, crypto.
14
+ */
15
+ import { spawn } from "node:child_process";
16
+ import { EventEmitter } from "node:events";
17
+ import { createInterface } from "node:readline";
18
+ // ─── Defaults ───
19
+ const DEFAULT_CONFIG = {
20
+ servers: [],
21
+ autoConnect: true,
22
+ toolPrefix: true,
23
+ maxConcurrentCalls: 5,
24
+ };
25
+ const DEFAULT_TIMEOUT = 30_000;
26
+ const REQUEST_TIMEOUT = 60_000;
27
+ const MCP_PROTOCOL_VERSION = "2024-11-05";
28
+ const CLIENT_INFO = { name: "yuan", version: "0.1.0" };
29
+ // ─── MCPServerConnection (internal) ───
30
+ /**
31
+ * Manages the lifecycle of a single MCP server process.
32
+ * Not exported — internal to MCPClient.
33
+ */
34
+ class MCPServerConnection {
35
+ process = null;
36
+ readline = null;
37
+ state;
38
+ pendingRequests = new Map();
39
+ requestIdCounter = 0;
40
+ config;
41
+ toolPrefix;
42
+ constructor(config, toolPrefix) {
43
+ this.config = config;
44
+ this.toolPrefix = toolPrefix;
45
+ this.state = {
46
+ name: config.name,
47
+ status: "disconnected",
48
+ tools: [],
49
+ callCount: 0,
50
+ };
51
+ }
52
+ /** Current server state (immutable snapshot). */
53
+ getState() {
54
+ return { ...this.state, tools: [...this.state.tools] };
55
+ }
56
+ /**
57
+ * Spawn the child process, perform MCP handshake, and discover tools.
58
+ * @throws {Error} On connection/initialization failure
59
+ */
60
+ async connect() {
61
+ if (this.state.status === "ready")
62
+ return;
63
+ this.state.status = "connecting";
64
+ this.state.error = undefined;
65
+ const timeout = this.config.timeout ?? DEFAULT_TIMEOUT;
66
+ await new Promise((resolve, reject) => {
67
+ const timer = setTimeout(() => {
68
+ this.killProcess();
69
+ const err = new Error(`MCP server "${this.config.name}" connection timed out after ${timeout}ms`);
70
+ this.state.status = "error";
71
+ this.state.error = err.message;
72
+ reject(err);
73
+ }, timeout);
74
+ try {
75
+ // Merge env: inherit process.env + server-specific env
76
+ const env = { ...process.env, ...this.config.env };
77
+ this.process = spawn(this.config.command, this.config.args, {
78
+ stdio: ["pipe", "pipe", "pipe"],
79
+ env,
80
+ // Don't let the child keep our event loop alive
81
+ detached: false,
82
+ });
83
+ this.state.pid = this.process.pid;
84
+ // Handle spawn errors
85
+ this.process.on("error", (err) => {
86
+ clearTimeout(timer);
87
+ this.handleProcessError(err);
88
+ reject(err);
89
+ });
90
+ // Handle unexpected exit during init
91
+ this.process.on("exit", (code) => {
92
+ if (this.state.status === "connecting") {
93
+ clearTimeout(timer);
94
+ const err = new Error(`MCP server "${this.config.name}" exited during init with code ${code}`);
95
+ this.state.status = "error";
96
+ this.state.error = err.message;
97
+ reject(err);
98
+ }
99
+ else {
100
+ this.handleProcessExit(code ?? 1);
101
+ }
102
+ });
103
+ // Set up stdout line reader for JSON-RPC messages
104
+ if (!this.process.stdout) {
105
+ clearTimeout(timer);
106
+ const err = new Error(`MCP server "${this.config.name}": stdout not available`);
107
+ this.state.status = "error";
108
+ this.state.error = err.message;
109
+ reject(err);
110
+ return;
111
+ }
112
+ this.readline = createInterface({ input: this.process.stdout });
113
+ this.readline.on("line", (line) => {
114
+ this.handleStdoutLine(line);
115
+ });
116
+ // Stderr → log (not part of protocol)
117
+ if (this.process.stderr) {
118
+ this.process.stderr.on("data", (data) => {
119
+ // Silently consume stderr; could add debug logging here
120
+ void data;
121
+ });
122
+ }
123
+ // Perform MCP handshake
124
+ this.initialize()
125
+ .then(() => this.listTools())
126
+ .then((tools) => {
127
+ clearTimeout(timer);
128
+ this.state.tools = tools;
129
+ this.state.status = "ready";
130
+ this.state.lastConnected = Date.now();
131
+ resolve();
132
+ })
133
+ .catch((err) => {
134
+ clearTimeout(timer);
135
+ this.state.status = "error";
136
+ this.state.error = err.message;
137
+ this.killProcess();
138
+ reject(err);
139
+ });
140
+ }
141
+ catch (err) {
142
+ clearTimeout(timer);
143
+ const error = err instanceof Error ? err : new Error(String(err));
144
+ this.state.status = "error";
145
+ this.state.error = error.message;
146
+ reject(error);
147
+ }
148
+ });
149
+ }
150
+ /**
151
+ * Gracefully disconnect the server.
152
+ */
153
+ async disconnect() {
154
+ this.rejectAllPending(new Error("Disconnecting"));
155
+ this.killProcess();
156
+ this.state.status = "disconnected";
157
+ this.state.pid = undefined;
158
+ this.state.tools = [];
159
+ }
160
+ /**
161
+ * Invoke a tool on this MCP server.
162
+ */
163
+ async callTool(name, args) {
164
+ if (this.state.status !== "ready") {
165
+ throw new Error(`MCP server "${this.config.name}" is not ready (status: ${this.state.status})`);
166
+ }
167
+ this.state.callCount++;
168
+ const result = (await this.sendRequest("tools/call", {
169
+ name,
170
+ arguments: args,
171
+ }));
172
+ return result;
173
+ }
174
+ // ─── MCP Protocol Methods ───
175
+ /**
176
+ * Send the MCP `initialize` request and the `notifications/initialized` notification.
177
+ */
178
+ async initialize() {
179
+ const result = (await this.sendRequest("initialize", {
180
+ protocolVersion: MCP_PROTOCOL_VERSION,
181
+ capabilities: {},
182
+ clientInfo: CLIENT_INFO,
183
+ }));
184
+ // After successful init, send the initialized notification
185
+ this.sendNotification("notifications/initialized");
186
+ void result;
187
+ }
188
+ /**
189
+ * Request the tool list from the server.
190
+ */
191
+ async listTools() {
192
+ const result = (await this.sendRequest("tools/list"));
193
+ return (result.tools ?? []).map((t) => ({
194
+ name: t.name,
195
+ prefixedName: this.toolPrefix
196
+ ? `${this.config.name}_${t.name}`
197
+ : t.name,
198
+ serverName: this.config.name,
199
+ description: t.description ?? "",
200
+ inputSchema: t.inputSchema ?? { type: "object", properties: {} },
201
+ }));
202
+ }
203
+ // ─── JSON-RPC Transport ───
204
+ /**
205
+ * Send a JSON-RPC request and wait for the response.
206
+ */
207
+ sendRequest(method, params) {
208
+ return new Promise((resolve, reject) => {
209
+ if (!this.process?.stdin || this.process.stdin.destroyed) {
210
+ reject(new Error(`MCP server "${this.config.name}": stdin not available`));
211
+ return;
212
+ }
213
+ const id = ++this.requestIdCounter;
214
+ const timer = setTimeout(() => {
215
+ this.pendingRequests.delete(id);
216
+ reject(new Error(`MCP request "${method}" timed out after ${REQUEST_TIMEOUT}ms`));
217
+ }, REQUEST_TIMEOUT);
218
+ this.pendingRequests.set(id, { resolve, reject, timer });
219
+ const message = {
220
+ jsonrpc: "2.0",
221
+ id,
222
+ method,
223
+ params,
224
+ };
225
+ const line = JSON.stringify(message) + "\n";
226
+ this.process.stdin.write(line);
227
+ });
228
+ }
229
+ /**
230
+ * Send a JSON-RPC notification (no response expected).
231
+ */
232
+ sendNotification(method, params) {
233
+ if (!this.process?.stdin || this.process.stdin.destroyed)
234
+ return;
235
+ const message = {
236
+ jsonrpc: "2.0",
237
+ method,
238
+ params,
239
+ };
240
+ const line = JSON.stringify(message) + "\n";
241
+ this.process.stdin.write(line);
242
+ }
243
+ /**
244
+ * Handle a single line from stdout (newline-delimited JSON-RPC).
245
+ */
246
+ handleStdoutLine(line) {
247
+ const trimmed = line.trim();
248
+ if (!trimmed)
249
+ return;
250
+ let message;
251
+ try {
252
+ message = JSON.parse(trimmed);
253
+ }
254
+ catch {
255
+ // Not valid JSON — skip (could be debug output)
256
+ return;
257
+ }
258
+ if (message.jsonrpc !== "2.0")
259
+ return;
260
+ this.handleMessage(message);
261
+ }
262
+ /**
263
+ * Route a parsed JSON-RPC message to the appropriate handler.
264
+ */
265
+ handleMessage(message) {
266
+ // Response to a pending request
267
+ if (message.id !== undefined) {
268
+ const pending = this.pendingRequests.get(message.id);
269
+ if (!pending)
270
+ return; // Orphan response — ignore
271
+ this.pendingRequests.delete(message.id);
272
+ clearTimeout(pending.timer);
273
+ if (message.error) {
274
+ pending.reject(new Error(`MCP error ${message.error.code}: ${message.error.message}`));
275
+ }
276
+ else {
277
+ pending.resolve(message.result);
278
+ }
279
+ return;
280
+ }
281
+ // Server-initiated notification — currently ignored
282
+ // Future: handle tools/list_changed, resources/updated, etc.
283
+ }
284
+ /**
285
+ * Handle child process unexpected exit.
286
+ */
287
+ handleProcessExit(code) {
288
+ this.rejectAllPending(new Error(`MCP server "${this.config.name}" exited with code ${code}`));
289
+ this.state.status = "crashed";
290
+ this.state.error = `Process exited with code ${code}`;
291
+ this.state.pid = undefined;
292
+ this.process = null;
293
+ this.readline = null;
294
+ }
295
+ /**
296
+ * Handle child process error event.
297
+ */
298
+ handleProcessError(err) {
299
+ this.rejectAllPending(err);
300
+ this.state.status = "error";
301
+ this.state.error = err.message;
302
+ this.state.pid = undefined;
303
+ this.process = null;
304
+ this.readline = null;
305
+ }
306
+ /**
307
+ * Kill the child process if alive.
308
+ */
309
+ killProcess() {
310
+ if (this.readline) {
311
+ this.readline.close();
312
+ this.readline = null;
313
+ }
314
+ if (this.process) {
315
+ try {
316
+ this.process.kill("SIGTERM");
317
+ }
318
+ catch {
319
+ // Already dead
320
+ }
321
+ this.process = null;
322
+ }
323
+ }
324
+ /**
325
+ * Reject all pending requests (used on disconnect/crash).
326
+ */
327
+ rejectAllPending(err) {
328
+ for (const [id, pending] of this.pendingRequests) {
329
+ clearTimeout(pending.timer);
330
+ pending.reject(err);
331
+ this.pendingRequests.delete(id);
332
+ }
333
+ }
334
+ }
335
+ // ─── MCPClient (exported) ───
336
+ /**
337
+ * MCP Client Bridge — connects to external MCP servers, discovers their tools,
338
+ * and invokes them. Extends YUAN's tool ecosystem dynamically.
339
+ *
340
+ * @example
341
+ * ```ts
342
+ * const client = new MCPClient({
343
+ * servers: [
344
+ * {
345
+ * name: "github",
346
+ * transport: "stdio",
347
+ * command: "npx",
348
+ * args: ["-y", "@modelcontextprotocol/server-github"],
349
+ * env: { GITHUB_PERSONAL_ACCESS_TOKEN: "ghp_xxx" },
350
+ * },
351
+ * ],
352
+ * });
353
+ *
354
+ * await client.connectAll();
355
+ * const tools = client.getAvailableTools();
356
+ * const result = await client.callTool("github_search_repositories", { query: "yuan" });
357
+ * ```
358
+ *
359
+ * @fires MCPClient#server:connected
360
+ * @fires MCPClient#server:disconnected
361
+ * @fires MCPClient#server:error
362
+ * @fires MCPClient#server:crashed
363
+ * @fires MCPClient#tools:discovered
364
+ * @fires MCPClient#tool:called
365
+ * @fires MCPClient#tool:result
366
+ */
367
+ export class MCPClient extends EventEmitter {
368
+ config;
369
+ servers = new Map();
370
+ allTools = new Map();
371
+ activeCalls = 0;
372
+ callQueue = [];
373
+ constructor(config) {
374
+ super();
375
+ this.config = {
376
+ ...DEFAULT_CONFIG,
377
+ ...config,
378
+ servers: config?.servers ? [...config.servers] : [],
379
+ };
380
+ }
381
+ // ─── Lifecycle ───
382
+ /**
383
+ * Connect to all configured servers.
384
+ * @returns Map of server name → state after connection attempt
385
+ */
386
+ async connectAll() {
387
+ const results = new Map();
388
+ const promises = this.config.servers.map(async (serverConfig) => {
389
+ try {
390
+ const state = await this.connect(serverConfig);
391
+ results.set(serverConfig.name, state);
392
+ }
393
+ catch {
394
+ results.set(serverConfig.name, this.getServerState(serverConfig.name) ?? {
395
+ name: serverConfig.name,
396
+ status: "error",
397
+ tools: [],
398
+ error: "Connection failed",
399
+ callCount: 0,
400
+ });
401
+ }
402
+ });
403
+ await Promise.allSettled(promises);
404
+ return results;
405
+ }
406
+ /**
407
+ * Connect to a single MCP server.
408
+ * @param serverConfig - Server configuration
409
+ * @returns Server state after connection
410
+ */
411
+ async connect(serverConfig) {
412
+ // Disconnect existing connection with same name
413
+ if (this.servers.has(serverConfig.name)) {
414
+ await this.disconnect(serverConfig.name);
415
+ }
416
+ const connection = new MCPServerConnection(serverConfig, this.config.toolPrefix);
417
+ this.servers.set(serverConfig.name, connection);
418
+ try {
419
+ await connection.connect();
420
+ const state = connection.getState();
421
+ // Register discovered tools
422
+ for (const tool of state.tools) {
423
+ this.allTools.set(tool.prefixedName, tool);
424
+ }
425
+ this.emit("server:connected", { name: serverConfig.name, toolCount: state.tools.length });
426
+ this.emit("tools:discovered", { serverName: serverConfig.name, tools: state.tools });
427
+ return state;
428
+ }
429
+ catch (err) {
430
+ const state = connection.getState();
431
+ this.emit("server:error", {
432
+ name: serverConfig.name,
433
+ error: err instanceof Error ? err.message : String(err),
434
+ });
435
+ return state;
436
+ }
437
+ }
438
+ /**
439
+ * Disconnect a specific server by name.
440
+ * @param serverName - Server to disconnect
441
+ */
442
+ async disconnect(serverName) {
443
+ const connection = this.servers.get(serverName);
444
+ if (!connection)
445
+ return;
446
+ // Remove this server's tools
447
+ for (const [key, tool] of this.allTools) {
448
+ if (tool.serverName === serverName) {
449
+ this.allTools.delete(key);
450
+ }
451
+ }
452
+ await connection.disconnect();
453
+ this.servers.delete(serverName);
454
+ this.emit("server:disconnected", { name: serverName });
455
+ }
456
+ /**
457
+ * Disconnect all servers and clean up.
458
+ */
459
+ async disconnectAll() {
460
+ const names = [...this.servers.keys()];
461
+ await Promise.allSettled(names.map((name) => this.disconnect(name)));
462
+ this.allTools.clear();
463
+ this.callQueue = [];
464
+ this.activeCalls = 0;
465
+ }
466
+ /**
467
+ * Add a server configuration at runtime (does not auto-connect).
468
+ * @param config - Server configuration to add
469
+ */
470
+ addServer(config) {
471
+ // Prevent duplicates
472
+ this.config.servers = this.config.servers.filter((s) => s.name !== config.name);
473
+ this.config.servers.push(config);
474
+ }
475
+ /**
476
+ * Remove a server configuration and disconnect if connected.
477
+ * @param name - Server name to remove
478
+ */
479
+ removeServer(name) {
480
+ this.config.servers = this.config.servers.filter((s) => s.name !== name);
481
+ // Fire-and-forget disconnect
482
+ void this.disconnect(name);
483
+ }
484
+ // ─── Tool Discovery ───
485
+ /**
486
+ * Get all available tools from all connected servers.
487
+ * @returns Array of all discovered MCP tools
488
+ */
489
+ getAvailableTools() {
490
+ return [...this.allTools.values()];
491
+ }
492
+ /**
493
+ * Get tools from a specific server.
494
+ * @param serverName - Server to get tools from
495
+ * @returns Array of tools from the specified server
496
+ */
497
+ getServerTools(serverName) {
498
+ return [...this.allTools.values()].filter((t) => t.serverName === serverName);
499
+ }
500
+ /**
501
+ * Find a tool by name (searches both prefixed and original names).
502
+ * @param name - Tool name to search for
503
+ * @returns The matching tool, or undefined if not found
504
+ */
505
+ findTool(name) {
506
+ // Direct lookup by prefixed name
507
+ const direct = this.allTools.get(name);
508
+ if (direct)
509
+ return direct;
510
+ // Search by original name (returns first match)
511
+ for (const tool of this.allTools.values()) {
512
+ if (tool.name === name)
513
+ return tool;
514
+ }
515
+ return undefined;
516
+ }
517
+ /**
518
+ * Convert all discovered MCP tools to YUAN ToolDefinition format.
519
+ * Allows seamless integration with the AgentLoop tool system.
520
+ * @returns Array of YUAN-compatible tool definitions
521
+ */
522
+ toToolDefinitions() {
523
+ return [...this.allTools.values()].map((tool) => ({
524
+ name: tool.prefixedName,
525
+ description: `[MCP:${tool.serverName}] ${tool.description}`,
526
+ parameters: {
527
+ type: "object",
528
+ properties: tool.inputSchema.properties ?? {},
529
+ required: tool.inputSchema.required ?? [],
530
+ },
531
+ }));
532
+ }
533
+ // ─── Tool Invocation ───
534
+ /**
535
+ * Call a tool on an MCP server.
536
+ * Respects maxConcurrentCalls — excess calls are queued.
537
+ *
538
+ * @param toolName - Tool name (prefixed or original)
539
+ * @param args - Tool arguments
540
+ * @returns Tool result
541
+ * @throws {Error} If tool not found or server not ready
542
+ */
543
+ async callTool(toolName, args) {
544
+ const tool = this.findTool(toolName);
545
+ if (!tool) {
546
+ throw new Error(`MCP tool "${toolName}" not found`);
547
+ }
548
+ const connection = this.servers.get(tool.serverName);
549
+ if (!connection) {
550
+ throw new Error(`MCP server "${tool.serverName}" not connected`);
551
+ }
552
+ // Concurrency control
553
+ await this.acquireCallSlot();
554
+ this.emit("tool:called", {
555
+ tool: tool.prefixedName,
556
+ serverName: tool.serverName,
557
+ args,
558
+ });
559
+ const startTime = Date.now();
560
+ try {
561
+ const result = await connection.callTool(tool.name, args);
562
+ this.emit("tool:result", {
563
+ tool: tool.prefixedName,
564
+ serverName: tool.serverName,
565
+ durationMs: Date.now() - startTime,
566
+ isError: result.isError ?? false,
567
+ });
568
+ return result;
569
+ }
570
+ catch (err) {
571
+ const error = err instanceof Error ? err : new Error(String(err));
572
+ // Check if server crashed
573
+ const state = connection.getState();
574
+ if (state.status === "crashed") {
575
+ this.emit("server:crashed", { name: tool.serverName, error: error.message });
576
+ // Auto-restart if configured
577
+ const serverConfig = this.config.servers.find((s) => s.name === tool.serverName);
578
+ if (serverConfig?.retryOnCrash !== false) {
579
+ void this.reconnectServer(serverConfig);
580
+ }
581
+ }
582
+ throw error;
583
+ }
584
+ finally {
585
+ this.releaseCallSlot();
586
+ }
587
+ }
588
+ /**
589
+ * Call a tool and convert the result to YUAN's ToolResult format.
590
+ * Suitable for direct use in the AgentLoop.
591
+ *
592
+ * @param toolName - Tool name (prefixed or original)
593
+ * @param args - Tool arguments
594
+ * @param callId - Unique call ID for correlation
595
+ * @returns YUAN-format ToolResult
596
+ */
597
+ async callToolAsYuan(toolName, args, callId) {
598
+ const startTime = Date.now();
599
+ try {
600
+ const result = await this.callTool(toolName, args);
601
+ const output = this.extractTextContent(result);
602
+ return {
603
+ tool_call_id: callId,
604
+ name: toolName,
605
+ output,
606
+ success: !result.isError,
607
+ durationMs: Date.now() - startTime,
608
+ };
609
+ }
610
+ catch (err) {
611
+ return {
612
+ tool_call_id: callId,
613
+ name: toolName,
614
+ output: `MCP tool error: ${err instanceof Error ? err.message : String(err)}`,
615
+ success: false,
616
+ durationMs: Date.now() - startTime,
617
+ };
618
+ }
619
+ }
620
+ // ─── Status ───
621
+ /**
622
+ * Get states of all registered servers.
623
+ * @returns Array of server states
624
+ */
625
+ getServerStates() {
626
+ return [...this.servers.values()].map((conn) => conn.getState());
627
+ }
628
+ /**
629
+ * Get state of a specific server.
630
+ * @param name - Server name
631
+ * @returns Server state or undefined if not registered
632
+ */
633
+ getServerState(name) {
634
+ return this.servers.get(name)?.getState();
635
+ }
636
+ /**
637
+ * Check if any servers are connected and ready.
638
+ * @returns True if at least one server is in "ready" status
639
+ */
640
+ hasConnections() {
641
+ for (const conn of this.servers.values()) {
642
+ if (conn.getState().status === "ready")
643
+ return true;
644
+ }
645
+ return false;
646
+ }
647
+ // ─── Private Helpers ───
648
+ /**
649
+ * Extract text content from an MCP call result.
650
+ * Concatenates all text blocks, includes base64 image markers, and resource URIs.
651
+ */
652
+ extractTextContent(result) {
653
+ const parts = [];
654
+ for (const block of result.content) {
655
+ switch (block.type) {
656
+ case "text":
657
+ if (block.text)
658
+ parts.push(block.text);
659
+ break;
660
+ case "image":
661
+ parts.push(`[image: ${block.mimeType ?? "unknown"}, ${(block.data?.length ?? 0)} bytes base64]`);
662
+ break;
663
+ case "resource":
664
+ if (block.text)
665
+ parts.push(block.text);
666
+ else
667
+ parts.push(`[resource: ${block.mimeType ?? "unknown"}]`);
668
+ break;
669
+ }
670
+ }
671
+ return parts.join("\n") || "(empty response)";
672
+ }
673
+ /**
674
+ * Acquire a concurrency slot, or wait in queue.
675
+ */
676
+ acquireCallSlot() {
677
+ if (this.activeCalls < this.config.maxConcurrentCalls) {
678
+ this.activeCalls++;
679
+ return Promise.resolve();
680
+ }
681
+ // Bound queue size to prevent unbounded memory growth
682
+ const MAX_QUEUE_SIZE = 100;
683
+ if (this.callQueue.length >= MAX_QUEUE_SIZE) {
684
+ return Promise.reject(new Error(`MCP call queue is full (max ${MAX_QUEUE_SIZE}). Try again later.`));
685
+ }
686
+ return new Promise((resolve) => {
687
+ this.callQueue.push(() => {
688
+ this.activeCalls++;
689
+ resolve();
690
+ });
691
+ });
692
+ }
693
+ /**
694
+ * Release a concurrency slot and dequeue the next waiter.
695
+ */
696
+ releaseCallSlot() {
697
+ this.activeCalls--;
698
+ const next = this.callQueue.shift();
699
+ if (next)
700
+ next();
701
+ }
702
+ /**
703
+ * Attempt to reconnect a crashed server after a brief delay.
704
+ */
705
+ async reconnectServer(config) {
706
+ if (!config)
707
+ return;
708
+ // Brief delay before reconnect
709
+ await new Promise((resolve) => setTimeout(resolve, 2000));
710
+ try {
711
+ await this.connect(config);
712
+ }
713
+ catch {
714
+ // Reconnect failed — server remains in error/crashed state
715
+ }
716
+ }
717
+ }
718
+ //# sourceMappingURL=mcp-client.js.map