@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.
- package/dist/claude-agent-sdk.js +8 -5
- package/dist/connector.js +36 -72
- package/dist/mcp/configuration.js +107 -0
- package/dist/mcp/naming.js +42 -0
- package/dist/runner/abstract-runner.js +3 -154
- package/dist/runner/claude/managed-run-controller.js +1 -4
- package/dist/runner/claude-code-runner.js +2 -2
- package/dist/runner/codex/managed-run-controller.js +6 -8
- package/dist/runner/codex-runner.js +2 -2
- package/dist/runner/mar-agent/computer-use-skill.js +6 -3
- package/dist/runner/mar-agent/computer-use.js +2 -1
- package/dist/runner/mar-agent/managed-run-controller.js +10 -11
- package/dist/runner/mar-agent/mcp-client-pool.js +3 -5
- package/dist/runner/opencode/managed-run-controller.js +1 -4
- package/dist/runner/opencode-runner.js +2 -2
- package/dist/service/continuous-run-coordinator.js +11 -18
- package/dist/service/{goal-mcp-server.js → goal-mcp-bridge.js} +19 -14
- package/dist/service/goal-mcp-stdio.js +5 -4
- package/dist/service/goal-runtime.js +21 -36
- package/dist/service/hosted-conversation-node-service.js +2 -0
- package/dist/service/mcp-installation-verifier.js +2 -1
- package/dist/service/mcp-node-operation-service.js +3 -1
- package/dist/service/{mail-notification-mcp-server.js → notification-mcp-bridge.js} +30 -22
- package/dist/service/{mail-notification-mcp-stdio.js → notification-mcp-stdio.js} +9 -8
- package/dist/service/restart-session-restorer.js +2 -2
- package/dist/service/{scheduled-task-mcp-server.js → schedule-mcp-bridge.js} +25 -19
- package/dist/service/{scheduled-task-mcp-stdio.js → schedule-mcp-stdio.js} +8 -7
- package/dist/service/session-catalog-service.js +3 -3
- package/package.json +4 -4
- /package/dist/{mcp-installation-model.js → mcp/model.js} +0 -0
package/dist/claude-agent-sdk.js
CHANGED
|
@@ -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 ? {
|
|
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:
|
|
176
|
-
version:
|
|
177
|
-
instructions: 'Use the
|
|
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(
|
|
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 {
|
|
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 {
|
|
75
|
+
import { ScheduleMcpBridge } from './service/schedule-mcp-bridge.js';
|
|
75
76
|
import { MailNotificationControl } from './service/mail-notification-control.js';
|
|
76
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
890
|
+
notificationMcp: async (input) => {
|
|
918
891
|
const run = this.runtime.getRun(input.runId);
|
|
919
892
|
if (run?.initiatedByUserId === undefined)
|
|
920
893
|
return undefined;
|
|
921
|
-
|
|
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.
|
|
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.
|
|
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:
|
|
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.
|
|
2271
|
-
const
|
|
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
|
|
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
|
-
|
|
2296
|
-
|
|
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' &&
|
|
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 {
|
|
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
|
|
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'
|