@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.
- package/LICENSE +663 -0
- package/README.md +15 -0
- package/dist/__tests__/context-manager.test.d.ts +6 -0
- package/dist/__tests__/context-manager.test.d.ts.map +1 -0
- package/dist/__tests__/context-manager.test.js +220 -0
- package/dist/__tests__/context-manager.test.js.map +1 -0
- package/dist/__tests__/governor.test.d.ts +6 -0
- package/dist/__tests__/governor.test.d.ts.map +1 -0
- package/dist/__tests__/governor.test.js +210 -0
- package/dist/__tests__/governor.test.js.map +1 -0
- package/dist/__tests__/model-router.test.d.ts +6 -0
- package/dist/__tests__/model-router.test.d.ts.map +1 -0
- package/dist/__tests__/model-router.test.js +329 -0
- package/dist/__tests__/model-router.test.js.map +1 -0
- package/dist/agent-logger.d.ts +384 -0
- package/dist/agent-logger.d.ts.map +1 -0
- package/dist/agent-logger.js +820 -0
- package/dist/agent-logger.js.map +1 -0
- package/dist/agent-loop.d.ts +163 -0
- package/dist/agent-loop.d.ts.map +1 -0
- package/dist/agent-loop.js +609 -0
- package/dist/agent-loop.js.map +1 -0
- package/dist/agent-modes.d.ts +85 -0
- package/dist/agent-modes.d.ts.map +1 -0
- package/dist/agent-modes.js +418 -0
- package/dist/agent-modes.js.map +1 -0
- package/dist/approval.d.ts +137 -0
- package/dist/approval.d.ts.map +1 -0
- package/dist/approval.js +299 -0
- package/dist/approval.js.map +1 -0
- package/dist/async-completion-queue.d.ts +56 -0
- package/dist/async-completion-queue.d.ts.map +1 -0
- package/dist/async-completion-queue.js +77 -0
- package/dist/async-completion-queue.js.map +1 -0
- package/dist/auto-fix.d.ts +174 -0
- package/dist/auto-fix.d.ts.map +1 -0
- package/dist/auto-fix.js +319 -0
- package/dist/auto-fix.js.map +1 -0
- package/dist/codebase-context.d.ts +396 -0
- package/dist/codebase-context.d.ts.map +1 -0
- package/dist/codebase-context.js +1260 -0
- package/dist/codebase-context.js.map +1 -0
- package/dist/conflict-resolver.d.ts +191 -0
- package/dist/conflict-resolver.d.ts.map +1 -0
- package/dist/conflict-resolver.js +524 -0
- package/dist/conflict-resolver.js.map +1 -0
- package/dist/constants.d.ts +52 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +141 -0
- package/dist/constants.js.map +1 -0
- package/dist/context-budget.d.ts +435 -0
- package/dist/context-budget.d.ts.map +1 -0
- package/dist/context-budget.js +903 -0
- package/dist/context-budget.js.map +1 -0
- package/dist/context-compressor.d.ts +143 -0
- package/dist/context-compressor.d.ts.map +1 -0
- package/dist/context-compressor.js +511 -0
- package/dist/context-compressor.js.map +1 -0
- package/dist/context-manager.d.ts +112 -0
- package/dist/context-manager.d.ts.map +1 -0
- package/dist/context-manager.js +247 -0
- package/dist/context-manager.js.map +1 -0
- package/dist/continuous-reflection.d.ts +267 -0
- package/dist/continuous-reflection.d.ts.map +1 -0
- package/dist/continuous-reflection.js +338 -0
- package/dist/continuous-reflection.js.map +1 -0
- package/dist/cross-file-refactor.d.ts +352 -0
- package/dist/cross-file-refactor.d.ts.map +1 -0
- package/dist/cross-file-refactor.js +1544 -0
- package/dist/cross-file-refactor.js.map +1 -0
- package/dist/dag-orchestrator.d.ts +138 -0
- package/dist/dag-orchestrator.d.ts.map +1 -0
- package/dist/dag-orchestrator.js +379 -0
- package/dist/dag-orchestrator.js.map +1 -0
- package/dist/debate-orchestrator.d.ts +301 -0
- package/dist/debate-orchestrator.d.ts.map +1 -0
- package/dist/debate-orchestrator.js +719 -0
- package/dist/debate-orchestrator.js.map +1 -0
- package/dist/dependency-analyzer.d.ts +113 -0
- package/dist/dependency-analyzer.d.ts.map +1 -0
- package/dist/dependency-analyzer.js +444 -0
- package/dist/dependency-analyzer.js.map +1 -0
- package/dist/design-loop.d.ts +59 -0
- package/dist/design-loop.d.ts.map +1 -0
- package/dist/design-loop.js +344 -0
- package/dist/design-loop.js.map +1 -0
- package/dist/doc-intelligence.d.ts +383 -0
- package/dist/doc-intelligence.d.ts.map +1 -0
- package/dist/doc-intelligence.js +1307 -0
- package/dist/doc-intelligence.js.map +1 -0
- package/dist/dynamic-role-generator.d.ts +76 -0
- package/dist/dynamic-role-generator.d.ts.map +1 -0
- package/dist/dynamic-role-generator.js +194 -0
- package/dist/dynamic-role-generator.js.map +1 -0
- package/dist/errors.d.ts +69 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +102 -0
- package/dist/errors.js.map +1 -0
- package/dist/event-bus.d.ts +159 -0
- package/dist/event-bus.d.ts.map +1 -0
- package/dist/event-bus.js +305 -0
- package/dist/event-bus.js.map +1 -0
- package/dist/execution-engine.d.ts +425 -0
- package/dist/execution-engine.d.ts.map +1 -0
- package/dist/execution-engine.js +1555 -0
- package/dist/execution-engine.js.map +1 -0
- package/dist/git-intelligence.d.ts +306 -0
- package/dist/git-intelligence.d.ts.map +1 -0
- package/dist/git-intelligence.js +1099 -0
- package/dist/git-intelligence.js.map +1 -0
- package/dist/governor.d.ts +77 -0
- package/dist/governor.d.ts.map +1 -0
- package/dist/governor.js +161 -0
- package/dist/governor.js.map +1 -0
- package/dist/hierarchical-planner.d.ts +313 -0
- package/dist/hierarchical-planner.d.ts.map +1 -0
- package/dist/hierarchical-planner.js +981 -0
- package/dist/hierarchical-planner.js.map +1 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +123 -0
- package/dist/index.js.map +1 -0
- package/dist/intent-inference.d.ts +103 -0
- package/dist/intent-inference.d.ts.map +1 -0
- package/dist/intent-inference.js +605 -0
- package/dist/intent-inference.js.map +1 -0
- package/dist/interrupt-manager.d.ts +143 -0
- package/dist/interrupt-manager.d.ts.map +1 -0
- package/dist/interrupt-manager.js +196 -0
- package/dist/interrupt-manager.js.map +1 -0
- package/dist/kernel.d.ts +564 -0
- package/dist/kernel.d.ts.map +1 -0
- package/dist/kernel.js +1419 -0
- package/dist/kernel.js.map +1 -0
- package/dist/language-support.d.ts +232 -0
- package/dist/language-support.d.ts.map +1 -0
- package/dist/language-support.js +1134 -0
- package/dist/language-support.js.map +1 -0
- package/dist/llm-client.d.ts +82 -0
- package/dist/llm-client.d.ts.map +1 -0
- package/dist/llm-client.js +475 -0
- package/dist/llm-client.js.map +1 -0
- package/dist/mcp-client.d.ts +232 -0
- package/dist/mcp-client.d.ts.map +1 -0
- package/dist/mcp-client.js +718 -0
- package/dist/mcp-client.js.map +1 -0
- package/dist/memory-manager.d.ts +200 -0
- package/dist/memory-manager.d.ts.map +1 -0
- package/dist/memory-manager.js +568 -0
- package/dist/memory-manager.js.map +1 -0
- package/dist/memory.d.ts +87 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +341 -0
- package/dist/memory.js.map +1 -0
- package/dist/model-router.d.ts +245 -0
- package/dist/model-router.d.ts.map +1 -0
- package/dist/model-router.js +632 -0
- package/dist/model-router.js.map +1 -0
- package/dist/parallel-executor.d.ts +125 -0
- package/dist/parallel-executor.d.ts.map +1 -0
- package/dist/parallel-executor.js +201 -0
- package/dist/parallel-executor.js.map +1 -0
- package/dist/perf-optimizer.d.ts +212 -0
- package/dist/perf-optimizer.d.ts.map +1 -0
- package/dist/perf-optimizer.js +721 -0
- package/dist/perf-optimizer.js.map +1 -0
- package/dist/persona.d.ts +305 -0
- package/dist/persona.d.ts.map +1 -0
- package/dist/persona.js +887 -0
- package/dist/persona.js.map +1 -0
- package/dist/planner.d.ts +70 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +264 -0
- package/dist/planner.js.map +1 -0
- package/dist/qa-pipeline.d.ts +365 -0
- package/dist/qa-pipeline.d.ts.map +1 -0
- package/dist/qa-pipeline.js +1352 -0
- package/dist/qa-pipeline.js.map +1 -0
- package/dist/reasoning-adapter.d.ts +116 -0
- package/dist/reasoning-adapter.d.ts.map +1 -0
- package/dist/reasoning-adapter.js +187 -0
- package/dist/reasoning-adapter.js.map +1 -0
- package/dist/role-registry.d.ts +55 -0
- package/dist/role-registry.d.ts.map +1 -0
- package/dist/role-registry.js +192 -0
- package/dist/role-registry.js.map +1 -0
- package/dist/sandbox-tiers.d.ts +327 -0
- package/dist/sandbox-tiers.d.ts.map +1 -0
- package/dist/sandbox-tiers.js +928 -0
- package/dist/sandbox-tiers.js.map +1 -0
- package/dist/security-scanner.d.ts +222 -0
- package/dist/security-scanner.d.ts.map +1 -0
- package/dist/security-scanner.js +1129 -0
- package/dist/security-scanner.js.map +1 -0
- package/dist/security.d.ts +93 -0
- package/dist/security.d.ts.map +1 -0
- package/dist/security.js +393 -0
- package/dist/security.js.map +1 -0
- package/dist/self-reflection.d.ts +397 -0
- package/dist/self-reflection.d.ts.map +1 -0
- package/dist/self-reflection.js +908 -0
- package/dist/self-reflection.js.map +1 -0
- package/dist/session-persistence.d.ts +191 -0
- package/dist/session-persistence.d.ts.map +1 -0
- package/dist/session-persistence.js +395 -0
- package/dist/session-persistence.js.map +1 -0
- package/dist/speculative-executor.d.ts +210 -0
- package/dist/speculative-executor.d.ts.map +1 -0
- package/dist/speculative-executor.js +618 -0
- package/dist/speculative-executor.js.map +1 -0
- package/dist/state-machine.d.ts +289 -0
- package/dist/state-machine.d.ts.map +1 -0
- package/dist/state-machine.js +695 -0
- package/dist/state-machine.js.map +1 -0
- package/dist/sub-agent.d.ts +177 -0
- package/dist/sub-agent.d.ts.map +1 -0
- package/dist/sub-agent.js +303 -0
- package/dist/sub-agent.js.map +1 -0
- package/dist/system-prompt.d.ts +26 -0
- package/dist/system-prompt.d.ts.map +1 -0
- package/dist/system-prompt.js +84 -0
- package/dist/system-prompt.js.map +1 -0
- package/dist/test-intelligence.d.ts +439 -0
- package/dist/test-intelligence.d.ts.map +1 -0
- package/dist/test-intelligence.js +1165 -0
- package/dist/test-intelligence.js.map +1 -0
- package/dist/types.d.ts +632 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/vector-index.d.ts +314 -0
- package/dist/vector-index.d.ts.map +1 -0
- package/dist/vector-index.js +618 -0
- package/dist/vector-index.js.map +1 -0
- 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
|