mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-31 06:53:31 +00:00
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({
|
||||
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)
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -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")]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue