From 7177d27b6c03e3b889766d061c2f9cf0790457a7 Mon Sep 17 00:00:00 2001 From: 4rcadia <97033226+Arcadi4@users.noreply.github.com> Date: Mon, 15 Jun 2026 20:58:33 -0400 Subject: [PATCH 1/7] feat(mcp): append server instructions to context --- packages/opencode/src/mcp/index.ts | 29 +++++- packages/opencode/src/session/prompt.ts | 5 +- packages/opencode/test/mcp/lifecycle.test.ts | 62 +++++++++++++ packages/opencode/test/session/prompt.test.ts | 92 +++++++++++++------ .../test/session/snapshot-tool-race.test.ts | 1 + 5 files changed, 158 insertions(+), 31 deletions(-) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 08d58118c992..0d2cf3a8953d 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -139,6 +139,7 @@ interface CreateResult { mcpClient?: MCPClient status: Status defs?: MCPToolDef[] + instructions?: string } interface AuthResult { @@ -154,11 +155,13 @@ interface State { status: Record clients: Record defs: Record + instructions: Record } export interface Interface { readonly status: () => Effect.Effect> readonly clients: () => Effect.Effect> + readonly instructions: () => Effect.Effect readonly tools: () => Effect.Effect> readonly prompts: () => Effect.Effect> readonly resources: () => Effect.Effect> @@ -379,7 +382,7 @@ export const layer = Layer.effect( if (!listed) { return yield* Effect.fail(new Error("Failed to get tools")) } - return { mcpClient, status, defs: listed } satisfies CreateResult + return { mcpClient, status, defs: listed, instructions: mcpClient.getInstructions()?.trim() } satisfies CreateResult }).pipe( Effect.catchCause((cause) => Effect.tryPromise(() => mcpClient.close()).pipe(Effect.ignore, Effect.andThen(Effect.failCause(cause))), @@ -426,6 +429,7 @@ export const layer = Layer.effect( if (s.clients[name] !== client) return delete s.clients[name] delete s.defs[name] + delete s.instructions[name] s.status[name] = { status: "failed", error: "Connection closed" } bridge.fork( Effect.logWarning("MCP connection closed", { server: name }).pipe( @@ -480,6 +484,7 @@ export const layer = Layer.effect( status: {}, clients: {}, defs: {}, + instructions: {}, } yield* Effect.forEach( @@ -501,6 +506,7 @@ export const layer = Layer.effect( if (result.mcpClient) { s.clients[key] = result.mcpClient s.defs[key] = result.defs! + if (result.instructions) s.instructions[key] = result.instructions watch(s, key, result.mcpClient, bridge, mcp.timeout) } }), @@ -512,6 +518,7 @@ export const layer = Layer.effect( const clients = Object.values(s.clients) s.clients = {} s.defs = {} + s.instructions = {} yield* Effect.forEach( clients, (client) => @@ -541,6 +548,7 @@ export const layer = Layer.effect( const client = s.clients[name] delete s.clients[name] delete s.defs[name] + delete s.instructions[name] if (!client) return Effect.void return Effect.tryPromise(() => client.close()).pipe(Effect.ignore) } @@ -550,6 +558,7 @@ export const layer = Layer.effect( name: string, client: MCPClient, listed: MCPToolDef[], + instructions: string | undefined, timeout?: number, ) { const bridge = yield* EffectBridge.make() @@ -557,6 +566,8 @@ export const layer = Layer.effect( s.status[name] = { status: "connected" } s.clients[name] = client s.defs[name] = listed + if (instructions) s.instructions[name] = instructions + else delete s.instructions[name] watch(s, name, client, bridge, timeout) if (previous) yield* Effect.tryPromise(() => previous.close()).pipe(Effect.ignore) return s.status[name] @@ -586,6 +597,17 @@ export const layer = Layer.effect( return s.clients }) + const instructions = Effect.fn("MCP.instructions")(function* () { + const s = yield* InstanceState.get(state) + return Object.entries(s.instructions) + .filter(([name]) => s.status[name]?.status === "connected") + .sort(([a], [b]) => a.localeCompare(b)) + .map( + ([name, item]) => + `Instructions from: MCP server ${name}\nThese instructions apply to MCP tools whose names start with \`${McpCatalog.sanitize(name)}_\`, and to prompts/resources from this MCP server.\n\n${item}`, + ) + }) + const createAndStore = Effect.fn("MCP.createAndStore")(function* (name: string, mcp: ConfigMCPV1.Info) { const s = yield* InstanceState.get(state) const result = yield* create(name, mcp) @@ -597,7 +619,7 @@ export const layer = Layer.effect( return result.status } - return yield* storeClient(s, name, result.mcpClient, result.defs!, mcp.timeout) + return yield* storeClient(s, name, result.mcpClient, result.defs!, result.instructions, mcp.timeout) }) const add = Effect.fn("MCP.add")(function* (name: string, mcp: ConfigMCPV1.Info) { @@ -830,7 +852,7 @@ export const layer = Layer.effect( const s = yield* InstanceState.get(state) yield* auth.clearOAuthState(mcpName) - return yield* storeClient(s, mcpName, client, listed, mcpConfig.timeout) + return yield* storeClient(s, mcpName, client, listed, client.getInstructions()?.trim(), mcpConfig.timeout) } const callbackPromise = McpOAuthCallback.waitForCallback(result.oauthState, mcpName) @@ -917,6 +939,7 @@ export const layer = Layer.effect( return Service.of({ status, clients, + instructions, tools, prompts, resources, diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index dad796c998ad..45a014569f11 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1306,13 +1306,14 @@ export const layer = Layer.effect( yield* plugin.trigger("experimental.chat.messages.transform", {}, { messages: msgs }) - const [skills, env, instructions, modelMsgs] = yield* Effect.all([ + const [skills, env, instructions, mcpInstructions, modelMsgs] = yield* Effect.all([ sys.skills(agent), sys.environment(model), instruction.system().pipe(Effect.orDie), + mcp.instructions(), MessageV2.toModelMessagesEffect(msgs, model), ]) - const system = [...env, ...instructions, ...(skills ? [skills] : [])] + const system = [...env, ...instructions, ...mcpInstructions, ...(skills ? [skills] : [])] const format = lastUser.format ?? { type: "text" as const } if (format.type === "json_schema") system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT) const result = yield* handle.process({ diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index 34304624e680..4e897129a15c 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -13,6 +13,7 @@ import { TestInstance } from "../fixture/fixture" interface MockClientState { capabilities: { tools?: object; prompts?: object; resources?: object } capabilitiesShouldThrow: boolean + instructions?: string tools: Array<{ name: string; description?: string; inputSchema: object; outputSchema?: object }> listToolsCalls: number listPromptsCalls: number @@ -179,6 +180,10 @@ void mock.module("@modelcontextprotocol/sdk/client/index.js", () => ({ return this._state?.capabilities } + getInstructions() { + return this._state?.instructions + } + async listTools(params?: { cursor?: string }) { if (this._state) this._state.listToolsCalls++ if (this._state?.listToolsShouldFail) { @@ -331,6 +336,63 @@ it.instance( { config: { mcp: {} } }, ) +it.instance( + "instructions() returns connected server instructions with tool prefix guidance", + () => + MCP.Service.use((mcp: MCPNS.Interface) => + Effect.gen(function* () { + lastCreatedClientName = "guide-server" + const serverState = getOrCreateClientState("guide-server") + serverState.instructions = "Use lookup before mutate." + + yield* mcp.add("guide-server", { + type: "local", + command: ["echo", "test"], + }) + + expect(yield* mcp.instructions()).toContain( + [ + "Instructions from: MCP server guide-server", + "These instructions apply to MCP tools whose names start with `guide-server_`, and to prompts/resources from this MCP server.", + "", + "Use lookup before mutate.", + ].join("\n"), + ) + }), + ), + { config: { mcp: {} } }, +) + +it.instance( + "instructions() omits empty and disconnected server instructions", + () => + MCP.Service.use((mcp: MCPNS.Interface) => + Effect.gen(function* () { + lastCreatedClientName = "temporary-server" + getOrCreateClientState("temporary-server").instructions = "Temporary guidance." + + yield* mcp.add("temporary-server", { + type: "local", + command: ["echo", "test"], + }) + yield* mcp.disconnect("temporary-server") + + lastCreatedClientName = "blank-server" + getOrCreateClientState("blank-server").instructions = " " + + yield* mcp.add("blank-server", { + type: "local", + command: ["echo", "test"], + }) + + const instructions = yield* mcp.instructions() + expect(instructions.some((item) => item.includes("temporary-server"))).toBe(false) + expect(instructions.some((item) => item.includes("blank-server"))).toBe(false) + }), + ), + { config: { mcp: {} } }, +) + it.instance( "follows cursors when listing tools, prompts, and resources", () => diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 5cd97f78e88a..bacd80df77a7 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -108,28 +108,31 @@ function errorTool(parts: SessionV1.Part[]) { return part?.state.status === "error" ? (part as ErrorToolPart) : undefined } -const mcp = Layer.succeed( - MCP.Service, - MCP.Service.of({ - status: () => Effect.succeed({}), - clients: () => Effect.succeed({}), - tools: () => Effect.succeed({}), - prompts: () => Effect.succeed({}), - resources: () => Effect.succeed({}), - add: () => Effect.succeed({ status: { status: "disabled" as const } }), - connect: () => Effect.void, - disconnect: () => Effect.void, - getPrompt: () => Effect.succeed(undefined), - readResource: () => Effect.succeed(undefined), - startAuth: () => Effect.die("unexpected MCP auth in prompt-effect tests"), - authenticate: () => Effect.die("unexpected MCP auth in prompt-effect tests"), - finishAuth: () => Effect.die("unexpected MCP auth in prompt-effect tests"), - removeAuth: () => Effect.void, - supportsOAuth: () => Effect.succeed(false), - hasStoredTokens: () => Effect.succeed(false), - getAuthStatus: () => Effect.succeed("not_authenticated" as const), - }), -) +function makeMcp(instructions: string[] = []) { + return Layer.succeed( + MCP.Service, + MCP.Service.of({ + status: () => Effect.succeed({}), + clients: () => Effect.succeed({}), + instructions: () => Effect.succeed(instructions), + tools: () => Effect.succeed({}), + prompts: () => Effect.succeed({}), + resources: () => Effect.succeed({}), + add: () => Effect.succeed({ status: { status: "disabled" as const } }), + connect: () => Effect.void, + disconnect: () => Effect.void, + getPrompt: () => Effect.succeed(undefined), + readResource: () => Effect.succeed(undefined), + startAuth: () => Effect.die("unexpected MCP auth in prompt-effect tests"), + authenticate: () => Effect.die("unexpected MCP auth in prompt-effect tests"), + finishAuth: () => Effect.die("unexpected MCP auth in prompt-effect tests"), + removeAuth: () => Effect.void, + supportsOAuth: () => Effect.succeed(false), + hasStoredTokens: () => Effect.succeed(false), + getAuthStatus: () => Effect.succeed("not_authenticated" as const), + }), + ) +} const lsp = Layer.succeed( LSP.Service, @@ -163,7 +166,7 @@ const blockingProcessor = Layer.succeed( }), ) -function makePrompt(input?: { processor?: "blocking" }) { +function makePrompt(input?: { mcpInstructions?: string[]; processor?: "blocking" }) { const deps = Layer.mergeAll( Session.defaultLayer, Snapshot.defaultLayer, @@ -176,7 +179,7 @@ function makePrompt(input?: { processor?: "blocking" }) { Config.defaultLayer, ProviderSvc.defaultLayer, lsp, - mcp, + makeMcp(input?.mcpInstructions), FSUtil.defaultLayer, BackgroundJob.defaultLayer, status, @@ -229,17 +232,29 @@ function makePrompt(input?: { processor?: "blocking" }) { ) } -function makeHttp(input?: { processor?: "blocking" }) { +function makeHttp(input?: { mcpInstructions?: string[]; processor?: "blocking" }) { return Layer.mergeAll(TestLLMServer.layer, makePrompt(input)) } -function makeHttpNoLLMServer(input?: { processor?: "blocking" }) { +function makeHttpNoLLMServer(input?: { mcpInstructions?: string[]; processor?: "blocking" }) { return makePrompt(input) } const it = testEffect(makeHttp()) const noLLMServer = testEffect(makeHttpNoLLMServer()) const raceNoLLMServer = testEffect(makeHttpNoLLMServer({ processor: "blocking" })) +const withMcpInstructions = testEffect( + makeHttp({ + mcpInstructions: [ + [ + "Instructions from: MCP server guide-server", + "These instructions apply to MCP tools whose names start with `guide-server_`, and to prompts/resources from this MCP server.", + "", + "Use lookup before mutate.", + ].join("\n"), + ], + }), +) const unix = process.platform !== "win32" ? it.instance : it.instance.skip const unixNoLLMServer = process.platform !== "win32" ? noLLMServer.instance : noLLMServer.instance.skip @@ -506,6 +521,31 @@ it.instance("loop calls LLM and returns assistant message", () => }), ) +withMcpInstructions.instance("loop includes MCP instructions in model system context", () => + Effect.gen(function* () { + const { llm } = yield* useServerConfig(providerCfg) + const prompt = yield* SessionPrompt.Service + const sessions = yield* Session.Service + const chat = yield* sessions.create({ + title: "Pinned", + permission: [{ permission: "*", pattern: "*", action: "allow" }], + }) + yield* llm.hang + yield* user(chat.id, "hello") + + const fiber = yield* prompt.loop({ sessionID: chat.id }).pipe(Effect.forkChild) + yield* awaitWithTimeout(llm.wait(1), "timed out waiting for MCP instruction request", "10 seconds") + + const hits = yield* llm.hits + const body = JSON.stringify(hits[0]?.body) + expect(body).toContain("Instructions from: MCP server guide-server") + expect(body).toContain("guide-server_") + expect(body).toContain("Use lookup before mutate.") + yield* Fiber.interrupt(fiber) + }), + 15_000, +) + it.instance("loop surfaces content-filter finishes as session errors", () => Effect.gen(function* () { const { llm } = yield* useServerConfig(providerCfg) diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index 8a3701e12518..21ffb6dfc424 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -37,6 +37,7 @@ const mcp = Layer.succeed( MCP.Service.of({ status: () => Effect.succeed({}), clients: () => Effect.succeed({}), + instructions: () => Effect.succeed([]), tools: () => Effect.succeed({}), prompts: () => Effect.succeed({}), resources: () => Effect.succeed({}), From 01d1cc1166d29e37863fd9758acc9c8f13756f13 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 23 Jun 2026 23:15:33 -0500 Subject: [PATCH 2/7] refactor(mcp): centralize server instructions --- packages/opencode/src/mcp/index.ts | 19 +++++--- packages/opencode/src/session/prompt.ts | 2 +- packages/opencode/src/session/system.ts | 24 +++++++++- packages/opencode/test/mcp/lifecycle.test.ts | 19 ++++---- packages/opencode/test/session/prompt.test.ts | 28 +++++++----- packages/opencode/test/session/system.test.ts | 44 +++++++++++++++++++ 6 files changed, 106 insertions(+), 30 deletions(-) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 0d2cf3a8953d..b1e0c11a68a1 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -158,10 +158,16 @@ interface State { instructions: Record } +export interface ServerInstructions { + name: string + instructions: string + tools: string[] +} + export interface Interface { readonly status: () => Effect.Effect> readonly clients: () => Effect.Effect> - readonly instructions: () => Effect.Effect + readonly instructions: () => Effect.Effect readonly tools: () => Effect.Effect> readonly prompts: () => Effect.Effect> readonly resources: () => Effect.Effect> @@ -602,10 +608,13 @@ export const layer = Layer.effect( return Object.entries(s.instructions) .filter(([name]) => s.status[name]?.status === "connected") .sort(([a], [b]) => a.localeCompare(b)) - .map( - ([name, item]) => - `Instructions from: MCP server ${name}\nThese instructions apply to MCP tools whose names start with \`${McpCatalog.sanitize(name)}_\`, and to prompts/resources from this MCP server.\n\n${item}`, - ) + .map(([name, item]) => ({ + name, + instructions: item, + tools: (s.defs[name] ?? []).map( + (tool) => McpCatalog.sanitize(name) + "_" + McpCatalog.sanitize(tool.name), + ), + })) }) const createAndStore = Effect.fn("MCP.createAndStore")(function* (name: string, mcp: ConfigMCPV1.Info) { diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 45a014569f11..8f39e7e8d601 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1310,7 +1310,7 @@ export const layer = Layer.effect( sys.skills(agent), sys.environment(model), instruction.system().pipe(Effect.orDie), - mcp.instructions(), + sys.mcp(agent), MessageV2.toModelMessagesEffect(msgs, model), ]) const system = [...env, ...instructions, ...mcpInstructions, ...(skills ? [skills] : [])] diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 74401779d353..c1980cc1c19b 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -21,6 +21,8 @@ import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-layer" import { PluginBoot } from "@opencode-ai/core/plugin/boot" import { Reference } from "@opencode-ai/core/reference" +import { McpCatalog } from "@/mcp/catalog" +import { MCP } from "@/mcp" export function provider(model: Provider.Model) { if (model.api.id.includes("gpt-4") || model.api.id.includes("o1") || model.api.id.includes("o3")) @@ -41,6 +43,7 @@ export function provider(model: Provider.Model) { export interface Interface { readonly environment: (model: Provider.Model) => Effect.Effect readonly skills: (agent: Agent.Info) => Effect.Effect + readonly mcp: (agent: Agent.Info) => Effect.Effect } export class Service extends Context.Service()("@opencode/SystemPrompt") {} @@ -49,6 +52,7 @@ export const layer = Layer.effect( Service, Effect.gen(function* () { const skill = yield* Skill.Service + const mcp = yield* MCP.Service const locations = yield* LocationServiceMap return Service.of({ @@ -104,14 +108,30 @@ export const layer = Layer.effect( Skill.fmt(list, { verbose: true }), ].join("\n") }), + + mcp: Effect.fn("SystemPrompt.mcp")(function* (agent: Agent.Info) { + return (yield* mcp.instructions()) + .filter((item) => { + if (item.tools.length === 0) return false + return Permission.disabled(item.tools, agent.permission).size < item.tools.length + }) + .map( + (item) => + `Instructions from: MCP server ${item.name}\nThese instructions apply to MCP tools whose names start with \`${McpCatalog.sanitize(item.name)}_\`, and to prompts/resources from this MCP server.\n\n${item.instructions}`, + ) + }), }) }), ) -export const defaultLayer = layer.pipe(Layer.provide(Skill.defaultLayer), Layer.provide(LocationServiceMap.layer)) +export const defaultLayer = layer.pipe( + Layer.provide(Skill.defaultLayer), + Layer.provide(MCP.defaultLayer), + Layer.provide(LocationServiceMap.layer), +) const locationServiceMapNode = LayerNode.make(LocationServiceMap.layer, []) -export const node = LayerNode.make(layer, [Skill.node, locationServiceMapNode]) +export const node = LayerNode.make(layer, [Skill.node, MCP.node, locationServiceMapNode]) export * as SystemPrompt from "./system" diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index 4e897129a15c..ceda81a4e6b3 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -337,7 +337,7 @@ it.instance( ) it.instance( - "instructions() returns connected server instructions with tool prefix guidance", + "instructions() returns connected server instructions with tool names", () => MCP.Service.use((mcp: MCPNS.Interface) => Effect.gen(function* () { @@ -350,14 +350,11 @@ it.instance( command: ["echo", "test"], }) - expect(yield* mcp.instructions()).toContain( - [ - "Instructions from: MCP server guide-server", - "These instructions apply to MCP tools whose names start with `guide-server_`, and to prompts/resources from this MCP server.", - "", - "Use lookup before mutate.", - ].join("\n"), - ) + expect(yield* mcp.instructions()).toContainEqual({ + name: "guide-server", + instructions: "Use lookup before mutate.", + tools: ["guide-server_test_tool"], + }) }), ), { config: { mcp: {} } }, @@ -386,8 +383,8 @@ it.instance( }) const instructions = yield* mcp.instructions() - expect(instructions.some((item) => item.includes("temporary-server"))).toBe(false) - expect(instructions.some((item) => item.includes("blank-server"))).toBe(false) + expect(instructions.some((item) => item.name === "temporary-server")).toBe(false) + expect(instructions.some((item) => item.name === "blank-server")).toBe(false) }), ), { config: { mcp: {} } }, diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index bacd80df77a7..61b0415e24ed 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -56,6 +56,7 @@ import { reply, TestLLMServer } from "../lib/llm-server" import { RuntimeFlags } from "@/effect/runtime-flags" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" +import { LocationServiceMap } from "@opencode-ai/core/location-layer" const summary = Layer.succeed( SessionSummary.Service, @@ -108,7 +109,7 @@ function errorTool(parts: SessionV1.Part[]) { return part?.state.status === "error" ? (part as ErrorToolPart) : undefined } -function makeMcp(instructions: string[] = []) { +function makeMcp(instructions: MCP.ServerInstructions[] = []) { return Layer.succeed( MCP.Service, MCP.Service.of({ @@ -166,7 +167,7 @@ const blockingProcessor = Layer.succeed( }), ) -function makePrompt(input?: { mcpInstructions?: string[]; processor?: "blocking" }) { +function makePrompt(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) { const deps = Layer.mergeAll( Session.defaultLayer, Snapshot.defaultLayer, @@ -225,18 +226,24 @@ function makePrompt(input?: { mcpInstructions?: string[]; processor?: "blocking" Layer.provideMerge(registry), Layer.provideMerge(trunc), Layer.provide(Instruction.defaultLayer), - Layer.provide(SystemPrompt.defaultLayer), + Layer.provide( + SystemPrompt.layer.pipe( + Layer.provide(Skill.defaultLayer), + Layer.provide(LocationServiceMap.layer), + Layer.provide(deps), + ), + ), Layer.provide(RuntimeFlags.layer({ experimentalEventSystem: true })), Layer.provideMerge(deps), Layer.provide(summary), ) } -function makeHttp(input?: { mcpInstructions?: string[]; processor?: "blocking" }) { +function makeHttp(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) { return Layer.mergeAll(TestLLMServer.layer, makePrompt(input)) } -function makeHttpNoLLMServer(input?: { mcpInstructions?: string[]; processor?: "blocking" }) { +function makeHttpNoLLMServer(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) { return makePrompt(input) } @@ -246,12 +253,11 @@ const raceNoLLMServer = testEffect(makeHttpNoLLMServer({ processor: "blocking" } const withMcpInstructions = testEffect( makeHttp({ mcpInstructions: [ - [ - "Instructions from: MCP server guide-server", - "These instructions apply to MCP tools whose names start with `guide-server_`, and to prompts/resources from this MCP server.", - "", - "Use lookup before mutate.", - ].join("\n"), + { + name: "guide-server", + instructions: "Use lookup before mutate.", + tools: ["guide-server_lookup"], + }, ], }), ) diff --git a/packages/opencode/test/session/system.test.ts b/packages/opencode/test/session/system.test.ts index 69cec7bdcca7..c5ab49e0c0cf 100644 --- a/packages/opencode/test/session/system.test.ts +++ b/packages/opencode/test/session/system.test.ts @@ -5,6 +5,7 @@ import { NamedError } from "@opencode-ai/core/util/error" import { Skill } from "../../src/skill" import { Permission } from "../../src/permission" import { SystemPrompt } from "../../src/session/system" +import { MCP } from "../../src/mcp" import { LocationServiceMap } from "@opencode-ai/core/location-layer" import { testEffect } from "../lib/effect" @@ -44,6 +45,18 @@ const build: Agent.Info = { const it = testEffect( SystemPrompt.layer.pipe( Layer.provide(LocationServiceMap.layer), + Layer.provide( + Layer.mock(MCP.Service, { + instructions: () => + Effect.succeed([ + { + name: "guide-server", + instructions: "Use lookup before mutate.", + tools: ["guide-server_lookup", "guide-server_mutate"], + }, + ]), + }), + ), Layer.provide( Layer.succeed( Skill.Service, @@ -83,4 +96,35 @@ describe("session.system", () => { expect(output).not.toContain("manual-skill") }), ) + + it.effect("MCP output includes servers with an allowed tool", () => + Effect.gen(function* () { + const prompt = yield* SystemPrompt.Service + const output = yield* prompt.mcp({ + ...build, + permission: Permission.fromConfig({ "guide-server_mutate": "deny" }), + }) + + expect(output).toEqual([ + [ + "Instructions from: MCP server guide-server", + "These instructions apply to MCP tools whose names start with `guide-server_`, and to prompts/resources from this MCP server.", + "", + "Use lookup before mutate.", + ].join("\n"), + ]) + }), + ) + + it.effect("MCP output omits servers when all tools are denied", () => + Effect.gen(function* () { + const prompt = yield* SystemPrompt.Service + const output = yield* prompt.mcp({ + ...build, + permission: Permission.fromConfig({ "guide-server_*": "deny" }), + }) + + expect(output).toEqual([]) + }), + ) }) From 43877bd7ca66a15c0ac6f3ee9dd761843e24b45e Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 23 Jun 2026 23:20:39 -0500 Subject: [PATCH 3/7] refactor(mcp): consolidate server instructions --- packages/opencode/src/session/prompt.ts | 7 +++- packages/opencode/src/session/system.ts | 32 +++++++++++++------ packages/opencode/test/session/prompt.test.ts | 4 +-- packages/opencode/test/session/system.test.ts | 21 ++++++++---- 4 files changed, 44 insertions(+), 20 deletions(-) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index b31a2310c836..fe0d42130e1f 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1363,7 +1363,12 @@ export const layer = Layer.effect( sys.mcp(agent), MessageV2.toModelMessagesEffect(msgs, model), ]) - const system = [...env, ...instructions, ...mcpInstructions, ...(skills ? [skills] : [])] + const system = [ + ...env, + ...instructions, + ...(mcpInstructions ? [mcpInstructions] : []), + ...(skills ? [skills] : []), + ] const format = lastUser.format ?? { type: "text" as const } if (format.type === "json_schema") system.push(STRUCTURED_OUTPUT_SYSTEM_PROMPT) const result = yield* handle.process({ diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 8485a8a36c06..57b102622474 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -42,7 +42,7 @@ export function provider(model: Provider.Model) { export interface Interface { readonly environment: (model: Provider.Model) => Effect.Effect readonly skills: (agent: Agent.Info) => Effect.Effect - readonly mcp: (agent: Agent.Info) => Effect.Effect + readonly mcp: (agent: Agent.Info) => Effect.Effect } export class Service extends Context.Service()("@opencode/SystemPrompt") {} @@ -108,15 +108,27 @@ export const layer = Layer.effect( }), mcp: Effect.fn("SystemPrompt.mcp")(function* (agent: Agent.Info) { - return (yield* mcp.instructions()) - .filter((item) => { - if (item.tools.length === 0) return false - return Permission.disabled(item.tools, agent.permission).size < item.tools.length - }) - .map( - (item) => - `Instructions from: MCP server ${item.name}\nThese instructions apply to MCP tools whose names start with \`mcp__${McpCatalog.sanitize(item.name)}__\`, and to prompts/resources from this MCP server.\n\n${item.instructions}`, - ) + const available = (yield* mcp.instructions()).filter((item) => { + if (item.tools.length === 0) return false + return Permission.disabled(item.tools, agent.permission).size < item.tools.length + }) + if (available.length === 0) return + + return [ + "MCP servers may provide instructions for using their tools, prompts, and resources.", + "Each entry applies to tools matching its tool prefix and to prompts and resources from that server.", + "", + ...available.flatMap((item) => [ + " ", + ` ${item.name}`, + ` mcp__${McpCatalog.sanitize(item.name)}__`, + " ", + ...item.instructions.split("\n").map((line) => ` ${line}`), + " ", + " ", + ]), + "", + ].join("\n") }), }) }), diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 6176458447c6..f4966c4dc43c 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -545,8 +545,8 @@ withMcpInstructions.instance("loop includes MCP instructions in model system con const hits = yield* llm.hits const body = JSON.stringify(hits[0]?.body) - expect(body).toContain("Instructions from: MCP server guide-server") - expect(body).toContain("mcp__guide-server__") + expect(body).toContain("guide-server") + expect(body).toContain("mcp__guide-server__") expect(body).toContain("Use lookup before mutate.") yield* Fiber.interrupt(fiber) }), diff --git a/packages/opencode/test/session/system.test.ts b/packages/opencode/test/session/system.test.ts index 8e6bf98a1342..98bf472fb415 100644 --- a/packages/opencode/test/session/system.test.ts +++ b/packages/opencode/test/session/system.test.ts @@ -105,14 +105,21 @@ describe("session.system", () => { permission: Permission.fromConfig({ "mcp__guide-server__mutate": "deny" }), }) - expect(output).toEqual([ + expect(output).toBe( [ - "Instructions from: MCP server guide-server", - "These instructions apply to MCP tools whose names start with `mcp__guide-server__`, and to prompts/resources from this MCP server.", - "", - "Use lookup before mutate.", + "MCP servers may provide instructions for using their tools, prompts, and resources.", + "Each entry applies to tools matching its tool prefix and to prompts and resources from that server.", + "", + " ", + " guide-server", + " mcp__guide-server__", + " ", + " Use lookup before mutate.", + " ", + " ", + "", ].join("\n"), - ]) + ) }), ) @@ -124,7 +131,7 @@ describe("session.system", () => { permission: Permission.fromConfig({ "mcp__guide-server__*": "deny" }), }) - expect(output).toEqual([]) + expect(output).toBeUndefined() }), ) }) From fbe04dea11251ce234fc479c77a77da97f08de3f Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 23 Jun 2026 23:37:04 -0500 Subject: [PATCH 4/7] refactor(mcp): simplify instruction markup --- packages/opencode/src/session/system.ts | 17 +++++------------ packages/opencode/test/session/prompt.test.ts | 3 +-- packages/opencode/test/session/system.test.ts | 16 +++++----------- 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 57b102622474..08c021bdabf6 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -20,7 +20,6 @@ import { AbsolutePath } from "@opencode-ai/core/schema" import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-layer" import { Reference } from "@opencode-ai/core/reference" -import { McpCatalog } from "@/mcp/catalog" import { MCP } from "@/mcp" export function provider(model: Provider.Model) { @@ -115,19 +114,13 @@ export const layer = Layer.effect( if (available.length === 0) return return [ - "MCP servers may provide instructions for using their tools, prompts, and resources.", - "Each entry applies to tools matching its tool prefix and to prompts and resources from that server.", - "", + "", ...available.flatMap((item) => [ - " ", - ` ${item.name}`, - ` mcp__${McpCatalog.sanitize(item.name)}__`, - " ", - ...item.instructions.split("\n").map((line) => ` ${line}`), - " ", - " ", + ` `, + ...item.instructions.split("\n").map((line) => ` ${line}`), + " ", ]), - "", + "", ].join("\n") }), }) diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index f4966c4dc43c..adbd7daf8d99 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -545,8 +545,7 @@ withMcpInstructions.instance("loop includes MCP instructions in model system con const hits = yield* llm.hits const body = JSON.stringify(hits[0]?.body) - expect(body).toContain("guide-server") - expect(body).toContain("mcp__guide-server__") + expect(body).toContain('') expect(body).toContain("Use lookup before mutate.") yield* Fiber.interrupt(fiber) }), diff --git a/packages/opencode/test/session/system.test.ts b/packages/opencode/test/session/system.test.ts index 98bf472fb415..a84be1b02425 100644 --- a/packages/opencode/test/session/system.test.ts +++ b/packages/opencode/test/session/system.test.ts @@ -107,17 +107,11 @@ describe("session.system", () => { expect(output).toBe( [ - "MCP servers may provide instructions for using their tools, prompts, and resources.", - "Each entry applies to tools matching its tool prefix and to prompts and resources from that server.", - "", - " ", - " guide-server", - " mcp__guide-server__", - " ", - " Use lookup before mutate.", - " ", - " ", - "", + "", + ' ', + " Use lookup before mutate.", + " ", + "", ].join("\n"), ) }), From 52dc5c67f03f4ac08287f2002220faac3cbb81bb Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 23 Jun 2026 23:43:31 -0500 Subject: [PATCH 5/7] refactor(mcp): separate instruction metadata --- packages/opencode/src/mcp/catalog.ts | 4 +++ packages/opencode/src/mcp/index.ts | 15 +++++++---- packages/opencode/src/session/system.ts | 9 ++++--- packages/opencode/test/mcp/lifecycle.test.ts | 2 +- packages/opencode/test/session/prompt.test.ts | 25 ++++++++++++++----- .../test/session/snapshot-tool-race.test.ts | 1 + packages/opencode/test/session/system.test.ts | 2 +- 7 files changed, 42 insertions(+), 16 deletions(-) diff --git a/packages/opencode/src/mcp/catalog.ts b/packages/opencode/src/mcp/catalog.ts index 0cd2238edffe..3df78b063501 100644 --- a/packages/opencode/src/mcp/catalog.ts +++ b/packages/opencode/src/mcp/catalog.ts @@ -113,6 +113,10 @@ export function fetch( export const sanitize = (value: string) => value.replace(/[^a-zA-Z0-9_-]/g, "_") +export const toolPrefix = (clientName: string) => "mcp__" + sanitize(clientName) + "__" + +export const toolName = (clientName: string, name: string) => toolPrefix(clientName) + sanitize(name) + export function prompts(client: Client, timeout?: number) { if (!client.getServerCapabilities()?.prompts) return Promise.resolve([]) return paginate( diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index 9a3132b6cc11..defe9aefa890 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -162,13 +162,13 @@ interface State { export interface ServerInstructions { name: string instructions: string - tools: string[] } export interface Interface { readonly status: () => Effect.Effect> readonly clients: () => Effect.Effect> readonly instructions: () => Effect.Effect + readonly toolNames: () => Effect.Effect readonly tools: () => Effect.Effect> readonly prompts: () => Effect.Effect> readonly resources: (clientName?: string) => Effect.Effect> @@ -615,12 +615,16 @@ export const layer = Layer.effect( .map(([name, item]) => ({ name, instructions: item, - tools: (s.defs[name] ?? []).map( - (tool) => "mcp__" + McpCatalog.sanitize(name) + "__" + McpCatalog.sanitize(tool.name), - ), })) }) + const toolNames = Effect.fn("MCP.toolNames")(function* () { + const s = yield* InstanceState.get(state) + return Object.entries(s.defs) + .filter(([name]) => s.status[name]?.status === "connected") + .flatMap(([name, defs]) => defs.map((tool) => McpCatalog.toolName(name, tool.name))) + }) + const createAndStore = Effect.fn("MCP.createAndStore")(function* (name: string, mcp: ConfigMCPV1.Info) { const s = yield* InstanceState.get(state) const result = yield* create(name, mcp) @@ -678,7 +682,7 @@ export const layer = Layer.effect( } const timeout = requestTimeout(s, clientName, mcpConfig, defaultTimeout) for (const mcpTool of listed) { - const key = "mcp__" + McpCatalog.sanitize(clientName) + "__" + McpCatalog.sanitize(mcpTool.name) + const key = McpCatalog.toolName(clientName, mcpTool.name) result[key] = McpCatalog.convertTool(mcpTool, client, timeout) } } @@ -974,6 +978,7 @@ export const layer = Layer.effect( status, clients, instructions, + toolNames, tools, prompts, resources, diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 08c021bdabf6..c7fa31a434f2 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -21,6 +21,7 @@ import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-layer" import { Reference } from "@opencode-ai/core/reference" import { MCP } from "@/mcp" +import { McpCatalog } from "@/mcp/catalog" export function provider(model: Provider.Model) { if (model.api.id.includes("gpt-4") || model.api.id.includes("o1") || model.api.id.includes("o3")) @@ -107,9 +108,11 @@ export const layer = Layer.effect( }), mcp: Effect.fn("SystemPrompt.mcp")(function* (agent: Agent.Info) { - const available = (yield* mcp.instructions()).filter((item) => { - if (item.tools.length === 0) return false - return Permission.disabled(item.tools, agent.permission).size < item.tools.length + const [instructions, toolNames] = yield* Effect.all([mcp.instructions(), mcp.toolNames()]) + const disabled = Permission.disabled(toolNames, agent.permission) + const available = instructions.filter((item) => { + const prefix = McpCatalog.toolPrefix(item.name) + return toolNames.some((name) => name.startsWith(prefix) && !disabled.has(name)) }) if (available.length === 0) return diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index d0bdf4a8f5ed..2d2c1dfda268 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -369,8 +369,8 @@ it.instance( expect(yield* mcp.instructions()).toContainEqual({ name: "guide-server", instructions: "Use lookup before mutate.", - tools: ["mcp__guide-server__test_tool"], }) + expect(yield* mcp.toolNames()).toContain("mcp__guide-server__test_tool") }), ), { config: { mcp: {} } }, diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index adbd7daf8d99..fd214c4e32be 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -109,13 +109,14 @@ function errorTool(parts: SessionV1.Part[]) { return part?.state.status === "error" ? (part as ErrorToolPart) : undefined } -function makeMcp(instructions: MCP.ServerInstructions[] = []) { +function makeMcp(instructions: MCP.ServerInstructions[] = [], toolNames: string[] = []) { return Layer.succeed( MCP.Service, MCP.Service.of({ status: () => Effect.succeed({}), clients: () => Effect.succeed({}), instructions: () => Effect.succeed(instructions), + toolNames: () => Effect.succeed(toolNames), tools: () => Effect.succeed({}), prompts: () => Effect.succeed({}), resources: () => Effect.succeed({}), @@ -168,7 +169,11 @@ const blockingProcessor = Layer.succeed( }), ) -function makePrompt(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) { +function makePrompt(input?: { + mcpInstructions?: MCP.ServerInstructions[] + mcpToolNames?: string[] + processor?: "blocking" +}) { const deps = Layer.mergeAll( Session.defaultLayer, Snapshot.defaultLayer, @@ -181,7 +186,7 @@ function makePrompt(input?: { mcpInstructions?: MCP.ServerInstructions[]; proces Config.defaultLayer, ProviderSvc.defaultLayer, lsp, - makeMcp(input?.mcpInstructions), + makeMcp(input?.mcpInstructions, input?.mcpToolNames), FSUtil.defaultLayer, BackgroundJob.defaultLayer, status, @@ -240,11 +245,19 @@ function makePrompt(input?: { mcpInstructions?: MCP.ServerInstructions[]; proces ) } -function makeHttp(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) { +function makeHttp(input?: { + mcpInstructions?: MCP.ServerInstructions[] + mcpToolNames?: string[] + processor?: "blocking" +}) { return Layer.mergeAll(TestLLMServer.layer, makePrompt(input)) } -function makeHttpNoLLMServer(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) { +function makeHttpNoLLMServer(input?: { + mcpInstructions?: MCP.ServerInstructions[] + mcpToolNames?: string[] + processor?: "blocking" +}) { return makePrompt(input) } @@ -257,9 +270,9 @@ const withMcpInstructions = testEffect( { name: "guide-server", instructions: "Use lookup before mutate.", - tools: ["mcp__guide-server__lookup"], }, ], + mcpToolNames: ["mcp__guide-server__lookup"], }), ) const unix = process.platform !== "win32" ? it.instance : it.instance.skip diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index 0a5065837d0a..26fff9d413af 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -38,6 +38,7 @@ const mcp = Layer.succeed( status: () => Effect.succeed({}), clients: () => Effect.succeed({}), instructions: () => Effect.succeed([]), + toolNames: () => Effect.succeed([]), tools: () => Effect.succeed({}), prompts: () => Effect.succeed({}), resources: () => Effect.succeed({}), diff --git a/packages/opencode/test/session/system.test.ts b/packages/opencode/test/session/system.test.ts index a84be1b02425..e219a63de965 100644 --- a/packages/opencode/test/session/system.test.ts +++ b/packages/opencode/test/session/system.test.ts @@ -52,9 +52,9 @@ const it = testEffect( { name: "guide-server", instructions: "Use lookup before mutate.", - tools: ["mcp__guide-server__lookup", "mcp__guide-server__mutate"], }, ]), + toolNames: () => Effect.succeed(["mcp__guide-server__lookup", "mcp__guide-server__mutate"]), }), ), Layer.provide( From b17e38784932c51b8f19107cebcc238c9b28e698 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 23 Jun 2026 23:50:39 -0500 Subject: [PATCH 6/7] refactor(mcp): reuse exposed tool inventory --- packages/opencode/src/mcp/catalog.ts | 2 +- packages/opencode/src/mcp/index.ts | 9 --------- packages/opencode/src/session/system.ts | 3 ++- packages/opencode/test/mcp/lifecycle.test.ts | 1 - packages/opencode/test/session/prompt.test.ts | 10 +++++++--- .../test/session/snapshot-tool-race.test.ts | 1 - packages/opencode/test/session/system.test.ts | 14 +++++++++++--- 7 files changed, 21 insertions(+), 19 deletions(-) diff --git a/packages/opencode/src/mcp/catalog.ts b/packages/opencode/src/mcp/catalog.ts index 3df78b063501..0e2758a1347f 100644 --- a/packages/opencode/src/mcp/catalog.ts +++ b/packages/opencode/src/mcp/catalog.ts @@ -113,7 +113,7 @@ export function fetch( export const sanitize = (value: string) => value.replace(/[^a-zA-Z0-9_-]/g, "_") -export const toolPrefix = (clientName: string) => "mcp__" + sanitize(clientName) + "__" +export const toolPrefix = (clientName: string) => sanitize(clientName) + "_" export const toolName = (clientName: string, name: string) => toolPrefix(clientName) + sanitize(name) diff --git a/packages/opencode/src/mcp/index.ts b/packages/opencode/src/mcp/index.ts index defe9aefa890..4d7b5174ed37 100644 --- a/packages/opencode/src/mcp/index.ts +++ b/packages/opencode/src/mcp/index.ts @@ -168,7 +168,6 @@ export interface Interface { readonly status: () => Effect.Effect> readonly clients: () => Effect.Effect> readonly instructions: () => Effect.Effect - readonly toolNames: () => Effect.Effect readonly tools: () => Effect.Effect> readonly prompts: () => Effect.Effect> readonly resources: (clientName?: string) => Effect.Effect> @@ -618,13 +617,6 @@ export const layer = Layer.effect( })) }) - const toolNames = Effect.fn("MCP.toolNames")(function* () { - const s = yield* InstanceState.get(state) - return Object.entries(s.defs) - .filter(([name]) => s.status[name]?.status === "connected") - .flatMap(([name, defs]) => defs.map((tool) => McpCatalog.toolName(name, tool.name))) - }) - const createAndStore = Effect.fn("MCP.createAndStore")(function* (name: string, mcp: ConfigMCPV1.Info) { const s = yield* InstanceState.get(state) const result = yield* create(name, mcp) @@ -978,7 +970,6 @@ export const layer = Layer.effect( status, clients, instructions, - toolNames, tools, prompts, resources, diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index c7fa31a434f2..85d2fb3df920 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -108,7 +108,8 @@ export const layer = Layer.effect( }), mcp: Effect.fn("SystemPrompt.mcp")(function* (agent: Agent.Info) { - const [instructions, toolNames] = yield* Effect.all([mcp.instructions(), mcp.toolNames()]) + const [instructions, tools] = yield* Effect.all([mcp.instructions(), mcp.tools()]) + const toolNames = Object.keys(tools) const disabled = Permission.disabled(toolNames, agent.permission) const available = instructions.filter((item) => { const prefix = McpCatalog.toolPrefix(item.name) diff --git a/packages/opencode/test/mcp/lifecycle.test.ts b/packages/opencode/test/mcp/lifecycle.test.ts index 2d2c1dfda268..b2368c11bf11 100644 --- a/packages/opencode/test/mcp/lifecycle.test.ts +++ b/packages/opencode/test/mcp/lifecycle.test.ts @@ -370,7 +370,6 @@ it.instance( name: "guide-server", instructions: "Use lookup before mutate.", }) - expect(yield* mcp.toolNames()).toContain("mcp__guide-server__test_tool") }), ), { config: { mcp: {} } }, diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index fd214c4e32be..00739e5c063c 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -57,6 +57,8 @@ import { RuntimeFlags } from "@/effect/runtime-flags" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" import { LocationServiceMap } from "@opencode-ai/core/location-layer" +import { tool } from "ai" +import { z } from "zod" const summary = Layer.succeed( SessionSummary.Service, @@ -116,8 +118,10 @@ function makeMcp(instructions: MCP.ServerInstructions[] = [], toolNames: string[ status: () => Effect.succeed({}), clients: () => Effect.succeed({}), instructions: () => Effect.succeed(instructions), - toolNames: () => Effect.succeed(toolNames), - tools: () => Effect.succeed({}), + tools: () => + Effect.succeed( + Object.fromEntries(toolNames.map((name) => [name, tool({ description: "", inputSchema: z.object({}) })])), + ), prompts: () => Effect.succeed({}), resources: () => Effect.succeed({}), resourceTemplates: () => Effect.succeed({}), @@ -272,7 +276,7 @@ const withMcpInstructions = testEffect( instructions: "Use lookup before mutate.", }, ], - mcpToolNames: ["mcp__guide-server__lookup"], + mcpToolNames: ["guide-server_lookup"], }), ) const unix = process.platform !== "win32" ? it.instance : it.instance.skip diff --git a/packages/opencode/test/session/snapshot-tool-race.test.ts b/packages/opencode/test/session/snapshot-tool-race.test.ts index 26fff9d413af..0a5065837d0a 100644 --- a/packages/opencode/test/session/snapshot-tool-race.test.ts +++ b/packages/opencode/test/session/snapshot-tool-race.test.ts @@ -38,7 +38,6 @@ const mcp = Layer.succeed( status: () => Effect.succeed({}), clients: () => Effect.succeed({}), instructions: () => Effect.succeed([]), - toolNames: () => Effect.succeed([]), tools: () => Effect.succeed({}), prompts: () => Effect.succeed({}), resources: () => Effect.succeed({}), diff --git a/packages/opencode/test/session/system.test.ts b/packages/opencode/test/session/system.test.ts index e219a63de965..d566a9f4d299 100644 --- a/packages/opencode/test/session/system.test.ts +++ b/packages/opencode/test/session/system.test.ts @@ -8,6 +8,8 @@ import { SystemPrompt } from "../../src/session/system" import { MCP } from "../../src/mcp" import { LocationServiceMap } from "@opencode-ai/core/location-layer" import { testEffect } from "../lib/effect" +import { tool } from "ai" +import { z } from "zod" const skills: Skill.Info[] = [ { @@ -42,6 +44,8 @@ const build: Agent.Info = { options: {}, } +const mcpTool = tool({ description: "", inputSchema: z.object({}) }) + const it = testEffect( SystemPrompt.layer.pipe( Layer.provide(LocationServiceMap.layer), @@ -54,7 +58,11 @@ const it = testEffect( instructions: "Use lookup before mutate.", }, ]), - toolNames: () => Effect.succeed(["mcp__guide-server__lookup", "mcp__guide-server__mutate"]), + tools: () => + Effect.succeed({ + "guide-server_lookup": mcpTool, + "guide-server_mutate": mcpTool, + }), }), ), Layer.provide( @@ -102,7 +110,7 @@ describe("session.system", () => { const prompt = yield* SystemPrompt.Service const output = yield* prompt.mcp({ ...build, - permission: Permission.fromConfig({ "mcp__guide-server__mutate": "deny" }), + permission: Permission.fromConfig({ "guide-server_mutate": "deny" }), }) expect(output).toBe( @@ -122,7 +130,7 @@ describe("session.system", () => { const prompt = yield* SystemPrompt.Service const output = yield* prompt.mcp({ ...build, - permission: Permission.fromConfig({ "mcp__guide-server__*": "deny" }), + permission: Permission.fromConfig({ "guide-server_*": "deny" }), }) expect(output).toBeUndefined() From a0540aef2eee1d5f278057c05d254953a139f3aa Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Wed, 24 Jun 2026 00:12:40 -0500 Subject: [PATCH 7/7] fix(mcp): include all server instructions --- packages/opencode/src/mcp/catalog.ts | 4 +-- packages/opencode/src/session/prompt.ts | 2 +- packages/opencode/src/session/system.ts | 17 ++++------- packages/opencode/test/session/prompt.test.ts | 15 ++-------- packages/opencode/test/session/system.test.ts | 28 ++----------------- 5 files changed, 12 insertions(+), 54 deletions(-) diff --git a/packages/opencode/src/mcp/catalog.ts b/packages/opencode/src/mcp/catalog.ts index 0e2758a1347f..466185f09ac9 100644 --- a/packages/opencode/src/mcp/catalog.ts +++ b/packages/opencode/src/mcp/catalog.ts @@ -113,9 +113,7 @@ export function fetch( export const sanitize = (value: string) => value.replace(/[^a-zA-Z0-9_-]/g, "_") -export const toolPrefix = (clientName: string) => sanitize(clientName) + "_" - -export const toolName = (clientName: string, name: string) => toolPrefix(clientName) + sanitize(name) +export const toolName = (clientName: string, name: string) => sanitize(clientName) + "_" + sanitize(name) export function prompts(client: Client, timeout?: number) { if (!client.getServerCapabilities()?.prompts) return Promise.resolve([]) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index fe0d42130e1f..beab5b2312f2 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1360,7 +1360,7 @@ export const layer = Layer.effect( sys.skills(agent), sys.environment(model), instruction.system().pipe(Effect.orDie), - sys.mcp(agent), + sys.mcp(), MessageV2.toModelMessagesEffect(msgs, model), ]) const system = [ diff --git a/packages/opencode/src/session/system.ts b/packages/opencode/src/session/system.ts index 85d2fb3df920..bd596a38d673 100644 --- a/packages/opencode/src/session/system.ts +++ b/packages/opencode/src/session/system.ts @@ -21,7 +21,6 @@ import { Location } from "@opencode-ai/core/location" import { LocationServiceMap } from "@opencode-ai/core/location-layer" import { Reference } from "@opencode-ai/core/reference" import { MCP } from "@/mcp" -import { McpCatalog } from "@/mcp/catalog" export function provider(model: Provider.Model) { if (model.api.id.includes("gpt-4") || model.api.id.includes("o1") || model.api.id.includes("o3")) @@ -42,7 +41,7 @@ export function provider(model: Provider.Model) { export interface Interface { readonly environment: (model: Provider.Model) => Effect.Effect readonly skills: (agent: Agent.Info) => Effect.Effect - readonly mcp: (agent: Agent.Info) => Effect.Effect + readonly mcp: () => Effect.Effect } export class Service extends Context.Service()("@opencode/SystemPrompt") {} @@ -107,19 +106,13 @@ export const layer = Layer.effect( ].join("\n") }), - mcp: Effect.fn("SystemPrompt.mcp")(function* (agent: Agent.Info) { - const [instructions, tools] = yield* Effect.all([mcp.instructions(), mcp.tools()]) - const toolNames = Object.keys(tools) - const disabled = Permission.disabled(toolNames, agent.permission) - const available = instructions.filter((item) => { - const prefix = McpCatalog.toolPrefix(item.name) - return toolNames.some((name) => name.startsWith(prefix) && !disabled.has(name)) - }) - if (available.length === 0) return + mcp: Effect.fn("SystemPrompt.mcp")(function* () { + const instructions = yield* mcp.instructions() + if (instructions.length === 0) return return [ "", - ...available.flatMap((item) => [ + ...instructions.flatMap((item) => [ ` `, ...item.instructions.split("\n").map((line) => ` ${line}`), " ", diff --git a/packages/opencode/test/session/prompt.test.ts b/packages/opencode/test/session/prompt.test.ts index 00739e5c063c..df186eb328cc 100644 --- a/packages/opencode/test/session/prompt.test.ts +++ b/packages/opencode/test/session/prompt.test.ts @@ -57,8 +57,6 @@ import { RuntimeFlags } from "@/effect/runtime-flags" import { ProviderV2 } from "@opencode-ai/core/provider" import { ModelV2 } from "@opencode-ai/core/model" import { LocationServiceMap } from "@opencode-ai/core/location-layer" -import { tool } from "ai" -import { z } from "zod" const summary = Layer.succeed( SessionSummary.Service, @@ -111,17 +109,14 @@ function errorTool(parts: SessionV1.Part[]) { return part?.state.status === "error" ? (part as ErrorToolPart) : undefined } -function makeMcp(instructions: MCP.ServerInstructions[] = [], toolNames: string[] = []) { +function makeMcp(instructions: MCP.ServerInstructions[] = []) { return Layer.succeed( MCP.Service, MCP.Service.of({ status: () => Effect.succeed({}), clients: () => Effect.succeed({}), instructions: () => Effect.succeed(instructions), - tools: () => - Effect.succeed( - Object.fromEntries(toolNames.map((name) => [name, tool({ description: "", inputSchema: z.object({}) })])), - ), + tools: () => Effect.succeed({}), prompts: () => Effect.succeed({}), resources: () => Effect.succeed({}), resourceTemplates: () => Effect.succeed({}), @@ -175,7 +170,6 @@ const blockingProcessor = Layer.succeed( function makePrompt(input?: { mcpInstructions?: MCP.ServerInstructions[] - mcpToolNames?: string[] processor?: "blocking" }) { const deps = Layer.mergeAll( @@ -190,7 +184,7 @@ function makePrompt(input?: { Config.defaultLayer, ProviderSvc.defaultLayer, lsp, - makeMcp(input?.mcpInstructions, input?.mcpToolNames), + makeMcp(input?.mcpInstructions), FSUtil.defaultLayer, BackgroundJob.defaultLayer, status, @@ -251,7 +245,6 @@ function makePrompt(input?: { function makeHttp(input?: { mcpInstructions?: MCP.ServerInstructions[] - mcpToolNames?: string[] processor?: "blocking" }) { return Layer.mergeAll(TestLLMServer.layer, makePrompt(input)) @@ -259,7 +252,6 @@ function makeHttp(input?: { function makeHttpNoLLMServer(input?: { mcpInstructions?: MCP.ServerInstructions[] - mcpToolNames?: string[] processor?: "blocking" }) { return makePrompt(input) @@ -276,7 +268,6 @@ const withMcpInstructions = testEffect( instructions: "Use lookup before mutate.", }, ], - mcpToolNames: ["guide-server_lookup"], }), ) const unix = process.platform !== "win32" ? it.instance : it.instance.skip diff --git a/packages/opencode/test/session/system.test.ts b/packages/opencode/test/session/system.test.ts index d566a9f4d299..cbfa27b49011 100644 --- a/packages/opencode/test/session/system.test.ts +++ b/packages/opencode/test/session/system.test.ts @@ -8,8 +8,6 @@ import { SystemPrompt } from "../../src/session/system" import { MCP } from "../../src/mcp" import { LocationServiceMap } from "@opencode-ai/core/location-layer" import { testEffect } from "../lib/effect" -import { tool } from "ai" -import { z } from "zod" const skills: Skill.Info[] = [ { @@ -44,8 +42,6 @@ const build: Agent.Info = { options: {}, } -const mcpTool = tool({ description: "", inputSchema: z.object({}) }) - const it = testEffect( SystemPrompt.layer.pipe( Layer.provide(LocationServiceMap.layer), @@ -58,11 +54,6 @@ const it = testEffect( instructions: "Use lookup before mutate.", }, ]), - tools: () => - Effect.succeed({ - "guide-server_lookup": mcpTool, - "guide-server_mutate": mcpTool, - }), }), ), Layer.provide( @@ -105,13 +96,10 @@ describe("session.system", () => { }), ) - it.effect("MCP output includes servers with an allowed tool", () => + it.effect("MCP output includes connected server instructions", () => Effect.gen(function* () { const prompt = yield* SystemPrompt.Service - const output = yield* prompt.mcp({ - ...build, - permission: Permission.fromConfig({ "guide-server_mutate": "deny" }), - }) + const output = yield* prompt.mcp() expect(output).toBe( [ @@ -124,16 +112,4 @@ describe("session.system", () => { ) }), ) - - it.effect("MCP output omits servers when all tools are denied", () => - Effect.gen(function* () { - const prompt = yield* SystemPrompt.Service - const output = yield* prompt.mcp({ - ...build, - permission: Permission.fromConfig({ "guide-server_*": "deny" }), - }) - - expect(output).toBeUndefined() - }), - ) })