mixdog 0.9.151 → 0.9.152
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/README.md +110 -68
- package/package.json +3 -2
- package/scripts/tool-stress.mjs +0 -2
- package/src/defaults/skills/setup/SKILL.md +18 -27
- package/src/headless-exec.mjs +3 -10
- package/src/headless-exec.test.mjs +2 -7
- package/src/runtime/agent/orchestrator/config.mjs +6 -66
- package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
- package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
- package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
- package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
- package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
- package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
- package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
- package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
- package/src/runtime/agent/orchestrator/session/compact/runner.mjs +24 -1
- package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
- package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
- package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/browser-bridge/client.mjs +103 -0
- package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
- package/src/runtime/computer-bridge/client.mjs +101 -0
- package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
- package/src/runtime/media/store.mjs +18 -8
- package/src/runtime/media/store.test.mjs +14 -0
- package/src/runtime/memory/index.mjs +1 -1
- package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
- package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
- package/src/runtime/shared/child-spawn-gate.mjs +17 -0
- package/src/runtime/shared/child-spawn-remote.mjs +69 -7
- package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
- package/src/runtime/shared/pristine-execution-contract.json +2 -1
- package/src/runtime/shared/provider-api-key.mjs +1 -0
- package/src/runtime/shared/session-runtime-health.mjs +91 -0
- package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
- package/src/runtime/shared/skill-document.mjs +86 -0
- package/src/runtime/shared/skill-document.test.mjs +81 -0
- package/src/runtime/shared/tool-surface.mjs +18 -0
- package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
- package/src/runtime/shared/turn-snapshot.mjs +32 -9
- package/src/runtime/shared/user-cwd.mjs +33 -14
- package/src/runtime/shared/user-cwd.test.mjs +43 -0
- package/src/session-runtime/cwd-plugins.mjs +26 -26
- package/src/session-runtime/cwd-plugins.test.mjs +75 -0
- package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
- package/src/session-runtime/global-extensions.mjs +26 -0
- package/src/session-runtime/global-extensions.test.mjs +28 -0
- package/src/session-runtime/goal-runtime.mjs +721 -0
- package/src/session-runtime/goal-runtime.test.mjs +108 -0
- package/src/session-runtime/lifecycle-api.mjs +18 -4
- package/src/session-runtime/lifecycle-api.test.mjs +21 -0
- package/src/session-runtime/mcp-glue.mjs +49 -47
- package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
- package/src/session-runtime/plugin-mcp.mjs +96 -22
- package/src/session-runtime/prewarm.mjs +1 -10
- package/src/session-runtime/resource-api-global.test.mjs +69 -0
- package/src/session-runtime/resource-api.mjs +173 -58
- package/src/session-runtime/runtime-core.mjs +148 -20
- package/src/session-runtime/runtime-tunables.mjs +6 -8
- package/src/session-runtime/session-turn-api.mjs +22 -11
- package/src/session-runtime/skills-api.mjs +79 -25
- package/src/session-runtime/skills-api.test.mjs +99 -0
- package/src/session-runtime/tool-catalog-schema.mjs +0 -5
- package/src/session-runtime/tool-catalog.mjs +4 -13
- package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
- package/src/session-runtime/tool-surface.mjs +12 -0
- package/src/standalone/agent-dispatch-broker.mjs +5 -9
- package/src/standalone/agent-tool/notify.mjs +6 -8
- package/src/standalone/agent-tool/notify.test.mjs +37 -0
- package/src/standalone/daemon.mjs +31 -15
- package/src/standalone/hook-bus/config.mjs +1 -0
- package/src/standalone/plugin-admin.mjs +17 -0
- package/src/standalone/plugin-admin.test.mjs +7 -0
- package/src/standalone/provider-admin.mjs +1 -0
- package/src/standalone/session-protocol.mjs +5 -0
- package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
- package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
- package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
- package/src/standalone/session-runtime-host-factory.mjs +17 -0
- package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
- package/src/standalone/session-runtime-host-health.test.mjs +5 -0
- package/src/standalone/session-runtime-host.mjs +696 -58
- package/src/standalone/session-runtime-inline-host.mjs +351 -0
- package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
- package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
- package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
- package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
- package/src/standalone/session-runtime-shard-router.mjs +149 -0
- package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
- package/src/standalone/session-runtime-worker.mjs +476 -18
- package/src/standalone/session-service.mjs +81 -6
- package/src/tui/app/app-view.jsx +1 -0
- package/src/tui/app/core-memory-picker.mjs +2 -0
- package/src/tui/app/extension-pickers.mjs +1 -1
- package/src/tui/app/maintenance-pickers.mjs +3 -3
- package/src/tui/app/model-options.mjs +2 -0
- package/src/tui/app/model-picker.mjs +14 -4
- package/src/tui/app/panel-handoff.test.mjs +311 -0
- package/src/tui/app/project-picker.mjs +1 -0
- package/src/tui/app/route-pickers.mjs +21 -6
- package/src/tui/app/settings-picker.mjs +26 -3
- package/src/tui/app/slash-commands.mjs +1 -0
- package/src/tui/app/slash-dispatch.mjs +73 -22
- package/src/tui/app/theme-effort-pickers.mjs +7 -2
- package/src/tui/components/Picker.jsx +10 -1
- package/src/tui/dist/index.mjs +163 -44
- package/src/tui/session/context-state.mjs +1 -0
- package/src/tui/session/goal-continuation.mjs +110 -0
- package/src/tui/session/goal-continuation.test.mjs +84 -0
- package/src/tui/session/queue-helpers.mjs +5 -2
- package/src/tui/session/session-action-surface.test.mjs +108 -0
- package/src/tui/session/session-api-ext.mjs +39 -8
- package/src/tui/session/session-api.mjs +58 -0
- package/src/tui/session/session-flow.mjs +6 -0
- package/src/tui/session/turn.mjs +1 -0
- package/src/tui/session-local.mjs +15 -1
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFileSync, existsSync, mkdirSync } from 'fs';
|
|
2
2
|
import { writeFile } from 'fs/promises';
|
|
3
|
-
import { join } from 'path';
|
|
3
|
+
import { basename, join, resolve } from 'path';
|
|
4
|
+
import { pathToFileURL } from 'url';
|
|
4
5
|
import { tmpdir } from 'os';
|
|
5
6
|
import { randomUUID } from 'crypto';
|
|
6
7
|
import { smartReadTruncate } from '../tools/builtin/read-formatting.mjs';
|
|
@@ -93,7 +94,7 @@ export function scrubMcpConnectionMessage(value, cfg = {}) {
|
|
|
93
94
|
}
|
|
94
95
|
} catch { /* invalid URL is reported without echoing it */ }
|
|
95
96
|
}
|
|
96
|
-
const headers =
|
|
97
|
+
const headers = resolveMcpHttpHeaders(cfg);
|
|
97
98
|
for (const [name, headerValue] of Object.entries(headers)) {
|
|
98
99
|
if (/authorization|cookie|token|secret|api[-_]?key|signature/i.test(name)) {
|
|
99
100
|
text = replaceSecret(text, headerValue);
|
|
@@ -158,12 +159,15 @@ async function loadMcpSdk() {
|
|
|
158
159
|
import('@modelcontextprotocol/sdk/client/streamableHttp.js'),
|
|
159
160
|
import('@modelcontextprotocol/sdk/client/sse.js'),
|
|
160
161
|
import('@modelcontextprotocol/sdk/client/websocket.js'),
|
|
161
|
-
|
|
162
|
+
import('@modelcontextprotocol/sdk/types.js'),
|
|
163
|
+
]).then(([clientMod, stdioMod, httpMod, sseMod, wsMod, typesMod]) => ({
|
|
162
164
|
Client: clientMod.Client,
|
|
163
165
|
StdioClientTransport: stdioMod.StdioClientTransport,
|
|
164
166
|
StreamableHTTPClientTransport: httpMod.StreamableHTTPClientTransport,
|
|
165
167
|
SSEClientTransport: sseMod.SSEClientTransport,
|
|
166
168
|
WebSocketClientTransport: wsMod.WebSocketClientTransport,
|
|
169
|
+
ListRootsRequestSchema: typesMod.ListRootsRequestSchema,
|
|
170
|
+
ToolListChangedNotificationSchema: typesMod.ToolListChangedNotificationSchema,
|
|
167
171
|
}));
|
|
168
172
|
return mcpSdkPromise;
|
|
169
173
|
}
|
|
@@ -191,6 +195,46 @@ function expandEnvVars(value, env = process.env) {
|
|
|
191
195
|
}
|
|
192
196
|
return value;
|
|
193
197
|
}
|
|
198
|
+
|
|
199
|
+
const DEFAULT_STDIO_ENV_KEYS = process.platform === 'win32'
|
|
200
|
+
? ['APPDATA', 'HOMEDRIVE', 'HOMEPATH', 'LOCALAPPDATA', 'PATH',
|
|
201
|
+
'PROCESSOR_ARCHITECTURE', 'SYSTEMDRIVE', 'SYSTEMROOT', 'TEMP',
|
|
202
|
+
'TMP', 'USERDOMAIN', 'USERNAME', 'USERPROFILE']
|
|
203
|
+
: ['HOME', 'LOGNAME', 'PATH', 'SHELL', 'TERM', 'USER'];
|
|
204
|
+
|
|
205
|
+
export function resolveMcpStdioEnvironment(cfg = {}, env = process.env) {
|
|
206
|
+
const explicit = cfg.env && typeof cfg.env === 'object' && !Array.isArray(cfg.env)
|
|
207
|
+
? expandEnvVars(cfg.env, env)
|
|
208
|
+
: {};
|
|
209
|
+
if (!Array.isArray(cfg.env_vars)) return { ...env, ...explicit };
|
|
210
|
+
const inherited = {};
|
|
211
|
+
const keys = new Set([...DEFAULT_STDIO_ENV_KEYS, ...cfg.env_vars.map((value) => String(value).trim()).filter(Boolean)]);
|
|
212
|
+
for (const requested of keys) {
|
|
213
|
+
const actual = process.platform === 'win32'
|
|
214
|
+
? Object.keys(env || {}).find((key) => key.toLowerCase() === requested.toLowerCase())
|
|
215
|
+
: requested;
|
|
216
|
+
if (actual && env?.[actual] != null) inherited[actual] = String(env[actual]);
|
|
217
|
+
}
|
|
218
|
+
return { ...inherited, ...explicit };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export function resolveMcpHttpHeaders(cfg = {}, env = process.env) {
|
|
222
|
+
const headers = {};
|
|
223
|
+
const envHeaders = cfg.env_http_headers && typeof cfg.env_http_headers === 'object'
|
|
224
|
+
&& !Array.isArray(cfg.env_http_headers) ? cfg.env_http_headers : {};
|
|
225
|
+
for (const [header, envName] of Object.entries(envHeaders)) {
|
|
226
|
+
const value = env?.[String(envName)];
|
|
227
|
+
if (value != null && String(value)) headers[String(header)] = String(value);
|
|
228
|
+
}
|
|
229
|
+
const bearerEnv = String(cfg.bearer_token_env_var || '').trim();
|
|
230
|
+
if (bearerEnv && env?.[bearerEnv] != null && String(env[bearerEnv])) {
|
|
231
|
+
headers.Authorization = `Bearer ${String(env[bearerEnv])}`;
|
|
232
|
+
}
|
|
233
|
+
const explicit = cfg.headers && typeof cfg.headers === 'object' && !Array.isArray(cfg.headers)
|
|
234
|
+
? expandEnvVars(cfg.headers, env)
|
|
235
|
+
: {};
|
|
236
|
+
return { ...headers, ...explicit };
|
|
237
|
+
}
|
|
194
238
|
/**
|
|
195
239
|
* Resolve the canonical transport kind for an MCP server config entry.
|
|
196
240
|
* Returns one of: 'autoDetect' | 'stdio' | 'http' | 'sse' | 'ws'.
|
|
@@ -260,6 +304,54 @@ export function getMcpTools(scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
|
260
304
|
}
|
|
261
305
|
return tools;
|
|
262
306
|
}
|
|
307
|
+
|
|
308
|
+
function mcpFeatureTools(serverName, capabilities, protocolTools) {
|
|
309
|
+
const tools = [];
|
|
310
|
+
const used = new Set(protocolTools.map((tool) => tool.name));
|
|
311
|
+
const add = (leaf, description, properties, required, operation) => {
|
|
312
|
+
let name = `mcp__${serverName}__${leaf}`;
|
|
313
|
+
while (used.has(name)) name += '_';
|
|
314
|
+
used.add(name);
|
|
315
|
+
tools.push({
|
|
316
|
+
name,
|
|
317
|
+
description,
|
|
318
|
+
inputSchema: {
|
|
319
|
+
type: 'object',
|
|
320
|
+
properties,
|
|
321
|
+
...(required?.length ? { required } : {}),
|
|
322
|
+
additionalProperties: false,
|
|
323
|
+
},
|
|
324
|
+
mcpOperation: operation,
|
|
325
|
+
});
|
|
326
|
+
};
|
|
327
|
+
if (capabilities?.resources) {
|
|
328
|
+
add('mixdog_list_resources', 'List resources exposed by this MCP server.',
|
|
329
|
+
{ cursor: { type: 'string', description: 'Pagination cursor from a previous result.' } },
|
|
330
|
+
[], 'list-resources');
|
|
331
|
+
add('mixdog_list_resource_templates', 'List resource URI templates exposed by this MCP server.',
|
|
332
|
+
{ cursor: { type: 'string', description: 'Pagination cursor from a previous result.' } },
|
|
333
|
+
[], 'list-resource-templates');
|
|
334
|
+
add('mixdog_read_resource', 'Read one resource exposed by this MCP server.',
|
|
335
|
+
{ uri: { type: 'string', description: 'Resource URI returned by the server.' } },
|
|
336
|
+
['uri'], 'read-resource');
|
|
337
|
+
}
|
|
338
|
+
if (capabilities?.prompts) {
|
|
339
|
+
add('mixdog_list_prompts', 'List reusable prompts exposed by this MCP server.',
|
|
340
|
+
{ cursor: { type: 'string', description: 'Pagination cursor from a previous result.' } },
|
|
341
|
+
[], 'list-prompts');
|
|
342
|
+
add('mixdog_get_prompt', 'Render one reusable prompt exposed by this MCP server.',
|
|
343
|
+
{
|
|
344
|
+
name: { type: 'string', description: 'Prompt name returned by the server.' },
|
|
345
|
+
arguments: {
|
|
346
|
+
type: 'object',
|
|
347
|
+
description: 'Prompt argument values.',
|
|
348
|
+
additionalProperties: { type: 'string' },
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
['name'], 'get-prompt');
|
|
352
|
+
}
|
|
353
|
+
return tools;
|
|
354
|
+
}
|
|
263
355
|
export function getMcpServerStatus(scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
264
356
|
return scopedServerEntries(scopeId).map(([, server]) => ({
|
|
265
357
|
name: server.name,
|
|
@@ -269,6 +361,11 @@ export function getMcpServerStatus(scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
|
269
361
|
name: tool.name,
|
|
270
362
|
description: tool.description || '',
|
|
271
363
|
})),
|
|
364
|
+
capabilities: {
|
|
365
|
+
tools: Boolean(server.capabilities?.tools),
|
|
366
|
+
prompts: Boolean(server.capabilities?.prompts),
|
|
367
|
+
resources: Boolean(server.capabilities?.resources),
|
|
368
|
+
},
|
|
272
369
|
transport: (() => {
|
|
273
370
|
try {
|
|
274
371
|
return resolveMcpTransportKind(server.cfg);
|
|
@@ -278,6 +375,36 @@ export function getMcpServerStatus(scopeId = DEFAULT_MCP_SCOPE_ID) {
|
|
|
278
375
|
})(),
|
|
279
376
|
}));
|
|
280
377
|
}
|
|
378
|
+
|
|
379
|
+
function requireMcpServer(name, options = {}) {
|
|
380
|
+
const serverName = String(name || '').trim();
|
|
381
|
+
const server = scopedServer(normalizeMcpScopeId(options), serverName);
|
|
382
|
+
if (!server) throw new Error(`MCP server "${serverName}" not connected`);
|
|
383
|
+
return server;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export async function listMcpResources(name, request = {}, options = {}) {
|
|
387
|
+
return requireMcpServer(name, options).client.listResources(request || {});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export async function listMcpResourceTemplates(name, request = {}, options = {}) {
|
|
391
|
+
return requireMcpServer(name, options).client.listResourceTemplates(request || {});
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export async function readMcpResource(name, uri, options = {}) {
|
|
395
|
+
return requireMcpServer(name, options).client.readResource({ uri: String(uri || '') });
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export async function listMcpPrompts(name, request = {}, options = {}) {
|
|
399
|
+
return requireMcpServer(name, options).client.listPrompts(request || {});
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export async function getMcpPrompt(name, promptName, args = {}, options = {}) {
|
|
403
|
+
return requireMcpServer(name, options).client.getPrompt({
|
|
404
|
+
name: String(promptName || ''),
|
|
405
|
+
arguments: args && typeof args === 'object' && !Array.isArray(args) ? args : {},
|
|
406
|
+
});
|
|
407
|
+
}
|
|
281
408
|
function positiveInt(value, fallback) {
|
|
282
409
|
const parsed = Math.floor(Number(value));
|
|
283
410
|
return Number.isFinite(parsed) && parsed >= 1 ? parsed : fallback;
|
|
@@ -376,6 +503,10 @@ export async function executeMcpTool(name, args, options = {}) {
|
|
|
376
503
|
const server = scopedServer(scopeId, serverName);
|
|
377
504
|
if (!server)
|
|
378
505
|
throw new Error(`MCP server "${serverName}" not connected`);
|
|
506
|
+
const definition = (server.tools || []).find((tool) => tool.name === name);
|
|
507
|
+
const dispatch = (target) => definition?.mcpOperation
|
|
508
|
+
? _callMcpFeatureWithTimeout(target, definition.mcpOperation, args, callSignal)
|
|
509
|
+
: _callToolWithTimeout(target, toolName, args, callSignal);
|
|
379
510
|
const gate = callAdmissionFor(server);
|
|
380
511
|
const callSignal = options?.signal || null;
|
|
381
512
|
return gate.run(options?.ownerKey || currentToolExecutionOwner(), async () => {
|
|
@@ -389,7 +520,7 @@ export async function executeMcpTool(name, args, options = {}) {
|
|
|
389
520
|
}
|
|
390
521
|
let result;
|
|
391
522
|
try {
|
|
392
|
-
result = await
|
|
523
|
+
result = await dispatch(server);
|
|
393
524
|
} catch (firstErr) {
|
|
394
525
|
const firstMsg = firstErr instanceof Error ? firstErr.message : String(firstErr);
|
|
395
526
|
if (isMcpCallAbortError(firstErr) || callSignal?.aborted) {
|
|
@@ -430,7 +561,7 @@ export async function executeMcpTool(name, args, options = {}) {
|
|
|
430
561
|
);
|
|
431
562
|
}
|
|
432
563
|
try {
|
|
433
|
-
result = await
|
|
564
|
+
result = await dispatch(retryServer);
|
|
434
565
|
} catch (retryErr) {
|
|
435
566
|
if (isMcpCallAbortError(retryErr) || callSignal?.aborted) throw retryErr;
|
|
436
567
|
const retryMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
@@ -447,6 +578,53 @@ export async function executeMcpTool(name, args, options = {}) {
|
|
|
447
578
|
});
|
|
448
579
|
}
|
|
449
580
|
|
|
581
|
+
async function _callMcpFeatureWithTimeout(server, operation, args, signal = null) {
|
|
582
|
+
const requestOptions = signal ? { signal } : undefined;
|
|
583
|
+
const cursor = typeof args?.cursor === 'string' && args.cursor ? { cursor: args.cursor } : {};
|
|
584
|
+
let request;
|
|
585
|
+
if (operation === 'list-resources') {
|
|
586
|
+
request = server.client.listResources(cursor, requestOptions);
|
|
587
|
+
} else if (operation === 'list-resource-templates') {
|
|
588
|
+
request = server.client.listResourceTemplates(cursor, requestOptions);
|
|
589
|
+
} else if (operation === 'read-resource') {
|
|
590
|
+
request = server.client.readResource({ uri: String(args?.uri || '') }, requestOptions);
|
|
591
|
+
} else if (operation === 'list-prompts') {
|
|
592
|
+
request = server.client.listPrompts(cursor, requestOptions);
|
|
593
|
+
} else if (operation === 'get-prompt') {
|
|
594
|
+
request = server.client.getPrompt({
|
|
595
|
+
name: String(args?.name || ''),
|
|
596
|
+
arguments: args?.arguments && typeof args.arguments === 'object' && !Array.isArray(args.arguments)
|
|
597
|
+
? args.arguments
|
|
598
|
+
: {},
|
|
599
|
+
}, requestOptions);
|
|
600
|
+
} else {
|
|
601
|
+
throw new Error(`Unsupported MCP feature operation: ${operation}`);
|
|
602
|
+
}
|
|
603
|
+
let timer;
|
|
604
|
+
const timeoutMs = resolveMcpCallTimeoutMs(server?.cfg);
|
|
605
|
+
const abortMessage = `MCP feature call aborted (server="${server?.name}", operation="${operation}")`;
|
|
606
|
+
const bounded = timeoutMs > 0
|
|
607
|
+
? Promise.race([request, new Promise((_, reject) => {
|
|
608
|
+
timer = setTimeout(() => {
|
|
609
|
+
try { _closeServer(server).catch(() => {}); } catch { /* ignore */ }
|
|
610
|
+
const error = new Error(`MCP feature call timed out after ${timeoutMs}ms (server="${server.name}", operation="${operation}")`);
|
|
611
|
+
error.code = 'EMCPTOOLTIMEOUT';
|
|
612
|
+
reject(error);
|
|
613
|
+
}, timeoutMs);
|
|
614
|
+
if (timer.unref) timer.unref();
|
|
615
|
+
})])
|
|
616
|
+
: request;
|
|
617
|
+
try {
|
|
618
|
+
const result = await raceMcpAbort(bounded, signal, abortMessage);
|
|
619
|
+
return {
|
|
620
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
621
|
+
isError: false,
|
|
622
|
+
};
|
|
623
|
+
} finally {
|
|
624
|
+
if (timer) clearTimeout(timer);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
450
628
|
// Preserve MCP failure metadata across the object→string boundary. The
|
|
451
629
|
// session loop classifies the canonical Error: prefix as toolKind:error.
|
|
452
630
|
export function normalizeMcpToolResult(result) {
|
|
@@ -746,13 +924,30 @@ async function connectServer(name, cfg, scopeId = DEFAULT_MCP_SCOPE_ID, genAtSta
|
|
|
746
924
|
StreamableHTTPClientTransport,
|
|
747
925
|
SSEClientTransport,
|
|
748
926
|
WebSocketClientTransport,
|
|
927
|
+
ListRootsRequestSchema,
|
|
928
|
+
ToolListChangedNotificationSchema,
|
|
749
929
|
} = await loadMcpSdk();
|
|
750
930
|
if (genAtStart !== currentConnectAbortGeneration(scopeId)) {
|
|
751
931
|
// disconnectAll() ran while the SDK was loading: nothing spawned yet —
|
|
752
932
|
// abort before creating a transport/child at all.
|
|
753
933
|
throw new Error(`MCP server "${name}" connect aborted by shutdown`);
|
|
754
934
|
}
|
|
755
|
-
const
|
|
935
|
+
const projectRoot = String(cfg?._mixdogProjectRoot || cfg?.cwd || '').trim();
|
|
936
|
+
const client = new Client(
|
|
937
|
+
{ name: `mixdog-agent/${name}`, version: '1.0.0' },
|
|
938
|
+
{ capabilities: projectRoot ? { roots: { listChanged: false } } : {} },
|
|
939
|
+
);
|
|
940
|
+
if (projectRoot && ListRootsRequestSchema) {
|
|
941
|
+
client.setRequestHandler(ListRootsRequestSchema, async () => {
|
|
942
|
+
const rootPath = resolve(projectRoot);
|
|
943
|
+
return {
|
|
944
|
+
roots: [{
|
|
945
|
+
uri: pathToFileURL(rootPath).href,
|
|
946
|
+
name: basename(rootPath) || rootPath,
|
|
947
|
+
}],
|
|
948
|
+
};
|
|
949
|
+
});
|
|
950
|
+
}
|
|
756
951
|
let transport;
|
|
757
952
|
const kind = resolveMcpTransportKind(cfg);
|
|
758
953
|
// When the autoDetect port comes from a discovery advert (not the legacy
|
|
@@ -804,7 +999,7 @@ async function connectServer(name, cfg, scopeId = DEFAULT_MCP_SCOPE_ID, genAtSta
|
|
|
804
999
|
}
|
|
805
1000
|
else if (kind === 'http') {
|
|
806
1001
|
const url = normalizeMcpTransportUrl(expandEnvVars(String(cfg.url ?? '')), 'http');
|
|
807
|
-
const headers =
|
|
1002
|
+
const headers = resolveMcpHttpHeaders(cfg);
|
|
808
1003
|
const opts = (headers && Object.keys(headers).length > 0)
|
|
809
1004
|
? { requestInit: { headers } }
|
|
810
1005
|
: undefined;
|
|
@@ -815,7 +1010,7 @@ async function connectServer(name, cfg, scopeId = DEFAULT_MCP_SCOPE_ID, genAtSta
|
|
|
815
1010
|
}
|
|
816
1011
|
else if (kind === 'sse') {
|
|
817
1012
|
const url = normalizeMcpTransportUrl(expandEnvVars(String(cfg.url ?? '')), 'sse');
|
|
818
|
-
const headers =
|
|
1013
|
+
const headers = resolveMcpHttpHeaders(cfg);
|
|
819
1014
|
const opts = (headers && Object.keys(headers).length > 0)
|
|
820
1015
|
? { requestInit: { headers } }
|
|
821
1016
|
: undefined;
|
|
@@ -835,7 +1030,7 @@ async function connectServer(name, cfg, scopeId = DEFAULT_MCP_SCOPE_ID, genAtSta
|
|
|
835
1030
|
command: expandEnvVars(String(cfg.command ?? '')),
|
|
836
1031
|
args: Array.isArray(cfg.args) ? expandEnvVars(cfg.args) : cfg.args,
|
|
837
1032
|
cwd: cfg.cwd,
|
|
838
|
-
env:
|
|
1033
|
+
env: resolveMcpStdioEnvironment(cfg),
|
|
839
1034
|
stderr: cfg.stderr ?? 'pipe',
|
|
840
1035
|
});
|
|
841
1036
|
transport.stderr?.on?.('data', (chunk) => {
|
|
@@ -862,11 +1057,13 @@ async function connectServer(name, cfg, scopeId = DEFAULT_MCP_SCOPE_ID, genAtSta
|
|
|
862
1057
|
? client.getInstructions()
|
|
863
1058
|
: undefined;
|
|
864
1059
|
const instr = typeof instructionsRaw === 'string' ? instructionsRaw.trim() : '';
|
|
865
|
-
const
|
|
866
|
-
|
|
1060
|
+
const capabilities = client.getServerCapabilities?.() || {};
|
|
1061
|
+
const result = capabilities.tools ? await client.listTools() : { tools: [] };
|
|
1062
|
+
return { instructions: instr, toolsResult: result, capabilities };
|
|
867
1063
|
})();
|
|
868
1064
|
let instructions;
|
|
869
1065
|
let toolsResult;
|
|
1066
|
+
let capabilities;
|
|
870
1067
|
try {
|
|
871
1068
|
if (startupTimeoutMs > 0) {
|
|
872
1069
|
const guard = new Promise((_, rej) => {
|
|
@@ -877,9 +1074,9 @@ async function connectServer(name, cfg, scopeId = DEFAULT_MCP_SCOPE_ID, genAtSta
|
|
|
877
1074
|
rej(err);
|
|
878
1075
|
}, startupTimeoutMs);
|
|
879
1076
|
});
|
|
880
|
-
({ instructions, toolsResult } = await Promise.race([handshake, guard]));
|
|
1077
|
+
({ instructions, toolsResult, capabilities } = await Promise.race([handshake, guard]));
|
|
881
1078
|
} else {
|
|
882
|
-
({ instructions, toolsResult } = await handshake);
|
|
1079
|
+
({ instructions, toolsResult, capabilities } = await handshake);
|
|
883
1080
|
}
|
|
884
1081
|
} catch (err) {
|
|
885
1082
|
// A discovery-advert port that fails to connect is a corpse (recycled
|
|
@@ -910,12 +1107,16 @@ async function connectServer(name, cfg, scopeId = DEFAULT_MCP_SCOPE_ID, genAtSta
|
|
|
910
1107
|
catch { /* ignore */ }
|
|
911
1108
|
throw new Error(`MCP server "${name}" connect aborted by shutdown`);
|
|
912
1109
|
}
|
|
913
|
-
const
|
|
1110
|
+
const protocolTools = toolsResult.tools.map((t) => ({
|
|
914
1111
|
name: `mcp__${name}__${t.name}`,
|
|
915
1112
|
description: t.description || '',
|
|
916
1113
|
inputSchema: (t.inputSchema || { type: 'object', properties: {} }),
|
|
917
1114
|
...(t.annotations && typeof t.annotations === 'object' ? { annotations: t.annotations } : {}),
|
|
918
1115
|
}));
|
|
1116
|
+
const tools = [
|
|
1117
|
+
...protocolTools,
|
|
1118
|
+
...mcpFeatureTools(name, capabilities, protocolTools),
|
|
1119
|
+
];
|
|
919
1120
|
const toolNames = tools.map(t => t.name);
|
|
920
1121
|
const registryKey = mcpServerRegistryKey(scopeId, name);
|
|
921
1122
|
servers.set(registryKey, {
|
|
@@ -927,8 +1128,31 @@ async function connectServer(name, cfg, scopeId = DEFAULT_MCP_SCOPE_ID, genAtSta
|
|
|
927
1128
|
tools,
|
|
928
1129
|
cfg,
|
|
929
1130
|
instructions,
|
|
1131
|
+
capabilities,
|
|
930
1132
|
generation: genAtStart,
|
|
931
1133
|
});
|
|
1134
|
+
if (capabilities?.tools?.listChanged && ToolListChangedNotificationSchema) {
|
|
1135
|
+
client.setNotificationHandler(ToolListChangedNotificationSchema, async () => {
|
|
1136
|
+
const live = servers.get(registryKey);
|
|
1137
|
+
if (!live || live.client !== client) return;
|
|
1138
|
+
try {
|
|
1139
|
+
const refreshed = await client.listTools();
|
|
1140
|
+
const refreshedProtocolTools = (refreshed.tools || []).map((tool) => ({
|
|
1141
|
+
name: `mcp__${name}__${tool.name}`,
|
|
1142
|
+
description: tool.description || '',
|
|
1143
|
+
inputSchema: tool.inputSchema || { type: 'object', properties: {} },
|
|
1144
|
+
...(tool.annotations && typeof tool.annotations === 'object' ? { annotations: tool.annotations } : {}),
|
|
1145
|
+
}));
|
|
1146
|
+
live.tools = [
|
|
1147
|
+
...refreshedProtocolTools,
|
|
1148
|
+
...mcpFeatureTools(name, capabilities, refreshedProtocolTools),
|
|
1149
|
+
];
|
|
1150
|
+
_invalidateMcpToolFieldMemo();
|
|
1151
|
+
} catch (error) {
|
|
1152
|
+
mcpLog(`[mcp-client] Failed to refresh tools for "${name}": ${scrubMcpConnectionMessage(error?.message || error, cfg)}\n`);
|
|
1153
|
+
}
|
|
1154
|
+
});
|
|
1155
|
+
}
|
|
932
1156
|
_invalidateMcpToolFieldMemo();
|
|
933
1157
|
mcpLog(`[mcp] connected: ${tools.length} tools — ${toolNames.join(', ')}\n`);
|
|
934
1158
|
}
|
|
@@ -948,18 +1172,21 @@ export function _registerMcpServerForTest(scopeId, name, rawTools = [], options
|
|
|
948
1172
|
: `mcp__${name}__${String(tool?.name || '')}`,
|
|
949
1173
|
inputSchema: tool?.inputSchema || { type: 'object', properties: {} },
|
|
950
1174
|
}));
|
|
1175
|
+
const capabilities = options.capabilities || {};
|
|
951
1176
|
const server = {
|
|
952
1177
|
scopeId: normalizedScopeId,
|
|
953
1178
|
registryKey,
|
|
954
1179
|
name,
|
|
955
|
-
tools,
|
|
1180
|
+
tools: [...tools, ...mcpFeatureTools(name, capabilities, tools)],
|
|
956
1181
|
cfg: options.cfg || {},
|
|
957
1182
|
instructions: options.instructions || '',
|
|
1183
|
+
capabilities,
|
|
958
1184
|
transport: null,
|
|
959
1185
|
client: {
|
|
960
1186
|
callTool: typeof options.callTool === 'function'
|
|
961
1187
|
? options.callTool
|
|
962
1188
|
: async () => ({ content: [{ type: 'text', text: 'ok' }] }),
|
|
1189
|
+
...(options.client && typeof options.client === 'object' ? options.client : {}),
|
|
963
1190
|
// Injectable so a test can stall teardown (and therefore the shared
|
|
964
1191
|
// reconnect) without a transport or a child process.
|
|
965
1192
|
close: typeof options.close === 'function' ? options.close : async () => {},
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
_registerMcpServerForTest,
|
|
6
|
+
disconnectAll,
|
|
7
|
+
executeMcpTool,
|
|
8
|
+
getMcpServerStatus,
|
|
9
|
+
getMcpTools,
|
|
10
|
+
} from './client.mjs';
|
|
11
|
+
|
|
12
|
+
test('resources and prompts become callable MCP feature tools', async () => {
|
|
13
|
+
const scopeId = 'mcp-features-test';
|
|
14
|
+
const calls = [];
|
|
15
|
+
_registerMcpServerForTest(scopeId, 'catalog', [], {
|
|
16
|
+
capabilities: {
|
|
17
|
+
resources: {},
|
|
18
|
+
prompts: {},
|
|
19
|
+
},
|
|
20
|
+
client: {
|
|
21
|
+
listResources: async (params) => {
|
|
22
|
+
calls.push(['listResources', params]);
|
|
23
|
+
return { resources: [{ uri: 'file:///guide.md', name: 'Guide' }] };
|
|
24
|
+
},
|
|
25
|
+
listResourceTemplates: async () => ({ resourceTemplates: [] }),
|
|
26
|
+
readResource: async ({ uri }) => ({ contents: [{ uri, text: '# Guide' }] }),
|
|
27
|
+
listPrompts: async () => ({ prompts: [{ name: 'review' }] }),
|
|
28
|
+
getPrompt: async ({ name, arguments: args }) => ({
|
|
29
|
+
description: 'Review prompt',
|
|
30
|
+
messages: [{ role: 'user', content: { type: 'text', text: `${name}:${args.depth}` } }],
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
try {
|
|
35
|
+
const names = getMcpTools(scopeId).map((tool) => tool.name);
|
|
36
|
+
assert.deepEqual(names, [
|
|
37
|
+
'mcp__catalog__mixdog_list_resources',
|
|
38
|
+
'mcp__catalog__mixdog_list_resource_templates',
|
|
39
|
+
'mcp__catalog__mixdog_read_resource',
|
|
40
|
+
'mcp__catalog__mixdog_list_prompts',
|
|
41
|
+
'mcp__catalog__mixdog_get_prompt',
|
|
42
|
+
]);
|
|
43
|
+
assert.deepEqual(getMcpServerStatus(scopeId)[0]?.capabilities, {
|
|
44
|
+
tools: false,
|
|
45
|
+
prompts: true,
|
|
46
|
+
resources: true,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const resources = await executeMcpTool(
|
|
50
|
+
'mcp__catalog__mixdog_list_resources',
|
|
51
|
+
{ cursor: 'next-page' },
|
|
52
|
+
{ scopeId },
|
|
53
|
+
);
|
|
54
|
+
assert.match(resources, /file:\/\/\/guide\.md/);
|
|
55
|
+
assert.deepEqual(calls, [['listResources', { cursor: 'next-page' }]]);
|
|
56
|
+
|
|
57
|
+
const prompt = await executeMcpTool(
|
|
58
|
+
'mcp__catalog__mixdog_get_prompt',
|
|
59
|
+
{ name: 'review', arguments: { depth: 'deep' } },
|
|
60
|
+
{ scopeId },
|
|
61
|
+
);
|
|
62
|
+
assert.match(prompt, /review:deep/);
|
|
63
|
+
} finally {
|
|
64
|
+
await disconnectAll({ scopeId });
|
|
65
|
+
}
|
|
66
|
+
});
|
|
@@ -1,31 +1,13 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
|
-
import { tmpdir } from 'node:os';
|
|
4
|
-
import { join } from 'node:path';
|
|
5
2
|
import test from 'node:test';
|
|
6
3
|
|
|
7
4
|
import {
|
|
8
5
|
mcpUrlForLog,
|
|
9
6
|
normalizeMcpTransportUrl,
|
|
7
|
+
resolveMcpHttpHeaders,
|
|
8
|
+
resolveMcpStdioEnvironment,
|
|
10
9
|
scrubMcpConnectionMessage,
|
|
11
10
|
} from './client.mjs';
|
|
12
|
-
import { readProjectMcpServers } from '../../../../session-runtime/plugin-mcp.mjs';
|
|
13
|
-
|
|
14
|
-
test('configured project MCP remains enabled by default', () => {
|
|
15
|
-
const root = mkdtempSync(join(tmpdir(), 'mixdog-mcp-on-'));
|
|
16
|
-
try {
|
|
17
|
-
mkdirSync(root, { recursive: true });
|
|
18
|
-
writeFileSync(join(root, '.mcp.json'), JSON.stringify({
|
|
19
|
-
mcpServers: { demo: { command: 'node', args: ['server.mjs'] } },
|
|
20
|
-
}));
|
|
21
|
-
const configured = readProjectMcpServers(root);
|
|
22
|
-
assert.equal(configured.demo.command, 'node');
|
|
23
|
-
assert.notEqual(configured.demo.enabled, false);
|
|
24
|
-
} finally {
|
|
25
|
-
rmSync(root, { recursive: true, force: true });
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
11
|
test('MCP transport requires encryption except for loopback', () => {
|
|
30
12
|
assert.equal(
|
|
31
13
|
normalizeMcpTransportUrl('http://127.0.0.1:3000/mcp', 'http'),
|
|
@@ -62,3 +44,37 @@ test('MCP logs and failures redact URL and header credentials', () => {
|
|
|
62
44
|
assert.doesNotMatch(scrubbed, new RegExp(secret));
|
|
63
45
|
assert.match(scrubbed, /redacted/);
|
|
64
46
|
});
|
|
47
|
+
|
|
48
|
+
test('MCP HTTP auth resolves from environment without storing secret values', () => {
|
|
49
|
+
const headers = resolveMcpHttpHeaders({
|
|
50
|
+
bearer_token_env_var: 'MCP_TOKEN',
|
|
51
|
+
env_http_headers: { 'X-API-Key': 'MCP_API_KEY' },
|
|
52
|
+
headers: { 'X-Static': '${MCP_STATIC}', Authorization: 'Explicit auth' },
|
|
53
|
+
}, {
|
|
54
|
+
MCP_TOKEN: 'bearer-secret',
|
|
55
|
+
MCP_API_KEY: 'key-secret',
|
|
56
|
+
MCP_STATIC: 'expanded',
|
|
57
|
+
});
|
|
58
|
+
assert.deepEqual(headers, {
|
|
59
|
+
Authorization: 'Explicit auth',
|
|
60
|
+
'X-API-Key': 'key-secret',
|
|
61
|
+
'X-Static': 'expanded',
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('MCP stdio passthrough narrows inherited environment only when configured', () => {
|
|
66
|
+
const env = {
|
|
67
|
+
PATH: '/bin',
|
|
68
|
+
HOME: '/home/test',
|
|
69
|
+
KEEP_ME: 'kept',
|
|
70
|
+
DROP_ME: 'dropped',
|
|
71
|
+
};
|
|
72
|
+
assert.equal(resolveMcpStdioEnvironment({ env: { LOCAL: '${KEEP_ME}' } }, env).DROP_ME, 'dropped');
|
|
73
|
+
const narrowed = resolveMcpStdioEnvironment({
|
|
74
|
+
env_vars: ['KEEP_ME'],
|
|
75
|
+
env: { LOCAL: '${KEEP_ME}' },
|
|
76
|
+
}, env);
|
|
77
|
+
assert.equal(narrowed.KEEP_ME, 'kept');
|
|
78
|
+
assert.equal(narrowed.LOCAL, 'kept');
|
|
79
|
+
assert.equal(narrowed.DROP_ME, undefined);
|
|
80
|
+
});
|