fix(core): scope MCP execute guidance to Code Mode (#38753)
This commit is contained in:
parent
d1d97014b4
commit
13b6845e7e
2 changed files with 80 additions and 14 deletions
|
|
@ -11,16 +11,20 @@ import { Instructions } from "../instructions/index"
|
||||||
const Summary = Schema.Struct({
|
const Summary = Schema.Struct({
|
||||||
server: Schema.String,
|
server: Schema.String,
|
||||||
instructions: Schema.String,
|
instructions: Schema.String,
|
||||||
|
codemode: Schema.optionalKey(Schema.Literal(false)),
|
||||||
})
|
})
|
||||||
type Summary = typeof Summary.Type
|
type Summary = typeof Summary.Type
|
||||||
|
|
||||||
const entries = (servers: ReadonlyArray<Summary>) =>
|
const entries = (servers: ReadonlyArray<Summary>) =>
|
||||||
servers.flatMap((server) => [
|
servers.flatMap((server) => {
|
||||||
` <server name="${server.server}">`,
|
const result = [` <server name="${server.server}">`]
|
||||||
` Use tools from this server through \`execute\` under \`tools[${JSON.stringify(McpTool.namespace(server.server))}]\`.`,
|
if (server.codemode !== false)
|
||||||
...server.instructions.split("\n").map((line) => ` ${line}`),
|
result.push(
|
||||||
" </server>",
|
` 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>) =>
|
const render = (servers: ReadonlyArray<Summary>) =>
|
||||||
["<mcp_instructions>", ...entries(servers), "</mcp_instructions>"].join("\n")
|
["<mcp_instructions>", ...entries(servers), "</mcp_instructions>"].join("\n")
|
||||||
|
|
@ -30,7 +34,7 @@ const update = (previous: ReadonlyArray<Summary>, current: ReadonlyArray<Summary
|
||||||
previous,
|
previous,
|
||||||
current,
|
current,
|
||||||
(server) => server.server,
|
(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.
|
// 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))
|
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.",
|
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()], {
|
const [instructions, tools] = yield* Effect.all([mcp.instructions(), mcp.tools()], {
|
||||||
concurrency: "unbounded",
|
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.
|
// Instructions are useful only when this agent can reach at least one server tool.
|
||||||
const visible = instructions
|
const visible = instructions
|
||||||
.filter((item) => {
|
.flatMap((item) => {
|
||||||
const owned = tools.filter((tool) => tool.server === item.server)
|
const owned = tools.filter((tool) => tool.server === item.server)
|
||||||
return owned.some(
|
const codemode = owned[0]?.codemode !== false
|
||||||
(tool) =>
|
if (codemode && !canExecute) return []
|
||||||
PermissionV2.evaluate(McpTool.name(tool.server, tool.name), "*", agent.permissions).effect !== "deny",
|
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))
|
.toSorted((a, b) => a.server.localeCompare(b.server))
|
||||||
return source(visible.length === 0 ? Instructions.removed : visible)
|
return source(visible.length === 0 ? Instructions.removed : visible)
|
||||||
}),
|
}),
|
||||||
|
|
|
||||||
|
|
@ -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", () => {
|
it.effect("renders additions, changes, and removal", () => {
|
||||||
let catalog = [instructions("alpha", "Alpha instructions")]
|
let catalog = [instructions("alpha", "Alpha instructions")]
|
||||||
const tools = [tool("alpha"), tool("beta")]
|
const tools = [tool("alpha"), tool("beta")]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue