mirror of
https://github.com/agentclientprotocol/claude-agent-acp.git
synced 2026-10-27 12:40:32 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4211f8cf05 | ||
|
|
ac2119a23a | ||
|
|
427117dce6 | ||
|
|
b5a7e0f28d |
@@ -1,5 +1,14 @@
|
||||
# Changelog
|
||||
|
||||
## 0.10.8
|
||||
|
||||
- Update to @anthropic-ai/claude-agent-sdk@v0.1.51 (adds support for Opus 4.5)
|
||||
|
||||
## 0.10.7
|
||||
|
||||
- Fix read/edit tool error handling so upstream errors surface
|
||||
- Update to @anthropic-ai/claude-agent-sdk@v0.1.50
|
||||
|
||||
## 0.10.6
|
||||
|
||||
- Disable experimental terminal auth support for now, as it was causing issues on Windows. Will revisit with a fix later.
|
||||
|
||||
660
package-lock.json
generated
660
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
16
package.json
16
package.json
@@ -3,7 +3,7 @@
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"version": "0.10.6",
|
||||
"version": "0.10.8",
|
||||
"description": "An ACP-compatible coding agent powered by the Claude Code SDK (TypeScript)",
|
||||
"main": "dist/lib.js",
|
||||
"bin": {
|
||||
@@ -52,25 +52,25 @@
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "0.5.1",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.1.46",
|
||||
"@anthropic-ai/claude-code": "2.0.46",
|
||||
"@anthropic-ai/claude-agent-sdk": "0.1.51",
|
||||
"@anthropic-ai/claude-code": "2.0.51",
|
||||
"@modelcontextprotocol/sdk": "1.22.0",
|
||||
"diff": "8.0.2",
|
||||
"uuid": "13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@anthropic-ai/sdk": "0.70.0",
|
||||
"@anthropic-ai/sdk": "0.71.0",
|
||||
"@types/node": "24.10.1",
|
||||
"@types/uuid": "10.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "8.47.0",
|
||||
"@typescript-eslint/parser": "8.47.0",
|
||||
"@vitest/ui": "4.0.10",
|
||||
"@typescript-eslint/eslint-plugin": "8.48.0",
|
||||
"@typescript-eslint/parser": "8.48.0",
|
||||
"@vitest/ui": "4.0.13",
|
||||
"eslint": "9.39.1",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"globals": "16.5.0",
|
||||
"prettier": "3.6.2",
|
||||
"ts-node": "10.9.2",
|
||||
"typescript": "5.9.3",
|
||||
"vitest": "4.0.10"
|
||||
"vitest": "4.0.13"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -393,6 +393,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
break;
|
||||
default:
|
||||
unreachable(message);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case "result": {
|
||||
@@ -430,6 +431,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
return { stopReason: "max_turn_requests" };
|
||||
default:
|
||||
unreachable(message);
|
||||
break;
|
||||
}
|
||||
break;
|
||||
}
|
||||
@@ -514,6 +516,7 @@ export class ClaudeAcpAgent implements Agent {
|
||||
break;
|
||||
default:
|
||||
unreachable(message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new Error("Session did not end in result");
|
||||
@@ -967,6 +970,7 @@ export function toAcpNotifications(
|
||||
}
|
||||
|
||||
case "tool_result":
|
||||
case "tool_search_tool_result":
|
||||
case "web_fetch_tool_result":
|
||||
case "web_search_tool_result":
|
||||
case "code_execution_tool_result":
|
||||
@@ -1008,6 +1012,7 @@ export function toAcpNotifications(
|
||||
|
||||
default:
|
||||
unreachable(chunk);
|
||||
break;
|
||||
}
|
||||
if (update) {
|
||||
output.push({ sessionId, update });
|
||||
@@ -1053,6 +1058,7 @@ export function streamEventToAcpNotifications(
|
||||
|
||||
default:
|
||||
unreachable(event);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -101,15 +101,19 @@ Usage:
|
||||
};
|
||||
}
|
||||
|
||||
const content = await agent.readTextFile({
|
||||
const readResponse = await agent.readTextFile({
|
||||
sessionId,
|
||||
path: input.file_path,
|
||||
line: input.offset,
|
||||
limit: input.limit,
|
||||
});
|
||||
|
||||
if (typeof readResponse?.content !== "string") {
|
||||
throw new Error(`No file contents for ${input.file_path}.`);
|
||||
}
|
||||
|
||||
// Extract lines with byte limit enforcement
|
||||
const result = extractLinesWithByteLimit(content.content, defaults.maxFileSize);
|
||||
const result = extractLinesWithByteLimit(readResponse.content, defaults.maxFileSize);
|
||||
|
||||
// Construct informative message about what was read
|
||||
let readInfo = "";
|
||||
@@ -253,47 +257,62 @@ Usage:
|
||||
},
|
||||
},
|
||||
async (input) => {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
try {
|
||||
const session = agent.sessions[sessionId];
|
||||
if (!session) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The user has left the building",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const readResponse = await agent.readTextFile({
|
||||
sessionId,
|
||||
path: input.file_path,
|
||||
});
|
||||
|
||||
if (typeof readResponse?.content !== "string") {
|
||||
throw new Error(`No file contents for ${input.file_path}.`);
|
||||
}
|
||||
|
||||
const { newContent } = replaceAndCalculateLocation(readResponse.content, [
|
||||
{
|
||||
oldText: input.old_string,
|
||||
newText: input.new_string,
|
||||
replaceAll: input.replace_all,
|
||||
},
|
||||
]);
|
||||
|
||||
const patch = diff.createPatch(input.file_path, readResponse.content, newContent);
|
||||
|
||||
await agent.writeTextFile({
|
||||
sessionId,
|
||||
path: input.file_path,
|
||||
content: newContent,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "The user has left the building",
|
||||
text: patch,
|
||||
},
|
||||
],
|
||||
};
|
||||
} catch (error: any) {
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "Editing file failed: " + (error?.message ?? String(error)),
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
const { content } = await agent.readTextFile({
|
||||
sessionId,
|
||||
path: input.file_path,
|
||||
});
|
||||
|
||||
const { newContent } = replaceAndCalculateLocation(content, [
|
||||
{
|
||||
oldText: input.old_string,
|
||||
newText: input.new_string,
|
||||
replaceAll: input.replace_all,
|
||||
},
|
||||
]);
|
||||
|
||||
const patch = diff.createPatch(input.file_path, content, newContent);
|
||||
|
||||
await agent.writeTextFile({
|
||||
sessionId,
|
||||
path: input.file_path,
|
||||
content: newContent,
|
||||
});
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: patch,
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -563,7 +582,8 @@ In sessions with ${toolNames.killShell} always use it instead of KillShell.`,
|
||||
content: [{ type: "text", text: "Command killed by timeout." }],
|
||||
};
|
||||
default: {
|
||||
return unreachable(bgTerm);
|
||||
unreachable(bgTerm);
|
||||
throw new Error("Unexpected background terminal status");
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
72
src/tests/mcp-server-read.test.ts
Normal file
72
src/tests/mcp-server-read.test.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { createMcpServer } from "../mcp-server.js";
|
||||
|
||||
const SESSION_ID = "session-id";
|
||||
|
||||
function getTool(server: any, name: string) {
|
||||
const tools = server?._registeredTools;
|
||||
if (!tools || !tools[name]) {
|
||||
throw new Error(`Tool ${name} not registered`);
|
||||
}
|
||||
return tools[name];
|
||||
}
|
||||
|
||||
function createAgent(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
sessions: {
|
||||
[SESSION_ID]: {},
|
||||
},
|
||||
readTextFile: vi.fn(),
|
||||
writeTextFile: vi.fn(),
|
||||
backgroundTerminals: {},
|
||||
clientCapabilities: {},
|
||||
client: {
|
||||
sessionUpdate: vi.fn(),
|
||||
createTerminal: vi.fn(),
|
||||
requestPermission: vi.fn(),
|
||||
},
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("MCP server read-related tools", () => {
|
||||
it("reports missing file contents instead of crashing the read tool", async () => {
|
||||
const agent = createAgent();
|
||||
agent.readTextFile.mockResolvedValue({});
|
||||
|
||||
const server = createMcpServer(agent as any, SESSION_ID, {
|
||||
fs: { readTextFile: true },
|
||||
});
|
||||
|
||||
const readTool = getTool(server, "Read");
|
||||
|
||||
const result = await readTool.callback({
|
||||
file_path: "/tmp/file.txt",
|
||||
offset: 1,
|
||||
limit: 2000,
|
||||
});
|
||||
|
||||
expect(result.content[0].text).toBe("Reading file failed: No file contents for /tmp/file.txt.");
|
||||
});
|
||||
|
||||
it("reports missing contents inside the edit tool and skips writes", async () => {
|
||||
const agent = createAgent();
|
||||
agent.readTextFile.mockResolvedValue({});
|
||||
|
||||
const server = createMcpServer(agent as any, SESSION_ID, {
|
||||
fs: { readTextFile: true, writeTextFile: true },
|
||||
});
|
||||
|
||||
const editTool = getTool(server, "Edit");
|
||||
|
||||
const result = await editTool.callback({
|
||||
file_path: "/tmp/file.txt",
|
||||
old_string: "foo",
|
||||
new_string: "bar",
|
||||
replace_all: false,
|
||||
});
|
||||
|
||||
expect(result.content[0].text).toBe("Editing file failed: No file contents for /tmp/file.txt.");
|
||||
expect(agent.writeTextFile).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
BetaCodeExecutionToolResultBlockParam,
|
||||
BetaRequestMCPToolResultBlockParam,
|
||||
BetaTextEditorCodeExecutionToolResultBlockParam,
|
||||
BetaToolSearchToolResultBlockParam,
|
||||
BetaWebFetchToolResultBlockParam,
|
||||
BetaWebSearchToolResultBlockParam,
|
||||
} from "@anthropic-ai/sdk/resources/beta.mjs";
|
||||
@@ -404,7 +405,8 @@ export function toolUpdateFromToolResult(
|
||||
| BetaCodeExecutionToolResultBlockParam
|
||||
| BetaBashCodeExecutionToolResultBlockParam
|
||||
| BetaTextEditorCodeExecutionToolResultBlockParam
|
||||
| BetaRequestMCPToolResultBlockParam,
|
||||
| BetaRequestMCPToolResultBlockParam
|
||||
| BetaToolSearchToolResultBlockParam,
|
||||
toolUse: any | undefined,
|
||||
): ToolUpdate {
|
||||
switch (toolUse?.name) {
|
||||
|
||||
@@ -75,14 +75,14 @@ export function nodeToWebReadable(nodeStream: Readable): ReadableStream<Uint8Arr
|
||||
});
|
||||
}
|
||||
|
||||
export function unreachable(value: never): never {
|
||||
export function unreachable(value: never) {
|
||||
let valueAsString;
|
||||
try {
|
||||
valueAsString = JSON.stringify(value);
|
||||
} catch {
|
||||
valueAsString = value;
|
||||
}
|
||||
throw new Error(`Unexpected case: ${valueAsString}`);
|
||||
console.error(`Unexpected case: ${valueAsString}`);
|
||||
}
|
||||
|
||||
export function sleep(time: number): Promise<void> {
|
||||
|
||||
Reference in New Issue
Block a user