From 1d2b92feae5eeccde3ac738edca1f1d71250762e Mon Sep 17 00:00:00 2001 From: Ben Brandt Date: Fri, 30 Oct 2026 15:39:09 +0100 Subject: [PATCH] fix: update to @agentclientprotocol/sdk 2.2.0 (#1491) Moves to ACP schema v1.28.0 and v2.0.0-alpha.11. - v2 dropped JSON-RPC batching, so `AnyWireMessage` is gone. The test harnesses use `AnyMessage`, and a request id can now be `null` in the types. - The v2 elicitation schema now requires `properties`. A forwarded MCP schema without them, such as `{ "type": "object" }`, gets an empty map, so clients on SDK 2.2.0 don't reject it. Runtime changes that come with the SDK and need no agent code: - The SDK handles a repeated `initialize`, rejects batches, and rejects messages sent before initialization. A failed `initialize` now leaves the connection open. - v2 `usage_update.cost` is now a patch field, where omitted means unchanged. The agent sends `cost` only on the result update, so the client keeps showing the last cost during a turn. Follow-up PRs for the spec changes in this range are stacked on this branch. --- package-lock.json | 8 ++++---- package.json | 2 +- src/elicitation.ts | 7 +++++-- src/tests/acp-scenarios/harness.ts | 14 +++++--------- src/tests/acp-v2.test.ts | 8 ++++---- src/tests/elicitation.test.ts | 13 +++++++++++++ src/tests/provider-session-configuration.test.ts | 4 ++-- 7 files changed, 34 insertions(+), 22 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9c9d1a72..79fd18fe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.99.1", "license": "Apache-2.0", "dependencies": { - "@agentclientprotocol/sdk": "2.1.0", + "@agentclientprotocol/sdk": "2.2.0", "@anthropic-ai/claude-agent-sdk": "0.3.312", "diff": "9.0.0", "zod": "4.6.7" @@ -38,9 +38,9 @@ } }, "node_modules/@agentclientprotocol/sdk": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-2.1.0.tgz", - "integrity": "sha512-IuKjJmHLPzbmbg26eK9Jopd8AVxgLuu8fX114YzYAU6tLYclCo2DUzLL4vbCpYGr9mzvs2T0kigE3ZCueL06tg==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@agentclientprotocol/sdk/-/sdk-2.2.0.tgz", + "integrity": "sha512-o1+eDQVb05yzPY+5v54awHyONU1oeLsbLVavkwrMQaZDuJDh/38pQiXJS4ix4d6QA3Y4bU0lHFRjwg99KKKSaQ==", "license": "Apache-2.0", "peerDependencies": { "@modelcontextprotocol/server": "^2.4.0", diff --git a/package.json b/package.json index 4f509168..c8f691f7 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "node": ">=22" }, "dependencies": { - "@agentclientprotocol/sdk": "2.1.0", + "@agentclientprotocol/sdk": "2.2.0", "@anthropic-ai/claude-agent-sdk": "0.3.312", "diff": "9.0.0", "zod": "4.6.7" diff --git a/src/elicitation.ts b/src/elicitation.ts index b705a3aa..6989fcb0 100644 --- a/src/elicitation.ts +++ b/src/elicitation.ts @@ -377,7 +377,9 @@ export function applyAskElicitationResponse( /** * Coerce an arbitrary MCP `requestedSchema` into an ACP `ElicitationSchema`. * The two are structurally compatible JSON Schemas; we just guarantee the - * `type: "object"` discriminator is present. + * `type: "object"` discriminator and a `properties` map are present. The v2 + * schema requires both, so a client on SDK 2.2.0 or later rejects an MCP + * schema such as `{ type: "object" }` that has no `properties`. */ function normalizeElicitationSchema( schema: Record | undefined, @@ -385,7 +387,8 @@ function normalizeElicitationSchema( if (!schema || typeof schema !== "object") { return { type: "object", properties: {} }; } - return { ...(schema as ElicitationSchema), type: "object" }; + const { properties } = schema as Partial; + return { ...(schema as ElicitationSchema), type: "object", properties: properties ?? {} }; } /** diff --git a/src/tests/acp-scenarios/harness.ts b/src/tests/acp-scenarios/harness.ts index 132aea5c..74ab42ba 100644 --- a/src/tests/acp-scenarios/harness.ts +++ b/src/tests/acp-scenarios/harness.ts @@ -500,21 +500,17 @@ export async function runScenarioV2( const idles: unknown[] = []; const failures: string[] = []; - const each = (message: v2.AnyWireMessage) => - (Array.isArray(message) ? message : [message]) as Record[]; - const toAgent = new TransformStream({ + const toAgent = new TransformStream({ transform(message, controller) { - for (const m of each(message)) { - if ("method" in m && "id" in m) methodsOfClientRequests.set(m.id, m.method); + if ("method" in message && "id" in message && message.id !== null) { + methodsOfClientRequests.set(message.id, message.method); } controller.enqueue(message); }, }); - const toClient = new TransformStream({ + const toClient = new TransformStream({ transform(message, controller) { - for (const m of each(message)) { - if (recording) raw.push(wireRecord(m, methodsOfClientRequests)); - } + if (recording) raw.push(wireRecord(message, methodsOfClientRequests)); controller.enqueue(message); }, }); diff --git a/src/tests/acp-v2.test.ts b/src/tests/acp-v2.test.ts index 830c6509..a1c0d0a8 100644 --- a/src/tests/acp-v2.test.ts +++ b/src/tests/acp-v2.test.ts @@ -162,8 +162,8 @@ afterEach(async () => { /** Connects a fresh router to a client, and returns the client end of the stream. */ function connectRouter(protocols?: AcpProtocols): v2.Stream { - const toAgent = new TransformStream(); - const toClient = new TransformStream(); + const toAgent = new TransformStream(); + const toClient = new TransformStream(); acpProtocolRouter(undefined, (agent) => agents.push(agent), protocols).connect({ readable: toAgent.readable, writable: toClient.writable, @@ -333,11 +333,11 @@ describe("ACP protocol routing", () => { params: { protocolVersion: v2.PROTOCOL_VERSION, info: V2_CLIENT_INFO, capabilities: {} }, }); const reader = stream.readable.getReader(); - let response: v2.AnyWireMessage | undefined; + let response: v2.AnyMessage | undefined; while (!response) { const { value, done } = await reader.read(); if (done) break; - if (!Array.isArray(value) && "id" in value && value.id === 1) response = value; + if ("id" in value && value.id === 1) response = value; } await writer.close(); diff --git a/src/tests/elicitation.test.ts b/src/tests/elicitation.test.ts index 12982a60..1d26a6e1 100644 --- a/src/tests/elicitation.test.ts +++ b/src/tests/elicitation.test.ts @@ -60,6 +60,19 @@ describe("mcpElicitationToCreateRequest", () => { }); }); + it("adds an empty properties map when the MCP schema has none", () => { + const request: ElicitationRequest = { + serverName: "server", + message: "Confirm?", + mode: "form", + requestedSchema: { type: "object" }, + }; + + const result = mcpElicitationToCreateRequest(request, SESSION_ID); + + expect(result).toMatchObject({ requestedSchema: { type: "object", properties: {} } }); + }); + it("maps a url-mode request and preserves the elicitation id", () => { const request: ElicitationRequest = { serverName: "server", diff --git a/src/tests/provider-session-configuration.test.ts b/src/tests/provider-session-configuration.test.ts index dc39f36a..1bb92339 100644 --- a/src/tests/provider-session-configuration.test.ts +++ b/src/tests/provider-session-configuration.test.ts @@ -750,8 +750,8 @@ describe("provider updates preserve session configuration", { timeout: 10_000 }, it("publishes the restored configuration through the v2 protocol router", async () => { await agent.dispose(); - const toAgent = new TransformStream(); - const toClient = new TransformStream(); + const toAgent = new TransformStream(); + const toClient = new TransformStream(); acpProtocolRouter({ log() {}, error() {} }, (created) => (agent = created)).connect({ readable: toAgent.readable, writable: toClient.writable,