@myagentroam/node 0.1.8 → 0.9.0

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 (42) hide show
  1. package/dist/capabilities.js +6 -3
  2. package/dist/claude-agent-sdk.d.ts +1 -0
  3. package/dist/claude-agent-sdk.js +8 -1
  4. package/dist/codex-app-server.d.ts +6 -0
  5. package/dist/codex-app-server.js +6 -0
  6. package/dist/connector/node-connector-options.d.ts +1 -0
  7. package/dist/connector.js +5 -2
  8. package/dist/database.d.ts +23 -0
  9. package/dist/database.js +84 -2
  10. package/dist/migrations/v004.d.ts +4 -0
  11. package/dist/migrations/v004.js +35 -0
  12. package/dist/opencode-server.d.ts +1 -0
  13. package/dist/opencode-server.js +12 -0
  14. package/dist/runner/abstract-runner.d.ts +12 -2
  15. package/dist/runner/abstract-runner.js +76 -2
  16. package/dist/runner/claude/managed-run-controller.js +2 -1
  17. package/dist/runner/claude-code-runner.d.ts +1 -0
  18. package/dist/runner/claude-code-runner.js +14 -0
  19. package/dist/runner/codex/managed-run-controller.js +5 -3
  20. package/dist/runner/codex-runner.d.ts +16 -2
  21. package/dist/runner/codex-runner.js +215 -4
  22. package/dist/runner/opencode/managed-run-controller.js +13 -3
  23. package/dist/runner/opencode-runner.d.ts +2 -1
  24. package/dist/runner/opencode-runner.js +23 -3
  25. package/dist/runner/runner-registry.d.ts +1 -1
  26. package/dist/runner/runner-registry.js +2 -2
  27. package/dist/runner-profiles.js +5 -25
  28. package/dist/runtime-command-detector.d.ts +3 -0
  29. package/dist/runtime-command-detector.js +78 -0
  30. package/dist/service/mcp-installation-verifier.d.ts +9 -0
  31. package/dist/service/mcp-installation-verifier.js +87 -0
  32. package/dist/service/mcp-node-operation-service.d.ts +11 -0
  33. package/dist/service/mcp-node-operation-service.js +90 -0
  34. package/dist/service/mcp-package-installer.d.ts +8 -0
  35. package/dist/service/mcp-package-installer.js +136 -0
  36. package/dist/service/runner-service.d.ts +2 -0
  37. package/dist/service/runner-service.js +5 -2
  38. package/dist/service/session-lifecycle-service.js +7 -4
  39. package/dist/service/workbench-manifest-service.d.ts +2 -0
  40. package/dist/service/workspace-queue-workbench-service.d.ts +2 -0
  41. package/dist/service/workspace-queue-workbench-service.js +2 -1
  42. package/package.json +2 -2
@@ -3,6 +3,7 @@ import nodePackage from '../package.json' with { type: 'json' };
3
3
  import { probeClaudeCode, probeClaudeCodeAsync } from './claude-agent-sdk.js';
4
4
  import { probeCodex, probeCodexAsync } from './codex-app-server.js';
5
5
  import { probeOpenCode, probeOpenCodeAsync } from './opencode-runtime.js';
6
+ import { detectRuntimeCommands } from './runtime-command-detector.js';
6
7
  const nodeAppVersion = typeof nodePackage.version === 'string' ? nodePackage.version : 'unknown';
