@ai-sdk/harness-acp 1.0.38 → 1.0.40
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/CHANGELOG.md +24 -0
- package/README.md +4 -3
- package/dist/bridge/host-tool-mcp.mjs +1 -1
- package/dist/bridge/index.mjs +556 -43
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +56 -6
- package/dist/index.js +280 -72
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/acp-harness.ts +51 -9
- package/src/index.ts +2 -0
- package/src/v1/acp-v1-bridge-protocol.ts +19 -0
- package/src/v1/acp-v1-harness.ts +307 -19
- package/src/v1/acp-v1-settings.ts +42 -0
- package/src/v1/acp-v1-skills.ts +4 -65
- package/src/v1/bridge/acp-v1-bridge-environment.ts +16 -0
- package/src/v1/bridge/create-emit-stream-event.ts +6 -0
- package/src/v1/bridge/host-tool-correlation.ts +34 -0
- package/src/v1/bridge/host-tool-mcp-definition.ts +69 -0
- package/src/v1/bridge/host-tool-mcp-http.ts +182 -0
- package/src/v1/bridge/host-tool-relay.ts +87 -7
- package/src/v1/bridge/index.ts +111 -24
- package/src/v1/bridge/instruction-mapping.ts +12 -0
- package/src/v1/bridge/stream-translator.ts +41 -2
- package/src/v1/index.ts +2 -0
package/dist/bridge/index.mjs
CHANGED
|
@@ -230,6 +230,7 @@ async function runBridge(options) {
|
|
|
230
230
|
}
|
|
231
231
|
}
|
|
232
232
|
const pendingToolResults = /* @__PURE__ */ new Map();
|
|
233
|
+
const bufferedToolResults = [];
|
|
233
234
|
const pendingToolApprovals = /* @__PURE__ */ new Map();
|
|
234
235
|
const writeBridgeMeta = async (state) => {
|
|
235
236
|
try {
|
|
@@ -409,9 +410,23 @@ async function runBridge(options) {
|
|
|
409
410
|
const userMessages = createBridgeUserMessageQueue({ respond: emit });
|
|
410
411
|
const turn = {
|
|
411
412
|
emit,
|
|
412
|
-
requestToolResult: (
|
|
413
|
-
|
|
414
|
-
|
|
413
|
+
requestToolResult: (requestInput) => {
|
|
414
|
+
const request = typeof requestInput === "string" ? { toolCallId: requestInput } : requestInput;
|
|
415
|
+
const bufferedIndex = bufferedToolResults.findIndex(
|
|
416
|
+
(buffered) => buffered.toolCallId === request.toolCallId || request.matches?.(buffered.result) === true
|
|
417
|
+
);
|
|
418
|
+
if (bufferedIndex >= 0) {
|
|
419
|
+
return Promise.resolve(
|
|
420
|
+
bufferedToolResults.splice(bufferedIndex, 1)[0].result
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
return new Promise((resolve) => {
|
|
424
|
+
pendingToolResults.set(request.toolCallId, {
|
|
425
|
+
resolve,
|
|
426
|
+
matches: request.matches
|
|
427
|
+
});
|
|
428
|
+
});
|
|
429
|
+
},
|
|
415
430
|
requestToolApproval: (approvalId) => new Promise((resolve) => {
|
|
416
431
|
pendingToolApprovals.set(approvalId, resolve);
|
|
417
432
|
}),
|
|
@@ -453,10 +468,25 @@ async function runBridge(options) {
|
|
|
453
468
|
return;
|
|
454
469
|
}
|
|
455
470
|
case "tool-result": {
|
|
456
|
-
const
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
471
|
+
const result = {
|
|
472
|
+
output: msg.output,
|
|
473
|
+
isError: msg.isError,
|
|
474
|
+
toolResult: msg.toolResult
|
|
475
|
+
};
|
|
476
|
+
const exactPending = pendingToolResults.get(msg.toolCallId);
|
|
477
|
+
const matchingPending = exactPending == null ? Array.from(pendingToolResults.entries()).find(
|
|
478
|
+
([, pending2]) => pending2.matches?.(result) === true
|
|
479
|
+
) : void 0;
|
|
480
|
+
const pending = exactPending ?? matchingPending?.[1];
|
|
481
|
+
const pendingId = exactPending != null ? msg.toolCallId : matchingPending?.[0];
|
|
482
|
+
if (pending != null && pendingId != null) {
|
|
483
|
+
pendingToolResults.delete(pendingId);
|
|
484
|
+
pending.resolve(result);
|
|
485
|
+
} else {
|
|
486
|
+
bufferedToolResults.push({
|
|
487
|
+
toolCallId: msg.toolCallId,
|
|
488
|
+
result
|
|
489
|
+
});
|
|
460
490
|
}
|
|
461
491
|
return;
|
|
462
492
|
}
|
|
@@ -634,7 +664,6 @@ function serialiseError(err) {
|
|
|
634
664
|
import * as acp5 from "@agentclientprotocol/sdk";
|
|
635
665
|
import { spawn } from "child_process";
|
|
636
666
|
import { readFile, writeFile as writeFile2 } from "fs/promises";
|
|
637
|
-
import { fileURLToPath } from "url";
|
|
638
667
|
import { Readable, Writable } from "stream";
|
|
639
668
|
import { argv, env as processEnv } from "process";
|
|
640
669
|
|
|
@@ -665,6 +694,10 @@ var profileValueSchema = z.lazy(
|
|
|
665
694
|
z.record(z.string(), profileValueSchema)
|
|
666
695
|
])
|
|
667
696
|
);
|
|
697
|
+
var hostToolMcpTransportSchema = z.enum([
|
|
698
|
+
"stdio",
|
|
699
|
+
"http"
|
|
700
|
+
]);
|
|
668
701
|
var serializableRecordSchema = z.record(z.string(), serializableValueSchema);
|
|
669
702
|
var profileRecordSchema = z.record(z.string(), profileValueSchema);
|
|
670
703
|
var authenticationSchema = z.object({
|
|
@@ -684,7 +717,9 @@ var bridgeConfigurationSchema = z.object({
|
|
|
684
717
|
providerAuthentication: providerAuthenticationSchema.optional(),
|
|
685
718
|
providerEnvironment: z.record(z.string(), z.string()).optional(),
|
|
686
719
|
sessionMeta: serializableRecordSchema.optional(),
|
|
687
|
-
clientCapabilities: serializableRecordSchema.optional()
|
|
720
|
+
clientCapabilities: serializableRecordSchema.optional(),
|
|
721
|
+
askUserQuestionsRequestMethod: z.string().optional(),
|
|
722
|
+
hostToolMcpTransport: hostToolMcpTransportSchema.optional()
|
|
688
723
|
});
|
|
689
724
|
async function readACPBridgeEnvironment({
|
|
690
725
|
env
|
|
@@ -1154,6 +1189,7 @@ function createACPStreamTranslator({
|
|
|
1154
1189
|
builtinTools,
|
|
1155
1190
|
builtinToolsByName
|
|
1156
1191
|
});
|
|
1192
|
+
emitToolCallCandidate?.({ toolCall: createACPToolCall({ state }) });
|
|
1157
1193
|
if (!forceEmit && builtin?.inputSchema != null && state.values.status !== "completed" && state.values.status !== "failed" && !hasRequiredBuiltinToolInput({
|
|
1158
1194
|
rawInput: state.values.rawInput,
|
|
1159
1195
|
inputSchema: builtin.inputSchema
|
|
@@ -1168,7 +1204,6 @@ function createACPStreamTranslator({
|
|
|
1168
1204
|
programmaticName,
|
|
1169
1205
|
toolCallId: state.toolCallId
|
|
1170
1206
|
});
|
|
1171
|
-
emitToolCallCandidate?.({ toolCall: createACPToolCall({ state }) });
|
|
1172
1207
|
}
|
|
1173
1208
|
emit({
|
|
1174
1209
|
type: "tool-call",
|
|
@@ -1277,6 +1312,10 @@ function createACPStreamTranslator({
|
|
|
1277
1312
|
close: closeBlock,
|
|
1278
1313
|
hostToolCall,
|
|
1279
1314
|
hostToolResult,
|
|
1315
|
+
getToolCall: ({ toolCallId }) => {
|
|
1316
|
+
const state = toolStates.get(toolCallId);
|
|
1317
|
+
return state == null ? void 0 : createACPToolCall({ state });
|
|
1318
|
+
},
|
|
1280
1319
|
finish: (response) => {
|
|
1281
1320
|
if (finished) return;
|
|
1282
1321
|
finished = true;
|
|
@@ -1382,7 +1421,24 @@ function resolveBuiltinTool({
|
|
|
1382
1421
|
const schemaMatches = builtinTools.filter(
|
|
1383
1422
|
(tool) => matchesBuiltinToolInput({ rawInput, inputSchema: tool.inputSchema })
|
|
1384
1423
|
);
|
|
1385
|
-
|
|
1424
|
+
if (schemaMatches.length <= 1) return schemaMatches[0];
|
|
1425
|
+
return findMostSpecificSchemaMatch({ rawInput, schemaMatches });
|
|
1426
|
+
}
|
|
1427
|
+
function findMostSpecificSchemaMatch({
|
|
1428
|
+
rawInput,
|
|
1429
|
+
schemaMatches
|
|
1430
|
+
}) {
|
|
1431
|
+
if (!isRecord3(rawInput)) return void 0;
|
|
1432
|
+
const rawInputKeys = Object.keys(rawInput);
|
|
1433
|
+
const coverageOf = (tool) => {
|
|
1434
|
+
const properties = isRecord3(tool.inputSchema) ? isRecord3(tool.inputSchema.properties) ? tool.inputSchema.properties : {} : {};
|
|
1435
|
+
return rawInputKeys.filter((key) => key in properties).length;
|
|
1436
|
+
};
|
|
1437
|
+
const highestCoverage = Math.max(...schemaMatches.map(coverageOf));
|
|
1438
|
+
const mostSpecificMatches = schemaMatches.filter(
|
|
1439
|
+
(tool) => coverageOf(tool) === highestCoverage
|
|
1440
|
+
);
|
|
1441
|
+
return mostSpecificMatches.length === 1 ? mostSpecificMatches[0] : void 0;
|
|
1386
1442
|
}
|
|
1387
1443
|
function findBuiltinToolByTitle({
|
|
1388
1444
|
title,
|
|
@@ -1731,6 +1787,7 @@ function createHostToolCorrelation({
|
|
|
1731
1787
|
const candidates = /* @__PURE__ */ new Map();
|
|
1732
1788
|
const suppressedToolCallIds = /* @__PURE__ */ new Set();
|
|
1733
1789
|
const releasedToolCallIds = /* @__PURE__ */ new Set();
|
|
1790
|
+
const observedToolCalls = /* @__PURE__ */ new Map();
|
|
1734
1791
|
let nextCandidateOrder = 0;
|
|
1735
1792
|
let buffered = [];
|
|
1736
1793
|
let flushTimer;
|
|
@@ -1807,6 +1864,13 @@ function createHostToolCorrelation({
|
|
|
1807
1864
|
return;
|
|
1808
1865
|
}
|
|
1809
1866
|
const toolCallId = message.update.toolCallId;
|
|
1867
|
+
observedToolCalls.set(
|
|
1868
|
+
toolCallId,
|
|
1869
|
+
mergeObservedToolCall({
|
|
1870
|
+
previous: observedToolCalls.get(toolCallId),
|
|
1871
|
+
update: message.update
|
|
1872
|
+
})
|
|
1873
|
+
);
|
|
1810
1874
|
if (suppressedToolCallIds.has(toolCallId)) {
|
|
1811
1875
|
if (isTerminalToolUpdate(message)) {
|
|
1812
1876
|
suppressedToolCallIds.delete(toolCallId);
|
|
@@ -1890,6 +1954,8 @@ function createHostToolCorrelation({
|
|
|
1890
1954
|
suppressToolCallUpdate({ toolCallId: toolCall.toolCallId });
|
|
1891
1955
|
return true;
|
|
1892
1956
|
},
|
|
1957
|
+
suppressToolCall: suppressToolCallUpdate,
|
|
1958
|
+
getToolCall: ({ toolCallId }) => observedToolCalls.get(toolCallId),
|
|
1893
1959
|
flush,
|
|
1894
1960
|
removeInvocation: ({ token }) => {
|
|
1895
1961
|
deleteInvocation({ invocations, token });
|
|
@@ -1905,9 +1971,26 @@ function createHostToolCorrelation({
|
|
|
1905
1971
|
candidates.clear();
|
|
1906
1972
|
suppressedToolCallIds.clear();
|
|
1907
1973
|
releasedToolCallIds.clear();
|
|
1974
|
+
observedToolCalls.clear();
|
|
1908
1975
|
}
|
|
1909
1976
|
};
|
|
1910
1977
|
}
|
|
1978
|
+
function mergeObservedToolCall({
|
|
1979
|
+
previous,
|
|
1980
|
+
update
|
|
1981
|
+
}) {
|
|
1982
|
+
return {
|
|
1983
|
+
toolCallId: update.toolCallId,
|
|
1984
|
+
title: update.title ?? previous?.title ?? `Tool ${update.toolCallId}`,
|
|
1985
|
+
...update.kind == null ? {} : { kind: update.kind },
|
|
1986
|
+
...update.status == null ? {} : { status: update.status },
|
|
1987
|
+
...update.content == null ? {} : { content: update.content },
|
|
1988
|
+
...update.locations == null ? {} : { locations: update.locations },
|
|
1989
|
+
...update.rawInput === void 0 ? {} : { rawInput: update.rawInput },
|
|
1990
|
+
...update.rawOutput === void 0 ? {} : { rawOutput: update.rawOutput },
|
|
1991
|
+
...update._meta === void 0 ? {} : { _meta: update._meta }
|
|
1992
|
+
};
|
|
1993
|
+
}
|
|
1911
1994
|
function hasPortableEvidence({
|
|
1912
1995
|
candidate,
|
|
1913
1996
|
invocation
|
|
@@ -2218,7 +2301,9 @@ function createEmitStreamEvent({
|
|
|
2218
2301
|
hostToolCall: translator.hostToolCall,
|
|
2219
2302
|
hostToolResult: translator.hostToolResult,
|
|
2220
2303
|
registerHostToolCorrelationInvocation: correlation.registerInvocation,
|
|
2221
|
-
removeHostToolCorrelationInvocation: correlation.removeInvocation
|
|
2304
|
+
removeHostToolCorrelationInvocation: correlation.removeInvocation,
|
|
2305
|
+
suppressToolCall: correlation.suppressToolCall,
|
|
2306
|
+
getToolCall: ({ toolCallId }) => correlation.getToolCall({ toolCallId }) ?? translator.getToolCall({ toolCallId })
|
|
2222
2307
|
};
|
|
2223
2308
|
}
|
|
2224
2309
|
|
|
@@ -2236,6 +2321,14 @@ async function resolveACPInstructionConfiguration({
|
|
|
2236
2321
|
if (instructionMapping == null || instructions == null || instructions.length === 0) {
|
|
2237
2322
|
return { sessionMeta, environment: resolvedEnvironment };
|
|
2238
2323
|
}
|
|
2324
|
+
if (instructionMapping.type === "filesystem") {
|
|
2325
|
+
if (typeof instructionMapping.path !== "string" || instructionMapping.path.trim().length === 0) {
|
|
2326
|
+
throw new Error(
|
|
2327
|
+
"ACP instruction mapping filesystem path must be a non-empty string."
|
|
2328
|
+
);
|
|
2329
|
+
}
|
|
2330
|
+
return { sessionMeta, environment: resolvedEnvironment };
|
|
2331
|
+
}
|
|
2239
2332
|
assertSafePath({ path: instructionMapping.path });
|
|
2240
2333
|
if (instructionMapping.type === "session-meta") {
|
|
2241
2334
|
return {
|
|
@@ -2307,11 +2400,263 @@ function isRecord5(value) {
|
|
|
2307
2400
|
}
|
|
2308
2401
|
|
|
2309
2402
|
// src/v1/bridge/host-tool-relay.ts
|
|
2310
|
-
import { randomBytes, timingSafeEqual } from "crypto";
|
|
2403
|
+
import { randomBytes, randomUUID as randomUUID3, timingSafeEqual } from "crypto";
|
|
2311
2404
|
import { createServer } from "http";
|
|
2405
|
+
|
|
2406
|
+
// src/v1/bridge/host-tool-mcp-http.ts
|
|
2407
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
2408
|
+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
2409
|
+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
2410
|
+
|
|
2411
|
+
// src/v1/bridge/host-tool-mcp-server.ts
|
|
2412
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
2413
|
+
import {
|
|
2414
|
+
CallToolRequestSchema,
|
|
2415
|
+
ErrorCode,
|
|
2416
|
+
ListToolsRequestSchema,
|
|
2417
|
+
McpError
|
|
2418
|
+
} from "@modelcontextprotocol/sdk/types.js";
|
|
2419
|
+
var VERSION = true ? "1.0.40" : "0.0.0-test";
|
|
2420
|
+
function createHostToolMCPServer({
|
|
2421
|
+
tools,
|
|
2422
|
+
revision = 1,
|
|
2423
|
+
invoke,
|
|
2424
|
+
onListTools
|
|
2425
|
+
}) {
|
|
2426
|
+
let catalog = createCatalog({ revision, tools });
|
|
2427
|
+
let catalogAcknowledgmentError;
|
|
2428
|
+
const server = new Server(
|
|
2429
|
+
{
|
|
2430
|
+
name: "@ai-sdk/harness-acp-host-tools",
|
|
2431
|
+
version: VERSION
|
|
2432
|
+
},
|
|
2433
|
+
{ capabilities: { tools: { listChanged: true } } }
|
|
2434
|
+
);
|
|
2435
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2436
|
+
if (catalogAcknowledgmentError != null) {
|
|
2437
|
+
throw catalogAcknowledgmentError;
|
|
2438
|
+
}
|
|
2439
|
+
const current = catalog;
|
|
2440
|
+
if (onListTools != null) {
|
|
2441
|
+
setImmediate(() => {
|
|
2442
|
+
void onListTools({ revision: current.revision }).catch((error) => {
|
|
2443
|
+
catalogAcknowledgmentError = error;
|
|
2444
|
+
});
|
|
2445
|
+
});
|
|
2446
|
+
}
|
|
2447
|
+
return { tools: current.tools.map(toMCPTool) };
|
|
2448
|
+
});
|
|
2449
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2450
|
+
const current = catalog;
|
|
2451
|
+
const tool = current.byName.get(request.params.name);
|
|
2452
|
+
if (tool == null) {
|
|
2453
|
+
throw new McpError(
|
|
2454
|
+
ErrorCode.InvalidParams,
|
|
2455
|
+
`Unknown host tool: ${request.params.name}`
|
|
2456
|
+
);
|
|
2457
|
+
}
|
|
2458
|
+
const input = request.params.arguments ?? {};
|
|
2459
|
+
const result = await invoke({
|
|
2460
|
+
toolName: tool.name,
|
|
2461
|
+
input,
|
|
2462
|
+
catalogRevision: current.revision
|
|
2463
|
+
});
|
|
2464
|
+
return toCallToolResult({ result });
|
|
2465
|
+
});
|
|
2466
|
+
return {
|
|
2467
|
+
server,
|
|
2468
|
+
updateCatalog: async ({ revision: nextRevision, tools: nextTools }) => {
|
|
2469
|
+
catalog = createCatalog({
|
|
2470
|
+
revision: nextRevision,
|
|
2471
|
+
tools: nextTools
|
|
2472
|
+
});
|
|
2473
|
+
await server.sendToolListChanged();
|
|
2474
|
+
}
|
|
2475
|
+
};
|
|
2476
|
+
}
|
|
2477
|
+
function createCatalog({
|
|
2478
|
+
revision,
|
|
2479
|
+
tools
|
|
2480
|
+
}) {
|
|
2481
|
+
return {
|
|
2482
|
+
revision,
|
|
2483
|
+
tools: [...tools],
|
|
2484
|
+
byName: new Map(tools.map((tool) => [tool.name, tool]))
|
|
2485
|
+
};
|
|
2486
|
+
}
|
|
2487
|
+
function toMCPTool(tool) {
|
|
2488
|
+
return {
|
|
2489
|
+
name: tool.name,
|
|
2490
|
+
...tool.description == null ? {} : { description: tool.description },
|
|
2491
|
+
inputSchema: requireObjectSchema({
|
|
2492
|
+
toolName: tool.name,
|
|
2493
|
+
schema: tool.inputSchema ?? { type: "object" }
|
|
2494
|
+
})
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
function requireObjectSchema({
|
|
2498
|
+
toolName,
|
|
2499
|
+
schema
|
|
2500
|
+
}) {
|
|
2501
|
+
if (schema == null || typeof schema !== "object" || Array.isArray(schema) || Reflect.get(schema, "type") !== void 0 && Reflect.get(schema, "type") !== "object") {
|
|
2502
|
+
throw new Error(
|
|
2503
|
+
`Host tool ${toolName} must use an object JSON Schema for MCP.`
|
|
2504
|
+
);
|
|
2505
|
+
}
|
|
2506
|
+
return schema;
|
|
2507
|
+
}
|
|
2508
|
+
function toCallToolResult({
|
|
2509
|
+
result
|
|
2510
|
+
}) {
|
|
2511
|
+
return {
|
|
2512
|
+
content: [
|
|
2513
|
+
{
|
|
2514
|
+
type: "text",
|
|
2515
|
+
text: stringifyOutput({ output: result.output })
|
|
2516
|
+
}
|
|
2517
|
+
],
|
|
2518
|
+
...result.isError ? { isError: true } : {},
|
|
2519
|
+
_meta: {
|
|
2520
|
+
"ai-sdk-harness-acp-correlation": result.correlationToken
|
|
2521
|
+
}
|
|
2522
|
+
};
|
|
2523
|
+
}
|
|
2524
|
+
function stringifyOutput({ output }) {
|
|
2525
|
+
if (output === void 0) return "null";
|
|
2526
|
+
try {
|
|
2527
|
+
return JSON.stringify(output);
|
|
2528
|
+
} catch {
|
|
2529
|
+
return JSON.stringify({
|
|
2530
|
+
error: "Host tool output could not be serialized as JSON."
|
|
2531
|
+
});
|
|
2532
|
+
}
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
// src/v1/bridge/host-tool-mcp-http.ts
|
|
2536
|
+
var HOST_TOOL_MCP_ENDPOINT_PATH = "/mcp";
|
|
2537
|
+
function createHostToolMCPHttpEndpoint({
|
|
2538
|
+
tools,
|
|
2539
|
+
revision,
|
|
2540
|
+
invoke,
|
|
2541
|
+
onListTools
|
|
2542
|
+
}) {
|
|
2543
|
+
let catalog = { revision, tools: [...tools] };
|
|
2544
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
2545
|
+
let closed = false;
|
|
2546
|
+
async function openSession({
|
|
2547
|
+
request,
|
|
2548
|
+
response,
|
|
2549
|
+
body
|
|
2550
|
+
}) {
|
|
2551
|
+
const hostToolServer = createHostToolMCPServer({
|
|
2552
|
+
tools: catalog.tools,
|
|
2553
|
+
revision: catalog.revision,
|
|
2554
|
+
invoke,
|
|
2555
|
+
onListTools
|
|
2556
|
+
});
|
|
2557
|
+
const transport = new StreamableHTTPServerTransport({
|
|
2558
|
+
sessionIdGenerator: () => randomUUID2(),
|
|
2559
|
+
onsessioninitialized: (sessionId) => {
|
|
2560
|
+
sessions.set(sessionId, { transport, hostToolServer });
|
|
2561
|
+
},
|
|
2562
|
+
onsessionclosed: (sessionId) => {
|
|
2563
|
+
sessions.delete(sessionId);
|
|
2564
|
+
}
|
|
2565
|
+
});
|
|
2566
|
+
transport.onclose = () => {
|
|
2567
|
+
const { sessionId } = transport;
|
|
2568
|
+
if (sessionId != null) sessions.delete(sessionId);
|
|
2569
|
+
};
|
|
2570
|
+
await hostToolServer.server.connect(transport);
|
|
2571
|
+
await transport.handleRequest(request, response, body);
|
|
2572
|
+
}
|
|
2573
|
+
return {
|
|
2574
|
+
handleRequest: async ({ request, response, body }) => {
|
|
2575
|
+
if (closed) {
|
|
2576
|
+
respondWithJSONRPCError({
|
|
2577
|
+
response,
|
|
2578
|
+
status: 503,
|
|
2579
|
+
code: -32e3,
|
|
2580
|
+
message: "The host tool MCP endpoint is closed."
|
|
2581
|
+
});
|
|
2582
|
+
return;
|
|
2583
|
+
}
|
|
2584
|
+
const sessionId = readSessionId({ request });
|
|
2585
|
+
if (sessionId != null) {
|
|
2586
|
+
const session2 = sessions.get(sessionId);
|
|
2587
|
+
if (session2 == null) {
|
|
2588
|
+
respondWithJSONRPCError({
|
|
2589
|
+
response,
|
|
2590
|
+
status: 404,
|
|
2591
|
+
code: -32001,
|
|
2592
|
+
message: "Unknown host tool MCP session."
|
|
2593
|
+
});
|
|
2594
|
+
return;
|
|
2595
|
+
}
|
|
2596
|
+
await session2.transport.handleRequest(request, response, body);
|
|
2597
|
+
return;
|
|
2598
|
+
}
|
|
2599
|
+
if (request.method !== "POST" || !isInitializeRequest(body)) {
|
|
2600
|
+
respondWithJSONRPCError({
|
|
2601
|
+
response,
|
|
2602
|
+
status: 400,
|
|
2603
|
+
code: -32e3,
|
|
2604
|
+
message: "Host tool MCP requests without a session id must be an initialize request."
|
|
2605
|
+
});
|
|
2606
|
+
return;
|
|
2607
|
+
}
|
|
2608
|
+
await openSession({ request, response, body });
|
|
2609
|
+
},
|
|
2610
|
+
updateCatalog: async ({ revision: nextRevision, tools: nextTools }) => {
|
|
2611
|
+
catalog = { revision: nextRevision, tools: [...nextTools] };
|
|
2612
|
+
await Promise.all(
|
|
2613
|
+
[...sessions.values()].map(
|
|
2614
|
+
(session2) => session2.hostToolServer.updateCatalog({
|
|
2615
|
+
revision: nextRevision,
|
|
2616
|
+
tools: nextTools
|
|
2617
|
+
})
|
|
2618
|
+
)
|
|
2619
|
+
);
|
|
2620
|
+
},
|
|
2621
|
+
close: async () => {
|
|
2622
|
+
if (closed) return;
|
|
2623
|
+
closed = true;
|
|
2624
|
+
const live = [...sessions.values()];
|
|
2625
|
+
sessions.clear();
|
|
2626
|
+
await Promise.all(
|
|
2627
|
+
live.map((session2) => session2.hostToolServer.server.close())
|
|
2628
|
+
);
|
|
2629
|
+
}
|
|
2630
|
+
};
|
|
2631
|
+
}
|
|
2632
|
+
function readSessionId({
|
|
2633
|
+
request
|
|
2634
|
+
}) {
|
|
2635
|
+
const value = request.headers["mcp-session-id"];
|
|
2636
|
+
const sessionId = Array.isArray(value) ? value[0] : value;
|
|
2637
|
+
return sessionId == null || sessionId.length === 0 ? void 0 : sessionId;
|
|
2638
|
+
}
|
|
2639
|
+
function respondWithJSONRPCError({
|
|
2640
|
+
response,
|
|
2641
|
+
status,
|
|
2642
|
+
code,
|
|
2643
|
+
message
|
|
2644
|
+
}) {
|
|
2645
|
+
response.writeHead(status, { "content-type": "application/json" });
|
|
2646
|
+
response.end(
|
|
2647
|
+
JSON.stringify({
|
|
2648
|
+
jsonrpc: "2.0",
|
|
2649
|
+
error: { code, message },
|
|
2650
|
+
id: null
|
|
2651
|
+
})
|
|
2652
|
+
);
|
|
2653
|
+
}
|
|
2654
|
+
|
|
2655
|
+
// src/v1/bridge/host-tool-relay.ts
|
|
2312
2656
|
async function startHostToolRelay({
|
|
2313
2657
|
tools,
|
|
2314
|
-
serverName
|
|
2658
|
+
serverName,
|
|
2659
|
+
mcpTransport = "stdio"
|
|
2315
2660
|
}) {
|
|
2316
2661
|
const state = {
|
|
2317
2662
|
tools: [...tools],
|
|
@@ -2326,8 +2671,44 @@ async function startHostToolRelay({
|
|
|
2326
2671
|
let activeTurn;
|
|
2327
2672
|
let invocationOrder = 0;
|
|
2328
2673
|
let closePromise;
|
|
2674
|
+
const mcpEndpoint = mcpTransport === "http" ? createHostToolMCPHttpEndpoint({
|
|
2675
|
+
tools: state.tools,
|
|
2676
|
+
revision: state.revision,
|
|
2677
|
+
invoke: ({ toolName, input, catalogRevision }) => handleInvocation({
|
|
2678
|
+
body: {
|
|
2679
|
+
requestId: randomUUID3(),
|
|
2680
|
+
toolName,
|
|
2681
|
+
input,
|
|
2682
|
+
catalogRevision
|
|
2683
|
+
},
|
|
2684
|
+
state,
|
|
2685
|
+
serverName,
|
|
2686
|
+
turn: activeTurn,
|
|
2687
|
+
nextInvocationOrder: () => ++invocationOrder
|
|
2688
|
+
}),
|
|
2689
|
+
onListTools: async ({ revision }) => {
|
|
2690
|
+
acknowledgeCatalog({ state, revision });
|
|
2691
|
+
}
|
|
2692
|
+
}) : void 0;
|
|
2329
2693
|
const server = createServer(async (request, response) => {
|
|
2330
2694
|
try {
|
|
2695
|
+
if (mcpEndpoint != null && request.url === HOST_TOOL_MCP_ENDPOINT_PATH) {
|
|
2696
|
+
if (!credentialsMatch({
|
|
2697
|
+
expected: credential,
|
|
2698
|
+
actual: request.headers.authorization
|
|
2699
|
+
})) {
|
|
2700
|
+
throw new RelayRequestError({
|
|
2701
|
+
status: 401,
|
|
2702
|
+
message: "Invalid host tool relay credential."
|
|
2703
|
+
});
|
|
2704
|
+
}
|
|
2705
|
+
await mcpEndpoint.handleRequest({
|
|
2706
|
+
request,
|
|
2707
|
+
response,
|
|
2708
|
+
...request.method === "POST" ? { body: await readJSONBody({ request }) } : {}
|
|
2709
|
+
});
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2331
2712
|
const result = await handleRequest({
|
|
2332
2713
|
request,
|
|
2333
2714
|
credential,
|
|
@@ -2339,6 +2720,10 @@ async function startHostToolRelay({
|
|
|
2339
2720
|
response.writeHead(200, { "content-type": "application/json" });
|
|
2340
2721
|
response.end(JSON.stringify(result));
|
|
2341
2722
|
} catch (error) {
|
|
2723
|
+
if (response.headersSent) {
|
|
2724
|
+
response.end();
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2342
2727
|
const status = error instanceof RelayRequestError ? error.status : 500;
|
|
2343
2728
|
response.writeHead(status, { "content-type": "application/json" });
|
|
2344
2729
|
response.end(
|
|
@@ -2352,6 +2737,9 @@ async function startHostToolRelay({
|
|
|
2352
2737
|
const address = server.address();
|
|
2353
2738
|
return {
|
|
2354
2739
|
url: `http://127.0.0.1:${address.port}/invoke`,
|
|
2740
|
+
...mcpEndpoint == null ? {} : {
|
|
2741
|
+
mcpUrl: `http://127.0.0.1:${address.port}${HOST_TOOL_MCP_ENDPOINT_PATH}`
|
|
2742
|
+
},
|
|
2355
2743
|
credential,
|
|
2356
2744
|
bindTurn: ({ turn }) => {
|
|
2357
2745
|
if (activeTurn != null && activeTurn !== turn) {
|
|
@@ -2371,6 +2759,10 @@ async function startHostToolRelay({
|
|
|
2371
2759
|
state.fingerprint = fingerprint;
|
|
2372
2760
|
state.revision += 1;
|
|
2373
2761
|
resolveCatalogChanges({ state });
|
|
2762
|
+
void mcpEndpoint?.updateCatalog({
|
|
2763
|
+
revision: state.revision,
|
|
2764
|
+
tools: state.tools
|
|
2765
|
+
});
|
|
2374
2766
|
return { changed: true, revision: state.revision };
|
|
2375
2767
|
},
|
|
2376
2768
|
waitForCatalogRefresh: ({ revision, timeoutMs }) => waitForCatalogRefresh({ state, revision, timeoutMs }),
|
|
@@ -2379,7 +2771,10 @@ async function startHostToolRelay({
|
|
|
2379
2771
|
state.closed = true;
|
|
2380
2772
|
resolveCatalogChanges({ state });
|
|
2381
2773
|
resolveRefreshWaiters({ state, closing: true });
|
|
2382
|
-
closePromise =
|
|
2774
|
+
closePromise = (async () => {
|
|
2775
|
+
await mcpEndpoint?.close();
|
|
2776
|
+
await closeServer({ server });
|
|
2777
|
+
})();
|
|
2383
2778
|
return closePromise;
|
|
2384
2779
|
}
|
|
2385
2780
|
};
|
|
@@ -2457,13 +2852,16 @@ function handleCatalogSeen({
|
|
|
2457
2852
|
message: "Invalid host tool catalog acknowledgment."
|
|
2458
2853
|
});
|
|
2459
2854
|
}
|
|
2460
|
-
state
|
|
2461
|
-
state.servedRevision,
|
|
2462
|
-
body.revision
|
|
2463
|
-
);
|
|
2464
|
-
resolveRefreshWaiters({ state, closing: false });
|
|
2855
|
+
acknowledgeCatalog({ state, revision: body.revision });
|
|
2465
2856
|
return { acknowledged: true };
|
|
2466
2857
|
}
|
|
2858
|
+
function acknowledgeCatalog({
|
|
2859
|
+
state,
|
|
2860
|
+
revision
|
|
2861
|
+
}) {
|
|
2862
|
+
state.servedRevision = Math.max(state.servedRevision, revision);
|
|
2863
|
+
resolveRefreshWaiters({ state, closing: false });
|
|
2864
|
+
}
|
|
2467
2865
|
async function handleInvocation({
|
|
2468
2866
|
body,
|
|
2469
2867
|
state,
|
|
@@ -2662,6 +3060,55 @@ var RelayRequestError = class extends Error {
|
|
|
2662
3060
|
}
|
|
2663
3061
|
};
|
|
2664
3062
|
|
|
3063
|
+
// src/v1/bridge/host-tool-mcp-definition.ts
|
|
3064
|
+
import { fileURLToPath } from "url";
|
|
3065
|
+
import { execPath } from "process";
|
|
3066
|
+
function createHostToolMcpServerDefinition({
|
|
3067
|
+
mcpTransport,
|
|
3068
|
+
relay,
|
|
3069
|
+
serverName,
|
|
3070
|
+
catalogPath,
|
|
3071
|
+
initialization,
|
|
3072
|
+
harnessId
|
|
3073
|
+
}) {
|
|
3074
|
+
if (mcpTransport === "stdio") {
|
|
3075
|
+
return {
|
|
3076
|
+
name: serverName,
|
|
3077
|
+
command: execPath,
|
|
3078
|
+
args: [fileURLToPath(new URL("./host-tool-mcp.mjs", import.meta.url))],
|
|
3079
|
+
env: [
|
|
3080
|
+
{
|
|
3081
|
+
name: "AI_SDK_ACP_HOST_TOOLS_FILE",
|
|
3082
|
+
value: catalogPath
|
|
3083
|
+
},
|
|
3084
|
+
{
|
|
3085
|
+
name: "AI_SDK_ACP_HOST_TOOL_RELAY_URL",
|
|
3086
|
+
value: relay.url
|
|
3087
|
+
},
|
|
3088
|
+
{
|
|
3089
|
+
name: "AI_SDK_ACP_HOST_TOOL_RELAY_CREDENTIAL",
|
|
3090
|
+
value: relay.credential
|
|
3091
|
+
}
|
|
3092
|
+
]
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
if (initialization.agentCapabilities?.mcpCapabilities?.http !== true) {
|
|
3096
|
+
throw new HarnessBridgeCapabilityUnsupportedError({
|
|
3097
|
+
harnessId,
|
|
3098
|
+
message: "This harness exposes host tools through an HTTP MCP server, but the ACP agent does not advertise support for HTTP MCP servers."
|
|
3099
|
+
});
|
|
3100
|
+
}
|
|
3101
|
+
if (relay.mcpUrl == null) {
|
|
3102
|
+
throw new Error("The host tool MCP HTTP endpoint is unavailable.");
|
|
3103
|
+
}
|
|
3104
|
+
return {
|
|
3105
|
+
type: "http",
|
|
3106
|
+
name: serverName,
|
|
3107
|
+
url: relay.mcpUrl,
|
|
3108
|
+
headers: [{ name: "Authorization", value: `Bearer ${relay.credential}` }]
|
|
3109
|
+
};
|
|
3110
|
+
}
|
|
3111
|
+
|
|
2665
3112
|
// src/v1/bridge/refresh-host-tool-catalog.ts
|
|
2666
3113
|
async function promptAndRefreshInitialHostToolCatalog({
|
|
2667
3114
|
startPrompt,
|
|
@@ -3150,6 +3597,7 @@ var sessionConfigurationFailure;
|
|
|
3150
3597
|
var historicalUpdatesSuppressed = false;
|
|
3151
3598
|
var coldRestorationMethod;
|
|
3152
3599
|
var activePermissionController;
|
|
3600
|
+
var activeQuestionRequest;
|
|
3153
3601
|
await runBridge({
|
|
3154
3602
|
bridgeType,
|
|
3155
3603
|
bridgeStateDir,
|
|
@@ -3293,15 +3741,31 @@ async function runTurn(start, turn) {
|
|
|
3293
3741
|
const emitStreamEvent = createEmitStreamEvent({
|
|
3294
3742
|
emit: (event) => turn.emit(event),
|
|
3295
3743
|
emitToolCallCandidate: ({ toolCall }) => {
|
|
3744
|
+
const requestId = crypto.randomUUID();
|
|
3296
3745
|
turn.emit({
|
|
3297
3746
|
type: "acp-tool-call-candidate",
|
|
3747
|
+
requestId,
|
|
3298
3748
|
toolCall
|
|
3299
3749
|
});
|
|
3750
|
+
void turn.requestToolResult(requestId).then((result) => {
|
|
3751
|
+
if (result.output != null && typeof result.output === "object" && "suppress" in result.output && result.output.suppress === true) {
|
|
3752
|
+
emitStreamEvent.suppressToolCall({
|
|
3753
|
+
toolCallId: toolCall.toolCallId
|
|
3754
|
+
});
|
|
3755
|
+
}
|
|
3756
|
+
});
|
|
3300
3757
|
},
|
|
3301
3758
|
builtinTools: start.builtinTools,
|
|
3302
3759
|
hostToolServerName: HOST_TOOL_MCP_SERVER_NAME,
|
|
3303
3760
|
hostTools: start.tools ?? []
|
|
3304
3761
|
});
|
|
3762
|
+
if (bridgeConfiguration.askUserQuestionsRequestMethod != null) {
|
|
3763
|
+
activeQuestionRequest = {
|
|
3764
|
+
method: bridgeConfiguration.askUserQuestionsRequestMethod,
|
|
3765
|
+
turn,
|
|
3766
|
+
emitStreamEvent
|
|
3767
|
+
};
|
|
3768
|
+
}
|
|
3305
3769
|
const permissionController = createACPPermissionController({
|
|
3306
3770
|
turn,
|
|
3307
3771
|
sessionId: activeSession.sessionId,
|
|
@@ -3391,6 +3855,9 @@ async function runTurn(start, turn) {
|
|
|
3391
3855
|
if (emitStreamEvent.message({ message })) return;
|
|
3392
3856
|
}
|
|
3393
3857
|
} finally {
|
|
3858
|
+
if (activeQuestionRequest?.turn === turn) {
|
|
3859
|
+
activeQuestionRequest = void 0;
|
|
3860
|
+
}
|
|
3394
3861
|
permissionController.cancelAll();
|
|
3395
3862
|
activePermissionController = void 0;
|
|
3396
3863
|
activeHostToolRelay.unbindTurn({ turn: relayTurn });
|
|
@@ -3483,7 +3950,7 @@ async function ensureSession({
|
|
|
3483
3950
|
stream: acp5.ndJsonStream(input, output)
|
|
3484
3951
|
});
|
|
3485
3952
|
streamCapture = capturedStream.capture;
|
|
3486
|
-
|
|
3953
|
+
let client2 = acp5.client({ name: clientApp.name }).onRequest(
|
|
3487
3954
|
acp5.methods.client.session.requestPermission,
|
|
3488
3955
|
({ params }) => activePermissionController?.requestPermission(params) ?? {
|
|
3489
3956
|
outcome: { outcome: "cancelled" }
|
|
@@ -3496,7 +3963,52 @@ async function ensureSession({
|
|
|
3496
3963
|
notification: params,
|
|
3497
3964
|
update: params.update
|
|
3498
3965
|
});
|
|
3499
|
-
})
|
|
3966
|
+
});
|
|
3967
|
+
if (bridgeConfiguration.askUserQuestionsRequestMethod != null) {
|
|
3968
|
+
const method = bridgeConfiguration.askUserQuestionsRequestMethod;
|
|
3969
|
+
client2 = client2.onRequest(
|
|
3970
|
+
method,
|
|
3971
|
+
(value) => value,
|
|
3972
|
+
async ({ params }) => {
|
|
3973
|
+
const active = activeQuestionRequest;
|
|
3974
|
+
if (active == null || active.method !== method) {
|
|
3975
|
+
throw acp5.RequestError.methodNotFound(method);
|
|
3976
|
+
}
|
|
3977
|
+
const requestId = crypto.randomUUID();
|
|
3978
|
+
const nativeToolCallId = getNativeQuestionToolCallId({ params });
|
|
3979
|
+
active.turn.emit({
|
|
3980
|
+
type: "acp-question-request",
|
|
3981
|
+
requestId,
|
|
3982
|
+
nativeRequest: params,
|
|
3983
|
+
...nativeToolCallId == null ? {} : {
|
|
3984
|
+
nativeToolCall: active.emitStreamEvent.getToolCall({
|
|
3985
|
+
toolCallId: nativeToolCallId
|
|
3986
|
+
}) ?? void 0
|
|
3987
|
+
}
|
|
3988
|
+
});
|
|
3989
|
+
const result = await active.turn.requestToolResult(requestId);
|
|
3990
|
+
const classification = result.output;
|
|
3991
|
+
if (classification?.type !== "handled") {
|
|
3992
|
+
throw acp5.RequestError.methodNotFound(method);
|
|
3993
|
+
}
|
|
3994
|
+
active.emitStreamEvent.suppressToolCall({
|
|
3995
|
+
toolCallId: nativeToolCallId ?? classification.toolCallId
|
|
3996
|
+
});
|
|
3997
|
+
try {
|
|
3998
|
+
const nativeResult = await active.turn.requestToolResult(
|
|
3999
|
+
classification.toolCallId
|
|
4000
|
+
);
|
|
4001
|
+
return nativeResult.output;
|
|
4002
|
+
} finally {
|
|
4003
|
+
active.turn.emit({
|
|
4004
|
+
type: "acp-question-resolved",
|
|
4005
|
+
requestId
|
|
4006
|
+
});
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
);
|
|
4010
|
+
}
|
|
4011
|
+
connection = client2.connect(capturedStream.stream);
|
|
3500
4012
|
const initializeRequest = createACPInitializeRequest({
|
|
3501
4013
|
protocolVersion: acp5.PROTOCOL_VERSION,
|
|
3502
4014
|
clientApp,
|
|
@@ -3525,33 +4037,26 @@ async function ensureSession({
|
|
|
3525
4037
|
initialization
|
|
3526
4038
|
});
|
|
3527
4039
|
const tools = start.tools ?? [];
|
|
4040
|
+
const mcpTransport = bridgeConfiguration.hostToolMcpTransport ?? "stdio";
|
|
3528
4041
|
const catalogPath = `${bridgeStateDir}/host-tools.json`;
|
|
3529
|
-
|
|
4042
|
+
if (mcpTransport === "stdio") {
|
|
4043
|
+
await writeFile2(catalogPath, JSON.stringify(tools), { mode: 384 });
|
|
4044
|
+
}
|
|
3530
4045
|
hostToolRelay = await startHostToolRelay({
|
|
3531
4046
|
tools,
|
|
3532
|
-
serverName: HOST_TOOL_MCP_SERVER_NAME
|
|
4047
|
+
serverName: HOST_TOOL_MCP_SERVER_NAME,
|
|
4048
|
+
mcpTransport
|
|
3533
4049
|
});
|
|
3534
4050
|
const mcpServers = [
|
|
3535
4051
|
...externalMcpServers,
|
|
3536
|
-
{
|
|
3537
|
-
|
|
3538
|
-
|
|
3539
|
-
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
|
|
3544
|
-
},
|
|
3545
|
-
{
|
|
3546
|
-
name: "AI_SDK_ACP_HOST_TOOL_RELAY_URL",
|
|
3547
|
-
value: hostToolRelay.url
|
|
3548
|
-
},
|
|
3549
|
-
{
|
|
3550
|
-
name: "AI_SDK_ACP_HOST_TOOL_RELAY_CREDENTIAL",
|
|
3551
|
-
value: hostToolRelay.credential
|
|
3552
|
-
}
|
|
3553
|
-
]
|
|
3554
|
-
}
|
|
4052
|
+
createHostToolMcpServerDefinition({
|
|
4053
|
+
mcpTransport,
|
|
4054
|
+
relay: hostToolRelay,
|
|
4055
|
+
serverName: HOST_TOOL_MCP_SERVER_NAME,
|
|
4056
|
+
catalogPath,
|
|
4057
|
+
initialization,
|
|
4058
|
+
harnessId: bridgeType
|
|
4059
|
+
})
|
|
3555
4060
|
];
|
|
3556
4061
|
let createdSession;
|
|
3557
4062
|
if (start.recoveryMode?.type === "lossy-rerun") {
|
|
@@ -3629,6 +4134,14 @@ async function ensureSession({
|
|
|
3629
4134
|
sessionConfigurationFingerprint = fingerprint;
|
|
3630
4135
|
return { initialHostToolCatalogRefreshRequired: tools.length > 0 };
|
|
3631
4136
|
}
|
|
4137
|
+
function getNativeQuestionToolCallId({
|
|
4138
|
+
params
|
|
4139
|
+
}) {
|
|
4140
|
+
if (!isRecord7(params)) return void 0;
|
|
4141
|
+
if (typeof params.toolCallId === "string") return params.toolCallId;
|
|
4142
|
+
const nestedParams = params.params;
|
|
4143
|
+
return isRecord7(nestedParams) && typeof nestedParams.toolCallId === "string" ? nestedParams.toolCallId : void 0;
|
|
4144
|
+
}
|
|
3632
4145
|
function createExternalMcpServers({
|
|
3633
4146
|
mcpServers,
|
|
3634
4147
|
initialization
|