@myagentroam/node 0.10.39 → 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 } from '../../mcp/naming.js';
1
2
  import { nodeExecutionEnvironment } from '../../node-execution-environment.js';
2
3
  import { extractId } from '../../util/runner-native-session-parsers.js';
3
4
  import { parseSecretEnvironment } from '../../util/secret-environment.js';
@@ -204,8 +205,8 @@ export class CodexManagedRunController {
204
205
  }
205
206
  if (notification.requestId !== undefined &&
206
207
  notification.method === 'mcpServer/elicitation/request' &&
207
- ((activeRun.goalMcp === true && params.serverName === 'myagentroam_goal') ||
208
- (activeRun.scheduleMcp === true && params.serverName === 'mar_schedule')) &&
208
+ ((activeRun.goalMcp === true && params.serverName === NODE_MCP.goal.name) ||
209
+ (activeRun.scheduleMcp === true && params.serverName === NODE_MCP.schedule.name)) &&
209
210
  isPlainRecord(params._meta) &&
210
211
  params._meta.codex_approval_kind === 'mcp_tool_call') {
211
212
  this.runner.respond(notification.requestId, {
@@ -559,11 +560,9 @@ export class CodexManagedRunController {
559
560
  const { runId, sessionId, input, cwd } = payload;
560
561
  let attachments;
561
562
  let secretEnvironment;
562
- let mcpSecretEnvironment;
563
563
  try {
564
564
  attachments = this.host.parseAttachments(payload.attachments);
565
565
  secretEnvironment = parseSecretEnvironment(payload.secretEnvironment);
566
- mcpSecretEnvironment = parseSecretEnvironment(payload.mcpSecretEnvironment);
567
566
  }
568
567
  catch (error) {
569
568
  this.emitError(runId, error, 'CODEX_RUN_FAILED');
@@ -615,9 +614,8 @@ export class CodexManagedRunController {
615
614
  : {}),
616
615
  mcpServers: this.runner.mcpConfiguration(payload.mcpInstallations, {
617
616
  ...secretEnvironment,
618
- ...mcpSecretEnvironment,
619
617
  MAR_USERNAME: secretEnvironment['MAR_USERNAME'] ?? ''
620
- })
618
+ }, payload.executionMcpServers)
621
619
  }, attachments);
622
620
  }
623
621
  async run(payload, attachments) {
@@ -733,10 +731,10 @@ export class CodexManagedRunController {
733
731
  }
734
732
  }
735
733
  function hasGoalMcp(value) {
736
- return isPlainRecord(value) && isPlainRecord(value.myagentroam_goal);
734
+ return isPlainRecord(value) && isPlainRecord(value[NODE_MCP.goal.name]);
737
735
  }
738
736
  function hasScheduleMcp(value) {
739
- return isPlainRecord(value) && isPlainRecord(value.mar_schedule);
737
+ return isPlainRecord(value) && isPlainRecord(value[NODE_MCP.schedule.name]);
740
738
  }
