@ai-sdk/harness-acp 1.0.1 → 1.0.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai-sdk/harness-acp",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "license": "Apache-2.0",
6
6
  "sideEffects": false,
@@ -27,8 +27,8 @@
27
27
  },
28
28
  "dependencies": {
29
29
  "ws": "^8.21.0",
30
- "@ai-sdk/harness": "1.0.64",
31
- "@ai-sdk/provider-utils": "5.0.25"
30
+ "@ai-sdk/harness": "1.0.65",
31
+ "@ai-sdk/provider-utils": "5.0.26"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "zod": "^3.25.76 || ^4.1.8"
@@ -2,6 +2,7 @@ import type { HarnessV1 } from '@ai-sdk/harness';
2
2
  import type { ToolSet } from '@ai-sdk/provider-utils';
3
3
  import { z } from 'zod/v4';
4
4
  import type { ACPClientApp } from './acp-auth';
5
+ import type { ACPToolCall } from './acp-tool-call';
5
6
  import { createACPV1, type ACPV1Settings } from './v1';
6
7
  import {
7
8
  acpColdSessionStateSchema,
@@ -16,6 +17,12 @@ const ACP_CLIENT_APP = {
16
17
 
17
18
  export type ACPHarnessSettings<TBuiltinTools extends ToolSet = {}> = {
18
19
  readonly builtinTools?: TBuiltinTools;
20
+ /**
21
+ * MCP server definitions keyed by server name. Each definition uses the
22
+ * underlying runtime's native MCP server configuration format.
23
+ */
24
+ readonly mcpServers?: Record<string, unknown>;
25
+ readonly isMcpToolCall?: (toolCall: ACPToolCall) => boolean;
19
26
  readonly port?: number;
20
27
  readonly startupTimeoutMs?: number;
21
28
  readonly clientApp?: ACPClientApp;
@@ -32,6 +39,7 @@ export type ACPHarnessSettings<TBuiltinTools extends ToolSet = {}> = {
32
39
  readonly modelId?: ACPV1Settings['modelId'];
33
40
  readonly permissionModeMapping?: ACPV1Settings['permissionModeMapping'];
34
41
  readonly session?: ACPV1Settings['session'];
42
+ readonly mintBridgeToken?: ACPV1Settings['mintBridgeToken'];
35
43
  };
36
44
 
37
45
  const ACP_BUILTIN_TOOLS = {} as const satisfies ToolSet;
@@ -85,6 +93,17 @@ const acpResumeStateSchema = z.object({
85
93
  export function createACP<TBuiltinTools extends ToolSet = {}>(
86
94
  settings: ACPHarnessSettings<TBuiltinTools>,
87
95
  ): HarnessV1<TBuiltinTools> {
96
+ if (
97
+ settings.mcpServers != null &&
98
+ Object.prototype.hasOwnProperty.call(
99
+ settings.mcpServers,
100
+ 'ai-sdk-harness-tools',
101
+ )
102
+ ) {
103
+ throw new Error(
104
+ 'ACP MCP server name "ai-sdk-harness-tools" is reserved for HarnessAgent tools.',
105
+ );
106
+ }
88
107
  const version = (settings as { readonly version?: string }).version ?? 'v1';
89
108
  switch (version) {
90
109
  case 'v1': {
@@ -0,0 +1,124 @@
1
+ export type ACPMetadata = Record<string, unknown>;
2
+
3
+ export type ACPRole = 'assistant' | 'user';
4
+
5
+ export type ACPAnnotations = {
6
+ audience?: Array<ACPRole> | null;
7
+ lastModified?: string | null;
8
+ priority?: number | null;
9
+ _meta?: ACPMetadata | null;
10
+ };
11
+
12
+ export type ACPTextContent = {
13
+ annotations?: ACPAnnotations | null;
14
+ text: string;
15
+ _meta?: ACPMetadata | null;
16
+ };
17
+
18
+ export type ACPImageContent = {
19
+ annotations?: ACPAnnotations | null;
20
+ data: string;
21
+ mimeType: string;
22
+ uri?: string | null;
23
+ _meta?: ACPMetadata | null;
24
+ };
25
+
26
+ export type ACPAudioContent = {
27
+ annotations?: ACPAnnotations | null;
28
+ data: string;
29
+ mimeType: string;
30
+ _meta?: ACPMetadata | null;
31
+ };
32
+
33
+ export type ACPResourceLink = {
34
+ annotations?: ACPAnnotations | null;
35
+ description?: string | null;
36
+ mimeType?: string | null;
37
+ name: string;
38
+ size?: number | null;
39
+ title?: string | null;
40
+ uri: string;
41
+ _meta?: ACPMetadata | null;
42
+ };
43
+
44
+ export type ACPTextResourceContents = {
45
+ mimeType?: string | null;
46
+ text: string;
47
+ uri: string;
48
+ _meta?: ACPMetadata | null;
49
+ };
50
+
51
+ export type ACPBlobResourceContents = {
52
+ blob: string;
53
+ mimeType?: string | null;
54
+ uri: string;
55
+ _meta?: ACPMetadata | null;
56
+ };
57
+
58
+ export type ACPEmbeddedResource = {
59
+ annotations?: ACPAnnotations | null;
60
+ resource: ACPTextResourceContents | ACPBlobResourceContents;
61
+ _meta?: ACPMetadata | null;
62
+ };
63
+
64
+ export type ACPContentBlock =
65
+ | (ACPTextContent & { type: 'text' })
66
+ | (ACPImageContent & { type: 'image' })
67
+ | (ACPAudioContent & { type: 'audio' })
68
+ | (ACPResourceLink & { type: 'resource_link' })
69
+ | (ACPEmbeddedResource & { type: 'resource' });
70
+
71
+ export type ACPToolCallContent =
72
+ | {
73
+ type: 'content';
74
+ content: ACPContentBlock;
75
+ _meta?: ACPMetadata | null;
76
+ }
77
+ | {
78
+ type: 'diff';
79
+ path: string;
80
+ oldText?: string | null;
81
+ newText: string;
82
+ _meta?: ACPMetadata | null;
83
+ }
84
+ | {
85
+ type: 'terminal';
86
+ terminalId: string;
87
+ _meta?: ACPMetadata | null;
88
+ };
89
+
90
+ export type ACPToolKind =
91
+ | 'read'
92
+ | 'edit'
93
+ | 'delete'
94
+ | 'move'
95
+ | 'search'
96
+ | 'execute'
97
+ | 'think'
98
+ | 'fetch'
99
+ | 'switch_mode'
100
+ | 'other';
101
+
102
+ export type ACPToolCallStatus =
103
+ | 'pending'
104
+ | 'in_progress'
105
+ | 'completed'
106
+ | 'failed';
107
+
108
+ export type ACPToolCallLocation = {
109
+ path: string;
110
+ line?: number | null;
111
+ _meta?: ACPMetadata | null;
112
+ };
113
+
114
+ export type ACPToolCall = {
115
+ toolCallId: string;
116
+ title: string;
117
+ kind?: ACPToolKind;
118
+ status?: ACPToolCallStatus;
119
+ content?: Array<ACPToolCallContent>;
120
+ locations?: Array<ACPToolCallLocation>;
121
+ rawInput?: unknown;
122
+ rawOutput?: unknown;
123
+ _meta?: ACPMetadata | null;
124
+ };
package/src/index.ts CHANGED
@@ -1,6 +1,24 @@
1
1
  export { createACP } from './acp-harness';
2
2
  export type { ACPAuthOptions, ACPClientApp } from './acp-auth';
3
3
  export type { ACPHarnessSettings } from './acp-harness';
4
+ export type {
5
+ ACPAnnotations,
6
+ ACPAudioContent,
7
+ ACPBlobResourceContents,
8
+ ACPContentBlock,
9
+ ACPEmbeddedResource,
10
+ ACPImageContent,
11
+ ACPMetadata,
12
+ ACPResourceLink,
13
+ ACPRole,
14
+ ACPTextContent,
15
+ ACPTextResourceContents,
16
+ ACPToolCall,
17
+ ACPToolCallContent,
18
+ ACPToolCallLocation,
19
+ ACPToolCallStatus,
20
+ ACPToolKind,
21
+ } from './acp-tool-call';
4
22
  export type {
5
23
  ACPAuthentication,
6
24
  ACPNpmLockedSource,
@@ -11,6 +11,7 @@ import {
11
11
  type HarnessV1ToolSpec,
12
12
  } from '@ai-sdk/harness';
13
13
  import { z } from 'zod/v4';
14
+ import type { ACPToolCall } from '../acp-tool-call';
14
15
  import type { ACPPermissionModeMapping } from './acp-v1-settings';
15
16
  import { acpTextContentBlockSchema } from './acp-v1-prompt';
16
17
 
@@ -126,11 +127,26 @@ const coldRestoreSchema = z.object({
126
127
  acpSessionId: z.string(),
127
128
  });
128
129
 
129
- export const outboundMessageSchema = harnessV1BridgeOutboundMessageSchema;
130
+ const acpToolCallCandidateSchema = z.object({
131
+ type: z.literal('acp-tool-call-candidate'),
132
+ toolCall: z.custom<ACPToolCall>(
133
+ value =>
134
+ value != null &&
135
+ typeof value === 'object' &&
136
+ typeof (value as { toolCallId?: unknown }).toolCallId === 'string' &&
137
+ typeof (value as { title?: unknown }).title === 'string',
138
+ ),
139
+ });
140
+
141
+ export const outboundMessageSchema = z.union([
142
+ harnessV1BridgeOutboundMessageSchema,
143
+ acpToolCallCandidateSchema,
144
+ ]);
130
145
  export type OutboundMessage = z.infer<typeof outboundMessageSchema>;
131
146
 
132
147
  export const startMessageSchema = harnessV1BridgeStartBaseSchema.extend({
133
148
  prompt: z.array(acpTextContentBlockSchema),
149
+ mcpServers: z.record(z.string(), z.unknown()).optional(),
134
150
  tools: z.array(acpSerializableToolSpecSchema).optional(),
135
151
  builtinTools: z.array(builtinToolSchema).readonly().default([]),
136
152
  permissionModeMapping: permissionModeMappingSchema.optional(),
@@ -40,6 +40,7 @@ import {
40
40
  type ACPAuthenticationProfileIdentity,
41
41
  type ACPClientApp,
42
42
  } from '../acp-auth';
43
+ import type { ACPToolCall } from '../acp-tool-call';
43
44
  import {
44
45
  createACPV1Implementation,
45
46
  createImplementationDescriptor,
@@ -126,6 +127,17 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
126
127
  HarnessV1<TBuiltinTools>['lifecycleStateSchema']
127
128
  >;
128
129
  }): HarnessV1<TBuiltinTools> {
130
+ if (
131
+ settings.mcpServers != null &&
132
+ Object.prototype.hasOwnProperty.call(
133
+ settings.mcpServers,
134
+ 'ai-sdk-harness-tools',
135
+ )
136
+ ) {
137
+ throw new Error(
138
+ 'ACP MCP server name "ai-sdk-harness-tools" is reserved for HarnessAgent tools.',
139
+ );
140
+ }
129
141
  if (!HARNESS_ID_REGEXP.test(settings.harnessId)) {
130
142
  throw new Error(
131
143
  `ACP harnessId must be a stable kebab-case identifier; received ${JSON.stringify(settings.harnessId)}.`,
@@ -370,6 +382,8 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
370
382
  builtinTools: builtinToolCatalog,
371
383
  permissionMode,
372
384
  permissionModeMapping,
385
+ mcpServers: settings.mcpServers,
386
+ isMcpToolCall: settings.isMcpToolCall,
373
387
  initialGuidanceApplied: resolveACPInitialGuidanceApplied({
374
388
  isResume: true,
375
389
  lifecycleState: lifecycleData,
@@ -421,6 +435,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
421
435
  sessionMeta: settings.session?.meta,
422
436
  builtinTools: builtinToolCatalog,
423
437
  permissionModeMapping,
438
+ mcpServers: settings.mcpServers,
424
439
  });
425
440
  respawnStrategy = {
426
441
  mode: 'lossy-rerun',
@@ -450,6 +465,7 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
450
465
  sessionMeta: settings.session?.meta,
451
466
  builtinTools: builtinToolCatalog,
452
467
  permissionModeMapping,
468
+ mcpServers: settings.mcpServers,
453
469
  debug: startOptions.observability?.debug,
454
470
  });
455
471
  respawnStrategy = {
@@ -465,7 +481,10 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
465
481
  override: portOverride,
466
482
  harnessId: settings.harnessId,
467
483
  });
468
- const token = randomBytes(32).toString('hex');
484
+ const token =
485
+ settings.mintBridgeToken == null
486
+ ? randomBytes(32).toString('hex')
487
+ : settings.mintBridgeToken(sandboxSession.id);
469
488
  await sandbox.run({
470
489
  command: `mkdir -p ${shellQuote(workDir)} ${shellQuote(bridgeStateDir)}`,
471
490
  abortSignal: startOptions.abortSignal,
@@ -570,6 +589,9 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
570
589
  builtinTools: builtinToolCatalog,
571
590
  permissionMode,
572
591
  permissionModeMapping,
592
+ ...(settings.mcpServers == null
593
+ ? {}
594
+ : { mcpServers: settings.mcpServers }),
573
595
  tools: respawnStrategy.turnStartConfig.tools,
574
596
  turnStartConfig: respawnStrategy.turnStartConfig,
575
597
  recoveryMode: {
@@ -606,6 +628,8 @@ export function createACPV1<TBuiltinTools extends ToolSet = {}>({
606
628
  builtinTools: builtinToolCatalog,
607
629
  permissionMode,
608
630
  permissionModeMapping,
631
+ mcpServers: settings.mcpServers,
632
+ isMcpToolCall: settings.isMcpToolCall,
609
633
  initialGuidanceApplied: resolveACPInitialGuidanceApplied({
610
634
  isResume,
611
635
  lifecycleState: lifecycleData,
@@ -807,6 +831,8 @@ function createSession({
807
831
  builtinTools,
808
832
  permissionMode,
809
833
  permissionModeMapping,
834
+ mcpServers,
835
+ isMcpToolCall,
810
836
  initialGuidanceApplied: initialGuidanceAppliedAtStart,
811
837
  skillCatalog,
812
838
  skillsFingerprint,
@@ -835,6 +861,8 @@ function createSession({
835
861
  builtinTools: ReadonlyArray<ACPBuiltinToolMapping>;
836
862
  permissionMode: NonNullable<StartMessage['permissionMode']>;
837
863
  permissionModeMapping: StartMessage['permissionModeMapping'];
864
+ mcpServers: Record<string, unknown> | undefined;
865
+ isMcpToolCall: ((toolCall: ACPToolCall) => boolean) | undefined;
838
866
  initialGuidanceApplied: boolean;
839
867
  skillCatalog: ReadonlyArray<ACPSkillCatalogEntry>;
840
868
  skillsFingerprint: string;
@@ -889,6 +917,8 @@ function createSession({
889
917
  let openBlock:
890
918
  | { readonly type: 'text' | 'reasoning'; readonly id: string }
891
919
  | undefined;
920
+ const dynamicToolCalls = new Map<string, boolean>();
921
+ const toolCallClassificationErrors = new Map<string, unknown>();
892
922
  const subscriptions: Array<() => void> = [];
893
923
  const forward = (event: HarnessV1StreamPart) => {
894
924
  if (event.type === 'text-start' || event.type === 'reasoning-start') {
@@ -923,9 +953,7 @@ function createSession({
923
953
  'reasoning-start',
924
954
  'reasoning-delta',
925
955
  'reasoning-end',
926
- 'tool-call',
927
956
  'tool-approval-request',
928
- 'tool-result',
929
957
  'file-change',
930
958
  'finish-step',
931
959
  'raw',
@@ -954,6 +982,45 @@ function createSession({
954
982
  settle({ error: abortError });
955
983
  }
956
984
  };
985
+ subscriptions.push(
986
+ channel.on('acp-tool-call-candidate', event => {
987
+ try {
988
+ dynamicToolCalls.set(
989
+ event.toolCall.toolCallId,
990
+ isMcpToolCall?.(event.toolCall) === true,
991
+ );
992
+ } catch (error) {
993
+ toolCallClassificationErrors.set(event.toolCall.toolCallId, error);
994
+ }
995
+ }),
996
+ );
997
+ subscriptions.push(
998
+ channel.on('tool-call', event => {
999
+ if (toolCallClassificationErrors.has(event.toolCallId)) {
1000
+ const error = toolCallClassificationErrors.get(event.toolCallId);
1001
+ closeForwardedBlock();
1002
+ forward({ type: 'error', error });
1003
+ try {
1004
+ channel.send({ type: 'abort' });
1005
+ } catch {}
1006
+ settle({ error });
1007
+ return;
1008
+ }
1009
+ forward(
1010
+ dynamicToolCalls.get(event.toolCallId) === true
1011
+ ? { ...event, dynamic: true }
1012
+ : event,
1013
+ );
1014
+ }),
1015
+ );
1016
+ subscriptions.push(
1017
+ channel.on('tool-result', event => {
1018
+ const dynamic = dynamicToolCalls.get(event.toolCallId) === true;
1019
+ dynamicToolCalls.delete(event.toolCallId);
1020
+ toolCallClassificationErrors.delete(event.toolCallId);
1021
+ forward(dynamic ? { ...event, dynamic: true } : event);
1022
+ }),
1023
+ );
957
1024
  for (const type of eventTypes) {
958
1025
  subscriptions.push(channel.on(type, event => forward(event)));
959
1026
  }
@@ -1110,6 +1177,7 @@ function createSession({
1110
1177
  builtinTools,
1111
1178
  permissionMode,
1112
1179
  permissionModeMapping,
1180
+ mcpServers,
1113
1181
  debug,
1114
1182
  authenticationProfile,
1115
1183
  sessionMeta,
@@ -1137,6 +1205,7 @@ function createSession({
1137
1205
  builtinTools,
1138
1206
  permissionMode,
1139
1207
  permissionModeMapping,
1208
+ ...(mcpServers == null ? {} : { mcpServers }),
1140
1209
  tools: options.tools == null ? undefined : turnStartConfig.tools,
1141
1210
  turnStartConfig,
1142
1211
  });
@@ -1174,6 +1243,7 @@ function createSession({
1174
1243
  builtinTools: turnStartConfig.builtinTools,
1175
1244
  permissionMode: turnStartConfig.permissionMode,
1176
1245
  permissionModeMapping: turnStartConfig.permissionModeMapping,
1246
+ ...(mcpServers == null ? {} : { mcpServers }),
1177
1247
  tools: turnStartConfig.tools,
1178
1248
  turnStartConfig,
1179
1249
  recoveryMode: {
@@ -1338,12 +1408,14 @@ function validateACPTurnStartConfig({
1338
1408
  sessionMeta,
1339
1409
  builtinTools,
1340
1410
  permissionModeMapping,
1411
+ mcpServers,
1341
1412
  }: {
1342
1413
  turnStartConfig: ACPTurnStartConfig;
1343
1414
  authenticationProfile: ACPAuthenticationProfileIdentity;
1344
1415
  sessionMeta: Readonly<Record<string, ACPSerializableValue>> | undefined;
1345
1416
  builtinTools: ReadonlyArray<ACPBuiltinToolMapping>;
1346
1417
  permissionModeMapping: ACPPermissionModeMapping | undefined;
1418
+ mcpServers: Record<string, unknown> | undefined;
1347
1419
  }): void {
1348
1420
  const current = createACPTurnStartConfig({
1349
1421
  prompt: turnStartConfig.prompt,
@@ -1351,6 +1423,7 @@ function validateACPTurnStartConfig({
1351
1423
  builtinTools,
1352
1424
  permissionMode: turnStartConfig.permissionMode,
1353
1425
  permissionModeMapping,
1426
+ mcpServers,
1354
1427
  debug: turnStartConfig.debug,
1355
1428
  authenticationProfile,
1356
1429
  sessionMeta,
@@ -1373,6 +1446,7 @@ function validateACPColdSessionConfiguration({
1373
1446
  sessionMeta,
1374
1447
  builtinTools,
1375
1448
  permissionModeMapping,
1449
+ mcpServers,
1376
1450
  debug,
1377
1451
  }: {
1378
1452
  coldSession: ACPColdSessionState;
@@ -1382,6 +1456,7 @@ function validateACPColdSessionConfiguration({
1382
1456
  sessionMeta: Readonly<Record<string, ACPSerializableValue>> | undefined;
1383
1457
  builtinTools: ReadonlyArray<ACPBuiltinToolMapping>;
1384
1458
  permissionModeMapping: ACPPermissionModeMapping | undefined;
1459
+ mcpServers: Record<string, unknown> | undefined;
1385
1460
  debug: HarnessV1DebugConfig | undefined;
1386
1461
  }): ACPTurnStartConfig {
1387
1462
  const current = createACPTurnStartConfig({
@@ -1390,6 +1465,7 @@ function validateACPColdSessionConfiguration({
1390
1465
  builtinTools,
1391
1466
  permissionMode,
1392
1467
  permissionModeMapping,
1468
+ mcpServers,
1393
1469
  debug,
1394
1470
  authenticationProfile,
1395
1471
  sessionMeta,
@@ -1,4 +1,5 @@
1
1
  import type { HarnessV1PermissionMode } from '@ai-sdk/harness';
2
+ import type { ACPToolCall } from '../acp-tool-call';
2
3
 
3
4
  export type ACPSerializablePrimitive = string | number | boolean | null;
4
5
 
@@ -77,6 +78,8 @@ export type ACPPermissionModeMapping = Readonly<
77
78
  export type ACPV1Settings = {
78
79
  readonly version?: 'v1';
79
80
  readonly harnessId: string;
81
+ readonly mcpServers?: Record<string, unknown>;
82
+ readonly isMcpToolCall?: (toolCall: ACPToolCall) => boolean;
80
83
  readonly auth?: ACPProviderAuthenticationMode;
81
84
  readonly source: ACPSource;
82
85
  readonly executable: string;
@@ -94,4 +97,9 @@ export type ACPV1Settings = {
94
97
  readonly session?: {
95
98
  readonly meta?: Readonly<Record<string, ACPSerializableValue>>;
96
99
  };
100
+ /**
101
+ * Creates the authentication token used by the sandbox bridge. Defaults to
102
+ * a random 32-byte hexadecimal token.
103
+ */
104
+ readonly mintBridgeToken?: (sandboxId: string) => string;
97
105
  };
@@ -20,6 +20,7 @@ export function createACPTurnStartConfig({
20
20
  builtinTools,
21
21
  permissionMode,
22
22
  permissionModeMapping,
23
+ mcpServers,
23
24
  debug,
24
25
  authenticationProfile,
25
26
  sessionMeta,
@@ -29,6 +30,7 @@ export function createACPTurnStartConfig({
29
30
  builtinTools: ReadonlyArray<ACPBuiltinToolMapping>;
30
31
  permissionMode: NonNullable<StartMessage['permissionMode']>;
31
32
  permissionModeMapping: ACPPermissionModeMapping | undefined;
33
+ mcpServers: Record<string, unknown> | undefined;
32
34
  debug: HarnessV1DebugConfig | undefined;
33
35
  authenticationProfile: ACPAuthenticationProfileIdentity;
34
36
  sessionMeta: Readonly<Record<string, ACPSerializableValue>> | undefined;
@@ -42,6 +44,7 @@ export function createACPTurnStartConfig({
42
44
  sessionMeta: sessionMeta ?? null,
43
45
  builtinTools,
44
46
  permissionModeMapping: permissionModeMapping ?? null,
47
+ mcpServers: mcpServers ?? null,
45
48
  }),
46
49
  )
47
50
  .digest('hex'),
@@ -6,17 +6,20 @@ import type {
6
6
  HarnessV1BridgeToolWire,
7
7
  HarnessV1StreamPart,
8
8
  } from '@ai-sdk/harness';
9
+ import type { ACPToolCall } from '../../acp-tool-call';
9
10
  import type { ACPBuiltinToolMapping } from '../acp-v1-bridge-protocol';
10
11
  import { createACPStreamTranslator } from './stream-translator';
11
12
  import { createHostToolCorrelation } from './host-tool-correlation';
12
13
 
13
14
  export function createEmitStreamEvent({
14
15
  emit,
16
+ emitToolCallCandidate,
15
17
  builtinTools,
16
18
  hostToolServerName,
17
19
  hostTools,
18
20
  }: {
19
21
  emit: (event: HarnessV1StreamPart) => void;
22
+ emitToolCallCandidate: (options: { toolCall: ACPToolCall }) => void;
20
23
  builtinTools: ReadonlyArray<ACPBuiltinToolMapping>;
21
24
  hostToolServerName: string;
22
25
  hostTools: ReadonlyArray<HarnessV1BridgeToolWire>;
@@ -49,7 +52,11 @@ export function createEmitStreamEvent({
49
52
  }) => void;
50
53
  removeHostToolCorrelationInvocation: (options: { token: string }) => void;
51
54
  } {
52
- const translator = createACPStreamTranslator({ emit, builtinTools });
55
+ const translator = createACPStreamTranslator({
56
+ emit,
57
+ emitToolCallCandidate,
58
+ builtinTools,
59
+ });
53
60
  const correlation = createHostToolCorrelation({
54
61
  emitSemanticUpdate: ({ message, rawUpdate }) => {
55
62
  translator.update({
@@ -230,6 +230,12 @@ async function runTurn(start: StartMessage, turn: BridgeTurn): Promise<void> {
230
230
  turn.emit({ type: 'stream-start' });
231
231
  const emitStreamEvent = createEmitStreamEvent({
232
232
  emit: event => turn.emit(event as BridgeEvent),
233
+ emitToolCallCandidate: ({ toolCall }) => {
234
+ turn.emit({
235
+ type: 'acp-tool-call-candidate',
236
+ toolCall,
237
+ });
238
+ },
233
239
  builtinTools: start.builtinTools,
234
240
  hostToolServerName: HOST_TOOL_MCP_SERVER_NAME,
235
241
  hostTools: start.tools ?? [],
@@ -322,6 +328,7 @@ async function ensureSession({
322
328
  sessionMeta: bridgeConfiguration.sessionMeta,
323
329
  permissionMode: start.permissionMode,
324
330
  permissionModeMapping: start.permissionModeMapping,
331
+ mcpServers: start.mcpServers,
325
332
  });
326
333
  if (session != null) {
327
334
  if (catalogRefreshError != null) throw catalogRefreshError;
@@ -437,6 +444,10 @@ async function ensureSession({
437
444
  authentication,
438
445
  });
439
446
  }
447
+ const externalMcpServers = createExternalMcpServers({
448
+ mcpServers: start.mcpServers,
449
+ initialization,
450
+ });
440
451
  const tools = start.tools ?? [];
441
452
  const catalogPath = `${bridgeStateDir}/host-tools.json`;
442
453
  await writeFile(catalogPath, JSON.stringify(tools), { mode: 0o600 });
@@ -446,6 +457,7 @@ async function ensureSession({
446
457
  });
447
458
 
448
459
  const mcpServers: acp.McpServer[] = [
460
+ ...externalMcpServers,
449
461
  {
450
462
  name: HOST_TOOL_MCP_SERVER_NAME,
451
463
  command: process.execPath,
@@ -557,6 +569,41 @@ async function ensureSession({
557
569
  sessionConfigurationFingerprint = fingerprint;
558
570
  }
559
571
 
572
+ function createExternalMcpServers({
573
+ mcpServers,
574
+ initialization,
575
+ }: {
576
+ mcpServers: Record<string, unknown> | undefined;
577
+ initialization: acp.InitializeResponse;
578
+ }): acp.McpServer[] {
579
+ if (mcpServers == null) return [];
580
+ return Object.entries(mcpServers).map(([name, value]) => {
581
+ if (!isRecord(value)) {
582
+ throw new Error(
583
+ `ACP MCP server ${JSON.stringify(name)} must be configured with an object value.`,
584
+ );
585
+ }
586
+ if (value.type === 'acp') {
587
+ throw new HarnessBridgeCapabilityUnsupportedError({
588
+ harnessId: bridgeType,
589
+ message:
590
+ 'ACP-transport MCP servers require client-side mcp/connect handling, which this harness does not provide.',
591
+ });
592
+ }
593
+ const mcpCapabilities = initialization.agentCapabilities?.mcpCapabilities;
594
+ if (
595
+ (value.type === 'http' && mcpCapabilities?.http !== true) ||
596
+ (value.type === 'sse' && mcpCapabilities?.sse !== true)
597
+ ) {
598
+ throw new HarnessBridgeCapabilityUnsupportedError({
599
+ harnessId: bridgeType,
600
+ message: `The ACP agent does not advertise support for ${value.type.toUpperCase()} MCP servers.`,
601
+ });
602
+ }
603
+ return { ...value, name } as acp.McpServer;
604
+ });
605
+ }
606
+
560
607
  function unknownUsage() {
561
608
  return {
562
609
  inputTokens: {