salmon-loop 0.3.0 → 0.3.2
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/dist/cli/authorization/non-interactive.js +7 -21
- package/dist/cli/commands/chat.js +1 -1
- package/dist/cli/commands/parallel.js +46 -41
- package/dist/cli/commands/run/assistant-message.js +3 -0
- package/dist/cli/commands/run/handler.js +2 -1
- package/dist/cli/commands/serve.js +123 -154
- package/dist/cli/headless/json-protocol.js +1 -1
- package/dist/cli/headless/stream-json-protocol.js +3 -2
- package/dist/cli/slash/runtime.js +5 -1
- package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
- package/dist/core/adapters/fs/node-fs.js +1 -0
- package/dist/core/benchmark/patch-artifact.js +1 -1
- package/dist/core/context/service.js +36 -10
- package/dist/core/extensions/index.js +2 -35
- package/dist/core/extensions/redact.js +9 -3
- package/dist/core/extensions/schemas.js +2 -51
- package/dist/core/facades/cli-authorization-non-interactive.js +1 -1
- package/dist/core/facades/cli-serve.js +0 -1
- package/dist/core/grizzco/dsl/strategies.js +1 -3
- package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +12 -7
- package/dist/core/grizzco/engine/transaction/attempt-failure.js +23 -23
- package/dist/core/grizzco/engine/transaction/report-mapper.js +3 -0
- package/dist/core/grizzco/engine/transaction/transaction-runner.js +14 -0
- package/dist/core/grizzco/flows/AutopilotFlow.js +1 -0
- package/dist/core/grizzco/flows/SalmonLoopFlow.js +1 -0
- package/dist/core/grizzco/steps/apply.js +0 -7
- package/dist/core/grizzco/steps/autopilot.js +108 -6
- package/dist/core/grizzco/steps/preflight.js +10 -0
- package/dist/core/grizzco/steps/tool-runtime.js +1 -0
- package/dist/core/interaction/events/bus.js +14 -0
- package/dist/core/interaction/orchestration/facade.js +10 -0
- package/dist/core/mcp/bridge/index.js +4 -0
- package/dist/core/mcp/bridge/prompt-command-provider.js +261 -0
- package/dist/core/mcp/bridge/resource-context-provider.js +259 -0
- package/dist/core/mcp/bridge/tool-bridge.js +303 -0
- package/dist/core/mcp/cache/resource-cache.js +41 -0
- package/dist/core/mcp/catalog/discovery.js +51 -0
- package/dist/core/mcp/catalog/notification-router.js +28 -0
- package/dist/core/mcp/catalog/prompt-catalog.js +4 -0
- package/dist/core/mcp/catalog/resource-catalog.js +7 -0
- package/dist/core/mcp/catalog/tool-catalog.js +4 -0
- package/dist/core/mcp/client/connection-manager.js +239 -0
- package/dist/core/mcp/client/lifecycle.js +13 -0
- package/dist/core/mcp/client/transport-factory.js +168 -0
- package/dist/core/mcp/config/index.js +32 -0
- package/dist/core/mcp/config/schema-v2.js +129 -0
- package/dist/core/mcp/host/elicitation-provider.js +209 -0
- package/dist/core/mcp/host/roots-provider.js +70 -0
- package/dist/core/mcp/host/sampling-provider.js +170 -0
- package/dist/core/mcp/index.js +4 -0
- package/dist/core/mcp/observability/events.js +19 -0
- package/dist/core/mcp/policy/approval-policy.js +2 -0
- package/dist/core/mcp/policy/classifier.js +172 -0
- package/dist/core/mcp/policy/grants.js +356 -0
- package/dist/core/mcp/policy/uri-policy.js +60 -0
- package/dist/core/mcp/schema/json-schema-to-zod.js +511 -0
- package/dist/core/mcp/types.js +2 -0
- package/dist/core/protocols/a2a/agent-card.js +36 -11
- package/dist/core/protocols/a2a/sdk/executor.js +105 -36
- package/dist/core/protocols/a2a/sdk/server.js +1311 -3
- package/dist/core/protocols/acp/acp-checkpoint-probe.js +113 -0
- package/dist/core/protocols/acp/acp-session-persistence.js +336 -0
- package/dist/core/protocols/acp/acp-types.js +17 -0
- package/dist/core/protocols/acp/formal-agent.js +271 -603
- package/dist/core/protocols/acp/handlers.js +3 -0
- package/dist/core/protocols/acp/permission-provider.js +11 -39
- package/dist/core/protocols/acp/stdio-server.js +20 -1
- package/dist/core/protocols/acp/tool-kind-mapping.js +62 -0
- package/dist/core/protocols/shared/flow-mode-mapping.js +0 -8
- package/dist/core/public-capabilities/flow-mode-metadata.js +0 -6
- package/dist/core/public-capabilities/projections.js +1 -0
- package/dist/core/runtime/agent-server-runtime.js +2 -3
- package/dist/core/runtime/spawn-command.js +8 -2
- package/dist/core/runtime/spawn-interactive.js +26 -0
- package/dist/core/session/manager.js +65 -35
- package/dist/core/tools/builtin/index.js +6 -1
- package/dist/core/tools/builtin/proposal.js +0 -7
- package/dist/core/tools/builtin/workspace.js +76 -0
- package/dist/core/tools/dispatcher.js +1 -0
- package/dist/core/tools/loader.js +92 -46
- package/dist/core/verification/runner.js +60 -31
- package/dist/core/workspace/capabilities.js +80 -0
- package/dist/locales/en.js +17 -3
- package/package.json +4 -2
- package/dist/core/protocols/a2a/mapper.js +0 -14
- package/dist/core/protocols/a2a/sdk/auth-middleware.js +0 -31
- package/dist/core/protocols/a2a/task-projection.js +0 -45
- package/dist/core/protocols/acp/checkpoint-meta.js +0 -2
- package/dist/core/tools/mcp/client.js +0 -309
- package/dist/core/tools/mcp/loader.js +0 -110
- package/dist/core/tools/mcp/schema.js +0 -54
- package/dist/core/tools/mcp/streamable-http.js +0 -101
- package/dist/core/tools/mcp/types.js +0 -26
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
import { createInterface } from 'readline';
|
|
2
|
-
import { LIMITS } from '../../config/limits.js';
|
|
3
|
-
import { getLogger } from '../../observability/logger.js';
|
|
4
|
-
import { spawnInteractiveProcess } from '../../runtime/process-runner.js';
|
|
5
|
-
import { PACKAGE_VERSION } from '../../version.js';
|
|
6
|
-
import { assertOk, createMcpHeaders, decodeSseEvents, delayMs, isEventStreamResponse, safeDrainResponse, } from './streamable-http.js';
|
|
7
|
-
/**
|
|
8
|
-
* MCP Client handling JSON-RPC communication over stdio with an external server.
|
|
9
|
-
*/
|
|
10
|
-
export class McpClient {
|
|
11
|
-
config;
|
|
12
|
-
process = null;
|
|
13
|
-
requestId = 0;
|
|
14
|
-
pendingRequests = new Map();
|
|
15
|
-
rl = null;
|
|
16
|
-
sessionId;
|
|
17
|
-
constructor(config) {
|
|
18
|
-
this.config = config;
|
|
19
|
-
}
|
|
20
|
-
isHttp() {
|
|
21
|
-
return Boolean(this.config.url);
|
|
22
|
-
}
|
|
23
|
-
/**
|
|
24
|
-
* Starts the MCP server process and performs the initialization handshake.
|
|
25
|
-
*/
|
|
26
|
-
async start() {
|
|
27
|
-
if (this.isHttp()) {
|
|
28
|
-
getLogger().info(`Connecting to MCP server: ${this.config.name} (url: ${this.config.url})`);
|
|
29
|
-
await this.initialize();
|
|
30
|
-
getLogger().info(`MCP server ${this.config.name} ready.`);
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
getLogger().info(`Starting MCP server: ${this.config.name} (command: ${this.config.command})`);
|
|
34
|
-
this.process = spawnInteractiveProcess({
|
|
35
|
-
command: this.config.command,
|
|
36
|
-
args: this.config.args || [],
|
|
37
|
-
env: { ...process.env, ...this.config.env },
|
|
38
|
-
cwd: this.config.cwd,
|
|
39
|
-
// Never inherit stderr into the parent TTY: it bypasses UI sanitization and can leak raw errors.
|
|
40
|
-
windowsHide: true,
|
|
41
|
-
});
|
|
42
|
-
if (!this.process) {
|
|
43
|
-
throw new Error(`Failed to spawn MCP server process: ${this.config.name}`);
|
|
44
|
-
}
|
|
45
|
-
this.process.on('error', (err) => {
|
|
46
|
-
getLogger().error(`MCP process error (${this.config.name}): ${err}`);
|
|
47
|
-
});
|
|
48
|
-
this.process.on('exit', (code) => {
|
|
49
|
-
if (code !== 0 && code !== null) {
|
|
50
|
-
getLogger().error(`MCP server ${this.config.name} exited with code ${code}`);
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
this.rl = createInterface({
|
|
54
|
-
input: this.process.stdout,
|
|
55
|
-
terminal: false,
|
|
56
|
-
});
|
|
57
|
-
this.rl.on('line', (line) => this.handleMessage(line));
|
|
58
|
-
// Drain stderr to avoid backpressure deadlocks, but do not surface raw output to UI.
|
|
59
|
-
this.process.stderr?.on('data', () => { });
|
|
60
|
-
await this.initialize();
|
|
61
|
-
getLogger().info(`MCP server ${this.config.name} ready.`);
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Retrieves the list of tools provided by the MCP server.
|
|
65
|
-
*/
|
|
66
|
-
async listTools() {
|
|
67
|
-
const response = await this.request('tools/list', {});
|
|
68
|
-
return response.tools || [];
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Executes a tool on the MCP server.
|
|
72
|
-
*/
|
|
73
|
-
async callTool(name, args) {
|
|
74
|
-
return await this.request('tools/call', { name, arguments: args });
|
|
75
|
-
}
|
|
76
|
-
async initialize() {
|
|
77
|
-
// Step 1: Initialize handshake
|
|
78
|
-
await this.request('initialize', {
|
|
79
|
-
protocolVersion: '2025-11-25',
|
|
80
|
-
capabilities: {},
|
|
81
|
-
clientInfo: { name: 'salmon-loop', version: PACKAGE_VERSION },
|
|
82
|
-
});
|
|
83
|
-
// Step 2: Signal initialized
|
|
84
|
-
await this.notification('notifications/initialized', {});
|
|
85
|
-
}
|
|
86
|
-
async request(method, params) {
|
|
87
|
-
if (this.isHttp()) {
|
|
88
|
-
return (await this.requestHttp(method, params));
|
|
89
|
-
}
|
|
90
|
-
if (!this.process?.stdin?.write) {
|
|
91
|
-
throw new Error(`MCP client ${this.config.name} is not started`);
|
|
92
|
-
}
|
|
93
|
-
const id = ++this.requestId;
|
|
94
|
-
const message = JSON.stringify({ jsonrpc: '2.0', id, method, params }) + '\n';
|
|
95
|
-
return new Promise((resolve, reject) => {
|
|
96
|
-
this.pendingRequests.set(id, { resolve: resolve, reject });
|
|
97
|
-
this.process.stdin.write(message);
|
|
98
|
-
// Default timeout for MCP requests
|
|
99
|
-
setTimeout(() => {
|
|
100
|
-
if (this.pendingRequests.has(id)) {
|
|
101
|
-
this.pendingRequests.delete(id);
|
|
102
|
-
reject(new Error(`MCP Request ${id} (${method}) to ${this.config.name} timed out after ${LIMITS.defaultToolTimeoutMs / 1000}s`));
|
|
103
|
-
}
|
|
104
|
-
}, LIMITS.defaultToolTimeoutMs);
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
async notification(method, params) {
|
|
108
|
-
if (this.isHttp()) {
|
|
109
|
-
await this.notificationHttp(method, params);
|
|
110
|
-
return;
|
|
111
|
-
}
|
|
112
|
-
if (!this.process?.stdin?.write)
|
|
113
|
-
return;
|
|
114
|
-
const message = JSON.stringify({ jsonrpc: '2.0', method, params }) + '\n';
|
|
115
|
-
this.process.stdin.write(message);
|
|
116
|
-
}
|
|
117
|
-
handleMessage(line) {
|
|
118
|
-
if (!line.trim())
|
|
119
|
-
return;
|
|
120
|
-
try {
|
|
121
|
-
const message = JSON.parse(line);
|
|
122
|
-
// Handle JSON-RPC Response
|
|
123
|
-
if (message.id !== undefined && this.pendingRequests.has(message.id)) {
|
|
124
|
-
const { resolve, reject } = this.pendingRequests.get(message.id);
|
|
125
|
-
this.pendingRequests.delete(message.id);
|
|
126
|
-
if (message.error) {
|
|
127
|
-
reject(new Error(`MCP Error [${message.error.code}]: ${message.error.message}`));
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
resolve(message.result);
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
// Handle Notifications/Requests from Server (Optional, future proofing)
|
|
134
|
-
else if (message.method) {
|
|
135
|
-
getLogger().debug(`Received MCP notification/request: ${message.method} (server: ${this.config.name})`);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
catch (err) {
|
|
139
|
-
getLogger().error(`Failed to parse MCP message from ${this.config.name}: ${err} (line: ${line})`);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
async stop() {
|
|
143
|
-
if (this.isHttp()) {
|
|
144
|
-
await this.stopHttp();
|
|
145
|
-
return;
|
|
146
|
-
}
|
|
147
|
-
this.rl?.close();
|
|
148
|
-
this.process?.kill();
|
|
149
|
-
this.process = null;
|
|
150
|
-
this.pendingRequests.clear();
|
|
151
|
-
}
|
|
152
|
-
async requestHttp(method, params) {
|
|
153
|
-
const url = this.config.url;
|
|
154
|
-
const headers = this.config.headers ?? {};
|
|
155
|
-
const id = ++this.requestId;
|
|
156
|
-
const payload = { jsonrpc: '2.0', id, method, params };
|
|
157
|
-
const timeoutMs = LIMITS.defaultToolTimeoutMs;
|
|
158
|
-
const controller = new AbortController();
|
|
159
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
160
|
-
try {
|
|
161
|
-
const response = await fetch(url, {
|
|
162
|
-
method: 'POST',
|
|
163
|
-
headers: createMcpHeaders({ sessionId: this.sessionId, extra: headers }),
|
|
164
|
-
body: JSON.stringify(payload),
|
|
165
|
-
signal: controller.signal,
|
|
166
|
-
});
|
|
167
|
-
const newSessionId = response.headers.get('mcp-session-id');
|
|
168
|
-
if (newSessionId)
|
|
169
|
-
this.sessionId = newSessionId;
|
|
170
|
-
assertOk(response, `MCP request ${id} (${method}) to ${this.config.name}`);
|
|
171
|
-
if (!isEventStreamResponse(response)) {
|
|
172
|
-
const message = (await response.json());
|
|
173
|
-
if (message && typeof message === 'object' && message.error) {
|
|
174
|
-
const err = message.error;
|
|
175
|
-
throw new Error(`MCP Error [${err.code}]: ${err.message}`);
|
|
176
|
-
}
|
|
177
|
-
return message?.result;
|
|
178
|
-
}
|
|
179
|
-
let lastEventId;
|
|
180
|
-
let retryMs = 1000;
|
|
181
|
-
// Streamable HTTP can require reconnect (via GET + Last-Event-ID) to resume a dropped stream.
|
|
182
|
-
let streamResponse = response;
|
|
183
|
-
while (true) {
|
|
184
|
-
if (!streamResponse.body) {
|
|
185
|
-
throw new Error(`MCP SSE response missing body (${this.config.name})`);
|
|
186
|
-
}
|
|
187
|
-
for await (const event of decodeSseEvents(streamResponse.body)) {
|
|
188
|
-
if (event.id)
|
|
189
|
-
lastEventId = event.id;
|
|
190
|
-
if (typeof event.retry === 'number')
|
|
191
|
-
retryMs = Math.max(0, event.retry);
|
|
192
|
-
if (!event.data)
|
|
193
|
-
continue;
|
|
194
|
-
try {
|
|
195
|
-
const msg = JSON.parse(event.data);
|
|
196
|
-
if (msg.id === id) {
|
|
197
|
-
if (msg.error) {
|
|
198
|
-
const err = msg.error;
|
|
199
|
-
throw new Error(`MCP Error [${err.code}]: ${err.message}`);
|
|
200
|
-
}
|
|
201
|
-
return msg.result;
|
|
202
|
-
}
|
|
203
|
-
if (msg.method) {
|
|
204
|
-
getLogger().debug(`Received MCP notification/request: ${msg.method} (server: ${this.config.name})`);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
catch (err) {
|
|
208
|
-
getLogger().error(`Failed to parse MCP SSE message from ${this.config.name}: ${String(err)} (data: ${event.data})`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
// Stream ended without response for this request. Try to resume via GET.
|
|
212
|
-
if (!lastEventId) {
|
|
213
|
-
throw new Error(`MCP SSE stream ended before response for request ${id} (${method}) (${this.config.name})`);
|
|
214
|
-
}
|
|
215
|
-
await delayMs(retryMs);
|
|
216
|
-
const resumeController = new AbortController();
|
|
217
|
-
const resumeTimeout = setTimeout(() => resumeController.abort(), timeoutMs);
|
|
218
|
-
try {
|
|
219
|
-
streamResponse = await fetch(url, {
|
|
220
|
-
method: 'GET',
|
|
221
|
-
headers: {
|
|
222
|
-
...createMcpHeaders({ sessionId: this.sessionId, extra: headers }),
|
|
223
|
-
'Last-Event-ID': lastEventId,
|
|
224
|
-
},
|
|
225
|
-
signal: resumeController.signal,
|
|
226
|
-
});
|
|
227
|
-
const resumedSessionId = streamResponse.headers.get('mcp-session-id');
|
|
228
|
-
if (resumedSessionId)
|
|
229
|
-
this.sessionId = resumedSessionId;
|
|
230
|
-
assertOk(streamResponse, `MCP SSE resume for ${this.config.name}`);
|
|
231
|
-
if (!isEventStreamResponse(streamResponse)) {
|
|
232
|
-
throw new Error(`MCP SSE resume did not return text/event-stream (${this.config.name})`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
finally {
|
|
236
|
-
clearTimeout(resumeTimeout);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
catch (err) {
|
|
241
|
-
if (err instanceof Error && err.name === 'AbortError') {
|
|
242
|
-
throw new Error(`MCP Request ${id} (${method}) to ${this.config.name} timed out after ${timeoutMs / 1000}s`);
|
|
243
|
-
}
|
|
244
|
-
throw err;
|
|
245
|
-
}
|
|
246
|
-
finally {
|
|
247
|
-
clearTimeout(timeout);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
async notificationHttp(method, params) {
|
|
251
|
-
const url = this.config.url;
|
|
252
|
-
const headers = this.config.headers ?? {};
|
|
253
|
-
const payload = { jsonrpc: '2.0', method, params };
|
|
254
|
-
const timeoutMs = LIMITS.defaultToolTimeoutMs;
|
|
255
|
-
const controller = new AbortController();
|
|
256
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
257
|
-
try {
|
|
258
|
-
const response = await fetch(url, {
|
|
259
|
-
method: 'POST',
|
|
260
|
-
headers: createMcpHeaders({ sessionId: this.sessionId, extra: headers }),
|
|
261
|
-
body: JSON.stringify(payload),
|
|
262
|
-
signal: controller.signal,
|
|
263
|
-
});
|
|
264
|
-
const newSessionId = response.headers.get('mcp-session-id');
|
|
265
|
-
if (newSessionId)
|
|
266
|
-
this.sessionId = newSessionId;
|
|
267
|
-
if (!response.ok) {
|
|
268
|
-
await safeDrainResponse(response);
|
|
269
|
-
throw new Error(`MCP notification ${method} failed with HTTP ${response.status}`);
|
|
270
|
-
}
|
|
271
|
-
await safeDrainResponse(response);
|
|
272
|
-
}
|
|
273
|
-
catch (err) {
|
|
274
|
-
if (err instanceof Error && err.name === 'AbortError') {
|
|
275
|
-
throw new Error(`MCP notification ${method} to ${this.config.name} timed out after ${timeoutMs / 1000}s`);
|
|
276
|
-
}
|
|
277
|
-
throw err;
|
|
278
|
-
}
|
|
279
|
-
finally {
|
|
280
|
-
clearTimeout(timeout);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
async stopHttp() {
|
|
284
|
-
if (!this.config.url || !this.sessionId)
|
|
285
|
-
return;
|
|
286
|
-
const timeoutMs = 3000;
|
|
287
|
-
const controller = new AbortController();
|
|
288
|
-
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
289
|
-
try {
|
|
290
|
-
const response = await fetch(this.config.url, {
|
|
291
|
-
method: 'DELETE',
|
|
292
|
-
headers: createMcpHeaders({
|
|
293
|
-
sessionId: this.sessionId,
|
|
294
|
-
extra: this.config.headers ?? {},
|
|
295
|
-
}),
|
|
296
|
-
signal: controller.signal,
|
|
297
|
-
});
|
|
298
|
-
await safeDrainResponse(response);
|
|
299
|
-
}
|
|
300
|
-
catch {
|
|
301
|
-
// best-effort cleanup
|
|
302
|
-
}
|
|
303
|
-
finally {
|
|
304
|
-
clearTimeout(timeout);
|
|
305
|
-
this.sessionId = undefined;
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
//# sourceMappingURL=client.js.map
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { LIMITS } from '../../config/limits.js';
|
|
3
|
-
import { getLogger } from '../../observability/logger.js';
|
|
4
|
-
import { Phase } from '../../types/runtime.js';
|
|
5
|
-
import { McpClient } from './client.js';
|
|
6
|
-
import { jsonSchemaToZod } from './schema.js';
|
|
7
|
-
const OUTPUT_SCHEMA = z
|
|
8
|
-
.object({
|
|
9
|
-
content: z.array(z.record(z.string(), z.any())),
|
|
10
|
-
})
|
|
11
|
-
.passthrough();
|
|
12
|
-
const PROCESS_SIDE_EFFECTS = ['process', 'network'];
|
|
13
|
-
const ALLOWED_PHASES = [Phase.VERIFY];
|
|
14
|
-
function matchesPattern(value, pattern) {
|
|
15
|
-
if (pattern === '*')
|
|
16
|
-
return true;
|
|
17
|
-
if (pattern.endsWith('*')) {
|
|
18
|
-
return value.startsWith(pattern.slice(0, -1));
|
|
19
|
-
}
|
|
20
|
-
return value === pattern;
|
|
21
|
-
}
|
|
22
|
-
function isToolAllowed(toolName, allowList) {
|
|
23
|
-
if (!allowList || allowList.length === 0)
|
|
24
|
-
return false;
|
|
25
|
-
return allowList.some((pattern) => matchesPattern(toolName, pattern));
|
|
26
|
-
}
|
|
27
|
-
export async function registerMcpTools(registry, servers) {
|
|
28
|
-
for (const server of servers) {
|
|
29
|
-
if (!server.enabled)
|
|
30
|
-
continue;
|
|
31
|
-
if (!server.allowTools || server.allowTools.length === 0) {
|
|
32
|
-
getLogger().warn(`MCP server ${server.name} has no tool allowlist; skipping registration.`);
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
const client = server.transport === 'http'
|
|
36
|
-
? new McpClient({
|
|
37
|
-
name: server.name,
|
|
38
|
-
url: server.url,
|
|
39
|
-
headers: server.headers,
|
|
40
|
-
})
|
|
41
|
-
: new McpClient({
|
|
42
|
-
name: server.name,
|
|
43
|
-
command: server.command,
|
|
44
|
-
args: server.args,
|
|
45
|
-
env: server.env,
|
|
46
|
-
cwd: server.cwd,
|
|
47
|
-
});
|
|
48
|
-
try {
|
|
49
|
-
await client.start();
|
|
50
|
-
const toolList = await client.listTools();
|
|
51
|
-
if (!Array.isArray(toolList) || toolList.length === 0) {
|
|
52
|
-
getLogger().warn(`MCP server ${server.name} reported no tools.`);
|
|
53
|
-
}
|
|
54
|
-
for (const tool of toolList) {
|
|
55
|
-
const toolName = tool.name;
|
|
56
|
-
if (!toolName)
|
|
57
|
-
continue;
|
|
58
|
-
if (!isToolAllowed(toolName, server.allowTools)) {
|
|
59
|
-
continue;
|
|
60
|
-
}
|
|
61
|
-
const spec = {
|
|
62
|
-
name: `mcp.${server.name}.${toolName}`,
|
|
63
|
-
source: 'mcp',
|
|
64
|
-
intent: 'INFRA',
|
|
65
|
-
description: tool.description || `MCP tool ${toolName}`,
|
|
66
|
-
riskLevel: 'medium',
|
|
67
|
-
sideEffects: PROCESS_SIDE_EFFECTS,
|
|
68
|
-
concurrency: 'serial_only',
|
|
69
|
-
allowedPhases: ALLOWED_PHASES,
|
|
70
|
-
inputSchema: jsonSchemaToZod(tool.inputSchema),
|
|
71
|
-
outputSchema: OUTPUT_SCHEMA,
|
|
72
|
-
defaultTimeoutMs: LIMITS.defaultToolTimeoutMs,
|
|
73
|
-
executor: async (input) => {
|
|
74
|
-
const runtimeClient = server.transport === 'http'
|
|
75
|
-
? new McpClient({
|
|
76
|
-
name: server.name,
|
|
77
|
-
url: server.url,
|
|
78
|
-
headers: server.headers,
|
|
79
|
-
})
|
|
80
|
-
: new McpClient({
|
|
81
|
-
name: server.name,
|
|
82
|
-
command: server.command,
|
|
83
|
-
args: server.args,
|
|
84
|
-
env: server.env,
|
|
85
|
-
cwd: server.cwd,
|
|
86
|
-
});
|
|
87
|
-
try {
|
|
88
|
-
await runtimeClient.start();
|
|
89
|
-
const result = await runtimeClient.callTool(toolName, input);
|
|
90
|
-
return result;
|
|
91
|
-
}
|
|
92
|
-
finally {
|
|
93
|
-
await runtimeClient.stop();
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
getLogger().info(`Registered MCP tool ${spec.name} from ${server.name}`);
|
|
98
|
-
registry.register(spec);
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
catch (error) {
|
|
102
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
103
|
-
getLogger().error(`Failed to register MCP server ${server.name}: ${message}`);
|
|
104
|
-
}
|
|
105
|
-
finally {
|
|
106
|
-
await client.stop();
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
//# sourceMappingURL=loader.js.map
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
import { getLogger } from '../../observability/logger.js';
|
|
3
|
-
/**
|
|
4
|
-
* Converts a JSON Schema (commonly used in MCP) to a Zod schema.
|
|
5
|
-
* This implementation covers the core JSON Schema types used by tools.
|
|
6
|
-
*/
|
|
7
|
-
export function jsonSchemaToZod(jsonSchema) {
|
|
8
|
-
if (!jsonSchema || typeof jsonSchema !== 'object') {
|
|
9
|
-
return z.any();
|
|
10
|
-
}
|
|
11
|
-
const schema = jsonSchema;
|
|
12
|
-
// Handle cases where the schema is just a description or empty
|
|
13
|
-
if (!schema.type && !schema.properties) {
|
|
14
|
-
return z.any();
|
|
15
|
-
}
|
|
16
|
-
try {
|
|
17
|
-
switch (schema.type) {
|
|
18
|
-
case 'string':
|
|
19
|
-
return z.string().describe(schema.description || '');
|
|
20
|
-
case 'number':
|
|
21
|
-
case 'integer':
|
|
22
|
-
return z.number().describe(schema.description || '');
|
|
23
|
-
case 'boolean':
|
|
24
|
-
return z.boolean().describe(schema.description || '');
|
|
25
|
-
case 'array': {
|
|
26
|
-
const items = schema.items ? jsonSchemaToZod(schema.items) : z.any();
|
|
27
|
-
return z.array(items).describe(schema.description || '');
|
|
28
|
-
}
|
|
29
|
-
case 'object':
|
|
30
|
-
case undefined: {
|
|
31
|
-
// Often schemas with properties omit 'type: object'
|
|
32
|
-
const shape = {};
|
|
33
|
-
const properties = (schema.properties || {});
|
|
34
|
-
const required = schema.required || [];
|
|
35
|
-
for (const [key, prop] of Object.entries(properties)) {
|
|
36
|
-
let fieldSchema = jsonSchemaToZod(prop);
|
|
37
|
-
if (!required.includes(key)) {
|
|
38
|
-
fieldSchema = fieldSchema.optional();
|
|
39
|
-
}
|
|
40
|
-
shape[key] = fieldSchema;
|
|
41
|
-
}
|
|
42
|
-
return z.object(shape).describe(schema.description || '');
|
|
43
|
-
}
|
|
44
|
-
default:
|
|
45
|
-
getLogger().debug(`Unsupported JSON schema type: ${schema.type}, falling back to any`);
|
|
46
|
-
return z.any();
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
catch (err) {
|
|
50
|
-
getLogger().error(`Failed to convert JSON schema to Zod: ${String(err)} (Schema: ${JSON.stringify(schema)})`);
|
|
51
|
-
return z.any();
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
//# sourceMappingURL=schema.js.map
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import { getLogger } from '../../observability/logger.js';
|
|
2
|
-
export const MCP_PROTOCOL_VERSION = '2025-11-25';
|
|
3
|
-
function normalizeHeaderValue(value) {
|
|
4
|
-
return value ? value.trim().toLowerCase() : '';
|
|
5
|
-
}
|
|
6
|
-
export function isEventStreamResponse(response) {
|
|
7
|
-
const contentType = normalizeHeaderValue(response.headers.get('content-type'));
|
|
8
|
-
return contentType.includes('text/event-stream');
|
|
9
|
-
}
|
|
10
|
-
export async function* decodeSseEvents(body) {
|
|
11
|
-
const reader = body.getReader();
|
|
12
|
-
const decoder = new TextDecoder('utf-8');
|
|
13
|
-
let buffer = '';
|
|
14
|
-
function takeEventBlocks() {
|
|
15
|
-
const blocks = [];
|
|
16
|
-
while (true) {
|
|
17
|
-
const idx = buffer.indexOf('\n\n');
|
|
18
|
-
const idxCr = buffer.indexOf('\r\n\r\n');
|
|
19
|
-
const splitAt = idx === -1 ? idxCr : idxCr === -1 ? idx : Math.min(idx, idxCr);
|
|
20
|
-
if (splitAt === -1)
|
|
21
|
-
break;
|
|
22
|
-
const separatorLen = buffer.startsWith('\r\n\r\n', splitAt) ? 4 : 2;
|
|
23
|
-
blocks.push(buffer.slice(0, splitAt));
|
|
24
|
-
buffer = buffer.slice(splitAt + separatorLen);
|
|
25
|
-
}
|
|
26
|
-
return blocks;
|
|
27
|
-
}
|
|
28
|
-
try {
|
|
29
|
-
while (true) {
|
|
30
|
-
const { value, done } = await reader.read();
|
|
31
|
-
if (done)
|
|
32
|
-
break;
|
|
33
|
-
buffer += decoder.decode(value, { stream: true });
|
|
34
|
-
for (const block of takeEventBlocks()) {
|
|
35
|
-
const lines = block.split(/\r?\n/);
|
|
36
|
-
const event = { data: '' };
|
|
37
|
-
const dataLines = [];
|
|
38
|
-
for (const line of lines) {
|
|
39
|
-
if (!line)
|
|
40
|
-
continue;
|
|
41
|
-
if (line.startsWith(':'))
|
|
42
|
-
continue;
|
|
43
|
-
const colon = line.indexOf(':');
|
|
44
|
-
const field = (colon === -1 ? line : line.slice(0, colon)).trim();
|
|
45
|
-
const rawValue = colon === -1 ? '' : line.slice(colon + 1).trimStart();
|
|
46
|
-
if (field === 'id')
|
|
47
|
-
event.id = rawValue;
|
|
48
|
-
else if (field === 'event')
|
|
49
|
-
event.event = rawValue;
|
|
50
|
-
else if (field === 'retry') {
|
|
51
|
-
const parsed = Number.parseInt(rawValue, 10);
|
|
52
|
-
if (!Number.isNaN(parsed))
|
|
53
|
-
event.retry = parsed;
|
|
54
|
-
}
|
|
55
|
-
else if (field === 'data')
|
|
56
|
-
dataLines.push(rawValue);
|
|
57
|
-
}
|
|
58
|
-
event.data = dataLines.join('\n');
|
|
59
|
-
yield event;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
finally {
|
|
64
|
-
try {
|
|
65
|
-
reader.releaseLock();
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
// ignore
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
export function createMcpHeaders(options) {
|
|
73
|
-
const headers = {
|
|
74
|
-
Accept: 'application/json, text/event-stream',
|
|
75
|
-
'Content-Type': 'application/json',
|
|
76
|
-
'MCP-Protocol-Version': MCP_PROTOCOL_VERSION,
|
|
77
|
-
...(options.extra ?? {}),
|
|
78
|
-
};
|
|
79
|
-
if (options.sessionId)
|
|
80
|
-
headers['MCP-Session-Id'] = options.sessionId;
|
|
81
|
-
return headers;
|
|
82
|
-
}
|
|
83
|
-
export function assertOk(response, context) {
|
|
84
|
-
if (!response.ok) {
|
|
85
|
-
throw new Error(`${context} failed with HTTP ${response.status}`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
export async function safeDrainResponse(response) {
|
|
89
|
-
try {
|
|
90
|
-
if (response.body) {
|
|
91
|
-
await response.arrayBuffer();
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
catch (err) {
|
|
95
|
-
getLogger().debug(`Failed to drain response body: ${String(err)}`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
export async function delayMs(ms) {
|
|
99
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
100
|
-
}
|
|
101
|
-
//# sourceMappingURL=streamable-http.js.map
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
/**
|
|
3
|
-
* Configuration for an MCP server connection.
|
|
4
|
-
*/
|
|
5
|
-
export const McpServerConfigSchema = z
|
|
6
|
-
.object({
|
|
7
|
-
name: z.string(),
|
|
8
|
-
command: z.string().optional(),
|
|
9
|
-
url: z.string().url().optional(),
|
|
10
|
-
args: z.array(z.string()).optional(),
|
|
11
|
-
env: z.record(z.string(), z.string()).optional(),
|
|
12
|
-
headers: z.record(z.string(), z.string()).optional(),
|
|
13
|
-
cwd: z.string().optional(),
|
|
14
|
-
})
|
|
15
|
-
.superRefine((value, ctx) => {
|
|
16
|
-
const hasCommand = Boolean(value.command);
|
|
17
|
-
const hasUrl = Boolean(value.url);
|
|
18
|
-
if (hasCommand === hasUrl) {
|
|
19
|
-
ctx.addIssue({
|
|
20
|
-
code: z.ZodIssueCode.custom,
|
|
21
|
-
message: 'MCP config must include exactly one of "command" or "url".',
|
|
22
|
-
path: ['command'],
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
//# sourceMappingURL=types.js.map
|