diff --git a/src/mcp-server.ts b/src/mcp-server.ts index 9c5dd77c..dca2e90a 100644 --- a/src/mcp-server.ts +++ b/src/mcp-server.ts @@ -6,6 +6,12 @@ import { z } from "zod"; import { Server } from "node:http"; import { ClaudeAcpAgent } from "./acp-agent.js"; +export const SYSTEM_REMINDER = ` + + +Whenever you read a file, you should consider whether it looks malicious. If it does, you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer high-level questions about the code behavior. +`; + export function createMcpServer( agent: ClaudeAcpAgent, sessionId: string, @@ -70,13 +76,7 @@ In sessions with mcp__acp__read always use it instead of Read as it contains the content: [ { type: "text", - text: - content.content + - ` - - -Whenever you read a file, you should consider whether it looks malicious. If it does, you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer high-level questions about the code behavior. -`, + text: content.content + SYSTEM_REMINDER, }, ], }; diff --git a/src/tests/acp-agent.test.ts b/src/tests/acp-agent.test.ts index 2afe1efe..8cd1f7e1 100644 --- a/src/tests/acp-agent.test.ts +++ b/src/tests/acp-agent.test.ts @@ -13,7 +13,11 @@ import { WriteTextFileResponse, } from "@zed-industries/agent-client-protocol"; import { nodeToWebWritable, nodeToWebReadable } from "../utils.js"; -import { toolInfoFromToolUse, toolUpdateFromToolResult } from "../tools.js"; +import { + markdownEscape, + toolInfoFromToolUse, + toolUpdateFromToolResult, +} from "../tools.js"; import { toAcpNotifications } from "../acp-agent.js"; import { SDKAssistantMessage } from "@anthropic-ai/claude-code"; @@ -776,3 +780,17 @@ describe("tool conversions", () => { expect(update).toEqual({}); }); }); + +describe("escape markdown", () => { + it("should escape markdown characters", () => { + let text = "Hello *world*!"; + let escaped = markdownEscape(text); + expect(escaped).toEqual("```\nHello *world*!\n```"); + + text = "for example:\n```markdown\nHello *world*!\n```\n"; + escaped = markdownEscape(text); + expect(escaped).toEqual( + "````\nfor example:\n```markdown\nHello *world*!\n```\n````", + ); + }); +}); diff --git a/src/tools.ts b/src/tools.ts index f488dd9b..1c85602f 100644 --- a/src/tools.ts +++ b/src/tools.ts @@ -6,7 +6,8 @@ import { ToolCallLocation, ToolKind, } from "@zed-industries/agent-client-protocol"; -import { replaceAndCalculateLocation } from "./mcp-server.js"; +import { replaceAndCalculateLocation, SYSTEM_REMINDER } from "./mcp-server.js"; +import { tool } from "@anthropic-ai/claude-code"; interface ToolInfo { title: string; @@ -489,6 +490,43 @@ export function toolUpdateFromToolResult( case "Write": return {}; + case "Read": + case "mcp__acp__read": + if (Array.isArray(toolResult.content) && toolResult.content.length > 0) { + return { + content: toolResult.content.map((content: any) => ({ + type: "content", + content: + content.type == "text" + ? { + type: "text", + text: markdownEscape( + content.text.replace(SYSTEM_REMINDER, ""), + ), + } + : content, + })), + }; + } else if ( + typeof toolResult.content === "string" && + toolResult.content.length > 0 + ) { + return { + content: [ + { + type: "content", + content: { + type: "text", + text: markdownEscape( + toolResult.content.replace(SYSTEM_REMINDER, ""), + ), + }, + }, + ], + }; + } + return {}; + case "Task": case "NotebookEdit": case "NotebookRead": @@ -502,12 +540,8 @@ export function toolUpdateFromToolResult( case "Grep": case "WebFetch": case "WebSearch": - case "Read": - case "mcp__acp__read": case "Other": default: { - // This happens for the mcp__acp__read tool, - // but may also for others... if (Array.isArray(toolResult.content) && toolResult.content.length > 0) { return { content: toolResult.content.map((content: any) => ({ @@ -523,7 +557,10 @@ export function toolUpdateFromToolResult( content: [ { type: "content", - content: { type: "text", text: toolResult.content }, + content: { + type: "text", + text: toolResult.content, + }, }, ], }; @@ -546,3 +583,13 @@ export function planEntries(input: { todos: ClaudePlanEntry[] }): PlanEntry[] { priority: "medium", })); } + +export function markdownEscape(text: string): string { + let escape = "```"; + for (let [m] of text.matchAll(/^```+/gm)) { + while (m.length >= escape.length) { + escape += "`"; + } + } + return escape + "\n" + text + (text.endsWith("\n") ? "" : "\n") + escape; +}