From efdab8b0a2d61f3f44a9ad90eadb6ead66f962b4 Mon Sep 17 00:00:00 2001 From: Andrea V <1577639+karimodm@users.noreply.github.com> Date: Sat, 19 Sep 2026 10:12:27 +0200 Subject: [PATCH] fix: keep the subagent list visible in the task tool description Claude Code cuts MCP tool descriptions at 2048 chars. OpenCode appends the available subagents after ~2300 chars of guidance, so Claude never saw valid subagent_type values. Move the list to the front of long descriptions; the cut then falls on the tail of the guidance. --- src/proxy.ts | 3 +- src/tool-description.ts | 20 +++++++++++ test/tool-description-regression.ts | 53 +++++++++++++++++++++++++++++ 3 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/tool-description.ts create mode 100644 test/tool-description-regression.ts diff --git a/src/proxy.ts b/src/proxy.ts index 8aae174..28c921c 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -29,6 +29,7 @@ import { EFFORT_HEADER, } from "./model-selection.js"; import { resolveClaudeModelId } from "./models.js"; +import { fitToolDescription } from "./tool-description.js"; import { DIRECTORY_HEADER, SESSION_HEADER, @@ -852,7 +853,7 @@ async function buildOpenCodeMcpServer( .map((t) => { const name = t.function?.name; if (!name) return null; - const description = t.function?.description || name; + const description = fitToolDescription(t.function?.description || name); const shape = jsonSchemaToZodShape( t.function?.parameters as Record | undefined, ); diff --git a/src/tool-description.ts b/src/tool-description.ts new file mode 100644 index 0000000..b74813e --- /dev/null +++ b/src/tool-description.ts @@ -0,0 +1,20 @@ +/** + * Claude Code truncates MCP tool descriptions at 2048 characters. + * OpenCode appends the list of subagents to the END of its task tool + * description, after ~2300 characters of guidance, so Claude never sees + * which subagent_type values exist. Move the list to the front: the cut + * then falls on the tail of the generic guidance instead. + */ +export const CLAUDE_TOOL_DESCRIPTION_LIMIT = 2048; + +const AGENT_LIST_MARKER = + "Available agent types and the tools they have access to:"; + +export function fitToolDescription(description: string): string { + if (description.length <= CLAUDE_TOOL_DESCRIPTION_LIMIT) return description; + const at = description.indexOf(AGENT_LIST_MARKER); + if (at <= 0) return description; + const agents = description.slice(at).trim(); + const guidance = description.slice(0, at).trim(); + return guidance ? `${agents}\n\n${guidance}` : agents; +} diff --git a/test/tool-description-regression.ts b/test/tool-description-regression.ts new file mode 100644 index 0000000..3eddbff --- /dev/null +++ b/test/tool-description-regression.ts @@ -0,0 +1,53 @@ +/** + * Regression: the subagent list OpenCode appends to the task tool description + * must survive Claude Code's 2048-char MCP description cut. + * + * Run: bun test/tool-description-regression.ts + */ +import assert from "node:assert/strict"; + +async function main() { + const { fitToolDescription, CLAUDE_TOOL_DESCRIPTION_LIMIT } = await import( + "../src/tool-description.ts" + ); + + const guidance = + "Launch a new agent to handle complex, multistep tasks autonomously.\n\n" + + "When to use the Task tool:\n".concat("- guidance line\n".repeat(150)); + const agents = [ + "Available agent types and the tools they have access to:", + "- general: General-purpose agent for multi-step tasks.", + "- explore: Fast codebase exploration agent.", + "- critic: Reviews plans and code.", + ].join("\n"); + const taskDescription = `${guidance}\n\n${agents}`; + assert.ok(taskDescription.length > CLAUDE_TOOL_DESCRIPTION_LIMIT); + + // Unpatched, the cut drops every agent. + assert.ok( + !taskDescription.slice(0, CLAUDE_TOOL_DESCRIPTION_LIMIT).includes("critic"), + ); + + const fitted = fitToolDescription(taskDescription); + const visible = fitted.slice(0, CLAUDE_TOOL_DESCRIPTION_LIMIT); + for (const name of ["general", "explore", "critic"]) { + assert.ok(visible.includes(`- ${name}:`), `${name} visible after the cut`); + } + // Guidance is kept (after the list), nothing is dropped by us. + assert.ok(fitted.includes(guidance.trim())); + assert.ok(fitted.includes(agents)); + assert.ok(visible.includes("Launch a new agent")); + + // Short descriptions and descriptions without the list are untouched. + const short = `Short guidance.\n\n${agents}`; + assert.equal(fitToolDescription(short), short); + const long = "x".repeat(CLAUDE_TOOL_DESCRIPTION_LIMIT + 10); + assert.equal(fitToolDescription(long), long); + + console.log("ok — tool description regression passed"); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +});