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.
This commit is contained in:
Ben Brandt
2026-10-30 15:39:09 +01:00
committed by GitHub
parent 99a6e76158
commit 1d2b92feae
7 changed files with 34 additions and 22 deletions

8
package-lock.json generated
View File

@@ -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",

View File

@@ -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"

View File

@@ -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<string, unknown> | 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<ElicitationSchema>;
return { ...(schema as ElicitationSchema), type: "object", properties: properties ?? {} };
}
/**

View File

@@ -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<string, any>[];
const toAgent = new TransformStream<v2.AnyWireMessage, v2.AnyWireMessage>({
const toAgent = new TransformStream<v2.AnyMessage, v2.AnyMessage>({
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<v2.AnyWireMessage, v2.AnyWireMessage>({
const toClient = new TransformStream<v2.AnyMessage, v2.AnyMessage>({
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);
},
});

View File

@@ -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<v2.AnyWireMessage, v2.AnyWireMessage>();
const toClient = new TransformStream<v2.AnyWireMessage, v2.AnyWireMessage>();
const toAgent = new TransformStream<v2.AnyMessage, v2.AnyMessage>();
const toClient = new TransformStream<v2.AnyMessage, v2.AnyMessage>();
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();

View File

@@ -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",

View File

@@ -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<v2.AnyWireMessage, v2.AnyWireMessage>();
const toClient = new TransformStream<v2.AnyWireMessage, v2.AnyWireMessage>();
const toAgent = new TransformStream<v2.AnyMessage, v2.AnyMessage>();
const toClient = new TransformStream<v2.AnyMessage, v2.AnyMessage>();
acpProtocolRouter({ log() {}, error() {} }, (created) => (agent = created)).connect({
readable: toAgent.readable,
writable: toClient.writable,