Skip to content

Commit eec6cd5

Browse files
authored
test(core): cover mcp instruction producer (#37303)
1 parent fc37ae4 commit eec6cd5

1 file changed

Lines changed: 150 additions & 0 deletions

File tree

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
import { describe, expect } from "bun:test"
2+
import { Effect, Layer } from "effect"
3+
import { AgentV2 } from "@opencode-ai/core/agent"
4+
import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
5+
import { MCP } from "@opencode-ai/core/mcp/index"
6+
import { McpInstructions } from "@opencode-ai/core/mcp/instructions"
7+
import { PermissionV2 } from "@opencode-ai/core/permission"
8+
import { McpTool } from "@opencode-ai/core/tool/mcp"
9+
import { it } from "./lib/effect"
10+
import { readInitial, readUpdate } from "./lib/instructions"
11+
12+
const build = AgentV2.ID.make("build")
13+
14+
const selection = (permissions: PermissionV2.Ruleset = []) => {
15+
const info = AgentV2.Info.make({ ...AgentV2.Info.empty(build), permissions })
16+
return { id: info.id, info }
17+
}
18+
19+
const instructions = (server: string, text: string) =>
20+
new MCP.ServerInstructions({ server: MCP.ServerName.make(server), instructions: text })
21+
22+
const tool = (server: string, name = "search") => new MCP.Tool({ server: MCP.ServerName.make(server), name })
23+
24+
const layer = (catalog: () => MCP.ServerInstructions[], tools: () => MCP.Tool[]) =>
25+
AppNodeBuilder.build(McpInstructions.node, [
26+
[
27+
MCP.node,
28+
Layer.mock(MCP.Service, {
29+
instructions: () => Effect.succeed(catalog()),
30+
tools: () => Effect.succeed(tools()),
31+
}),
32+
],
33+
])
34+
35+
describe("McpInstructions", () => {
36+
it.effect("renders instructions for servers with at least one permitted tool", () =>
37+
Effect.gen(function* () {
38+
const service = yield* McpInstructions.Service
39+
const generation = yield* service
40+
.load(
41+
selection([
42+
{ action: McpTool.name("alpha", "restricted"), resource: "*", effect: "deny" },
43+
{ action: McpTool.name("hidden", "search"), resource: "*", effect: "deny" },
44+
]),
45+
)
46+
.pipe(Effect.flatMap(readInitial))
47+
48+
expect(generation.text).toBe(
49+
[
50+
"<mcp_instructions>",
51+
' <server name="alpha">',
52+
' Use tools from this server through `execute` under `tools["alpha"]`.',
53+
" Alpha line one",
54+
" Alpha line two",
55+
" </server>",
56+
' <server name="beta">',
57+
' Use tools from this server through `execute` under `tools["beta"]`.',
58+
" Beta instructions",
59+
" </server>",
60+
"</mcp_instructions>",
61+
].join("\n"),
62+
)
63+
}).pipe(
64+
Effect.provide(
65+
layer(
66+
() => [
67+
instructions("beta", "Beta instructions"),
68+
instructions("unused", "No tools"),
69+
instructions("hidden", "Denied tool"),
70+
instructions("alpha", "Alpha line one\nAlpha line two"),
71+
],
72+
() => [tool("alpha"), tool("alpha", "restricted"), tool("beta"), tool("hidden")],
73+
),
74+
),
75+
),
76+
)
77+
78+
it.effect("omits instructions when the agent cannot use execute", () =>
79+
Effect.gen(function* () {
80+
const service = yield* McpInstructions.Service
81+
const generation = yield* service
82+
.load(selection([{ action: "execute", resource: "*", effect: "deny" }]))
83+
.pipe(Effect.flatMap(readInitial))
84+
85+
expect(generation.text).toBe("")
86+
}).pipe(
87+
Effect.provide(
88+
layer(
89+
() => [instructions("alpha", "Alpha instructions")],
90+
() => [tool("alpha")],
91+
),
92+
),
93+
),
94+
)
95+
96+
it.effect("renders additions, changes, and removal", () => {
97+
let catalog = [instructions("alpha", "Alpha instructions")]
98+
const tools = [tool("alpha"), tool("beta")]
99+
return Effect.gen(function* () {
100+
const service = yield* McpInstructions.Service
101+
const initialized = yield* service.load(selection()).pipe(Effect.flatMap(readInitial))
102+
103+
catalog = [instructions("alpha", "Alpha instructions"), instructions("beta", "Beta instructions")]
104+
const added = yield* readUpdate(yield* service.load(selection()), initialized)
105+
expect(added.text).toBe(
106+
[
107+
"New MCP server instructions are available in addition to those previously listed:",
108+
' <server name="beta">',
109+
' Use tools from this server through `execute` under `tools["beta"]`.',
110+
" Beta instructions",
111+
" </server>",
112+
].join("\n"),
113+
)
114+
115+
catalog = [instructions("alpha", "Updated alpha"), instructions("beta", "Beta instructions")]
116+
const changed = yield* readUpdate(yield* service.load(selection()), added)
117+
expect(changed.text).toBe(
118+
[
119+
"The available MCP server instructions have changed. This list supersedes the previous one.",
120+
"<mcp_instructions>",
121+
' <server name="alpha">',
122+
' Use tools from this server through `execute` under `tools["alpha"]`.',
123+
" Updated alpha",
124+
" </server>",
125+
' <server name="beta">',
126+
' Use tools from this server through `execute` under `tools["beta"]`.',
127+
" Beta instructions",
128+
" </server>",
129+
"</mcp_instructions>",
130+
].join("\n"),
131+
)
132+
133+
catalog = [instructions("beta", "Beta instructions")]
134+
const removed = yield* readUpdate(yield* service.load(selection()), changed)
135+
expect(removed.text).toBe("Instructions for the following MCP servers are no longer available: alpha.")
136+
137+
catalog = []
138+
expect((yield* readUpdate(yield* service.load(selection()), removed)).text).toBe(
139+
"MCP server instructions are no longer available.",
140+
)
141+
}).pipe(
142+
Effect.provide(
143+
layer(
144+
() => catalog,
145+
() => tools,
146+
),
147+
),
148+
)
149+
})
150+
})

0 commit comments

Comments
 (0)