diff --git a/packages/opencode/src/session/code-mode.ts b/packages/opencode/src/session/code-mode.ts index d211e8353a..7389b324a6 100644 --- a/packages/opencode/src/session/code-mode.ts +++ b/packages/opencode/src/session/code-mode.ts @@ -16,8 +16,6 @@ type Metadata = { error?: boolean } -export type Namespace = { name: string; description?: string } - // `new Function`/`AsyncFunction` is not on the global scope, so reach it via the // prototype of an async function literal. The body may use top-level `await` and `return`. const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor as { @@ -105,7 +103,7 @@ const signatureFor = (entry: CatalogEntry) => * list of namespaces only — never the full tool catalog. Per-tool signatures are * fetched on demand with `tools.describe` so the prompt stays small. */ -export function describe(groups: Map, descriptions?: Map): string { +export function describe(groups: Map): string { const lines = [ "Execute JavaScript with access to connected MCP tools, grouped into namespaces (one per MCP server).", "", @@ -122,9 +120,7 @@ export function describe(groups: Map, descriptions?: Map } lines.push("", "Available namespaces:") for (const [server, entries] of [...groups].sort(([a], [b]) => a.localeCompare(b))) { - const note = brief(descriptions?.get(server)) - const count = `${entries.length} tool${entries.length === 1 ? "" : "s"}` - lines.push(`- ${server} (${count})${note ? ` — ${note}` : ""}`) + lines.push(`- ${server} (${entries.length} tool${entries.length === 1 ? "" : "s"})`) } return lines.join("\n") } @@ -170,12 +166,8 @@ function errorMessage(error: unknown): string { } } -export function define(mcpTools: Record, namespaces: ReadonlyArray) { - const groups = groupByServer( - mcpTools, - namespaces.map((n) => n.name), - ) - const descriptions = new Map(namespaces.map((n) => [n.name, n.description] as const)) +export function define(mcpTools: Record, servers: readonly string[]) { + const groups = groupByServer(mcpTools, servers) const catalog: CatalogEntry[] = [...groups.values()].flat() const byKey = new Map(catalog.map((entry) => [entry.key, entry] as const)) @@ -216,7 +208,7 @@ export function define(mcpTools: Record, namespaces: ReadonlyArr return Tool.define( CODE_MODE_TOOL, Effect.succeed>({ - description: describe(groups, descriptions), + description: describe(groups), parameters: Parameters, execute: Effect.fn("CodeMode.execute")(function* (params, ctx) { const run = yield* EffectBridge.make() diff --git a/packages/opencode/src/session/tools.ts b/packages/opencode/src/session/tools.ts index 59df662117..7f65c64301 100644 --- a/packages/opencode/src/session/tools.ts +++ b/packages/opencode/src/session/tools.ts @@ -94,18 +94,14 @@ export const resolve = Effect.fn("SessionTools.resolve")(function* (input: { // When code mode is enabled and MCP tools are present, expose them through the // single code-mode `execute` tool instead of registering each MCP tool directly // (see the early return below). Code mode is experimental and off by default. - let codeModeTool: Tool.Def | undefined - if (flags.experimentalCodeMode && Object.keys(mcpTools).length > 0) { - // Namespaces are sanitized client names (tool keys are `sanitize(server)_sanitize(tool)`, - // so they match the catalog key prefixes). The brief per-namespace note reuses the - // server's MCP instructions, whose full text is already in the system prompt. - const instructions = new Map((yield* mcp.instructions()).map((item) => [item.name, item.instructions] as const)) - const namespaces = Object.keys(yield* mcp.clients()).map((name) => ({ - name: McpCatalog.sanitize(name), - description: instructions.get(name), - })) - codeModeTool = yield* Tool.init(yield* CodeModeTool.define(mcpTools, namespaces)) - } + // Namespaces are sanitized client names; tool keys are `sanitize(server)_sanitize(tool)`, + // so the names match the catalog key prefixes. + const codeModeTool = + flags.experimentalCodeMode && Object.keys(mcpTools).length > 0 + ? yield* Tool.init( + yield* CodeModeTool.define(mcpTools, Object.keys(yield* mcp.clients()).map(McpCatalog.sanitize)), + ) + : undefined const registryTools = yield* registry.tools({ modelID: ModelV2.ID.make(input.model.api.id), providerID: input.model.providerID, diff --git a/packages/opencode/test/session/code-mode.test.ts b/packages/opencode/test/session/code-mode.test.ts index 629aa1d6c2..2433e47a3a 100644 --- a/packages/opencode/test/session/code-mode.test.ts +++ b/packages/opencode/test/session/code-mode.test.ts @@ -42,9 +42,9 @@ const layer = Layer.mergeAll( ) // Derive sanitized server namespaces from the catalog keys, mirroring how -// session/tools.ts passes namespaces built from `mcp.clients()` + `mcp.instructions()`. -function build(mcpTools: Record, namespaces?: Array<{ name: string; description?: string }>) { - const names = namespaces ?? [...new Set(Object.keys(mcpTools).map((key) => key.split("_")[0]!))].map((name) => ({ name })) +// session/tools.ts passes `Object.keys(mcp.clients()).map(sanitize)`. +function build(mcpTools: Record, servers?: string[]) { + const names = servers ?? [...new Set(Object.keys(mcpTools).map((key) => key.split("_")[0]!))] return Effect.runPromise(define(mcpTools, names).pipe(Effect.flatMap(Tool.init), Effect.provide(layer))) } @@ -64,17 +64,11 @@ describe("code mode execute", () => { }, ["github", "linear"], ) - const description = describeTools( - groups, - new Map([ - ["github", "GitHub repository automation.\nmore detail here"], - ["linear", undefined], - ]), - ) + const description = describeTools(groups) expect(description).toContain("tools.search(query") expect(description).toContain("tools.describe(path)") - expect(description).toContain("- github (2 tools) — GitHub repository automation.") + expect(description).toContain("- github (2 tools)") expect(description).toContain("- linear (1 tool)") // The full catalog must NOT be inlined in the prompt. expect(description).not.toContain("create_issue")