From 6104e5e9f23a9298cffa062fdacf921229fac21c Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 3 Sep 2025 18:58:39 -0300 Subject: [PATCH 1/4] Send available commands over notifications Co-authored-by: Cole Miller --- package-lock.json | 8 ++++---- package.json | 2 +- src/acp-agent.ts | 41 +++++++++++++++++++------------------ src/mcp-server.ts | 19 ++++++++++++++--- src/tests/acp-agent.test.ts | 30 ++++++++++++++++++++------- 5 files changed, 65 insertions(+), 35 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3bf925ba..8f70343c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "@anthropic-ai/claude-code": "^1.0.100", "@modelcontextprotocol/sdk": "^1.17.4", "@types/express": "^5.0.3", - "@zed-industries/agent-client-protocol": "0.2.0-alpha.4", + "@zed-industries/agent-client-protocol": "0.2.0-alpha.6", "diff": "^8.0.2", "express": "^5.1.0", "minimist": "^1.2.8", @@ -1797,9 +1797,9 @@ } }, "node_modules/@zed-industries/agent-client-protocol": { - "version": "0.2.0-alpha.4", - "resolved": "https://registry.npmjs.org/@zed-industries/agent-client-protocol/-/agent-client-protocol-0.2.0-alpha.4.tgz", - "integrity": "sha512-l/MdK0MrvEb55/U3wAtAN/MC04YK+N6vNyakNgLsK2JTcmG54n45gXa0OZ08lYUmEvLe0shdu1dDtzlOV/Luxw==", + "version": "0.2.0-alpha.6", + "resolved": "https://registry.npmjs.org/@zed-industries/agent-client-protocol/-/agent-client-protocol-0.2.0-alpha.6.tgz", + "integrity": "sha512-Mepcrp9ztLKRSkUuKJ7gnxIL1NRoGbSlY+BWr6WyhJ/sqcocaPSiINs0rt24RZGiU8E+TUwy5FNFW5T9tbIR1A==", "license": "Apache-2.0", "dependencies": { "zod": "^3.0.0" diff --git a/package.json b/package.json index 46f58552..ef5e4693 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,7 @@ "@anthropic-ai/claude-code": "^1.0.100", "@modelcontextprotocol/sdk": "^1.17.4", "@types/express": "^5.0.3", - "@zed-industries/agent-client-protocol": "0.2.0-alpha.4", + "@zed-industries/agent-client-protocol": "0.2.0-alpha.6", "diff": "^8.0.2", "express": "^5.1.0", "minimist": "^1.2.8", diff --git a/src/acp-agent.ts b/src/acp-agent.ts index 65927b3b..37599ba0 100644 --- a/src/acp-agent.ts +++ b/src/acp-agent.ts @@ -31,7 +31,7 @@ import * as fs from "node:fs"; import * as path from "node:path"; import * as os from "node:os"; import { v7 as uuidv7 } from "uuid"; -import { nodeToWebReadable, nodeToWebWritable, Pushable, sleep, unreachable } from "./utils.js"; +import { nodeToWebReadable, nodeToWebWritable, Pushable, unreachable } from "./utils.js"; import { SessionNotification } from "@zed-industries/agent-client-protocol"; import { createMcpServer } from "./mcp-server.js"; import { AddressInfo } from "node:net"; @@ -102,6 +102,13 @@ export class ClaudeAcpAgent implements Agent { }; } async newSession(params: NewSessionRequest): Promise { + if ( + fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) && + !fs.existsSync(path.resolve(os.homedir(), ".claude.json")) + ) { + throw RequestError.authRequired(); + } + const sessionId = uuidv7(); const input = new Pushable(); @@ -171,10 +178,18 @@ export class ClaudeAcpAgent implements Agent { cancelled: false, }; - const availableCommands = await availableSlashCommands(q); + getAvailableSlashCommands(q).then((availableCommands) => { + this.client.sessionUpdate({ + sessionId, + update: { + sessionUpdate: "available_commands_update", + availableCommands, + }, + }); + }); + return { sessionId, - availableCommands, }; } @@ -277,7 +292,7 @@ export class ClaudeAcpAgent implements Agent { } } -async function availableSlashCommands(query: Query): Promise { +async function getAvailableSlashCommands(query: Query): Promise { const UNSUPPORTED_COMMANDS = [ "add-dir", "agents", // Modal @@ -313,22 +328,8 @@ async function availableSlashCommands(query: Query): Promise "todos", // Escape Codes "vim", // Not needed ]; - - const commands = await Promise.race([ - //todo: Do not use `as any` once `supportedCommands` is exposed via the typescript interface - (query as any).supportedCommands(), - sleep(10000).then(() => { - if ( - fs.existsSync(path.resolve(os.homedir(), ".claude.json.backup")) && - !fs.existsSync(path.resolve(os.homedir(), ".claude.json")) - ) { - throw RequestError.authRequired(); - } - throw new Error( - "Failed to intialize Claude Code.\n\nThis may be caused by incorrect MCP server configuration, try disabling them.", - ); - }), - ]); + //todo: Do not use `as any` once `supportedCommands` is exposed via the typescript interface + const commands = await (query as any).supportedCommands(); return commands .map((command: { name: string; description: string; argumentHint: string }) => { diff --git a/src/mcp-server.ts b/src/mcp-server.ts index 550aa5c6..5025b205 100644 --- a/src/mcp-server.ts +++ b/src/mcp-server.ts @@ -2,6 +2,7 @@ import express from "express"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; +import { ListPromptsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import { Server } from "node:http"; import { ClaudeAcpAgent } from "./acp-agent.js"; @@ -22,9 +23,21 @@ export function createMcpServer( clientCapabilities: ClientCapabilities | undefined, ): Promise { // Create MCP server - const server = new McpServer({ - name: "acp-mcp-server", - version: "1.0.0", + const server = new McpServer( + { + name: "acp-mcp-server", + version: "1.0.0", + }, + { + capabilities: { + prompts: {}, + }, + }, + ); + + server.server.setRequestHandler(ListPromptsRequestSchema, async (_request, _extra) => { + await sleep(10_000); + return { prompts: [] }; }); if (clientCapabilities?.fs?.readTextFile) { diff --git a/src/tests/acp-agent.test.ts b/src/tests/acp-agent.test.ts index dd7bb7e4..67e72157 100644 --- a/src/tests/acp-agent.test.ts +++ b/src/tests/acp-agent.test.ts @@ -2,6 +2,7 @@ import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { spawn, spawnSync } from "child_process"; import { Agent, + AvailableCommand, Client, ClientSideConnection, NewSessionResponse, @@ -47,9 +48,15 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration" agent: Agent; files: Map = new Map(); receivedText: string = ""; + resolveAvailableCommands: (commands: AvailableCommand[]) => void; + availableCommandsPromise: Promise; constructor(agent: Agent) { this.agent = agent; + this.resolveAvailableCommands = () => {}; + this.availableCommandsPromise = new Promise((resolve) => { + this.resolveAvailableCommands = resolve; + }); } takeReceivedText() { @@ -67,11 +74,18 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration" async sessionUpdate(params: SessionNotification): Promise { console.error("RECEIVED", JSON.stringify(params, null, 4)); - if ( - params.update.sessionUpdate === "agent_message_chunk" && - params.update.content.type === "text" - ) { - this.receivedText += params.update.content.text; + switch (params.update.sessionUpdate) { + case "agent_message_chunk": { + if (params.update.content.type === "text") { + this.receivedText += params.update.content.text; + } + break; + } + case "available_commands_update": + this.resolveAvailableCommands(params.update.availableCommands); + break; + default: + break; } } @@ -140,12 +154,14 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)("ACP subprocess integration" it("should include available commands", async () => { const { client, connection, newSessionResponse } = await setupTestSession(__dirname); - expect(newSessionResponse.availableCommands).toContainEqual({ + const commands = await client.availableCommandsPromise; + + expect(commands).toContainEqual({ name: "quick-math", description: "10 * 3 = 30 (project)", input: null, }); - expect(newSessionResponse.availableCommands).toContainEqual({ + expect(commands).toContainEqual({ name: "say-hello", description: "Say hello (project)", input: { hint: "[name]" }, From f1893a64c6c46dd64cc952dd4eead70a9b4d01b1 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 3 Sep 2025 18:59:43 -0300 Subject: [PATCH 2/4] Remove sleep Co-authored-by: Cole Miller --- src/mcp-server.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/mcp-server.ts b/src/mcp-server.ts index 5025b205..b964f494 100644 --- a/src/mcp-server.ts +++ b/src/mcp-server.ts @@ -27,19 +27,9 @@ export function createMcpServer( { name: "acp-mcp-server", version: "1.0.0", - }, - { - capabilities: { - prompts: {}, - }, - }, + } ); - server.server.setRequestHandler(ListPromptsRequestSchema, async (_request, _extra) => { - await sleep(10_000); - return { prompts: [] }; - }); - if (clientCapabilities?.fs?.readTextFile) { server.registerTool( "read", From 2f8e2bbe5f9195e4e316ced1200ea077878cf124 Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 3 Sep 2025 19:00:42 -0300 Subject: [PATCH 3/4] Remove unused import --- src/mcp-server.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/mcp-server.ts b/src/mcp-server.ts index b964f494..64c10b6e 100644 --- a/src/mcp-server.ts +++ b/src/mcp-server.ts @@ -2,7 +2,6 @@ import express from "express"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; -import { ListPromptsRequestSchema } from "@modelcontextprotocol/sdk/types.js"; import { z } from "zod"; import { Server } from "node:http"; import { ClaudeAcpAgent } from "./acp-agent.js"; From ab2fd6e674141b4aa210ebef1d3905ce6a31c49e Mon Sep 17 00:00:00 2001 From: Agus Zubiaga Date: Wed, 3 Sep 2025 19:07:32 -0300 Subject: [PATCH 4/4] Fix format Co-authored-by: Cole Miller --- src/mcp-server.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/mcp-server.ts b/src/mcp-server.ts index 64c10b6e..550aa5c6 100644 --- a/src/mcp-server.ts +++ b/src/mcp-server.ts @@ -22,12 +22,10 @@ export function createMcpServer( clientCapabilities: ClientCapabilities | undefined, ): Promise { // Create MCP server - const server = new McpServer( - { - name: "acp-mcp-server", - version: "1.0.0", - } - ); + const server = new McpServer({ + name: "acp-mcp-server", + version: "1.0.0", + }); if (clientCapabilities?.fs?.readTextFile) { server.registerTool(