7
8
  function reportedPlatform() {
8
9
  return process.platform === 'win32' ? 'windows' : 'linux';
@@ -59,10 +60,11 @@ export async function detectCapabilitiesAsync(commands = {}) {
59
60
  if (process.platform !== 'win32' && process.platform !== 'linux') {
60
61
  throw new Error('NODE_PLATFORM_UNSUPPORTED');
61
62
  }
62
- const [codex, claudeCode, openCode] = await Promise.all([
63
+ const [codex, claudeCode, openCode, runtimeCommands] = await Promise.all([
63
64
  probeCodexAsync(commands.codex),
64
65
  probeClaudeCodeAsync(commands.claudeCode),
65
- probeOpenCodeAsync(commands.openCode)
66
+ probeOpenCodeAsync(commands.openCode),
67
+ detectRuntimeCommands()
66
68
  ]);
67
69
  return {
68
70
  platform: reportedPlatform(),
@@ -71,7 +73,8 @@ export async function detectCapabilitiesAsync(commands = {}) {
71
73
  nodeVersion: process.version,
72
74
  metadata: {
73
75
  osRelease: release(),
74
- osVersion: version()
76
+ osVersion: version(),
77
+ runtimeCommands
75
78
  },
76
79
  codex: {
77
80
  available: codex.available && codex.compatible,
@@ -22,6 +22,7 @@ export interface ClaudeQueryInput {
22
22
  readonly onMessage: (message: SDKMessage) => void;
23
23
  readonly onChannelReply?: (content: string) => void;
24
24
  readonly environment?: Readonly<Record<string, string>>;
25
+ readonly mcpServers?: NonNullable<Options['mcpServers']>;
25
26
  }
26
27
  /** Image blocks accepted by the Claude Agent SDK message input. */
27
28
  export interface ClaudeImageAttachment {
@@ -66,7 +66,14 @@ export class ClaudeAgentSdkAdapter {
66
66
  settings: { showThinkingSummaries: true },
67
67
  abortController,
68
68
  canUseTool: input.onPermission,
69
- ...(channelEnabled ? { mcpServers: { myagentroam: channelMcpServer(input) } } : {})
69
+ ...(input.mcpServers !== undefined || channelEnabled
70
+ ? {
71
+ mcpServers: {
72
+ ...(input.mcpServers ?? {}),
73
+ ...(channelEnabled ? { myagentroam: channelMcpServer(input) } : {})
74
+ }
75
+ }
76
+ : {})
70
77
  }
71
78
  });
72
79
  const completed = consume(stream, input.onMessage);
@@ -49,6 +49,7 @@ export interface CodexRunConfiguration {
49
49
  readonly access?: string | null;
50
50
  readonly serviceTier?: 'fast';
51
51
  readonly collaborationMode?: 'default' | 'plan';
52
+ readonly mcpServers?: unknown;
52
53
  }
53
54
  /** Public App Server turn inputs supported by the Workbench Composer. */
54
55
  export type CodexComposerInput = {
@@ -84,6 +85,11 @@ export declare class CodexAppServerClient {
84
85
  startThread(cwd: string, configuration?: CodexRunConfiguration): Promise<unknown>;
85
86
  listThreads(): Promise<unknown>;
86
87
  listCollaborationModes(): Promise<unknown>;
88
+ listModels(params: {
89
+ readonly cursor: string | null;
90
+ readonly limit: number;
91
+ readonly includeHidden: boolean;
92
+ }): Promise<unknown>;
87
93
  readThread(threadId: string, includeTurns?: boolean): Promise<unknown>;
88
94
  forkThread(threadId: string, cwd: string, lastTurnId: string, configuration?: CodexRunConfiguration): Promise<unknown>;
89
95
  setThreadName(threadId: string, name: string): Promise<unknown>;
@@ -170,6 +170,9 @@ export class CodexAppServerClient {
170
170
  listCollaborationModes() {
171
171
  return this.request('collaborationMode/list', {});
172
172
  }
173
+ listModels(params) {
174
+ return this.request('model/list', params);
175
+ }
173
176
  readThread(threadId, includeTurns = true) {
174
177
  return this.request('thread/read', { threadId, includeTurns });
175
178
  }
@@ -263,6 +266,9 @@ export class CodexAppServerClient {
263
266
  function codexThreadConfiguration(cwd, configuration) {
264
267
  return {
265
268
  ...modelOption(configuration.model),
269
+ ...(configuration.mcpServers === undefined
270
+ ? {}
271
+ : { config: { mcp_servers: configuration.mcpServers } }),
266
272
  ...codexPermissionConfiguration(cwd, configuration.access, 'thread')
267
273
  };
268
274
  }
@@ -16,6 +16,7 @@ export interface NodeConnectorOptions {
16
16
  readonly fakeRunner?: FakeRunner;
17
17
  readonly fakeScript?: (input: unknown, runId: string) => readonly FakeRunnerStep[];
18
18
  readonly codexClient?: CodexAppServerClient;
19
+ readonly codexProfileClientFactory?: () => CodexAppServerClient;
19
20
  readonly claudeClient?: ClaudeAgentSdkAdapter;
20
21
  readonly openCodeClient?: OpenCodeServerClient;
21
22
  readonly database?: NodeDatabase;
package/dist/connector.js CHANGED
@@ -19,6 +19,7 @@ import { OpenCodeRunner } from './runner/opencode-runner.js';
19
19
  import { OpenCodeManagedRunController } from './runner/opencode/managed-run-controller.js';
20
20
  import { NodeOperationRouter } from './connector/node-operation-router.js';
21
21
  import { SkillNodeOperationService } from './service/skill-node-operation-service.js';
22
+ import { McpNodeOperationService } from './service/mcp-node-operation-service.js';
22
23
  import { RunnerService } from './service/runner-service.js';
23
24
  import { TerminalService } from './service/terminal-service.js';
24
25
  import { WorkspaceService } from './service/workspace-service.js';
@@ -230,7 +231,7 @@ export class NodeConnector {
230
231
  options.fakeRunner === undefined ? undefined : new FakeTestRunner(options.fakeRunner);
231
232
  this.fakeScript = options.fakeScript;
232
233
  this.runAttachmentService = new RunAttachmentService((session) => this.runners.require(session.runner).nativeImageRoot(session));
233
- this.codexClient = new CodexRunner(options.codexClient);
234
+ this.codexClient = new CodexRunner(options.codexClient, options.codexProfileClientFactory);
234
235
  this.codexRunController = new CodexManagedRunController(this.codexClient, {
235
236
  available: () => this.capabilities.codex.available,
236
237
  intercepted: () => this.fakeRunner !== undefined,
@@ -404,7 +405,8 @@ export class NodeConnector {
404
405
  control: (operation, payload) => this.controlMessages.handle(JSON.stringify(createEnvelope(operation, payload))),
405
406
  stopped: () => this.stopped,
406
407
  markInterrupted: (runId) => this.runState.interruptRequested.add(runId),
407
- prepareInput: (runner, input, collaborationMode) => this.runners.require(runner).prepareMessageInput(input, collaborationMode)
408
+ prepareInput: (runner, input, collaborationMode) => this.runners.require(runner).prepareMessageInput(input, collaborationMode),
409
+ mcps: (workspaceId) => this.database?.effectiveMcpInstallations(workspaceId) ?? []
408
410
  });
409
411
  this.sessionIdentityService = new SessionIdentityService({
410
412
  runtime: this.runtime,
@@ -714,6 +716,7 @@ export class NodeConnector {
714
716
  throw new Error('NODE_CONFIG_UNAVAILABLE');
715
717
  return this.config;
716
718
  }).operations());
719
+ this.operationRouter.registerAll(new McpNodeOperationService(requireDatabase).operations());
717
720
  this.operationRouter.register('node.metrics', () => ({
718
721
  metrics: {
719
722
  ...this.metrics.snapshot(),
@@ -1,6 +1,19 @@
1
1
  import { DatabaseSync } from 'node:sqlite';
2
2
  import { type AgentRunStatus, type RunnerDefaultConfiguration, type RunnerName } from '@myagentroam/protocol';
3
+ import { type McpCatalogEntry, type McpInstallationConfiguration, type McpRuntimeDescriptor } from '@myagentroam/protocol';
3
4
  import type { NativeSessionHistory } from './native-session-history.js';
5
+ export interface NodeMcpInstallation {
6
+ readonly targetKind: 'NODE' | 'WORKSPACE';
7
+ readonly targetId: string;
8
+ readonly catalogEntryId: string;
9
+ readonly serverName: string;
10
+ readonly displayName: string;
11
+ readonly version: string;
12
+ readonly enabled: boolean;
13
+ readonly runtime: McpRuntimeDescriptor;
14
+ readonly configuration: McpInstallationConfiguration;
15
+ readonly installedAt: number;
16
+ }
4
17
  export type RunRecoveryState = 'ACTIVE' | 'SUSPECT' | 'LOST';
5
18
  export type ApprovalStatus = 'PENDING' | 'APPROVED' | 'REJECTED' | 'EXPIRED';
6
19
  /** Whether a native session ID was created by this Node or merely discovered. */
@@ -153,6 +166,16 @@ export declare class NodeDatabase {
153
166
  close(): void;
154
167
  getRunnerDefaults(): readonly RunnerDefaultConfiguration[];
155
168
  saveRunnerDefaults(input: RunnerDefaultConfiguration): RunnerDefaultConfiguration;
169
+ listMcpInstallations(targetKind: 'NODE' | 'WORKSPACE', targetId?: string): readonly NodeMcpInstallation[];
170
+ saveMcpInstallation(input: {
171
+ readonly targetKind: 'NODE' | 'WORKSPACE';
172
+ readonly targetId?: string;
173
+ readonly entry: McpCatalogEntry;
174
+ readonly enabled: boolean;
175
+ readonly configuration: NodeMcpInstallation['configuration'];
176
+ }): NodeMcpInstallation;
177
+ removeMcpInstallation(targetKind: 'NODE' | 'WORKSPACE', targetId: string, serverName: string): boolean;
178
+ effectiveMcpInstallations(workspaceId: string): readonly NodeMcpInstallation[];
156
179
  /**
157
180
  * Stores only terminal lifecycle metadata. PTY input/output must remain in
158
181
  * the Node-local bounded replay buffer and must never be passed here.
package/dist/database.js CHANGED
@@ -3,10 +3,12 @@ import { closeSync, existsSync, mkdirSync, openSync, rmSync } from 'node:fs';
3
3
  import { dirname } from 'node:path';
4
4
  import { DatabaseSync } from 'node:sqlite';
5
5
  import { runnerDefaultConfigurationSchema } from '@myagentroam/protocol';
6
+ import { mcpCatalogEntrySchema, mcpInstallationConfigurationSchema, mcpRuntimeDescriptorSchema } from '@myagentroam/protocol';
6
7
  import { assertNodeSchema, NODE_SCHEMA_V1 } from './migrations/v001.js';
7
8
  import { assertNodeSchemaV2, migrateNodeSchemaV2 } from './migrations/v002.js';
8
- import { assertNodeSchemaV3, migrateNodeSchemaV3, NODE_SCHEMA_VERSION_V3 } from './migrations/v003.js';
9
- const NODE_SCHEMA_VERSION = NODE_SCHEMA_VERSION_V3;
9
+ import { assertNodeSchemaV3, migrateNodeSchemaV3 } from './migrations/v003.js';
10
+ import { assertNodeSchemaV4, migrateNodeSchemaV4, NODE_SCHEMA_VERSION_V4 } from './migrations/v004.js';
11
+ const NODE_SCHEMA_VERSION = NODE_SCHEMA_VERSION_V4;
10
12
  export class NodeDatabase {
11
13
  now;
12
14
  activeWorkspaceLeaseCount() {
@@ -63,6 +65,57 @@ export class NodeDatabase {
63
65
  .run(input.runner, input.model, input.effort, input.access);
64
66
  return input;
65
67
  }
68
+ listMcpInstallations(targetKind, targetId = '') {
69
+ const rows = this.raw
70
+ .prepare(`SELECT target_kind,target_id,catalog_entry_id,server_name,display_name,version,
71
+ enabled,runtime,configuration,installed_at
72
+ FROM mcp_installations WHERE target_kind=? AND target_id=? ORDER BY display_name,server_name`)
73
+ .all(targetKind, targetKind === 'NODE' ? '' : targetId);
74
+ return rows.map(mcpInstallation);
75
+ }
76
+ saveMcpInstallation(input) {
77
+ const entry = mcpCatalogEntrySchema.parse(input.entry);
78
+ const configuration = mcpInstallationConfigurationSchema.parse(input.configuration);
79
+ const targetId = input.targetKind === 'NODE' ? '' : (input.targetId ?? '');
80
+ if (input.targetKind === 'WORKSPACE' && this.getWorkspace(targetId) === undefined)
81
+ throw new Error('WORKSPACE_NOT_FOUND');
82
+ const installedAt = this.now();
83
+ this.raw
84
+ .prepare(`INSERT INTO mcp_installations
85
+ (target_kind,target_id,catalog_entry_id,server_name,display_name,version,enabled,runtime,configuration,installed_at)
86
+ VALUES (?,?,?,?,?,?,?,?,?,?)
87
+ ON CONFLICT(target_kind,target_id,server_name) DO UPDATE SET
88
+ catalog_entry_id=excluded.catalog_entry_id,display_name=excluded.display_name,
89
+ version=excluded.version,enabled=excluded.enabled,runtime=excluded.runtime,
90
+ configuration=excluded.configuration,installed_at=excluded.installed_at`)
91
+ .run(input.targetKind, targetId, entry.id, entry.serverName, entry.displayName, entry.version, input.enabled ? 1 : 0, JSON.stringify(entry.runtime), JSON.stringify(configuration), installedAt);
92
+ return {
93
+ targetKind: input.targetKind,
94
+ targetId,
95
+ catalogEntryId: entry.id,
96
+ serverName: entry.serverName,
97
+ displayName: entry.displayName,
98
+ version: entry.version,
99
+ enabled: input.enabled,
100
+ runtime: entry.runtime,
101
+ configuration,
102
+ installedAt
103
+ };
104
+ }
105
+ removeMcpInstallation(targetKind, targetId, serverName) {
106
+ return (Number(this.raw
107
+ .prepare('DELETE FROM mcp_installations WHERE target_kind=? AND target_id=? AND server_name=?')
108
+ .run(targetKind, targetKind === 'NODE' ? '' : targetId, serverName).changes) > 0);
109
+ }
110
+ effectiveMcpInstallations(workspaceId) {
111
+ const effective = new Map(this.listMcpInstallations('NODE').map((installation) => [
112
+ installation.serverName,
113
+ installation
114
+ ]));
115
+ for (const installation of this.listMcpInstallations('WORKSPACE', workspaceId))
116
+ effective.set(installation.serverName, installation);
117
+ return [...effective.values()].filter((installation) => installation.enabled);
118
+ }
66
119
  /**
67
120
  * Stores only terminal lifecycle metadata. PTY input/output must remain in
68
121
  * the Node-local bounded replay buffer and must never be passed here.
@@ -138,6 +191,9 @@ export class NodeDatabase {
138
191
  // record itself never represents a filesystem deletion.
139
192
  this.raw.prepare('DELETE FROM agent_sessions WHERE workspace_id = ?').run(id);
140
193
  this.raw.prepare('DELETE FROM legacy_agent_sessions WHERE workspace_id = ?').run(id);
194
+ this.raw
195
+ .prepare("DELETE FROM mcp_installations WHERE target_kind='WORKSPACE' AND target_id = ?")
196
+ .run(id);
141
197
  this.raw.prepare('DELETE FROM workspaces WHERE id = ?').run(id);
142
198
  this.raw.exec('COMMIT');
143
199
  return current;
@@ -1028,6 +1084,8 @@ function migrate(database) {
1028
1084
  database.exec('COMMIT');
1029
1085
  migrateNodeSchemaV3(database);
1030
1086
  assertNodeSchemaV3(database);
1087
+ migrateNodeSchemaV4(database);
1088
+ assertNodeSchemaV4(database);
1031
1089
  return;
1032
1090
  }
1033
1091
  catch (error) {
@@ -1066,6 +1124,8 @@ function migrate(database) {
1066
1124
  assertNodeSchemaV2(database);
1067
1125
  migrateNodeSchemaV3(database);
1068
1126
  assertNodeSchemaV3(database);
1127
+ migrateNodeSchemaV4(database);
1128
+ assertNodeSchemaV4(database);
1069
1129
  return;
1070
1130
  }
1071
1131
  if (current === 0) {
@@ -1092,7 +1152,29 @@ function migrate(database) {
1092
1152
  .get();
1093
1153
  if (numberValue(afterV2, 'version') === 2)
1094
1154
  migrateNodeSchemaV3(database);
1155
+ const afterV3 = database
1156
+ .prepare('SELECT MAX(version) AS version FROM node_schema_migrations')
1157
+ .get();
1158
+ if (numberValue(afterV3, 'version') === 3)
1159
+ migrateNodeSchemaV4(database);
1095
1160
  assertNodeSchemaV3(database);
1161
+ assertNodeSchemaV4(database);
1162
+ }
1163
+ function mcpInstallation(row) {
1164
+ const runtime = mcpRuntimeDescriptorSchema.parse(JSON.parse(stringValue(row, 'runtime')));
1165
+ const configuration = mcpInstallationConfigurationSchema.parse(JSON.parse(stringValue(row, 'configuration')));
1166
+ return {
1167
+ targetKind: stringValue(row, 'target_kind'),
1168
+ targetId: stringValue(row, 'target_id'),
1169
+ catalogEntryId: stringValue(row, 'catalog_entry_id'),
1170
+ serverName: stringValue(row, 'server_name'),
1171
+ displayName: stringValue(row, 'display_name'),
1172
+ version: stringValue(row, 'version'),
1173
+ enabled: numberValue(row, 'enabled') === 1,
1174
+ runtime,
1175
+ configuration,
1176
+ installedAt: numberValue(row, 'installed_at')
1177
+ };
1096
1178
  }
1097
1179
  function sessionMetadata(row) {
1098
1180
  const raw = JSON.parse(stringValue(row, 'metadata'));
@@ -0,0 +1,4 @@
1
+ import type { DatabaseSync } from 'node:sqlite';
2
+ export declare const NODE_SCHEMA_VERSION_V4 = 4;
3
+ export declare function migrateNodeSchemaV4(database: DatabaseSync): void;
4
+ export declare function assertNodeSchemaV4(database: DatabaseSync): void;
@@ -0,0 +1,35 @@
1
+ export const NODE_SCHEMA_VERSION_V4 = 4;
2
+ export function migrateNodeSchemaV4(database) {
3
+ database.exec('BEGIN IMMEDIATE');
4
+ try {
5
+ database.exec(`CREATE TABLE mcp_installations (
6
+ target_kind TEXT NOT NULL CHECK (target_kind IN ('NODE','WORKSPACE')),
7
+ target_id TEXT NOT NULL,
8
+ catalog_entry_id TEXT NOT NULL,
9
+ server_name TEXT NOT NULL,
10
+ display_name TEXT NOT NULL,
11
+ version TEXT NOT NULL,
12
+ enabled INTEGER NOT NULL CHECK (enabled IN (0,1)),
13
+ runtime TEXT NOT NULL CHECK (json_valid(runtime)),
14
+ configuration TEXT NOT NULL CHECK (json_valid(configuration)),
15
+ installed_at INTEGER NOT NULL,
16
+ PRIMARY KEY (target_kind,target_id,server_name),
17
+ CHECK ((target_kind='NODE' AND target_id='') OR (target_kind='WORKSPACE' AND target_id<>''))
18
+ ) STRICT;`);
19
+ database
20
+ .prepare('INSERT INTO node_schema_migrations (version, applied_at) VALUES (?, ?)')
21
+ .run(NODE_SCHEMA_VERSION_V4, Date.now());
22
+ database.exec('COMMIT');
23
+ }
24
+ catch (error) {
25
+ database.exec('ROLLBACK');
26
+ throw error;
27
+ }
28
+ }
29
+ export function assertNodeSchemaV4(database) {
30
+ const row = database
31
+ .prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='mcp_installations'")
32
+ .get();
33
+ if (typeof row?.sql !== 'string' || !row.sql.includes("target_kind IN ('NODE','WORKSPACE')"))
34
+ throw new Error('NODE_DATABASE_SCHEMA_INVALID:mcp_installations');
35
+ }
@@ -16,6 +16,7 @@ export declare class OpenCodeServerClient {
16
16
  start(): Promise<void>;
17
17
  private startServer;
18
18
  providers(directory: string): Promise<OpenCodeProviderCatalog>;
19
+ configureMcp(directory: string, mcp: unknown): Promise<void>;
19
20
  sessions(directory: string, limit?: number): Promise<readonly unknown[]>;
20
21
  sessionStatus(directory: string): Promise<Readonly<Record<string, unknown>>>;
21
22
  createSession(directory: string, title?: string): Promise<unknown>;
@@ -77,6 +77,18 @@ export class OpenCodeServerClient {
77
77
  const client = await this.requireClient();
78
78
  return unwrap(await client.provider.list({ directory }));
79
79
  }
80
+ async configureMcp(directory, mcp) {
81
+ const client = await this.requireClient();
82
+ if (typeof mcp !== 'object' || mcp === null || Array.isArray(mcp))
83
+ return;
84
+ for (const [name, config] of Object.entries(mcp)) {
85
+ await client.mcp.add({
86
+ directory,
87
+ name,
88
+ config: config
89
+ });
90
+ }
91
+ }
80
92
  async sessions(directory, limit = 100) {
81
93
  const client = await this.requireClient();
82
94
  return unwrap(await client.session.list({ directory, limit }));
@@ -21,14 +21,15 @@ export declare abstract class AbstractRunner<TName extends RegisteredRunnerName
21
21
  abstract profile(capabilities: NodeCapabilities): RunnerProfile | undefined;
22
22
  refreshProfile(capabilities: NodeCapabilities, workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): Promise<RunnerProfile | undefined>;
23
23
  abstract available(capabilities: NodeCapabilities): boolean;
24
- abstract defaultConfiguration(workspace?: NodeWorkspace): RunnerDefaultConfiguration;
25
- supportsConfiguration(configuration: RunnerDefaultConfiguration, workspace?: NodeWorkspace): boolean;
24
+ abstract defaultConfiguration(workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): RunnerDefaultConfiguration;
25
+ supportsConfiguration(configuration: RunnerDefaultConfiguration, workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): boolean;
26
26
  protected abstract profileForValidation(): RunnerProfile;
27
27
  abstract resumeExternal(session: NodeAgentSession, context: ExternalResumeContext, force?: boolean): Promise<ExternalResumeResult>;
28
28
  abstract discoverSessions(context: RunnerDiscoveryContext, workspace?: NodeWorkspace): Promise<readonly NodeAgentSession[]>;
29
29
  abstract readContextUsage(session: NodeAgentSession): Promise<RunnerContextUsage | undefined>;
30
30
  presentSession(session: NodeAgentSession, capabilities: NodeCapabilities, context: RunnerSessionPresentationContext): RunnerSessionPresentation;
31
31
  prepareMessageInput(input: string, collaborationMode: 'default' | 'plan'): string;
32
+ mcpConfiguration(rawInstallations: unknown, secretEnvironment: Readonly<Record<string, string>>): unknown;
32
33
  channelToken(sessionId: string): string | undefined;
33
34
  deliverChannel(sessionId: string, messageId: string, senderId: string, token: string, content: string, notify: (runId: string, messageId: string, delivery: ChannelDelivery) => void): ChannelDelivery;
34
35
  executeGlobalCommand(commandId: string, input: string): unknown;
@@ -66,6 +67,15 @@ export declare abstract class AbstractRunner<TName extends RegisteredRunnerName
66
67
  readonly secretEnvironment: Readonly<Record<string, string>>;
67
68
  }): Promise<unknown>;
68
69
  }
70
+ export interface ResolvedMcpServer {
71
+ readonly name: string;
72
+ readonly transport: 'STDIO' | 'HTTP';
73
+ readonly command?: string;
74
+ readonly args?: readonly string[];
75
+ readonly environment?: Readonly<Record<string, string>>;
76
+ readonly url?: string;
77
+ readonly headers?: Readonly<Record<string, string>>;
78
+ }
69
79
  export type RunnerCancellationIntent = 'CANCEL' | 'INTERRUPT';
70
80
  export interface RunnerControl {
71
81
  start(envelope: NodeEnvelope): void;
@@ -1,3 +1,4 @@
1
+ import { mcpRuntimeDescriptorSchema } from '@myagentroam/protocol';
1
2
  /**
2
3
  * Stable lifecycle boundary for every Node Runner implementation.
3
4
  *
@@ -15,8 +16,8 @@ export class AbstractRunner {
15
16
  void [workspace, environment];
16
17
  return this.profile(capabilities);
17
18
  }
18
- supportsConfiguration(configuration, workspace) {
19
- void workspace;
19
+ supportsConfiguration(configuration, workspace, environment) {
20
+ void [workspace, environment];
20
21
  const profile = this.profileForValidation();
21
22
  const model = profile.models.find((candidate) => candidate.id === configuration.model);
22
23
  return (configuration.runner === this.name &&
@@ -32,6 +33,9 @@ export class AbstractRunner {
32
33
  void collaborationMode;
33
34
  return input;
34
35
  }
36
+ mcpConfiguration(rawInstallations, secretEnvironment) {
37
+ return resolvedMcpServers(rawInstallations, secretEnvironment);
38
+ }
35
39
  channelToken(sessionId) {
36
40
  void sessionId;
37
41
  return undefined;
@@ -143,6 +147,76 @@ export class AbstractRunner {
143
147
  return this.commandControl.execute(session, command, input, context);
144
148
  }
145
149
  }
150
+ function resolvedMcpServers(raw, secrets) {
151
+ if (!Array.isArray(raw))
152
+ return [];
153
+ const result = [];
154
+ for (const value of raw) {
155
+ if (value === null || typeof value !== 'object' || Array.isArray(value))
156
+ continue;
157
+ const record = value;
158
+ if (typeof record['serverName'] !== 'string' || record['enabled'] !== true)
159
+ continue;
160
+ const runtime = mcpRuntimeDescriptorSchema.safeParse(record['runtime']);
161
+ if (!runtime.success)
162
+ continue;
163
+ const configuration = isRecord(record['configuration']) ? record['configuration'] : {};
164
+ const resolveReferences = (candidate) => {
165
+ if (!isRecord(candidate))
166
+ return {};
167
+ const result = {};
168
+ for (const [name, reference] of Object.entries(candidate)) {
169
+ if (!isRecord(reference) || typeof reference['secretName'] !== 'string')
170
+ return undefined;
171
+ const secret = secrets[reference['secretName']];
172
+ if (secret === undefined)
173
+ return undefined;
174
+ result[name] = secret;
175
+ }
176
+ return result;
177
+ };
178
+ if (runtime.data.transport === 'STDIO') {
179
+ const runtimeEnvironment = resolveReferences(runtime.data.environment);
180
+ const configuredEnvironment = resolveReferences(configuration['environment']);
181
+ if (runtimeEnvironment === undefined || configuredEnvironment === undefined)
182
+ continue;
183
+ result.push({
184
+ name: record['serverName'],
185
+ transport: 'STDIO',
186
+ command: runtime.data.command,
187
+ args: [
188
+ ...runtime.data.args,
189
+ ...(Array.isArray(configuration['arguments'])
190
+ ? configuration['arguments'].filter((item) => typeof item === 'string')
191
+ : [])
192
+ ],
193
+ environment: {
194
+ ...runtimeEnvironment,
195
+ ...configuredEnvironment
196
+ }
197
+ });
198
+ }
199
+ else {
200
+ const runtimeHeaders = resolveReferences(runtime.data.headers);
201
+ const configuredHeaders = resolveReferences(configuration['headers']);
202
+ if (runtimeHeaders === undefined || configuredHeaders === undefined)
203
+ continue;
204
+ result.push({
205
+ name: record['serverName'],
206
+ transport: 'HTTP',
207
+ url: runtime.data.url,
208
+ headers: {
209
+ ...runtimeHeaders,
210
+ ...configuredHeaders
211
+ }
212
+ });
213
+ }
214
+ }
215
+ return result;
216
+ }
217
+ function isRecord(value) {
218
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
219
+ }
146
220
  export class RunnerCapabilityError extends Error {
147
221
  runner;
148
222
  capability;
@@ -27,7 +27,7 @@ export class ClaudeManagedRunController {
27
27
  detail: '后续消息将以标签式规划模式运行,只读规划并输出计划卡。',
28
28
  closable: true,
29
29
  closeInput: '/plan',
30
- continuation: { label: 'Implement', prompt: '请根据上述计划开始实施。' }
30
+ continuation: { label: '执行', prompt: '请根据上述计划开始实施。' }
31
31
  });
32
32
  return { command: command.id, states: this.host.commandStates(session.id) };
33
33
  }
@@ -138,6 +138,7 @@ export class ClaudeManagedRunController {
138
138
  ...(typeof payload.effort === 'string' ? { effort: payload.effort } : {}),
139
139
  ...(typeof payload.access === 'string' ? { access: payload.access } : {}),
140
140
  environment: secretEnvironment,
141
+ mcpServers: this.runner.mcpConfiguration(payload.mcpInstallations, secretEnvironment),
141
142
  ...(attachments.length === 0
142
143
  ? {}
143
144
  : {
@@ -17,6 +17,7 @@ export declare class ClaudeCodeRunner extends AbstractRunner<'claude-code'> {
17
17
  available(capabilities: NodeCapabilities): boolean;
18
18
  protected profileForValidation(): RunnerProfile;
19
19
  defaultConfiguration(): RunnerDefaultConfiguration;
20
+ mcpConfiguration(raw: unknown, secrets: Readonly<Record<string, string>>): unknown;
20
21
  resumeExternal(external: NodeAgentSession, context: ExternalResumeContext): Promise<ExternalResumeResult>;
21
22
  discoverSessions(context: RunnerDiscoveryContext, workspace?: NodeWorkspace): Promise<readonly NodeAgentSession[]>;
22
23
  readContextUsage(session: NodeAgentSession): Promise<import("../native-session-history.js").NativeClaudeContextUsage | undefined>;
@@ -45,6 +45,20 @@ export class ClaudeCodeRunner extends AbstractRunner {
45
45
  access: 'default'
46
46
  };
47
47
  }
48
+ mcpConfiguration(raw, secrets) {
49
+ const servers = super.mcpConfiguration(raw, secrets);
50
+ return Object.fromEntries(servers.map((server) => [
51
+ server.name,
52
+ server.transport === 'STDIO'
53
+ ? {
54
+ type: 'stdio',
55
+ command: server.command,
56
+ args: [...(server.args ?? [])],
57
+ env: { ...(server.environment ?? {}) }
58
+ }
59
+ : { type: 'http', url: server.url, headers: { ...(server.headers ?? {}) } }
60
+ ]));
61
+ }
48
62
  async resumeExternal(external, context) {
49
63
  if (external.runner !== this.name ||
50
64
  external.nativeControl !== 'EXTERNAL' ||
@@ -35,7 +35,7 @@ export class CodexManagedRunController {
35
35
  detail: '后续消息将以协作规划模式运行。',
36
36
  closable: true,
37
37
  closeInput: '/plan',
38
- continuation: { label: 'Implement', prompt: '请根据上述计划开始实施。' }
38
+ continuation: { label: '执行', prompt: '请根据上述计划开始实施。' }
39
39
  });
40
40
  return { command: command.id, states: this.host.commandStates(session.id) };
41
41
  }
@@ -325,7 +325,8 @@ export class CodexManagedRunController {
325
325
  effort: payload.effort,
326
326
  access: payload.access,
327
327
  collaborationMode: payload.collaborationMode,
328
- serviceTier: payload.serviceTier
328
+ serviceTier: payload.serviceTier,
329
+ mcpServers: this.runner.mcpConfiguration(payload.mcpInstallations, secretEnvironment)
329
330
  }, attachments);
330
331
  }
331
332
  async run(payload, attachments) {
@@ -340,7 +341,8 @@ export class CodexManagedRunController {
340
341
  const configuration = {
341
342
  ...(typeof payload.model === 'string' ? { model: payload.model } : {}),
342
343
  ...(typeof payload.effort === 'string' ? { effort: payload.effort } : {}),
343
- ...(typeof payload.access === 'string' ? { access: payload.access } : {})
344
+ ...(typeof payload.access === 'string' ? { access: payload.access } : {}),
345
+ ...(payload.mcpServers === undefined ? {} : { mcpServers: payload.mcpServers })
344
346
  };
345
347
  const threadResult = typeof payload.externalSessionId === 'string'
346
348
  ? await this.runner.resumeThread(payload.externalSessionId, cwd, configuration)
@@ -5,17 +5,29 @@ import type { NodeAgentSession, NodeDatabase } from '../database.js';
5
5
  import type { NodeWorkspace } from '../database.js';
6
6
  export declare class CodexRunner extends AbstractRunner<'codex'> {
7
7
  private readonly client;
8
+ private readonly profileClientFactory;
8
9
  readonly name: "codex";
9
10
  readonly discoveryScope: "ALL_WORKSPACES";
10
11
  readonly execution: CodexExecutionState;
11
12
  private fastEnabled;
12
13
  private environmentFingerprint;
13
14
  private readonly environmentRuns;
14
- constructor(client?: CodexAppServerClient);
15
+ private readonly profileCache;
16
+ private readonly profileRefreshes;
17
+ private readonly profileClients;
18
+ private readonly profileKeyByEnvironment;
19
+ private latestProfileCache;
20
+ private selectedProfileKey;
21
+ constructor(client?: CodexAppServerClient, profileClientFactory?: () => CodexAppServerClient);
15
22
  profile(capabilities: NodeCapabilities): RunnerProfile;
23
+ refreshProfile(capabilities: NodeCapabilities, workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): Promise<RunnerProfile>;
24
+ private refreshCodexProfile;
25
+ private selectCachedProfile;
26
+ private fetchCodexProfile;
16
27
  available(capabilities: NodeCapabilities): boolean;
17
28
  executeGlobalCommand(commandId: string, input: string): unknown;
18
29
  serviceTier(): 'fast' | undefined;
30
+ mcpConfiguration(raw: unknown, secrets: Readonly<Record<string, string>>): unknown;
19
31
  usesRunnerTitleForRename(): boolean;
20
32
  renameNative(session: NodeAgentSession, input: {
21
33
  readonly title?: string | null;
@@ -35,7 +47,9 @@ export declare class CodexRunner extends AbstractRunner<'codex'> {
35
47
  nativeImageRoot(session: NodeAgentSession): string | undefined;
36
48
  managedRunForNativeTurn(nativeTurnId: string): string | undefined;
37
49
  protected profileForValidation(): RunnerProfile;
38
- defaultConfiguration(): RunnerDefaultConfiguration;
50
+ defaultConfiguration(_workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): RunnerDefaultConfiguration;
51
+ supportsConfiguration(configuration: RunnerDefaultConfiguration, _workspace?: NodeWorkspace, environment?: Readonly<Record<string, string>>): boolean;
52
+ private profileCacheForEnvironment;
39
53
  resumeExternal(external: NodeAgentSession, context: ExternalResumeContext, force?: boolean): Promise<ExternalResumeResult>;
40
54
  discoverSessions(context: RunnerDiscoveryContext, workspace?: NodeWorkspace): Promise<readonly NodeAgentSession[]>;
41
55
  readContextUsage(session: NodeAgentSession): Promise<import("../native-session-history.js").NativeCodexContextUsage | undefined>;