741
739
  function codexOutputTokens(value) {
742
740
  if (!isPlainRecord(value) ||
@@ -215,8 +215,8 @@ export class CodexRunner extends AbstractRunner {
215
215
  serviceTier() {
216
216
  return this.fastEnabled ? 'fast' : undefined;
217
217
  }
218
- mcpConfiguration(raw, secrets) {
219
- const servers = super.mcpConfiguration(raw, secrets);
218
+ mcpConfiguration(raw, secrets, executionServers = []) {
219
+ const servers = super.mcpConfiguration(raw, secrets, executionServers);
220
220
  return Object.fromEntries(servers.map((server) => [
221
221
  server.name,
222
222
  server.transport === 'STDIO'
@@ -1,13 +1,16 @@
1
+ import { NODE_COMPUTER_TOOL_NAME } from '../../mcp/naming.js';
1
2
  export const computerUseHostSkill = {
2
3
  name: 'computer-use',
3
- description: 'Use when the user asks to operate or verify a real desktop GUI through mcp__computer__exec. Load before the first desktop action.',
4
- requiredTools: ['mcp__computer__exec'],
4
+ description: 'Use when the user asks to operate or verify a real desktop GUI through ' +
5
+ NODE_COMPUTER_TOOL_NAME +
6
+ '. Load before the first desktop action.',
7
+ requiredTools: [NODE_COMPUTER_TOOL_NAME],
5
8
  load: async () => computerUseSkill()
6
9
  };
7
10
  export function computerUseSkill() {
8
11
  return `## Operating the desktop
9
12
 
10
- Use \`mcp__computer__exec\` only for user-authorized interaction with applications on the current desktop, or when the requested result can only be verified through the real GUI. Prefer ordinary workspace and shell tools for file and code work that does not require the GUI.
13
+ Use \`${NODE_COMPUTER_TOOL_NAME}\` only for user-authorized interaction with applications on the current desktop, or when the requested result can only be verified through the real GUI. Prefer ordinary workspace and shell tools for file and code work that does not require the GUI.
11
14
 
12
15
  ### Observe before acting
13
16
  - Start with one observation-only call using empty \`code\`. Do not repeat observation while the returned state is still current and the next steps do not depend on new visual information.
@@ -1,3 +1,4 @@
1
+ import { NODE_COMPUTER_TOOL_NAME } from '../../mcp/naming.js';
1
2
  import { spawn } from 'node:child_process';
2
3
  import { randomUUID } from 'node:crypto';
3
4
  import { access, mkdtemp, rm, writeFile } from 'node:fs/promises';
@@ -7,7 +8,7 @@ import { createInterface } from 'node:readline';
7
8
  import { terminateProcessTree } from '../../process-tree.js';
8
9
  import { ComputerUseControlIndicator } from './computer-use-indicator.js';
9
10
  import { PYTHON_PROBE_SOURCE } from './computer-use-probe.js';
10
- const TOOL_NAME = 'mcp__computer__exec';
11
+ const TOOL_NAME = NODE_COMPUTER_TOOL_NAME;
11
12
  const DEFAULT_TIMEOUT_MS = 30_000;
12
13
  const MAX_TIMEOUT_MS = 120_000;
13
14
  const MAX_CODE_BYTES = 1024 * 1024;
@@ -1,3 +1,4 @@
1
+ import { NODE_MCP } from '../../mcp/naming.js';
1
2
  import { computerUseHostSkill } from './computer-use-skill.js';
2
3
  import { marctlExecutionEnvironment, marctlHostSkill } from './marctl-skill.js';
3
4
  import { compactRuntimeConfigurations, marAgentCompactExtensionSource, updateMarAgentCompactState } from './compact-extensions.js';
@@ -185,12 +186,10 @@ export class MarAgentManagedRunController {
185
186
  let secretEnvironment;
186
187
  try {
187
188
  secretEnvironment = parseSecretEnvironment(payload.secretEnvironment);
188
- const mcpSecretEnvironment = parseSecretEnvironment(payload.mcpSecretEnvironment);
189
189
  mcpServers = this.#resolveMcpServers(payload.mcpInstallations, {
190
190
  ...secretEnvironment,
191
- ...mcpSecretEnvironment,
192
191
  MAR_USERNAME: secretEnvironment['MAR_USERNAME'] ?? ''
193
- });
192
+ }, payload.executionMcpServers);
194
193
  images = parseRunnerImageAttachments(payload.attachments);
195
194
  }
196
195
  catch {
@@ -350,7 +349,7 @@ export class MarAgentManagedRunController {
350
349
  controller.signal.throwIfAborted();
351
350
  const userTools = executionMcpLease?.pool.definitions() ?? [];
352
351
  const nodeTools = computerUseBinding?.tools ?? [];
353
- if (userTools.some((tool) => tool.name.startsWith('mcp__computer__')))
352
+ if (userTools.some((tool) => tool.name.startsWith('mcp__mar_computer__')))
354
353
  throw new Error('MAR_AGENT_MCP_RESERVED_NAMESPACE');
355
354
  if (new Set([...userTools, ...nodeTools].map((tool) => tool.name)).size !==
356
355
  userTools.length + nodeTools.length)
@@ -391,8 +390,8 @@ export class MarAgentManagedRunController {
391
390
  };
392
391
  const computer = nodeTools.map((definition) => ({
393
392
  definition,
394
- serverId: 'computer',
395
- rawName: definition.name,
393
+ serverId: NODE_MCP.computer.name,
394
+ rawName: NODE_MCP.computer.tools.exec,
396
395
  sourceIdentity: `node:${input.runId}:computer`,
397
396
  definitionRevision: createHash('sha256')
398
397
  .update(JSON.stringify(definition))
@@ -404,7 +403,9 @@ export class MarAgentManagedRunController {
404
403
  revision: `${external.revision}:${input.runId}`,
405
404
  servers: [
406
405
  ...external.servers,
407
- ...(computer.length ? [{ id: 'computer', name: 'Computer' }] : [])
406
+ ...(computer.length
407
+ ? [{ id: NODE_MCP.computer.name, name: NODE_MCP.computer.name }]
408
+ : [])
408
409
  ],
409
410
  tools: [...external.tools, ...computer]
410
411
  };
@@ -753,10 +754,8 @@ export class MarAgentManagedRunController {
753
754
  throw error;
754
755
  }
755
756
  }
756
- #resolveMcpServers(installations, environment) {
757
- const servers = this.runner.mcpConfiguration(installations, environment);
758
- if (servers.some((server) => server.name === 'computer'))
759
- throw new Error('MAR_AGENT_MCP_RESERVED_NAMESPACE');
757
+ #resolveMcpServers(installations, environment, executionServers = []) {
758
+ const servers = this.runner.mcpConfiguration(installations, environment, executionServers);
760
759
  return servers;
761
760
  }
762
761
  #runtimeInUse(runtime) {
@@ -1,3 +1,4 @@
1
+ import { mcpNameAlias, NODE_MCP_CLIENT_INFO } from '../../mcp/naming.js';
1
2
  import { createHash, randomUUID } from 'node:crypto';
2
3
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3
4
  import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
@@ -327,7 +328,7 @@ export class MarAgentMcpClientPool {
327
328
  if (bytes > MCP_CATALOG_LIMITS.maximumBytes)
328
329
  throw new Error('MAR_AGENT_MCP_CATALOG_LIMIT');
329
330
  for (const tool of listed.tools) {
330
- const name = `mcp__${safeName(state.server.name)}__${safeName(tool.name)}`;
331
+ const name = `mcp__${mcpNameAlias(state.server.name)}__${mcpNameAlias(tool.name)}`;
331
332
  if (tools.has(name))
332
333
  throw new Error('MAR_AGENT_MCP_TOOL_NAME_CONFLICT');
333
334
  const definition = {
@@ -371,7 +372,7 @@ export class MarAgentMcpClientPool {
371
372
  throw new Error('MAR_AGENT_MCP_CATALOG_LIMIT');
372
373
  }
373
374
  async #connect(server, cwd, options) {
374
- const client = new Client({ name: 'maragent', version: '1.0.0' });
375
+ const client = new Client(NODE_MCP_CLIENT_INFO);
375
376
  this.#clients.push(client);
376
377
  let state;
377
378
  let initialDirty = false;
@@ -469,9 +470,6 @@ function mcpConfigurationFingerprint(servers, cwd, authorizationScope) {
469
470
  }))
470
471
  .digest('hex');
471
472
  }
472
- function safeName(value) {
473
- return value.replace(/[^a-zA-Z0-9_-]/gu, '_').slice(0, 64);
474
- }
475
473
  function requireMcpOwner(owner) {
476
474
  if (owner === undefined)
477
475
  throw new Error('MAR_AGENT_MCP_SANDBOX_OWNER_REQUIRED');
@@ -110,7 +110,6 @@ export class OpenCodeManagedRunController {
110
110
  this.host.active.has(payload.runId))
111
111
  return;
112
112
  let environment;
113
- let mcpSecretEnvironment;
114
113
  let mcp;
115
114
  let attachments;
116
115
  try {
@@ -118,12 +117,10 @@ export class OpenCodeManagedRunController {
118
117
  ...parseSecretEnvironment(payload.secretEnvironment),
119
118
  MAR_CTL_CONTEXT_REQUIRED: '1'
120
119
  }, { PATH: process.env['PATH'], Path: process.env['Path'] });
121
- mcpSecretEnvironment = parseSecretEnvironment(payload.mcpSecretEnvironment);
122
120
  mcp = this.runner.mcpConfiguration(payload.mcpInstallations, {
123
121
  ...environment,
124
- ...mcpSecretEnvironment,
125
122
  MAR_USERNAME: environment['MAR_USERNAME'] ?? ''
126
- });
123
+ }, payload.executionMcpServers);
127
124
  attachments = this.host.parseAttachments(payload.attachments);
128
125
  }
129
126
  catch (error) {
@@ -168,8 +168,8 @@ export class OpenCodeRunner extends AbstractRunner {
168
168
  }, delayMs);
169
169
  this.profileRefreshTimer.unref();
170
170
  }
171
- mcpConfiguration(raw, secrets) {
172
- const servers = super.mcpConfiguration(raw, secrets);
171
+ mcpConfiguration(raw, secrets, executionServers = []) {
172
+ const servers = super.mcpConfiguration(raw, secrets, executionServers);
173
173
  return Object.fromEntries(servers.map((server) => [
174
174
  server.name,
175
175
  server.transport === 'STDIO'
@@ -381,13 +381,14 @@ export class ContinuousRunCoordinator {
381
381
  return;
382
382
  }
383
383
  this.dispatchedExecutions.add(executionId);
384
+ let dispatched = false;
384
385
  try {
385
386
  // Capabilities belong to this Execution, never the Run or a claimed QueueItem.
386
387
  const executionInput = { ...pending, runId: executionId };
387
388
  const goalMcp = pending.goal === undefined ? undefined : await this.options.goalMcp?.(executionInput);
388
- const scheduledMcp = await this.options.scheduledMcp?.(executionInput);
389
- const mailMcp = await this.options.mailMcp?.(executionInput);
390
- if (this.suspended || this.options.stopped())
389
+ const scheduleMcp = await this.options.scheduleMcp?.(executionInput);
390
+ const notificationMcp = await this.options.notificationMcp?.(executionInput);
391
+ if (this.suspended || this.options.stopped() || !this.dispatchedExecutions.has(executionId))
391
392
  return;
392
393
  if (!this.goalCurrent(pending)) {
393
394
  this.dispatchedExecutions.delete(executionId);
@@ -423,15 +424,6 @@ export class ContinuousRunCoordinator {
423
424
  MAR_CTL_RUN_ID: active.id,
424
425
  MAR_CTL_RUNNER: pending.runner
425
426
  },
426
- ...(goalMcp === undefined && scheduledMcp === undefined && mailMcp === undefined
427
- ? {}
428
- : {
429
- mcpSecretEnvironment: {
430
- ...goalMcp?.secrets,
431
- ...scheduledMcp?.secrets,
432
- ...mailMcp?.secrets
433
- }
434
- }),
435
427
  ...(personalInstructions === undefined ? {} : { personalInstructions }),
436
428
  ...(pending.marAgentModel === undefined ? {} : { marAgentModel: pending.marAgentModel }),
437
429
  ...(pending.marAgentModels === undefined ? {} : { marAgentModels: pending.marAgentModels }),
@@ -442,17 +434,18 @@ export class ContinuousRunCoordinator {
442
434
  ? {}
443
435
  : { marAgentRuntimeScopeId: pending.marAgentRuntimeScopeId }),
444
436
  ...(pending.computerUse === undefined ? {} : { computerUse: pending.computerUse }),
445
- mcpInstallations: [
446
- ...this.options.mcps(pending.workspaceId),
447
- ...(goalMcp === undefined ? [] : [goalMcp.installation]),
448
- ...(scheduledMcp === undefined ? [] : [scheduledMcp.installation]),
449
- ...(mailMcp === undefined ? [] : [mailMcp.installation])
450
- ]
437
+ mcpInstallations: this.options.mcps(pending.workspaceId),
438
+ executionMcpServers: [goalMcp, scheduleMcp, notificationMcp].filter((server) => server !== undefined)
451
439
  });
440
+ dispatched = true;
452
441
  }
453
442
  catch (error) {
454
443
  this.options.emitRun(executionId, 'run.rejected', { code: safeErrorCode(error, 'RUN_START_FAILED') }, 'FAILED');
455
444
  }
445
+ finally {
446
+ if (!dispatched)
447
+ this.options.revokeExecutionMcps?.(executionId);
448
+ }
456
449
  }
457
450
  executionStarted(executionId) {
458
451
  if (this.startedExecutions.has(executionId))
@@ -1,3 +1,4 @@
1
+ import { NODE_MCP } from '../mcp/naming.js';
1
2
  import { randomBytes, randomUUID } from 'node:crypto';
2
3
  import { existsSync } from 'node:fs';
3
4
  import { createServer } from 'node:net';
@@ -5,7 +6,7 @@ import { tmpdir } from 'node:os';
5
6
  import { join } from 'node:path';
6
7
  import { unlink } from 'node:fs/promises';
7
8
  import { fileURLToPath } from 'node:url';
8
- export class GoalMcpServer {
9
+ export class GoalMcpBridge {
9
10
  goals;
10
11
  changed;
11
12
  capabilities = new Map();
@@ -26,22 +27,26 @@ export class GoalMcpServer {
26
27
  });
27
28
  return this.listening;
28
29
  }
29
- async installation(capability) {
30
+ async prepare(capability) {
30
31
  await this.listen();
32
+ const args = goalMcpStdioArgs();
31
33
  const token = randomBytes(32).toString('base64url');
32
- const secretName = `MAR_GOAL_MCP_${randomBytes(16).toString('hex').toUpperCase()}`;
33
34
  this.capabilities.set(token, capability);
34
35
  return {
35
- installation: {
36
- __marGoal: true,
37
- serverName: 'myagentroam_goal',
38
- enabled: true,
39
- command: process.execPath,
40
- args: goalMcpStdioArgs(),
41
- endpoint: this.endpoint,
42
- secretName
36
+ name: NODE_MCP.goal.name,
37
+ origin: 'BUILTIN',
38
+ lifecycle: 'EXECUTION',
39
+ transport: 'STDIO',
40
+ command: process.execPath,
41
+ args,
42
+ environment: {
43
+ MAR_GOAL_MCP_ENDPOINT: this.endpoint,
44
+ MAR_GOAL_MCP_TOKEN: token
43
45
  },
44
- secrets: { [secretName]: token }
46
+ allowedToolCallers: {
47
+ [NODE_MCP.goal.tools.get]: ['direct', 'programmatic'],
48
+ [NODE_MCP.goal.tools.update]: ['direct', 'programmatic']
49
+ }
45
50
  };
46
51
  }
47
52
  async close() {
@@ -89,11 +94,11 @@ export class GoalMcpServer {
89
94
  if (capability === undefined)
90
95
  throw new Error('GOAL_CAPABILITY_INVALID');
91
96
  const current = this.current(capability);
92
- if (request.tool === 'mar_get_goal') {
97
+ if (request.tool === NODE_MCP.goal.tools.get) {
93
98
  socket.end(`${JSON.stringify({ result: current })}\n`);
94
99
  return;
95
100
  }
96
- if (request.tool !== 'mar_update_goal')
101
+ if (request.tool !== NODE_MCP.goal.tools.update)
97
102
  throw new Error('GOAL_TOOL_INVALID');
98
103
  const status = request.arguments !== null &&
99
104
  typeof request.arguments === 'object' &&
@@ -1,3 +1,4 @@
1
+ import { NODE_MCP, NODE_MCP_VERSION } from '../mcp/naming.js';
1
2
  import { connect } from 'node:net';
2
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
4
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
@@ -6,12 +7,12 @@ const endpoint = process.env['MAR_GOAL_MCP_ENDPOINT'];
6
7
  const token = process.env['MAR_GOAL_MCP_TOKEN'];
7
8
  if (endpoint === undefined || token === undefined)
8
9
  throw new Error('GOAL_CAPABILITY_INVALID');
9
- const server = new McpServer({ name: 'myagentroam_goal', version: '1.0.0' });
10
- server.registerTool('mar_get_goal', { description: 'Get the authoritative current Goal, status, and objective.' }, async () => result(await request('mar_get_goal', {})));
11
- server.registerTool('mar_update_goal', {
10
+ const server = new McpServer({ name: NODE_MCP.goal.name, version: NODE_MCP_VERSION });
11
+ server.registerTool(NODE_MCP.goal.tools.get, { description: 'Get the authoritative current Goal, status, and objective.' }, async () => result(await request(NODE_MCP.goal.tools.get, {})));
12
+ server.registerTool(NODE_MCP.goal.tools.update, {
12
13
  description: 'Explicitly mark the current Goal completed or blocked.',
13
14
  inputSchema: { status: z.enum(['completed', 'blocked']) }
14
- }, async ({ status }) => result(await request('mar_update_goal', { status })));
15
+ }, async ({ status }) => result(await request(NODE_MCP.goal.tools.update, { status })));
15
16
  await server.connect(new StdioServerTransport());
16
17
  function request(tool, args) {
17
18
  return new Promise((resolve, reject) => {
@@ -1,3 +1,4 @@
1
+ import { NODE_MCP } from '../mcp/naming.js';
1
2
  import { randomUUID } from 'node:crypto';
2
3
  import { parseComposerTags, serializeComposerTag } from '@myagentroam/protocol';
3
4
  export function goalUnrestrictedAccess(runner, currentAccess) {
@@ -9,47 +10,25 @@ export function goalUnrestrictedAccess(runner, currentAccess) {
9
10
  return 'bypass';
10
11
  return 'auto';
11
12
  }
12
- const START_OPEN = '<mar_goal_start>';
13
- const START_CLOSE = '</mar_goal_start>';
14
- const CONTINUE_OPEN = '<mar_goal_continue>';
15
- const CONTINUE_CLOSE = '</mar_goal_continue>';
16
- function goalPromptTags(text) {
17
- if (text.startsWith(START_OPEN))
18
- return { open: START_OPEN, close: START_CLOSE, continuation: false };
19
- if (text.startsWith(CONTINUE_OPEN))
20
- return { open: CONTINUE_OPEN, close: CONTINUE_CLOSE, continuation: true };
21
- return undefined;
22
- }
23
13
  export function publicGoalStartText(objective) {
24
14
  return serializeComposerTag('mar_goal_start', { objective: objective.trim() });
25
15
  }
26
16
  export function stripGoalStartPrompt(text) {
27
- const tags = goalPromptTags(text);
28
- if (tags === undefined)
29
- return text;
30
- const close = text.lastIndexOf(tags.close);
31
- return close === -1 ? '' : text.slice(close + tags.close.length).trimStart();
17
+ const first = parseComposerTags(text)[0];
18
+ return first?.kind === 'mar_goal_start' || first?.kind === 'mar_goal_continue'
19
+ ? text.slice(first.raw.length).trimStart()
20
+ : text;
32
21
  }
33
22
  export function publicGoalHistoryText(text) {
34
- if (parseComposerTags(text)[0]?.kind === 'mar_restart')
23
+ const first = parseComposerTags(text)[0];
24
+ if (first?.kind === 'mar_restart')
35
25
  return serializeComposerTag('mar_restart', {});
36
- const tags = goalPromptTags(text);
37
- if (tags === undefined)
38
- return text;
39
- const safe = parseComposerTags(text)[0];
40
- if (safe?.kind === 'mar_goal_start' || safe?.kind === 'mar_goal_continue')
26
+ if (first?.kind !== 'mar_goal_start' && first?.kind !== 'mar_goal_continue')
41
27
  return text;
42
- const stripped = stripGoalStartPrompt(text);
43
- if (tags.continuation)
44
- return [serializeComposerTag('mar_goal_continue', {}), stripped].filter(Boolean).join('\n');
45
- const close = text.lastIndexOf(tags.close);
46
- const prompt = close === -1 ? text : text.slice(0, close);
47
- const objectiveStart = prompt.indexOf('\nOriginal objective:\n');
48
- const objectiveEnd = prompt.lastIndexOf('\n\nContinue executing this Goal across turns:');
49
- const objective = objectiveStart === -1 || objectiveEnd === -1
50
- ? ''
51
- : prompt.slice(objectiveStart + '\nOriginal objective:\n'.length, objectiveEnd);
52
- return [publicGoalStartText(objective), stripped].filter(Boolean).join('\n');
28
+ const marker = first.kind === 'mar_goal_start'
29
+ ? serializeComposerTag('mar_goal_start', { objective: first.payload.objective })
30
+ : serializeComposerTag('mar_goal_continue', {});
31
+ return [marker, stripGoalStartPrompt(text)].filter(Boolean).join('\n');
53
32
  }
54
33
  export class GoalRuntime {
55
34
  changed;
@@ -237,9 +216,15 @@ export class GoalRuntime {
237
216
  if (goal?.status !== 'active')
238
217
  throw new Error('GOAL_NOT_ACTIVE');
239
218
  const heading = kind === 'start' ? 'A Goal has started or changed.' : 'Continue the active Goal.';
240
- const open = kind === 'start' ? START_OPEN : CONTINUE_OPEN;
241
- const close = kind === 'start' ? START_CLOSE : CONTINUE_CLOSE;
242
- return `${open}\n${heading}\n\nOriginal objective:\n${goal.objective}\n\nContinue executing this Goal across turns:\n- This Goal is owned by the Node's myagentroam_goal MCP server. Prefer myagentroam_goal.mar_update_goal to complete it. In Codex, a runner-native Goal created during this Goal may also complete it when Codex reports that native Goal complete; native Goal state from another task or turn does not count. Do not claim completion based only on a final message or an unrelated Goal.\n- Work on the objective now. If it is incomplete, use tools to finish a coherent unit or obtain decisive evidence; do not stop at an acknowledgement, status, plan, or promise.\n- Preserve the original scope. Before completion, review the full objective and verify it with real evidence, then call mar_update_goal with status=completed when available.\n- Adapt and retry after individual failures. Request blocked only after the same blocker persists for three turns and the required additional retry.\n- Use mar_get_goal only when you need to reread the authoritative objective or status.\n- After mar_update_goal confirms completed or finally blocked, summarize the completed work, verification evidence, remaining issues, and the tool-reported elapsedWallMs converted from milliseconds into a concise readable duration in your final user-facing response. If Codex instead confirms completion with its native Goal, summarize the work without claiming a MAR elapsedWallMs value. Name whether the Goal completed or is finally blocked. MAR elapsedWallMs includes waiting and retries through the status update, not tool execution time or the final response. If blocked returns active for the required retry, continue work instead of claiming a final summary.\n${close}${goal.inputTags ? `\n${goal.inputTags}` : ''}`;
219
+ const instruction = `${heading}
220
+
221
+ Continue executing this Goal across turns:
222
+ - This Goal is owned by the Node's ${NODE_MCP.goal.name} MCP server. Prefer ${NODE_MCP.goal.name}.${NODE_MCP.goal.tools.update} to complete it. In Codex, a runner-native Goal created during this Goal may also complete it when Codex reports that native Goal complete; native Goal state from another task or turn does not count. Do not claim completion based only on a final message or an unrelated Goal.\n- Work on the objective now. If it is incomplete, use tools to finish a coherent unit or obtain decisive evidence; do not stop at an acknowledgement, status, plan, or promise.\n- Preserve the original scope. Before completion, review the full objective and verify it with real evidence, then call ${NODE_MCP.goal.tools.update} with status=completed when available.\n- Adapt and retry after individual failures. Request blocked only after the same blocker persists for three turns and the required additional retry.\n- Use ${NODE_MCP.goal.tools.get} only when you need to reread the authoritative objective or status.\n- After ${NODE_MCP.goal.tools.update} confirms completed or finally blocked, summarize the completed work, verification evidence, remaining issues, and the tool-reported elapsedWallMs converted from milliseconds into a concise readable duration in your final user-facing response. If Codex instead confirms completion with its native Goal, summarize the work without claiming a MAR elapsedWallMs value. Name whether the Goal completed or is finally blocked. MAR elapsedWallMs includes waiting and retries through the status update, not tool execution time or the final response. If blocked returns active for the required retry, continue work instead of claiming a final summary.`;
223
+ const marker = serializeComposerTag(kind === 'start' ? 'mar_goal_start' : 'mar_goal_continue', {
224
+ objective: goal.objective,
225
+ instruction
226
+ });
227
+ return [marker, goal.inputTags].filter(Boolean).join('\n');
243
228
  }
244
229
  requireCurrent(sessionId, goalId, revision) {
245
230
  const current = this.goals.get(sessionId);
@@ -1,3 +1,4 @@
1
+ import { assertMcpServerNames } from '../mcp/naming.js';
1
2
  import { modelUsageGrantSchema, steerComposerMessage } from '@myagentroam/protocol';
2
3
  import { createHash, randomUUID } from 'node:crypto';
3
4
  import { constants } from 'node:fs';
@@ -1891,6 +1892,7 @@ function resolveMcpServers(version, environment) {
1891
1892
  }
1892
1893
  return resolved;
1893
1894
  };
1895
+ assertMcpServerNames(version.mcpServers.map((server) => ({ name: server.serverName })));
1894
1896
  return version.mcpServers.flatMap((server) => {
1895
1897
  if (server.runtime.transport === 'HTTP') {
1896
1898
  if (version.draft.networkMode !== 'UNRESTRICTED')
@@ -1,10 +1,11 @@
1
+ import { NODE_MCP_CLIENT_INFO } from '../mcp/naming.js';
1
2
  import { Client } from '@modelcontextprotocol/sdk/client/index.js';
2
3
  import { getDefaultEnvironment, StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
3
4
  import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
4
5
  const INSTALL_TIMEOUT_MS = 90_000;
5
6
  export class McpInstallationVerifier {
6
7
  async verify(input) {
7
- const client = new Client({ name: 'myagentroam-node-installer', version: '0.9.1' });
8
+ const client = new Client(NODE_MCP_CLIENT_INFO);
8
9
  try {
9
10
  await withTimeout((async () => {
10
11
  if (input.entry.runtime.transport === 'STDIO') {
@@ -1,3 +1,4 @@
1
+ import { assertMcpServerNames, NODE_MCP } from '../mcp/naming.js';
1
2
  import { mcpCatalogEntrySchema, mcpInstallationInputSchema, mcpTargetInspectionSchema } from '@myagentroam/protocol';
2
3
  import { homedir } from 'node:os';
3
4
  import { McpInstallationVerifier } from './mcp-installation-verifier.js';
@@ -43,7 +44,7 @@ export class McpNodeOperationService {
43
44
  targetKind,
44
45
  installations,
45
46
  ...(targetKind === 'WORKSPACE'
46
- ? { executionBuiltIns: ['mar_schedule', 'mar_notification'] }
47
+ ? { executionBuiltIns: [NODE_MCP.schedule.name, NODE_MCP.notification.name] }
47
48
  : {})
48
49
  });
49
50
  }
@@ -51,6 +52,7 @@ export class McpNodeOperationService {
51
52
  if (!isRecord(raw))
52
53
  throw new Error('MCP_INSTALL_INPUT_INVALID');
53
54
  const entry = mcpCatalogEntrySchema.parse(raw['entry']);
55
+ assertMcpServerNames([{ name: entry.serverName }]);
54
56
  const input = mcpInstallationInputSchema.parse({
55
57
  catalogEntryId: entry.id,
56
58
  enabled: raw['enabled'] ?? true,
@@ -1,3 +1,4 @@
1
+ import { NODE_MCP } from '../mcp/naming.js';
1
2
  import { randomBytes, randomUUID } from 'node:crypto';
2
3
  import { existsSync } from 'node:fs';
3
4
  import { unlink } from 'node:fs/promises';
@@ -6,38 +7,45 @@ import { tmpdir } from 'node:os';
6
7
  import { join } from 'node:path';
7
8
  import { fileURLToPath } from 'node:url';
8
9
  import { notificationInputSchema, notificationChannelsRequestSchema, mailNotificationRequestSchema } from '@myagentroam/protocol';
9
- export class MailNotificationMcpServer {
10
+ export class NotificationMcpBridge {
10
11
  resolve;
11
12
  send;
12
13
  channels;
13
14
  capabilities = new Map();
14
15
  sockets = new Set();
15
16
  endpoint = process.platform === 'win32'
16
- ? `\\\\.\\pipe\\mar-mail-${process.pid}-${randomUUID()}`
17
- : join(tmpdir(), `mar-mail-${process.pid}-${randomUUID()}.sock`);
17
+ ? `\\\\.\\pipe\\mar-notification-${process.pid}-${randomUUID()}`
18
+ : join(tmpdir(), `mar-notification-${process.pid}-${randomUUID()}.sock`);
18
19
  listening;
19
20
  constructor(resolve, send, channels) {
20
21
  this.resolve = resolve;
21
22
  this.send = send;
22
23
  this.channels = channels;
23
24
  }
24
- async installation(capability) {
25
+ async prepare(capability) {
25
26
  await this.listen();
27
+ const args = notificationMcpStdioArgs();
26
28
  const token = randomBytes(32).toString('base64url');
27
- const secretName = `MAR_MAIL_MCP_${randomBytes(16).toString('hex').toUpperCase()}`;
29
+ const notificationChannels = this.channels?.supported() === true;
28
30
  this.capabilities.set(token, capability);
29
31
  return {
30
- installation: {
31
- __marMail: true,
32
- notificationChannels: this.channels?.supported() === true,
33
- serverName: 'mar_notification',
34
- enabled: true,
35
- command: process.execPath,
36
- args: mailMcpStdioArgs(),
37
- endpoint: this.endpoint,
38
- secretName
32
+ name: NODE_MCP.notification.name,
33
+ origin: 'BUILTIN',
34
+ lifecycle: 'EXECUTION',
35
+ transport: 'STDIO',
36
+ command: process.execPath,
37
+ args,
38
+ environment: {
39
+ MAR_NOTIFICATION_MCP_ENDPOINT: this.endpoint,
40
+ MAR_NOTIFICATION_MCP_TOKEN: token,
41
+ MAR_NOTIFICATION_MCP_CHANNELS: notificationChannels ? '1' : '0'
39
42
  },
40
- secrets: { [secretName]: token }
43
+ allowedToolCallers: {
44
+ [NODE_MCP.notification.tools.email]: ['direct', 'programmatic'],
45
+ ...(notificationChannels
46
+ ? { [NODE_MCP.notification.tools.send]: ['direct', 'programmatic'] }
47
+ : {})
48
+ }
41
49
  };
42
50
  }
43
51
  revoke(executionId) {
@@ -97,7 +105,7 @@ export class MailNotificationMcpServer {
97
105
  const current = this.resolve(capability);
98
106
  if (current === undefined)
99
107
  throw new Error('MAIL_CAPABILITY_STALE');
100
- if (request['tool'] === 'mar_send_notification') {
108
+ if (request['tool'] === NODE_MCP.notification.tools.send) {
101
109
  if (this.channels?.supported() !== true)
102
110
  throw new Error('NOTIFICATION_UNSUPPORTED');
103
111
  const args = request['arguments'];
@@ -123,7 +131,7 @@ export class MailNotificationMcpServer {
123
131
  socket.end(`${JSON.stringify({ result })}\n`);
124
132
  return;
125
133
  }
126
- if (request['tool'] !== 'mar_send_email_notification')
134
+ if (request['tool'] !== NODE_MCP.notification.tools.email)
127
135
  throw new Error('MAIL_TOOL_INVALID');
128
136
  const args = request['arguments'];
129
137
  if (args === null || typeof args !== 'object' || Array.isArray(args))
@@ -157,7 +165,7 @@ export class MailNotificationMcpServer {
157
165
  }
158
166
  }
159
167
  }
160
- export function resolveMailNotificationCapability(runtime, capability) {
168
+ export function resolveNotificationCapability(runtime, capability) {
161
169
  const run = runtime.getRun(capability.executionId);
162
170
  const session = runtime.getAgentSession(capability.sessionId);
163
171
  if (run === undefined ||
@@ -171,12 +179,12 @@ export function resolveMailNotificationCapability(runtime, capability) {
171
179
  return undefined;
172
180
  return { ...capability, sessionId: session.id };
173
181
  }
174
- function mailMcpStdioArgs() {
175
- const javascript = fileURLToPath(new URL('./mail-notification-mcp-stdio.js', import.meta.url));
182
+ function notificationMcpStdioArgs() {
183
+ const javascript = fileURLToPath(new URL('./notification-mcp-stdio.js', import.meta.url));
176
184
  if (existsSync(javascript))
177
185
  return [javascript];
178
- const typescript = fileURLToPath(new URL('./mail-notification-mcp-stdio.ts', import.meta.url));
186
+ const typescript = fileURLToPath(new URL('./notification-mcp-stdio.ts', import.meta.url));
179
187
  if (!existsSync(typescript))
180
- throw new Error('MAIL_MCP_STDIO_ENTRY_UNAVAILABLE');
188
+ throw new Error('NOTIFICATION_MCP_STDIO_ENTRY_UNAVAILABLE');
181
189
  return ['--import', import.meta.resolve('tsx'), typescript];
182
190
  }