@myagentroam/node 0.10.40 → 0.10.41

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/dist/claude-agent-sdk.js +8 -5
  2. package/dist/connector.js +36 -72
  3. package/dist/mcp/configuration.js +107 -0
  4. package/dist/mcp/naming.js +42 -0
  5. package/dist/runner/abstract-runner.js +3 -154
  6. package/dist/runner/claude/managed-run-controller.js +1 -4
  7. package/dist/runner/claude-code-runner.js +2 -2
  8. package/dist/runner/codex/managed-run-controller.js +6 -8
  9. package/dist/runner/codex-runner.js +2 -2
  10. package/dist/runner/mar-agent/computer-use-skill.js +6 -3
  11. package/dist/runner/mar-agent/computer-use.js +2 -1
  12. package/dist/runner/mar-agent/managed-run-controller.js +10 -11
  13. package/dist/runner/mar-agent/mcp-client-pool.js +3 -5
  14. package/dist/runner/opencode/managed-run-controller.js +1 -4
  15. package/dist/runner/opencode-runner.js +2 -2
  16. package/dist/service/continuous-run-coordinator.js +11 -18
  17. package/dist/service/{goal-mcp-server.js → goal-mcp-bridge.js} +19 -14
  18. package/dist/service/goal-mcp-stdio.js +5 -4
  19. package/dist/service/goal-runtime.js +21 -36
  20. package/dist/service/hosted-conversation-node-service.js +2 -0
  21. package/dist/service/mcp-installation-verifier.js +2 -1
  22. package/dist/service/mcp-node-operation-service.js +3 -1
  23. package/dist/service/{mail-notification-mcp-server.js → notification-mcp-bridge.js} +30 -22
  24. package/dist/service/{mail-notification-mcp-stdio.js → notification-mcp-stdio.js} +9 -8
  25. package/dist/service/restart-session-restorer.js +2 -2
  26. package/dist/service/{scheduled-task-mcp-server.js → schedule-mcp-bridge.js} +25 -19
  27. package/dist/service/{scheduled-task-mcp-stdio.js → schedule-mcp-stdio.js} +8 -7
  28. package/dist/service/session-catalog-service.js +3 -3
  29. package/package.json +4 -4
  30. /package/dist/{mcp-installation-model.js → mcp/model.js} +0 -0
@@ -1,3 +1,4 @@
1
+ import { NODE_MCP, NODE_MCP_VERSION } from './mcp/naming.js';
1
2
  import { spawnSync } from 'node:child_process';
2
3
  import { createSdkMcpServer, forkSession, getSessionInfo, getSessionMessages, listSessions, query } from '@anthropic-ai/claude-agent-sdk';
3
4
  import { tool } from '@anthropic-ai/claude-agent-sdk';
@@ -44,6 +45,8 @@ export class ClaudeAgentSdkAdapter {
44
45
  };
45
46
  }
