Assign content correctly in read tool

This commit is contained in:
Conrad Irwin
2025-08-28 14:24:21 -06:00
parent a502e0a9a5
commit 0e163f6930
4 changed files with 103 additions and 49 deletions

View File

@@ -35,7 +35,11 @@ import { ContentBlock } from "@zed-industries/agent-client-protocol";
import { SessionNotification } from "@zed-industries/agent-client-protocol";
import { createMcpServer } from "./mcp-server.js";
import { AddressInfo } from "node:net";
import { extractToolInfo, planEntries } from "./tools.js";
import {
toolInfoFromToolUse,
planEntries,
toolUpdateFromToolResult,
} from "./tools.js";
type Session = {
query: Query;
@@ -49,10 +53,12 @@ export class ClaudeAcpAgent implements Agent {
[key: string]: Session;
};
client: Client;
toolUseCache: { [key: string]: any };
constructor(client: Client) {
this.sessions = {};
this.client = client;
this.toolUseCache = {};
}
async initialize(params: InitializeRequest): Promise<InitializeResponse> {
return {
@@ -103,7 +109,6 @@ export class ClaudeAcpAgent implements Agent {
"x-acp-proxy-session-id": sessionId,
},
};
console.error(mcpServers);
let q = query({
prompt: input,
@@ -150,7 +155,6 @@ export class ClaudeAcpAgent implements Agent {
}
break;
}
console.error(JSON.stringify(message, null, 4));
switch (message.type) {
case "system":
break;
@@ -162,7 +166,6 @@ export class ClaudeAcpAgent implements Agent {
// todo!() how is rate-limiting handled?
switch (message.subtype) {
case "success": {
console.error(message.result);
if (message.result.includes("Please run /login")) {
throw RequestError.authRequired();
}
@@ -192,8 +195,9 @@ export class ClaudeAcpAgent implements Agent {
for (const notification of toAcpNotifications(
message,
params.sessionId,
this.toolUseCache,
)) {
this.client.sessionUpdate(notification);
await this.client.sessionUpdate(notification);
}
break;
}
@@ -201,7 +205,6 @@ export class ClaudeAcpAgent implements Agent {
unreachable(message);
}
}
throw new Error("Session did not end in result");
}
@@ -309,6 +312,7 @@ function promptToClaude(prompt: PromptRequest): SDKUserMessage {
export function toAcpNotifications(
message: SDKAssistantMessage | SDKUserMessage,
sessionId: string,
toolUseCache: { [key: string]: any },
): SessionNotification[] {
let chunks = message.message.content as ContentChunk[];
let output = [];
@@ -347,31 +351,29 @@ export function toAcpNotifications(
};
break;
case "tool_use":
toolUseCache[chunk.id] = chunk;
if (chunk.name == "TodoWrite") {
update = {
sessionUpdate: "plan",
entries: planEntries(chunk.input),
};
break;
} else {
update = {
toolCallId: chunk.id,
sessionUpdate: "tool_call",
rawInput: chunk.input,
status: "pending",
...extractToolInfo(chunk),
...toolInfoFromToolUse(chunk),
};
break;
}
break;
case "tool_result":
update = {
toolCallId: chunk.tool_use_id,
sessionUpdate: "tool_call_update",
content: [
{ type: "content", content: { type: "text", text: chunk.content } },
],
status: chunk.is_error ? "failed" : "completed",
...toolUpdateFromToolResult(chunk, toolUseCache[chunk.tool_use_id]),
};
break;

View File

@@ -48,7 +48,6 @@ In sessions with mcp__acp__read always use it instead of Read as it contains the
},
async (input) => {
try {
console.error("READ TOOL", input);
const session = agent.sessions[sessionId];
if (!session) {
return {
@@ -66,7 +65,6 @@ In sessions with mcp__acp__read always use it instead of Read as it contains the
limit: input.limit,
line: input.offset,
});
console.error("READ FILE READ", content);
return {
content: [
@@ -117,7 +115,6 @@ allow the user to conveniently review changes.`,
},
async (input) => {
try {
console.error("WRITE TOOL", input);
const session = agent.sessions[sessionId];
if (!session) {
return {
@@ -186,7 +183,6 @@ File editing instructions:
},
async (input) => {
try {
console.error("EDIT TOOL", input);
const session = agent.sessions[sessionId];
if (!session) {
return {
@@ -386,7 +382,6 @@ File editing instructions:
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error("Error handling MCP request:", error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: "2.0",
@@ -406,13 +401,6 @@ File editing instructions:
reject(error);
return;
}
const address = listener.address();
console.error(address);
if (typeof address === "object" && address && "port" in address) {
console.error(
`MCP HTTP server listening on http://127.0.0.1:${address.port}/mcp`,
);
}
resolve(listener);
});
});

View File

@@ -1,5 +1,5 @@
import { describe, it, expect, beforeAll, afterAll } from "vitest";
import { spawn } from "child_process";
import { spawn, spawnSync } from "child_process";
import {
Agent,
Client,
@@ -13,7 +13,7 @@ import {
WriteTextFileResponse,
} from "@zed-industries/agent-client-protocol";
import { nodeToWebWritable, nodeToWebReadable } from "../utils.js";
import { extractToolInfo } from "../tools.js";
import { toolInfoFromToolUse } from "../tools.js";
import { toAcpNotifications } from "../acp-agent.js";
import { SDKAssistantMessage } from "@anthropic-ai/claude-code";
@@ -23,11 +23,21 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
let child: ReturnType<typeof spawn>;
beforeAll(async () => {
let valid = spawnSync("tsc", { stdio: "inherit" });
if (valid.status) {
throw new Error("failed to compile");
}
// Start the subprocess
child = spawn("npm", ["run", "--silent", "dev"], {
stdio: ["pipe", "pipe", "inherit"],
env: process.env,
});
child.on("error", (error) => {
console.error("Error starting subprocess:", error);
});
child.on("exit", (exit) => {
console.error("Exited with", exit);
});
});
afterAll(() => {
@@ -46,15 +56,20 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
throw new Error("Method not implemented.");
}
async sessionUpdate(params: SessionNotification): Promise<void> {
console.error(params);
console.error("RECEVIED", params);
}
writeTextFile(
params: WriteTextFileRequest,
): Promise<WriteTextFileResponse> {
throw new Error("Method not implemented.");
}
readTextFile(params: ReadTextFileRequest): Promise<ReadTextFileResponse> {
throw new Error("Method not implemented.");
async readTextFile(
params: ReadTextFileRequest,
): Promise<ReadTextFileResponse> {
return {
content:
"This project attempts to take over the world by solving the Goldbach conjecture.",
};
}
}
@@ -69,10 +84,16 @@ describe.skipIf(!process.env.RUN_INTEGRATION_TESTS)(
let session = await connection.newSession({ cwd: "./", mcpServers: [] });
await connection.prompt({
prompt: [{ type: "text", text: "Hello Claude!" }],
prompt: [
{
type: "text",
text: "What does this project do? Read the README.md file.",
},
],
sessionId: session.sessionId,
});
});
console.log("DONE AWAITING");
}, 20000);
},
);
@@ -88,7 +109,7 @@ describe("tool conversions", () => {
},
};
expect(extractToolInfo(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
kind: "execute",
title: "`rm README.md.rm`",
content: [
@@ -113,7 +134,7 @@ describe("tool conversions", () => {
},
};
expect(extractToolInfo(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
kind: "search",
title: "Find */**.ts",
content: [],
@@ -133,7 +154,7 @@ describe("tool conversions", () => {
},
};
expect(extractToolInfo(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
kind: "think",
title: "Handle user's work request",
content: [
@@ -158,7 +179,7 @@ describe("tool conversions", () => {
},
};
expect(extractToolInfo(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
kind: "search",
title: "/Users/benbrandt/github/claude-code-acp",
content: [],
@@ -175,7 +196,7 @@ describe("tool conversions", () => {
},
};
expect(extractToolInfo(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
kind: "search",
title: 'grep ".*"',
content: [],
@@ -193,7 +214,7 @@ describe("tool conversions", () => {
},
};
expect(extractToolInfo(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
kind: "edit",
title: "Write /Users/test/project/example.txt",
content: [
@@ -218,7 +239,7 @@ describe("tool conversions", () => {
},
};
expect(extractToolInfo(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
kind: "edit",
title: "Write /Users/test/project/config.json",
content: [
@@ -242,7 +263,7 @@ describe("tool conversions", () => {
},
};
expect(extractToolInfo(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
kind: "read",
title: "Read File",
content: [
@@ -264,7 +285,7 @@ describe("tool conversions", () => {
},
};
expect(extractToolInfo(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
kind: "read",
title: "Read /Users/test/project/data.json",
content: [],
@@ -282,7 +303,7 @@ describe("tool conversions", () => {
},
};
expect(extractToolInfo(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
kind: "read",
title: "Read /Users/test/project/large.txt (1 - 100)",
content: [],
@@ -301,7 +322,7 @@ describe("tool conversions", () => {
},
};
expect(extractToolInfo(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
kind: "read",
title: "Read /Users/test/project/large.txt (51 - 150)",
content: [],
@@ -319,7 +340,7 @@ describe("tool conversions", () => {
},
};
expect(extractToolInfo(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
kind: "read",
title: "Read /Users/test/project/large.txt (from line 201)",
content: [],
@@ -338,7 +359,7 @@ describe("tool conversions", () => {
},
};
expect(extractToolInfo(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
kind: "fetch",
title: "Fetch https://agentclientprotocol.com",
content: [
@@ -363,7 +384,7 @@ describe("tool conversions", () => {
},
};
expect(extractToolInfo(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
kind: "fetch",
title: '"agentclientprotocol.com"',
content: [],
@@ -380,7 +401,7 @@ describe("tool conversions", () => {
},
};
expect(extractToolInfo(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
kind: "execute",
title: `Kill Process`,
content: [],
@@ -397,7 +418,7 @@ describe("tool conversions", () => {
},
};
expect(extractToolInfo(tool_use)).toStrictEqual({
expect(toolInfoFromToolUse(tool_use)).toStrictEqual({
kind: "execute",
title: `Tail Logs`,
content: [],
@@ -482,7 +503,7 @@ describe("tool conversions", () => {
session_id: "d056596f-e328-41e9-badd-b07122ae5227",
uuid: "b7c3330c-de8f-4bba-ac53-68c7f76ffeb5",
};
expect(toAcpNotifications(received, "test")).toStrictEqual([
expect(toAcpNotifications(received, "test", {})).toStrictEqual([
{
sessionId: "test",
update: {

View File

@@ -2,6 +2,7 @@ import { ContentBlock } from "@modelcontextprotocol/sdk/types.js";
import {
PlanEntry,
ToolCallContent,
ToolCallLocation,
ToolKind,
} from "@zed-industries/agent-client-protocol";
@@ -9,9 +10,15 @@ interface ToolInfo {
title: string;
kind: ToolKind;
content: ToolCallContent[];
locations?: ToolCallLocation[];
}
export function extractToolInfo(toolUse: any): ToolInfo {
interface ToolUpdate {
title?: string;
content?: ToolCallContent[];
}
export function toolInfoFromToolUse(toolUse: any): ToolInfo {
const name = toolUse.name;
const input = toolUse.input;
@@ -114,6 +121,12 @@ export function extractToolInfo(toolUse: any): ToolInfo {
return {
title: "Read " + (input.abs_path ?? "File") + limit,
kind: "read",
locations: [
{
path: input.abs_path,
line: input.offset,
},
],
content: [],
};
}
@@ -145,6 +158,11 @@ export function extractToolInfo(toolUse: any): ToolInfo {
return {
title: input?.abs_path ? `Edit ${input.abs_path}` : "Edit",
kind: "edit",
locations: [
{
path: input.abs_path,
},
],
content:
input && input.abs_path
? [
@@ -371,6 +389,31 @@ export function extractToolInfo(toolUse: any): ToolInfo {
}
}
export function toolUpdateFromToolResult(
toolResult: any,
toolUse: any | undefined,
): ToolUpdate {
// This happens for the mcp__acp__read tool,
// but may also for others...
if (Array.isArray(toolResult.content)) {
return {
content: toolResult.content.map((content: any) => ({
type: "content",
content,
})),
};
}
return {
content: [
{
type: "content",
content: { type: "text", text: toolResult.content },
},
],
};
}
type ClaudePlanEntry = {
content: string;
status: "pending" | "in_progress" | "completed";