mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-28 21:12:01 +00:00
fix(v2): reject MCP transports and prompt content the agent doesn't advertise (#1459)
Stacked on #1457 (SDK 2.1.0). The v2 spec says Agents MUST reject with `-32602` a transport or content type they did not advertise. Before this PR, both of these were accepted and then silently dropped: - **`acp` MCP servers:** the agent advertises only `stdio` and `http`. The v1 setup skipped these servers. - **`audio` blocks:** not advertised, and `promptToClaude` ignored them. This also applies to a v2 `systemPrompt`, since it is converted the same way. Embedded blob `resource` blocks are still accepted; see the blob resources PR.
This commit is contained in:
@@ -452,6 +452,21 @@ describe("ACP v2 sessions", () => {
|
||||
expect(sdk.queryOptions).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("rejects an MCP-over-ACP server, which the agent does not advertise", async () => {
|
||||
const { app, authUpdate } = v2Client();
|
||||
await app.connectWith(connectRouter(), async (agent) => {
|
||||
await initializeV2(agent);
|
||||
await expect(
|
||||
agent.request(v2.methods.agent.session.new, {
|
||||
cwd,
|
||||
mcpServers: [{ type: "acp", name: "tools", serverId: "server-1" }],
|
||||
}),
|
||||
).rejects.toMatchObject({ code: -32602, data: { type: "acp" } });
|
||||
await authUpdate(1);
|
||||
});
|
||||
expect(sdk.queryOptions).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("lists, resumes, closes, and deletes sessions", async () => {
|
||||
const { app, authUpdate } = v2Client();
|
||||
await app.connectWith(connectRouter(), async (agent) => {
|
||||
@@ -5422,6 +5437,32 @@ describe("ACP v2 session translation", () => {
|
||||
expect(prompt([])).toEqual({ audience: [], priority: 1 });
|
||||
});
|
||||
|
||||
it("rejects audio prompt content, which the agent does not advertise", () => {
|
||||
expect(() =>
|
||||
v1PromptRequest({
|
||||
sessionId: "s",
|
||||
prompt: [{ type: "audio", data: "AAAA", mimeType: "audio/wav" }],
|
||||
}),
|
||||
).toThrow(expect.objectContaining({ code: -32602, data: { type: "audio" } }));
|
||||
// An embedded blob resource still converts.
|
||||
expect(
|
||||
v1PromptRequest({
|
||||
sessionId: "s",
|
||||
prompt: [
|
||||
{
|
||||
type: "resource",
|
||||
resource: { uri: "file:///a.bin", blob: "AAAA", mimeType: "application/octet-stream" },
|
||||
},
|
||||
],
|
||||
}).prompt,
|
||||
).toEqual([
|
||||
{
|
||||
type: "resource",
|
||||
resource: { uri: "file:///a.bin", blob: "AAAA", mimeType: "application/octet-stream" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("fails on a message chunk without a messageId, which v2 requires", () => {
|
||||
expect(() =>
|
||||
v2SessionUpdate({ sessionUpdate: "agent_message_chunk", content: text("hi")[0] }),
|
||||
|
||||
@@ -141,9 +141,11 @@ function idle({ stopReason, usage, _meta }: TurnOutcome): v2.SessionUpdate {
|
||||
}
|
||||
|
||||
/**
|
||||
* A v2 prompt may hold content blocks that v1 cannot express: a custom one,
|
||||
* or one from a newer ACP version. Clients send only the kinds the agent
|
||||
* advertised, so such a block is rejected rather than silently dropped.
|
||||
* A v2 prompt may hold content blocks that the agent does not take: audio,
|
||||
* which it does not advertise and Claude cannot read, a custom one, or one
|
||||
* from a newer ACP version. Clients send only the kinds the agent advertised,
|
||||
* so such a block fails the request with invalid params (-32602) rather than
|
||||
* being dropped from the prompt.
|
||||
*/
|
||||
export function v1PromptRequest({ prompt, ...request }: v2.PromptRequest): PromptRequest {
|
||||
return { ...request, prompt: prompt.map(v1ContentBlock) };
|
||||
@@ -153,7 +155,6 @@ export function v1ContentBlock(block: v2.ContentBlock): ContentBlock {
|
||||
if (
|
||||
v2.ContentBlock.isText(block) ||
|
||||
v2.ContentBlock.isImage(block) ||
|
||||
v2.ContentBlock.isAudio(block) ||
|
||||
v2.ContentBlock.isResourceLink(block) ||
|
||||
v2.ContentBlock.isResource(block)
|
||||
) {
|
||||
|
||||
@@ -165,8 +165,10 @@ function v2ConfigOptionsField(options: SessionConfigOption[] | null | undefined)
|
||||
/**
|
||||
* v2 requires a `type` on every transport and makes the empty lists optional.
|
||||
* The agent recognizes a v1 stdio server by its missing `type`, so the v2 tag
|
||||
* is dropped. A transport that v1 cannot express is rejected rather than
|
||||
* silently left out of the session.
|
||||
* is dropped. Any other transport, such as `acp` or a custom `_` type, fails
|
||||
* the request with invalid params (-32602): the agent advertises only stdio
|
||||
* and HTTP, and the v1 session setup would otherwise drop the server without
|
||||
* telling the client.
|
||||
*/
|
||||
function v1McpServer(server: v2.McpServer): McpServer {
|
||||
if (v2.McpServer.isStdio(server)) {
|
||||
@@ -176,9 +178,6 @@ function v1McpServer(server: v2.McpServer): McpServer {
|
||||
if (v2.McpServer.isHttp(server)) {
|
||||
return { ...server, headers: server.headers ?? [] };
|
||||
}
|
||||
if (v2.McpServer.isAcp(server)) {
|
||||
return server;
|
||||
}
|
||||
throw RequestError.invalidParams(
|
||||
{ type: server.type },
|
||||
`MCP servers of type ${server.type} are not supported`,
|
||||
|
||||
Reference in New Issue
Block a user