46
47
  startWithPrompt(input, prompt, channelEnabled = false) {
48
+ if (channelEnabled && Object.hasOwn(input.mcpServers ?? {}, NODE_MCP.channel.name))
49
+ throw Object.assign(new Error('MCP_SERVER_NAME_CONFLICT: A user MCP conflicts with the managed Channel.'), { code: 'MCP_SERVER_NAME_CONFLICT' });
47
50
  const abortController = new AbortController();
48
51
  const stream = this.sdk.query({
49
52
  prompt,
@@ -80,7 +83,7 @@ export class ClaudeAgentSdkAdapter {
80
83
  ? {
81
84
  mcpServers: {
82
85
  ...(input.mcpServers ?? {}),
83
- ...(channelEnabled ? { myagentroam: channelMcpServer(input) } : {})
86
+ ...(channelEnabled ? { [NODE_MCP.channel.name]: channelMcpServer(input) } : {})
84
87
  }
85
88
  }
86
89
  : {})
@@ -172,12 +175,12 @@ function claudePermissionMode(access) {
172
175
  }
173
176
  function channelMcpServer(input) {
174
177
  return createSdkMcpServer({
175
- name: 'myagentroam',
176
- version: '0.1.4',
177
- instructions: 'Use the myagentroam_channel_reply tool only to send a concise reply to the Admin Channel.',
178
+ name: NODE_MCP.channel.name,
179
+ version: NODE_MCP_VERSION,
180
+ instructions: 'Use the reply tool from the mar_channel MCP server only to send a concise reply to the Admin Channel.',
178
181
  alwaysLoad: true,
179
182
  tools: [
180
- tool('channel_reply', 'Send one text reply to the authorized MyAgentRoam Admin Channel.', { content: z.string().min(1).max(20_000) }, async ({ content }) => {
183
+ tool(NODE_MCP.channel.tools.reply, 'Send one text reply to the authorized MyAgentRoam Admin Channel.', { content: z.string().min(1).max(20_000) }, async ({ content }) => {
181
184
  input.onChannelReply?.(content);
182
185
  return { content: [{ type: 'text', text: 'Channel reply delivered.' }] };
183
186
  }, { alwaysLoad: true })
package/dist/connector.js CHANGED
@@ -12,7 +12,8 @@ import { TerminalManager } from './terminal.js';
12
12
  import { NodeRuntimeState } from './runtime-state.js';
13
13
  import { CommandStateStore } from './runner-command-engine.js';
14
14
  import { goalUnrestrictedAccess, GoalRuntime } from './service/goal-runtime.js';
15
- import { GoalMcpServer } from './service/goal-mcp-server.js';
15
+ import { GoalMcpBridge } from './service/goal-mcp-bridge.js';
16
+ import { NODE_MCP } from './mcp/naming.js';
16
17
  import { RunnerUsageReader } from './runner-usage.js';
17
18
  import { ClaudeCodeRunner } from './runner/claude-code-runner.js';
18
19
  import { ClaudeManagedRunController } from './runner/claude/managed-run-controller.js';
@@ -71,9 +72,9 @@ import { ScheduledSessionStore } from './service/scheduled-session-store.js';
71
72
  import { RunAuthorizationControl } from './service/run-authorization-control.js';
72
73
  import { PlannedRestartHandoff } from './service/planned-restart-handoff.js';
73
74
  import { RestartSessionRestorer } from './service/restart-session-restorer.js';
74
- import { ScheduledTaskMcpServer } from './service/scheduled-task-mcp-server.js';
75
+ import { ScheduleMcpBridge } from './service/schedule-mcp-bridge.js';
75
76
  import { MailNotificationControl } from './service/mail-notification-control.js';
76
- import { MailNotificationMcpServer, resolveMailNotificationCapability } from './service/mail-notification-mcp-server.js';
77
+ import { NotificationMcpBridge, resolveNotificationCapability } from './service/notification-mcp-bridge.js';
77
78
  import { SessionCatalogService } from './service/session-catalog-service.js';
78
79
  import { SessionCommandService } from './service/session-command-service.js';
79
80
  import { SessionPresentationService } from './service/session-presentation-service.js';
@@ -287,7 +288,7 @@ export class NodeConnector {
287
288
  goalExecutionProgress = new Set();
288
289
  nativeGoalBindings = new Map();
289
290
  sessionScheduleService;
290
- goalMcpServer = new GoalMcpServer(this.goalRuntime, (sessionId) => {
291
+ goalMcpBridge = new GoalMcpBridge(this.goalRuntime, (sessionId) => {
291
292
  const goal = this.goalRuntime.get(sessionId);
292
293
  const session = this.runtime.getAgentSession(sessionId);
293
294
  if (goal === undefined || session === undefined)
@@ -299,7 +300,7 @@ export class NodeConnector {
299
300
  }
300
301
  this.publishGoalState(session, goal);
301
302
  });
302
- scheduledTaskMcpServer = new ScheduledTaskMcpServer(() => {
303
+ scheduleMcpBridge = new ScheduleMcpBridge(() => {
303
304
  if (this.database === undefined)
304
305
  throw new Error('NODE_DATABASE_UNAVAILABLE');
305
306
  return new ScheduledSessionStore(this.database.raw);
@@ -315,7 +316,7 @@ export class NodeConnector {
315
316
  throw new Error('SCHEDULE_SERVICE_UNAVAILABLE');
316
317
  return this.sessionScheduleService.createFromMcp(input);
317
318
  }, (sessionId) => this.runtime.getAgentSession(sessionId)?.externalSessionId ?? sessionId);
318
- mailNotificationMcpServer = new MailNotificationMcpServer((capability) => resolveMailNotificationCapability(this.runtime, capability), (request) => this.mailNotificationControl.request(request), {
319
+ notificationMcpBridge = new NotificationMcpBridge((capability) => resolveNotificationCapability(this.runtime, capability), (request) => this.mailNotificationControl.request(request), {
319
320
  supported: () => this.mailNotificationControl.supportsChannels(),
320
321
  send: (request) => this.mailNotificationControl.notification(request)
321
322
  });
@@ -326,7 +327,7 @@ export class NodeConnector {
326
327
  workspaceWatchService = new WorkspaceWatchService(this.workspaceState, (workspaceId) => this.workspaceSessionService.ensureWatch(workspaceId), (workspaceId) => this.workspaceSessionService.stopWatch(workspaceId), (workspaceId) => this.workspaceWorkbenchService?.releaseWorkspace(workspaceId), (watch) => this.workspaceWorkbenchService?.cancelFilePrewarm(watch));
327
328
  workspaceSessionService = new WorkspaceSessionService(this.workspaceState, {
328
329
  discover: (workspaceId, onRunnerDiscovered) => this.discoverWorkspaceSessions(workspaceId, onRunnerDiscovered),
329
- listPage: (workspaceId) => this.executeNodeOperation('session.list', { workspaceId, limit: 20 }),
330
+ listPage: (workspaceId) => this.executeNodeOperation('session.list', { workspaceId, limit: 30 }),
330
331
  active: (workspaceId) => this.workspaceWatchService.topicActive(workspaceId, 'sessions'),
331
332
  invalidate: (workspaceId) => void this.workspaceWorkbenchService?.invalidateTopic(workspaceId, 'sessions')
332
333
  });
@@ -469,12 +470,15 @@ export class NodeConnector {
469
470
  releaseExecution: (executionId) => {
470
471
  this.modelUsage.release(executionId);
471
472
  this.goalExecutionProgress.delete(executionId);
472
- this.goalMcpServer.revoke(executionId);
473
- this.scheduledTaskMcpServer.revoke(executionId);
474
- this.mailNotificationMcpServer.revoke(executionId);
473
+ this.revokeExecutionMcps(executionId);
475
474
  this.continuousRunCoordinator.remove(executionId);
476
475
  }
477
476
  });
477
+ revokeExecutionMcps(executionId) {
478
+ this.goalMcpBridge.revoke(executionId);
479
+ this.scheduleMcpBridge.revoke(executionId);
480
+ this.notificationMcpBridge.revoke(executionId);
481
+ }
478
482
  sessionWatchSlots = new SessionWatchSlotCoordinator();
479
483
  nativeSessionWatchService = new NativeSessionWatchService({
480
484
  resolve: async (sessionId) => this.runtime.getAgentSession(sessionId) ??
@@ -856,40 +860,25 @@ export class NodeConnector {
856
860
  code: safeErrorCode(error, 'GOAL_EXECUTION_START_FAILED')
857
861
  });
858
862
  },
863
+ revokeExecutionMcps: (executionId) => this.revokeExecutionMcps(executionId),
859
864
  goalMcp: async (input) => {
860
865
  if (input.goal === undefined)
861
866
  throw new Error('GOAL_NOT_ACTIVE');
862
- const resolved = await this.goalMcpServer.installation({
867
+ return this.goalMcpBridge.prepare({
863
868
  executionId: input.runId,
864
869
  sessionId: input.sessionId,
865
870
  goalId: input.goal.id,
866
871
  revision: input.goal.revision
867
872
  });
868
- return {
869
- installation: {
870
- targetKind: 'WORKSPACE',
871
- targetId: input.workspaceId,
872
- catalogEntryId: 'mar-internal-goal',
873
- serverName: 'myagentroam_goal',
874
- displayName: 'Goal',
875
- version: '1.0.0',
876
- enabled: true,
877
- runtime: { transport: 'HTTP', url: 'https://127.0.0.1.invalid/mcp', headers: {} },
878
- configuration: resolved.installation,
879
- installedAt: Date.now(),
880
- ...resolved.installation
881
- },
882
- secrets: resolved.secrets
883
- };
884
873
  },
885
- scheduledMcp: async (input) => {
874
+ scheduleMcp: async (input) => {
886
875
  if (this.database === undefined)
887
876
  return undefined;
888
877
  const task = new ScheduledSessionStore(this.database.raw).get(input.sessionId);
889
878
  const run = this.runtime.getRun(input.runId);
890
879
  if (run?.initiatedByUserId === undefined)
891
880
  return undefined;
892
- const resolved = await this.scheduledTaskMcpServer.installation({
881
+ return this.scheduleMcpBridge.prepare({
893
882
  executionId: input.runId,
894
883
  sessionId: input.sessionId,
895
884
  workspaceId: input.workspaceId,
@@ -897,50 +886,18 @@ export class NodeConnector {
897
886
  actorUserId: run.initiatedByUserId,
898
887
  readonly: run.source === 'SCHEDULED'
899
888
  });
900
- return {
901
- installation: {
902
- targetKind: 'WORKSPACE',
903
- targetId: input.workspaceId,
904
- catalogEntryId: 'mar-internal-schedule',
905
- serverName: 'mar_schedule',
906
- displayName: 'MAR scheduled task',
907
- version: '1.0.0',
908
- enabled: true,
909
- runtime: { transport: 'HTTP', url: 'https://127.0.0.1.invalid/mcp', headers: {} },
910
- configuration: resolved.installation,
911
- installedAt: Date.now(),
912
- ...resolved.installation
913
- },
914
- secrets: resolved.secrets
915
- };
916
889
  },
917
- mailMcp: async (input) => {
890
+ notificationMcp: async (input) => {
918
891
  const run = this.runtime.getRun(input.runId);
919
892
  if (run?.initiatedByUserId === undefined)
920
893
  return undefined;
921
- const resolved = await this.mailNotificationMcpServer.installation({
894
+ return this.notificationMcpBridge.prepare({
922
895
  executionId: input.runId,
923
896
  sessionId: input.sessionId,
924
897
  workspaceId: input.workspaceId,
925
898
  userId: run.initiatedByUserId,
926
899
  source: run.source === 'SCHEDULED' ? 'SCHEDULED' : 'MANUAL'
927
900
  });
928
- return {
929
- installation: {
930
- targetKind: 'WORKSPACE',
931
- targetId: input.workspaceId,
932
- catalogEntryId: 'mar-internal-mail',
933
- serverName: 'mar_notification',
934
- displayName: 'MAR notification',
935
- version: '1.0.0',
936
- enabled: true,
937
- runtime: { transport: 'HTTP', url: 'https://127.0.0.1.invalid/mcp', headers: {} },
938
- configuration: resolved.installation,
939
- installedAt: Date.now(),
940
- ...resolved.installation
941
- },
942
- secrets: resolved.secrets
943
- };
944
901
  }
945
902
  });
946
903
  this.workspaceQueueWorkbenchService = new WorkspaceQueueWorkbenchService({
@@ -974,7 +931,7 @@ export class NodeConnector {
974
931
  migrateGoal: (previousId, nextId) => {
975
932
  this.runAttachmentService.migrateSession(previousId, nextId);
976
933
  this.goalRuntime.migrateSession(previousId, nextId);
977
- this.goalMcpServer.migrateSession(previousId, nextId);
934
+ this.goalMcpBridge.migrateSession(previousId, nextId);
978
935
  const retry = this.goalRetryTimers.get(previousId);
979
936
  if (retry !== undefined) {
980
937
  this.goalRetryTimers.delete(previousId);
@@ -1368,7 +1325,7 @@ export class NodeConnector {
1368
1325
  this.scheduledRunAuthorization.disconnected();
1369
1326
  this.restartAuthorization.disconnected();
1370
1327
  this.mailNotificationControl.disconnected();
1371
- this.mailNotificationMcpServer.revokeAll();
1328
+ this.notificationMcpBridge.revokeAll();
1372
1329
  this.terminalChannel.disconnect();
1373
1330
  if (code === 4000 && reason === 'NODE_CONNECTION_REPLACED')
1374
1331
  nodeLog('node.connection.replaced', {
@@ -1562,7 +1519,7 @@ export class NodeConnector {
1562
1519
  files: this.workspaceFileService,
1563
1520
  uploads: this.workspaceUploadService,
1564
1521
  changes: this.workspaceChangeService,
1565
- listSessions: (workspaceId) => this.executeNodeOperation('session.list', { workspaceId, limit: 20 }),
1522
+ listSessions: (workspaceId) => this.executeNodeOperation('session.list', { workspaceId, limit: 30 }),
1566
1523
  invalidate: (workspaceId) => {
1567
1524
  this.workspaceFileService.dispose(workspaceId);
1568
1525
  this.workspaceWorkbenchService.invalidateWorkspaceFiles(workspaceId);
@@ -2267,8 +2224,8 @@ export class NodeConnector {
2267
2224
  this.workspaceSessionService.clear();
2268
2225
  this.workspaceChangeService.clear();
2269
2226
  const runnerDrain = this.runners.stop();
2270
- const goalMcpDrain = this.goalMcpServer.close();
2271
- const scheduledMcpDrain = this.scheduledTaskMcpServer.close();
2227
+ const goalMcpDrain = this.goalMcpBridge.close();
2228
+ const scheduleMcpDrain = this.scheduleMcpBridge.close();
2272
2229
  // close() transitions metadata before awaiting process-tree termination,
2273
2230
  // so the lifecycle is persisted before this synchronous shutdown returns.
2274
2231
  const terminalDrain = this.terminalManager.shutdown();
@@ -2279,7 +2236,7 @@ export class NodeConnector {
2279
2236
  this.scheduledRunAuthorization.close();
2280
2237
  this.restartAuthorization.close();
2281
2238
  this.mailNotificationControl.close();
2282
- const mailMcpDrain = this.mailNotificationMcpServer.close();
2239
+ const notificationMcpDrain = this.notificationMcpBridge.close();
2283
2240
  const webToolDrain = this.webToolService.close();
2284
2241
  this.directTransferService?.dispose();
2285
2242
  this.directTransferService = undefined;
@@ -2292,8 +2249,8 @@ export class NodeConnector {
2292
2249
  terminalDrain,
2293
2250
  uploadDrain,
2294
2251
  goalMcpDrain,
2295
- scheduledMcpDrain,
2296
- mailMcpDrain,
2252
+ scheduleMcpDrain,
2253
+ notificationMcpDrain,
2297
2254
  webToolDrain
2298
2255
  ]);
2299
2256
  return (async () => {
@@ -2521,6 +2478,11 @@ export class NodeConnector {
2521
2478
  this.controlChannel.sendEnvelope(envelope);
2522
2479
  }
2523
2480
  }
2481
+ const goalReadToolNames = new Set([
2482
+ `${NODE_MCP.goal.name}.${NODE_MCP.goal.tools.get}`,
2483
+ `${NODE_MCP.goal.name}_${NODE_MCP.goal.tools.get}`,
2484
+ `mcp__${NODE_MCP.goal.name}__${NODE_MCP.goal.tools.get}`
2485
+ ]);
2524
2486
  function isGoalProgressItem(item) {
2525
2487
  if (!isPlainRecord(item) || typeof item.kind !== 'string')
2526
2488
  return false;
@@ -2529,5 +2491,7 @@ function isGoalProgressItem(item) {
2529
2491
  if (item.kind !== 'tool_call' || !isPlainRecord(item.payload))
2530
2492
  return false;
2531
2493
  const toolName = item.payload.toolName;
2532
- return typeof toolName === 'string' && !toolName.endsWith('mar_get_goal');
2494
+ return (typeof toolName === 'string' &&
2495
+ !goalReadToolNames.has(toolName) &&
2496
+ !(item.payload.namespace === NODE_MCP.goal.name && toolName === NODE_MCP.goal.tools.get));
2533
2497
  }
@@ -0,0 +1,107 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { mcpRuntimeDescriptorSchema } from '@myagentroam/protocol';
3
+ import { nodeExecutionEnvironment } from '../node-execution-environment.js';
4
+ import { canonicalJson } from '../util/canonical-json.js';
5
+ import { assertMcpServerNames } from './naming.js';
6
+ export function resolveMcpServers(raw, secrets, executionServers = []) {
7
+ const installations = Array.isArray(raw) ? raw : [];
8
+ const marctlEnvironment = {
9
+ MAR_USERNAME: secrets['MAR_USERNAME'] ?? '',
10
+ ...(secrets['MAR_CTL_SERVER_URL'] === undefined
11
+ ? {}
12
+ : { MAR_CTL_SERVER_URL: secrets['MAR_CTL_SERVER_URL'] }),
13
+ ...(secrets['MAR_CTL_USER_TOKEN'] === undefined
14
+ ? {}
15
+ : { MAR_CTL_USER_TOKEN: secrets['MAR_CTL_USER_TOKEN'] }),
16
+ MAR_CTL_CONTEXT_REQUIRED: '1'
17
+ };
18
+ const mcpEnvironment = (provided) => nodeExecutionEnvironment({ ...provided, ...marctlEnvironment }, { PATH: process.env['PATH'], Path: process.env['Path'] });
19
+ const result = [];
20
+ for (const value of installations) {
21
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
22
+ continue;
23
+ const record = value;
24
+ if (typeof record['serverName'] !== 'string' || record['enabled'] !== true)
25
+ continue;
26
+ const runtime = mcpRuntimeDescriptorSchema.safeParse(record['runtime']);
27
+ if (!runtime.success)
28
+ continue;
29
+ const configuration = isRecord(record['configuration']) ? record['configuration'] : {};
30
+ const sourceIdentity = createHash('sha256')
31
+ .update(canonicalJson({
32
+ id: record['id'] ?? null,
33
+ name: record['serverName'],
34
+ runtime: runtime.data,
35
+ configuration
36
+ }))
37
+ .digest('hex');
38
+ const authorizationIdentity = secrets['MAR_USERNAME']?.trim() || undefined;
39
+ const resolveReferences = (candidate) => {
40
+ if (!isRecord(candidate))
41
+ return {};
42
+ const result = {};
43
+ for (const [name, reference] of Object.entries(candidate)) {
44
+ if (!isRecord(reference) || typeof reference['secretName'] !== 'string')
45
+ return undefined;
46
+ const secret = secrets[reference['secretName']];
47
+ if (secret === undefined)
48
+ return undefined;
49
+ result[name] = secret;
50
+ }
51
+ return result;
52
+ };
53
+ if (runtime.data.transport === 'STDIO') {
54
+ const runtimeEnvironment = resolveReferences(runtime.data.environment);
55
+ const configuredEnvironment = resolveReferences(configuration['environment']);
56
+ if (runtimeEnvironment === undefined || configuredEnvironment === undefined)
57
+ continue;
58
+ result.push({
59
+ name: record['serverName'],
60
+ origin: 'USER',
61
+ sourceIdentity,
62
+ ...(authorizationIdentity === undefined ? {} : { authorizationIdentity }),
63
+ transport: 'STDIO',
64
+ command: runtime.data.command,
65
+ args: [
66
+ ...runtime.data.args,
67
+ ...(Array.isArray(configuration['arguments'])
68
+ ? configuration['arguments'].filter((item) => typeof item === 'string')
69
+ : [])
70
+ ],
71
+ environment: mcpEnvironment({
72
+ ...runtimeEnvironment,
73
+ ...configuredEnvironment
74
+ })
75
+ });
76
+ }
77
+ else {
78
+ const runtimeHeaders = resolveReferences(runtime.data.headers);
79
+ const configuredHeaders = resolveReferences(configuration['headers']);
80
+ if (runtimeHeaders === undefined || configuredHeaders === undefined)
81
+ continue;
82
+ result.push({
83
+ name: record['serverName'],
84
+ origin: 'USER',
85
+ sourceIdentity,
86
+ ...(authorizationIdentity === undefined ? {} : { authorizationIdentity }),
87
+ transport: 'HTTP',
88
+ url: runtime.data.url,
89
+ headers: {
90
+ ...runtimeHeaders,
91
+ ...configuredHeaders
92
+ }
93
+ });
94
+ }
95
+ }
96
+ const servers = [
97
+ ...result,
98
+ ...executionServers.map((server) => server.transport === 'STDIO'
99
+ ? { ...server, environment: mcpEnvironment(server.environment ?? {}) }
100
+ : server)
101
+ ];
102
+ assertMcpServerNames(servers);
103
+ return servers;
104
+ }
105
+ function isRecord(value) {
106
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
107
+ }
@@ -0,0 +1,42 @@
1
+ import { createRequire } from 'node:module';
2
+ export const NODE_MCP = {
3
+ goal: { name: 'mar_goal', tools: { get: 'get_goal', update: 'update_goal' } },
4
+ schedule: {
5
+ name: 'mar_schedule',
6
+ tools: {
7
+ get: 'get_scheduled_task',
8
+ create: 'create_scheduled_task',
9
+ update: 'update_scheduled_task'
10
+ }
11
+ },
12
+ notification: {
13
+ name: 'mar_notification',
14
+ tools: { send: 'send_notification', email: 'send_email_notification' }
15
+ },
16
+ computer: { name: 'mar_computer', tools: { exec: 'exec' } },
17
+ channel: { name: 'mar_channel', tools: { reply: 'reply' } }
18
+ };
19
+ const { version } = createRequire(import.meta.url)('../../package.json');
20
+ export const NODE_MCP_VERSION = version;
21
+ export const NODE_MCP_CLIENT_INFO = { name: 'mar-node', version };
22
+ export const NODE_COMPUTER_TOOL_NAME = [
23
+ 'mcp',
24
+ NODE_MCP.computer.name,
25
+ NODE_MCP.computer.tools.exec
26
+ ].join('__');
27
+ const reservedNames = new Set(Object.values(NODE_MCP).map(({ name }) => name));
28
+ export function mcpNameAlias(value) {
29
+ return value.replace(/[^a-zA-Z0-9_-]/gu, '_').slice(0, 64);
30
+ }
31
+ /** Validate the final effective set, including trusted execution-only servers. */
32
+ export function assertMcpServerNames(servers) {
33
+ const seen = new Set();
34
+ for (const server of servers) {
35
+ const alias = mcpNameAlias(server.name);
36
+ if (server.origin !== 'BUILTIN' && reservedNames.has(alias))
37
+ throw Object.assign(new Error('MCP_RESERVED_NAMESPACE: The MCP server name is reserved for Node.'), { code: 'MCP_RESERVED_NAMESPACE' });
38
+ if (seen.has(alias))
39
+ throw Object.assign(new Error('MCP_SERVER_NAME_CONFLICT: Multiple MCP servers have the same exposed name.'), { code: 'MCP_SERVER_NAME_CONFLICT' });
40
+ seen.add(alias);
41
+ }
42
+ }
@@ -1,7 +1,4 @@
1
- import { createHash } from 'node:crypto';
2
- import { mcpRuntimeDescriptorSchema } from '@myagentroam/protocol';
3
- import { nodeExecutionEnvironment } from '../node-execution-environment.js';
4
- import { canonicalJson } from '../util/canonical-json.js';
1
+ import { resolveMcpServers } from '../mcp/configuration.js';
5
2
  /**
6
3
  * Stable lifecycle boundary for every Node Runner implementation.
7
4
  *
@@ -40,8 +37,8 @@ export class AbstractRunner {
40
37
  void collaborationMode;
41
38
  return input;
42
39
  }
43
- mcpConfiguration(rawInstallations, secretEnvironment) {
44
- return resolvedMcpServers(rawInstallations, secretEnvironment);
40
+ mcpConfiguration(rawInstallations, secretEnvironment, executionServers = []) {
41
+ return resolveMcpServers(rawInstallations, secretEnvironment, executionServers);
45
42
  }
46
43
  channelToken(sessionId) {
47
44
  void sessionId;
@@ -178,154 +175,6 @@ export class AbstractRunner {
178
175
  return this.commandControl.execute(session, command, input, context);
179
176
  }
180
177
  }
181
- function resolvedMcpServers(raw, secrets) {
182
- if (!Array.isArray(raw))
183
- return [];
184
- const marctlEnvironment = {
185
- MAR_USERNAME: secrets['MAR_USERNAME'] ?? '',
186
- ...(secrets['MAR_CTL_SERVER_URL'] === undefined
187
- ? {}
188
- : { MAR_CTL_SERVER_URL: secrets['MAR_CTL_SERVER_URL'] }),
189
- ...(secrets['MAR_CTL_USER_TOKEN'] === undefined
190
- ? {}
191
- : { MAR_CTL_USER_TOKEN: secrets['MAR_CTL_USER_TOKEN'] }),
192
- MAR_CTL_CONTEXT_REQUIRED: '1'
193
- };
194
- const mcpEnvironment = (provided) => nodeExecutionEnvironment({ ...provided, ...marctlEnvironment }, { PATH: process.env['PATH'], Path: process.env['Path'] });
195
- const result = [];
196
- for (const value of raw) {
197
- if (value === null || typeof value !== 'object' || Array.isArray(value))
198
- continue;
199
- const record = value;
200
- if (typeof record['serverName'] !== 'string' || record['enabled'] !== true)
201
- continue;
202
- if ((record['__marGoal'] === true ||
203
- record['__marScheduled'] === true ||
204
- record['__marMail'] === true) &&
205
- typeof record['command'] === 'string' &&
206
- Array.isArray(record['args']) &&
207
- record['args'].every((value) => typeof value === 'string') &&
208
- typeof record['endpoint'] === 'string' &&
209
- typeof record['secretName'] === 'string' &&
210
- secrets[record['secretName']] !== undefined) {
211
- result.push({
212
- name: record['serverName'],
213
- lifecycle: 'EXECUTION',
214
- origin: 'BUILTIN',
215
- allowedToolCallers: record['__marGoal'] === true
216
- ? {
217
- mar_get_goal: ['direct', 'programmatic'],
218
- mar_update_goal: ['direct', 'programmatic']
219
- }
220
- : record['__marScheduled'] === true
221
- ? {
222
- mar_get_scheduled_task: ['direct', 'programmatic'],
223
- mar_create_scheduled_task: ['direct', 'programmatic'],
224
- mar_update_scheduled_task: ['direct', 'programmatic']
225
- }
226
- : {
227
- mar_send_email_notification: ['direct', 'programmatic'],
228
- ...(record['notificationChannels'] === true
229
- ? { mar_send_notification: ['direct', 'programmatic'] }
230
- : {})
231
- },
232
- transport: 'STDIO',
233
- command: record['command'],
234
- args: record['args'],
235
- environment: mcpEnvironment(record['__marGoal'] === true
236
- ? {
237
- MAR_GOAL_MCP_ENDPOINT: record['endpoint'],
238
- MAR_GOAL_MCP_TOKEN: secrets[record['secretName']]
239
- }
240
- : record['__marScheduled'] === true
241
- ? {
242
- MAR_SCHEDULE_MCP_ENDPOINT: record['endpoint'],
243
- MAR_SCHEDULE_MCP_TOKEN: secrets[record['secretName']],
244
- MAR_SCHEDULE_MCP_READ_ONLY: record['readOnly'] === true ? '1' : '0'
245
- }
246
- : {
247
- MAR_MAIL_MCP_ENDPOINT: record['endpoint'],
248
- MAR_MAIL_MCP_TOKEN: secrets[record['secretName']],
249
- MAR_NOTIFICATION_CHANNELS: record['notificationChannels'] === true ? '1' : '0'
250
- })
251
- });
252
- continue;
253
- }
254
- const runtime = mcpRuntimeDescriptorSchema.safeParse(record['runtime']);
255
- if (!runtime.success)
256
- continue;
257
- const configuration = isRecord(record['configuration']) ? record['configuration'] : {};
258
- const sourceIdentity = createHash('sha256')
259
- .update(canonicalJson({
260
- id: record['id'] ?? null,
261
- name: record['serverName'],
262
- runtime: runtime.data,
263
- configuration
264
- }))
265
- .digest('hex');
266
- const authorizationIdentity = secrets['MAR_USERNAME']?.trim() || undefined;
267
- const resolveReferences = (candidate) => {
268
- if (!isRecord(candidate))
269
- return {};
270
- const result = {};
271
- for (const [name, reference] of Object.entries(candidate)) {
272
- if (!isRecord(reference) || typeof reference['secretName'] !== 'string')
273
- return undefined;
274
- const secret = secrets[reference['secretName']];
275
- if (secret === undefined)
276
- return undefined;
277
- result[name] = secret;
278
- }
279
- return result;
280
- };
281
- if (runtime.data.transport === 'STDIO') {
282
- const runtimeEnvironment = resolveReferences(runtime.data.environment);
283
- const configuredEnvironment = resolveReferences(configuration['environment']);
284
- if (runtimeEnvironment === undefined || configuredEnvironment === undefined)
285
- continue;
286
- result.push({
287
- name: record['serverName'],
288
- origin: 'USER',
289
- sourceIdentity,
290
- ...(authorizationIdentity === undefined ? {} : { authorizationIdentity }),
291
- transport: 'STDIO',
292
- command: runtime.data.command,
293
- args: [
294
- ...runtime.data.args,
295
- ...(Array.isArray(configuration['arguments'])
296
- ? configuration['arguments'].filter((item) => typeof item === 'string')
297
- : [])
298
- ],
299
- environment: mcpEnvironment({
300
- ...runtimeEnvironment,
301
- ...configuredEnvironment
302
- })
303
- });
304
- }
305
- else {
306
- const runtimeHeaders = resolveReferences(runtime.data.headers);
307
- const configuredHeaders = resolveReferences(configuration['headers']);
308
- if (runtimeHeaders === undefined || configuredHeaders === undefined)
309
- continue;
310
- result.push({
311
- name: record['serverName'],
312
- origin: 'USER',
313
- sourceIdentity,
314
- ...(authorizationIdentity === undefined ? {} : { authorizationIdentity }),
315
- transport: 'HTTP',
316
- url: runtime.data.url,
317
- headers: {
318
- ...runtimeHeaders,
319
- ...configuredHeaders
320
- }
321
- });
322
- }
323
- }
324
- return result;
325
- }
326
- function isRecord(value) {
327
- return value !== null && typeof value === 'object' && !Array.isArray(value);
328
- }
329
178
  export class RunnerCapabilityError extends Error {
330
179
  runner;
331
180
  capability;
@@ -88,11 +88,9 @@ export class ClaudeManagedRunController {
88
88
  : undefined;
89
89
  let attachments;
90
90
  let secretEnvironment;
91
- let mcpSecretEnvironment;
92
91
  try {
93
92
  attachments = this.host.parseAttachments(payload.attachments);
94
93
  secretEnvironment = parseSecretEnvironment(payload.secretEnvironment);
95
- mcpSecretEnvironment = parseSecretEnvironment(payload.mcpSecretEnvironment);
96
94
  }
97
95
  catch (error) {
98
96
  this.host.emit(runId, 'run.rejected', { code: safeErrorCode(error, 'MESSAGE_ATTACHMENTS_INVALID') }, 'FAILED');
@@ -209,9 +207,8 @@ export class ClaudeManagedRunController {
209
207
  : {}),
210
208
  mcpServers: this.runner.mcpConfiguration(payload.mcpInstallations, {
211
209
  ...secretEnvironment,
212
- ...mcpSecretEnvironment,
213
210
  MAR_USERNAME: secretEnvironment['MAR_USERNAME'] ?? ''
214
- }),
211
+ }, payload.executionMcpServers),
215
212
  ...(attachments.length === 0
216
213
  ? {}
217
214
  : {
@@ -45,8 +45,8 @@ export class ClaudeCodeRunner extends AbstractRunner {
45
45
  access: 'default'
46
46
  };
47
47
  }
48
- mcpConfiguration(raw, secrets) {
49
- const servers = super.mcpConfiguration(raw, secrets);
48
+ mcpConfiguration(raw, secrets, executionServers = []) {
49
+ const servers = super.mcpConfiguration(raw, secrets, executionServers);
50
50
  return Object.fromEntries(servers.map((server) => [
51
51
  server.name,
52
52
  server.transport === 'STDIO'