@ai-sdk/harness-acp 1.0.39 → 1.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +4 -3
- package/dist/bridge/host-tool-mcp.mjs +1 -1
- package/dist/bridge/index.mjs +408 -32
- package/dist/bridge/index.mjs.map +1 -1
- package/dist/index.d.ts +27 -2
- package/dist/index.js +68 -69
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/src/acp-harness.ts +8 -0
- package/src/index.ts +1 -0
- package/src/v1/acp-v1-bridge-protocol.ts +4 -0
- package/src/v1/acp-v1-harness.ts +62 -19
- package/src/v1/acp-v1-settings.ts +20 -0
- package/src/v1/acp-v1-skills.ts +4 -65
- package/src/v1/bridge/acp-v1-bridge-environment.ts +10 -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 +14 -21
- package/src/v1/bridge/instruction-mapping.ts +12 -0
- package/src/v1/bridge/stream-translator.ts +35 -1
- package/src/v1/index.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# @ai-sdk/harness-acp
|
|
2
2
|
|
|
3
|
+
## 1.0.41
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [9e1d1b2]
|
|
8
|
+
- Updated dependencies [a495511]
|
|
9
|
+
- @ai-sdk/provider-utils@5.0.37
|
|
10
|
+
- @ai-sdk/harness@1.0.103
|
|
11
|
+
|
|
12
|
+
## 1.0.40
|
|
13
|
+
|
|
14
|
+
### Patch Changes
|
|
15
|
+
|
|
16
|
+
- ceb601c: fix(harness-acp): fix stream translator to match most appropriate tool based on input shape when no name is present and multiple candidates would match the required arguments
|
|
17
|
+
- 255cccf: feat(harness): add `writeInstructions` helper and support filesystem based `instructionsMapping` in ACP backed harness adapters
|
|
18
|
+
- 668b51d: fix(harness-acp): remove problematic condition that leads to errors when using `stopWhen`
|
|
19
|
+
- eeed977: feat(harness): allow passing arbitrary headers with inference requests via `headers` property in `HarnessAgentSettings`
|
|
20
|
+
- 1291df7: feat(harness-acp): support control over how host tools are exposed via `hostToolMcpTransport` config
|
|
21
|
+
- a39c8bf: chore(harness): clarify `writeSkills` helper intent to require materializing skills in HOME directory
|
|
22
|
+
- Updated dependencies [fe86f8f]
|
|
23
|
+
- Updated dependencies [255cccf]
|
|
24
|
+
- Updated dependencies [eeed977]
|
|
25
|
+
- Updated dependencies [a39c8bf]
|
|
26
|
+
- @ai-sdk/harness@1.0.102
|
|
27
|
+
|
|
3
28
|
## 1.0.39
|
|
4
29
|
|
|
5
30
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -111,9 +111,10 @@ Use `instructionMapping` when the ACP implementation exposes a native system
|
|
|
111
111
|
or developer prompt. A `session-meta` mapping writes `HarnessAgent`
|
|
112
112
|
instructions below the ACP session request's `_meta` field. A
|
|
113
113
|
`launch-env-json` mapping merges them into a JSON environment variable before
|
|
114
|
-
the implementation starts.
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
the implementation starts. A `filesystem` mapping writes instructions to a
|
|
115
|
+
markdown file at a relative path under the implementation's effective `$HOME`.
|
|
116
|
+
Without a mapping, the adapter preserves its backward-compatible behavior and
|
|
117
|
+
prepends instructions to the first user prompt.
|
|
117
118
|
|
|
118
119
|
Skills are written to `.agents/skills` below the ACP implementation's effective
|
|
119
120
|
`$HOME` and discovered natively by the implementation. Set `skillsDirectory`
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
ListToolsRequestSchema,
|
|
13
13
|
McpError
|
|
14
14
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
15
|
-
var VERSION = true ? "1.0.
|
|
15
|
+
var VERSION = true ? "1.0.41" : "0.0.0-test";
|
|
16
16
|
function createHostToolMCPServer({
|
|
17
17
|
tools: tools2,
|
|
18
18
|
revision = 1,
|
package/dist/bridge/index.mjs
CHANGED
|
@@ -664,7 +664,6 @@ function serialiseError(err) {
|
|
|
664
664
|
import * as acp5 from "@agentclientprotocol/sdk";
|
|
665
665
|
import { spawn } from "child_process";
|
|
666
666
|
import { readFile, writeFile as writeFile2 } from "fs/promises";
|
|
667
|
-
import { fileURLToPath } from "url";
|
|
668
667
|
import { Readable, Writable } from "stream";
|
|
669
668
|
import { argv, env as processEnv } from "process";
|
|
670
669
|
|
|
@@ -695,6 +694,10 @@ var profileValueSchema = z.lazy(
|
|
|
695
694
|
z.record(z.string(), profileValueSchema)
|
|
696
695
|
])
|
|
697
696
|
);
|
|
697
|
+
var hostToolMcpTransportSchema = z.enum([
|
|
698
|
+
"stdio",
|
|
699
|
+
"http"
|
|
700
|
+
]);
|
|
698
701
|
var serializableRecordSchema = z.record(z.string(), serializableValueSchema);
|
|
699
702
|
var profileRecordSchema = z.record(z.string(), profileValueSchema);
|
|
700
703
|
var authenticationSchema = z.object({
|
|
@@ -715,7 +718,8 @@ var bridgeConfigurationSchema = z.object({
|
|
|
715
718
|
providerEnvironment: z.record(z.string(), z.string()).optional(),
|
|
716
719
|
sessionMeta: serializableRecordSchema.optional(),
|
|
717
720
|
clientCapabilities: serializableRecordSchema.optional(),
|
|
718
|
-
askUserQuestionsRequestMethod: z.string().optional()
|
|
721
|
+
askUserQuestionsRequestMethod: z.string().optional(),
|
|
722
|
+
hostToolMcpTransport: hostToolMcpTransportSchema.optional()
|
|
719
723
|
});
|
|
720
724
|
async function readACPBridgeEnvironment({
|
|
721
725
|
env
|
|
@@ -1417,7 +1421,24 @@ function resolveBuiltinTool({
|
|
|
1417
1421
|
const schemaMatches = builtinTools.filter(
|
|
1418
1422
|
(tool) => matchesBuiltinToolInput({ rawInput, inputSchema: tool.inputSchema })
|
|
1419
1423
|
);
|
|
1420
|
-
|
|
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;
|
|
1421
1442
|
}
|
|
1422
1443
|
function findBuiltinToolByTitle({
|
|
1423
1444
|
title,
|
|
@@ -2300,6 +2321,14 @@ async function resolveACPInstructionConfiguration({
|
|
|
2300
2321
|
if (instructionMapping == null || instructions == null || instructions.length === 0) {
|
|
2301
2322
|
return { sessionMeta, environment: resolvedEnvironment };
|
|
2302
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
|
+
}
|
|
2303
2332
|
assertSafePath({ path: instructionMapping.path });
|
|
2304
2333
|
if (instructionMapping.type === "session-meta") {
|
|
2305
2334
|
return {
|
|
@@ -2371,11 +2400,263 @@ function isRecord5(value) {
|
|
|
2371
2400
|
}
|
|
2372
2401
|
|
|
2373
2402
|
// src/v1/bridge/host-tool-relay.ts
|
|
2374
|
-
import { randomBytes, timingSafeEqual } from "crypto";
|
|
2403
|
+
import { randomBytes, randomUUID as randomUUID3, timingSafeEqual } from "crypto";
|
|
2375
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.41" : "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
|
|
2376
2656
|
async function startHostToolRelay({
|
|
2377
2657
|
tools,
|
|
2378
|
-
serverName
|
|
2658
|
+
serverName,
|
|
2659
|
+
mcpTransport = "stdio"
|
|
2379
2660
|
}) {
|
|
2380
2661
|
const state = {
|
|
2381
2662
|
tools: [...tools],
|
|
@@ -2390,8 +2671,44 @@ async function startHostToolRelay({
|
|
|
2390
2671
|
let activeTurn;
|
|
2391
2672
|
let invocationOrder = 0;
|
|
2392
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;
|
|
2393
2693
|
const server = createServer(async (request, response) => {
|
|
2394
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
|
+
}
|
|
2395
2712
|
const result = await handleRequest({
|
|
2396
2713
|
request,
|
|
2397
2714
|
credential,
|
|
@@ -2403,6 +2720,10 @@ async function startHostToolRelay({
|
|
|
2403
2720
|
response.writeHead(200, { "content-type": "application/json" });
|
|
2404
2721
|
response.end(JSON.stringify(result));
|
|
2405
2722
|
} catch (error) {
|
|
2723
|
+
if (response.headersSent) {
|
|
2724
|
+
response.end();
|
|
2725
|
+
return;
|
|
2726
|
+
}
|
|
2406
2727
|
const status = error instanceof RelayRequestError ? error.status : 500;
|
|
2407
2728
|
response.writeHead(status, { "content-type": "application/json" });
|
|
2408
2729
|
response.end(
|
|
@@ -2416,6 +2737,9 @@ async function startHostToolRelay({
|
|
|
2416
2737
|
const address = server.address();
|
|
2417
2738
|
return {
|
|
2418
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
|
+
},
|
|
2419
2743
|
credential,
|
|
2420
2744
|
bindTurn: ({ turn }) => {
|
|
2421
2745
|
if (activeTurn != null && activeTurn !== turn) {
|
|
@@ -2435,6 +2759,10 @@ async function startHostToolRelay({
|
|
|
2435
2759
|
state.fingerprint = fingerprint;
|
|
2436
2760
|
state.revision += 1;
|
|
2437
2761
|
resolveCatalogChanges({ state });
|
|
2762
|
+
void mcpEndpoint?.updateCatalog({
|
|
2763
|
+
revision: state.revision,
|
|
2764
|
+
tools: state.tools
|
|
2765
|
+
});
|
|
2438
2766
|
return { changed: true, revision: state.revision };
|
|
2439
2767
|
},
|
|
2440
2768
|
waitForCatalogRefresh: ({ revision, timeoutMs }) => waitForCatalogRefresh({ state, revision, timeoutMs }),
|
|
@@ -2443,7 +2771,10 @@ async function startHostToolRelay({
|
|
|
2443
2771
|
state.closed = true;
|
|
2444
2772
|
resolveCatalogChanges({ state });
|
|
2445
2773
|
resolveRefreshWaiters({ state, closing: true });
|
|
2446
|
-
closePromise =
|
|
2774
|
+
closePromise = (async () => {
|
|
2775
|
+
await mcpEndpoint?.close();
|
|
2776
|
+
await closeServer({ server });
|
|
2777
|
+
})();
|
|
2447
2778
|
return closePromise;
|
|
2448
2779
|
}
|
|
2449
2780
|
};
|
|
@@ -2521,13 +2852,16 @@ function handleCatalogSeen({
|
|
|
2521
2852
|
message: "Invalid host tool catalog acknowledgment."
|
|
2522
2853
|
});
|
|
2523
2854
|
}
|
|
2524
|
-
state
|
|
2525
|
-
state.servedRevision,
|
|
2526
|
-
body.revision
|
|
2527
|
-
);
|
|
2528
|
-
resolveRefreshWaiters({ state, closing: false });
|
|
2855
|
+
acknowledgeCatalog({ state, revision: body.revision });
|
|
2529
2856
|
return { acknowledged: true };
|
|
2530
2857
|
}
|
|
2858
|
+
function acknowledgeCatalog({
|
|
2859
|
+
state,
|
|
2860
|
+
revision
|
|
2861
|
+
}) {
|
|
2862
|
+
state.servedRevision = Math.max(state.servedRevision, revision);
|
|
2863
|
+
resolveRefreshWaiters({ state, closing: false });
|
|
2864
|
+
}
|
|
2531
2865
|
async function handleInvocation({
|
|
2532
2866
|
body,
|
|
2533
2867
|
state,
|
|
@@ -2726,6 +3060,55 @@ var RelayRequestError = class extends Error {
|
|
|
2726
3060
|
}
|
|
2727
3061
|
};
|
|
2728
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
|
+
|
|
2729
3112
|
// src/v1/bridge/refresh-host-tool-catalog.ts
|
|
2730
3113
|
async function promptAndRefreshInitialHostToolCatalog({
|
|
2731
3114
|
startPrompt,
|
|
@@ -3654,33 +4037,26 @@ async function ensureSession({
|
|
|
3654
4037
|
initialization
|
|
3655
4038
|
});
|
|
3656
4039
|
const tools = start.tools ?? [];
|
|
4040
|
+
const mcpTransport = bridgeConfiguration.hostToolMcpTransport ?? "stdio";
|
|
3657
4041
|
const catalogPath = `${bridgeStateDir}/host-tools.json`;
|
|
3658
|
-
|
|
4042
|
+
if (mcpTransport === "stdio") {
|
|
4043
|
+
await writeFile2(catalogPath, JSON.stringify(tools), { mode: 384 });
|
|
4044
|
+
}
|
|
3659
4045
|
hostToolRelay = await startHostToolRelay({
|
|
3660
4046
|
tools,
|
|
3661
|
-
serverName: HOST_TOOL_MCP_SERVER_NAME
|
|
4047
|
+
serverName: HOST_TOOL_MCP_SERVER_NAME,
|
|
4048
|
+
mcpTransport
|
|
3662
4049
|
});
|
|
3663
4050
|
const mcpServers = [
|
|
3664
4051
|
...externalMcpServers,
|
|
3665
|
-
{
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
|
|
3669
|
-
|
|
3670
|
-
|
|
3671
|
-
|
|
3672
|
-
|
|
3673
|
-
},
|
|
3674
|
-
{
|
|
3675
|
-
name: "AI_SDK_ACP_HOST_TOOL_RELAY_URL",
|
|
3676
|
-
value: hostToolRelay.url
|
|
3677
|
-
},
|
|
3678
|
-
{
|
|
3679
|
-
name: "AI_SDK_ACP_HOST_TOOL_RELAY_CREDENTIAL",
|
|
3680
|
-
value: hostToolRelay.credential
|
|
3681
|
-
}
|
|
3682
|
-
]
|
|
3683
|
-
}
|
|
4052
|
+
createHostToolMcpServerDefinition({
|
|
4053
|
+
mcpTransport,
|
|
4054
|
+
relay: hostToolRelay,
|
|
4055
|
+
serverName: HOST_TOOL_MCP_SERVER_NAME,
|
|
4056
|
+
catalogPath,
|
|
4057
|
+
initialization,
|
|
4058
|
+
harnessId: bridgeType
|
|
4059
|
+
})
|
|
3684
4060
|
];
|
|
3685
4061
|
let createdSession;
|
|
3686
4062
|
if (start.recoveryMode?.type === "lossy-rerun") {
|