fix(core): scope MCP execute guidance to Code Mode (#38753)

This commit is contained in:
Aiden Cline 2026-07-24 16:02:57 -05:00 committed by GitHub
parent d1d97014b4
commit 13b6845e7e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 80 additions and 14 deletions

View file

@ -11,16 +11,20 @@ import { Instructions } from "../instructions/index"
const Summary = Schema.Struct({
server: Schema.String,
instructions: Schema.String,
codemode: Schema.optionalKey(Schema.Literal(false)),
})
type Summary = typeof Summary.Type
const entries = (servers: ReadonlyArray<Summary>) =>
servers.flatMap((server) => [
` <server name="${server.server}">`,
` Use tools from this server through \`execute\` under \`tools[${JSON.stringify(McpTool.namespace(server.server))}]\`.`,
...server.instructions.split("\n").map((line) => ` ${line}`),
" </server>",
])
servers.flatMap((server) => {
const result = [` <server name="${server.server}">`]
if (server.codemode !== false)
result.push(
` Use tools from this server through \`execute\` under \`tools[${JSON.stringify(McpTool.namespace(server.server))}]\`.`,
)
result.push(...server.instructions.split("\n").map((line) => ` ${line}`), " </server>")
return result
})
const render = (servers: ReadonlyArray<Summary>) =>
["<mcp_instructions>", ...entries(servers), "</mcp_instructions>"].join("\n")
@ -30,7 +34,7 @@ const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary
previous,
current,
(server) => server.server,
(before, after) => before.instructions !== after.instructions,
(before, after) => before.instructions !== after.instructions || before.codemode !== after.codemode,
)
// Additions and removals render as small deltas; anything else restates the full list.
if (diff.changed.length > 0 || (diff.added.length === 0 && diff.removed.length === 0))
@ -76,21 +80,29 @@ export const layer = Layer.effect(
removed: () => "MCP server instructions are no longer available.",
},
})
if (PermissionV2.evaluate("execute", "*", agent.permissions).effect === "deny")
return source(Instructions.removed)
const [instructions, tools] = yield* Effect.all([mcp.instructions(), mcp.tools()], {
concurrency: "unbounded",
})
const canExecute = PermissionV2.evaluate("execute", "*", agent.permissions).effect !== "deny"
// Instructions are useful only when this agent can reach at least one server tool.
const visible = instructions
.filter((item) => {
.flatMap((item) => {
const owned = tools.filter((tool) => tool.server === item.server)
return owned.some(
(tool) =>
PermissionV2.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny",
const codemode = owned[0]?.codemode !== false
if (codemode && !canExecute) return []
if (
!owned.some(
(tool) =>
PermissionV2.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny",
)
)
return []
return [
codemode
? { server: item.server, instructions: item.instructions }
: { server: item.server, instructions: item.instructions, codemode: false as const },
]
})
.map((item) => ({ server: item.server, instructions: item.instructions }))
.toSorted((a, b) => a.server.localeCompare(b.server))
return source(visible.length === 0 ? Instructions.removed : visible)
}),

View file

@ -93,6 +93,60 @@ describe("McpInstructions", () => {
),
)
it.effect("keeps MCP instructions when Code Mode is disabled and execute is denied", () =>
Effect.gen(function* () {
const service = yield* McpInstructions.Service
const generation = yield* service
.load(selection([{ action: "execute", resource: "*", effect: "deny" }]))
.pipe(Effect.flatMap(readInitial))
expect(generation.text).toBe(
[
"<mcp_instructions>",
' <server name="alpha">',
" Alpha instructions",
" </server>",
"</mcp_instructions>",
].join("\n"),
)
}).pipe(
Effect.provide(
layer(
() => [instructions("alpha", "Alpha instructions")],
() => [new MCP.Tool({ server: MCP.ServerName.make("alpha"), name: "search", codemode: false })],
),
),
),
)
it.effect("restates guidance when Code Mode is disabled for a server", () => {
let tools = [tool("alpha")]
return Effect.gen(function* () {
const service = yield* McpInstructions.Service
const initialized = yield* service.load(selection()).pipe(Effect.flatMap(readInitial))
tools = [new MCP.Tool({ server: MCP.ServerName.make("alpha"), name: "search", codemode: false })]
const changed = yield* readUpdate(yield* service.load(selection()), initialized)
expect(changed.text).toBe(
[
"The available MCP server instructions have changed. This list supersedes the previous one.",
"<mcp_instructions>",
' <server name="alpha">',
" Alpha instructions",
" </server>",
"</mcp_instructions>",
].join("\n"),
)
}).pipe(
Effect.provide(
layer(
() => [instructions("alpha", "Alpha instructions")],
() => tools,
),
),
)
})
it.effect("renders additions, changes, and removal", () => {
let catalog = [instructions("alpha", "Alpha instructions")]
const tools = [tool("alpha"), tool("beta")]