feat(core): allow MCP Code Mode opt-out (#37681) · argszero/opencode@fe9b051 · GitHub
Skip to content

Commit fe9b051

Browse files
feat(core): allow MCP Code Mode opt-out (anomalyco#37681)
Co-authored-by: Dax Raad <d@ironbay.co>
1 parent 33f1b26 commit fe9b051

6 files changed

Lines changed: 57 additions & 9 deletions

File tree

packages/core/src/config/mcp.ts

Lines changed: 6 additions & 0 deletions

packages/core/src/mcp/index.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ export class ServerInstructions extends Schema.Class<ServerInstructions>("MCP.Se
4242
export class Tool extends Schema.Class<Tool>("MCP.Tool")({
4343
server: ServerName,
4444
name: Schema.String,
45+
codemode: Schema.Boolean.pipe(Schema.optional),
4546
description: Schema.String.pipe(Schema.optional),
4647
inputSchema: Schema.Unknown.pipe(Schema.optional),
4748
outputSchema: Schema.Unknown.pipe(Schema.optional),
@@ -362,10 +363,11 @@ export const layer = Layer.effect(
362363
}),
363364
} satisfies MCPClient.ElicitationHandler
364365

365-
const toTool = (server: ServerName, def: MCPClient.ToolDefinition) =>
366+
const toTool = (server: ServerName, entry: ServerEntry, def: MCPClient.ToolDefinition) =>
366367
new Tool({
367368
server,
368369
name: def.name,
370+
codemode: entry.config.codemode,
369371
description: def.description,
370372
inputSchema: def.inputSchema,
371373
outputSchema: def.outputSchema,
@@ -407,7 +409,7 @@ export const layer = Layer.effect(
407409
const refreshTools = (name: ServerName, entry: ServerEntry, connection: MCPClient.Connection) =>
408410
connection.tools().pipe(
409411
Effect.map((defs) => {
410-
entry.tools = defs.map((def) => toTool(name, def))
412+
entry.tools = defs.map((def) => toTool(name, entry, def))
411413
}),
412414
)
413415

@@ -498,7 +500,7 @@ export const layer = Layer.effect(
498500
)
499501
if (Exit.isSuccess(result)) {
500502
entry.client = result.value.connection
501-
entry.tools = result.value.tools.map((def) => toTool(name, def))
503+
entry.tools = result.value.tools.map((def) => toTool(name, entry, def))
502504
entry.prompts = []
503505
entry.status = { status: "connected" }
504506
watch(name, entry, result.value.connection)

packages/core/src/tool/mcp.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,7 @@ import { ToolRegistry } from "./registry"
1616
* Registry namespace and permission action names for MCP tools.
1717
*/
1818
export const namespace = (server: string) => server.replace(/[^a-zA-Z0-9_-]/g, "_")
19-
export const name = (server: string, tool: string) =>
20-
`${namespace(server)}_${tool.replace(/[^a-zA-Z0-9_-]/g, "_")}`
19+
export const name = (server: string, tool: string) => `${namespace(server)}_${tool.replace(/[^a-zA-Z0-9_-]/g, "_")}`
2120

2221
export const layer = Layer.effectDiscard(
2322
Effect.gen(function* () {
@@ -33,11 +32,11 @@ export const layer = Layer.effectDiscard(
3332
// registry never has a gap where MCP tools disappear mid-swap.
3433
const reconcile = lock.withPermit(
3534
Effect.gen(function* () {
36-
const groups = new Map<string, Record<string, Tool.AnyTool>>()
35+
const groups = new Map<string, { tools: Record<string, Tool.AnyTool>; codemode: boolean }>()
3736
for (const tool of yield* mcp.tools()) {
38-
const group = groups.get(tool.server) ?? {}
37+
const group = groups.get(tool.server) ?? { tools: {}, codemode: tool.codemode !== false }
3938
const schema = (tool.inputSchema ?? {}) as JsonSchema.JsonSchema
40-
group[tool.name] = Tool.withPermission(
39+
group.tools[tool.name] = Tool.withPermission(
4140
Tool.make({
4241
description: tool.description ?? "",
4342
jsonSchema: {
@@ -108,7 +107,7 @@ export const layer = Layer.effectDiscard(
108107
const next = yield* Scope.fork(scope)
109108
yield* Effect.forEach(
110109
groups,
111-
([server, record]) => tools.register(record, { namespace: namespace(server) }),
110+
([server, group]) => tools.register(group.tools, { namespace: namespace(server), codemode: group.codemode }),
112111
{
113112
discard: true,
114113
},

packages/core/test/config/config.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -655,6 +655,7 @@ describe("Config", () => {
655655
command: ["node", "./mcp/server.js"],
656656
environment: { API_KEY: "secret" },
657657
disabled: false,
658+
codemode: false,
658659
timeout: { catalog: 10000 },
659660
},
660661
remote: {
@@ -663,6 +664,7 @@ describe("Config", () => {
663664
headers: { Authorization: "Bearer token" },
664665
oauth: { client_id: "client", scope: "read write", callback_port: 19876 },
665666
disabled: true,
667+
codemode: false,
666668
timeout: { startup: 15000 },
667669
},
668670
},
@@ -740,6 +742,7 @@ describe("Config", () => {
740742
command: ["node", "./mcp/server.js"],
741743
environment: { API_KEY: "secret" },
742744
disabled: false,
745+
codemode: false,
743746
timeout: { catalog: 10000 },
744747
},
745748
remote: {
@@ -748,6 +751,7 @@ describe("Config", () => {
748751
headers: { Authorization: "Bearer token" },
749752
oauth: { client_id: "client", scope: "read write", callback_port: 19876 },
750753
disabled: true,
754+
codemode: false,
751755
timeout: { startup: 15000 },
752756
},
753757
},

packages/core/test/mcp.test.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,13 @@ const mcp = Layer.mock(MCP.Service, {
232232
required: ["ok"],
233233
},
234234
}),
235+
new MCP.Tool({
236+
server: MCP.ServerName.make("direct"),
237+
name: "lookup",
238+
codemode: false,
239+
description: "Lookup",
240+
inputSchema: { type: "object", properties: {} },
241+
}),
235242
]),
236243
callTool: (input) =>
237244
Effect.sync(() => {
@@ -766,6 +773,18 @@ it.effect("advertises MCP output schemas to Code Mode", () =>
766773
}),
767774
)
768775

776+
it.effect("advertises MCP tools directly when Code Mode is disabled for the server", () =>
777+
Effect.gen(function* () {
778+
const registry = yield* ToolRegistry.Service
779+
yield* waitForTool(registry, "direct_lookup")
780+
const definitions = yield* toolDefinitions(registry)
781+
const execute = definitions.find((tool) => tool.name === "execute")
782+
783+
expect(definitions.some((tool) => tool.name === "direct_lookup")).toBe(true)
784+
expect(execute?.description).not.toContain("tools.direct.lookup")
785+
}),
786+
)
787+
769788
it.effect("waits for permission before calling an MCP tool", () =>
770789
Effect.gen(function* () {
771790
calls = 0

packages/docs/mcp-servers.mdx

Lines changed: 18 additions & 0 deletions

0 commit comments

Comments
 (0)