Skip to content

Commit 4a76ed5

Browse files
lu-zeroclaude
andcommitted
test(mcp): assert permission ask payload from tool wrapper
Fixes anomalyco#19549 Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
1 parent b157177 commit 4a76ed5

1 file changed

Lines changed: 110 additions & 2 deletions

File tree

‎packages/opencode/test/session/prompt.test.ts‎

Lines changed: 110 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
1010
import path from "path"
1111
import { fileURLToPath } from "url"
1212
import { NamedError } from "@opencode-ai/core/util/error"
13+
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
14+
import type { Client } from "@modelcontextprotocol/sdk/client/index.js"
15+
import type { Tool } from "@modelcontextprotocol/sdk/types.js"
1316
import { Agent as AgentSvc } from "../../src/agent/agent"
1417
import { BackgroundJob } from "@/background/job"
1518
import { Command } from "../../src/command"
@@ -221,12 +224,16 @@ function makePrompt(input?: { mcpInstructions?: MCP.ServerInstructions[]; proces
221224
return LayerNode.compile(promptRoot, replacements)
222225
}
223226

224-
function makeHttp(input?: { mcpInstructions?: MCP.ServerInstructions[]; processor?: "blocking" }) {
227+
function makeHttp(input?: {
228+
mcpInstructions?: MCP.ServerInstructions[]
229+
processor?: "blocking"
230+
mcp?: Layer.Layer<MCP.Service>
231+
}) {
225232
const root = LayerNode.group([promptRoot, testLLMServerNode])
226233
const replacements = [
227234
[SessionSummary.node, summary],
228235
[LSP.node, lsp],
229-
[MCP.node, makeMcp(input?.mcpInstructions)],
236+
[MCP.node, input?.mcp ?? makeMcp(input?.mcpInstructions)],
230237
[RuntimeFlags.node, runtimeFlags],
231238
] as const
232239
if (input?.processor === "blocking") {
@@ -2468,3 +2475,104 @@ noLLMServer.instance(
24682475
}),
24692476
30_000,
24702477
)
2478+
2479+
// MCP wrapper permission payload — see packages/opencode/src/session/tools.ts.
2480+
// The wrapper must forward the tool name as the permission key/pattern and the
2481+
// raw tool arguments as metadata so SDK consumers can reason about the call.
2482+
const mcpEchoCalls: Array<{ name: string; arguments: Record<string, unknown> | undefined }> = []
2483+
const mcpEchoToolDef = {
2484+
name: "say",
2485+
description: "echo back the greeting",
2486+
inputSchema: {
2487+
type: "object",
2488+
properties: { greeting: { type: "string" } },
2489+
required: ["greeting"],
2490+
additionalProperties: false,
2491+
},
2492+
} satisfies Tool
2493+
const mcpEchoClient = {
2494+
callTool: async (request: { name: string; arguments?: Record<string, unknown> }) => {
2495+
mcpEchoCalls.push({ name: request.name, arguments: request.arguments })
2496+
return { content: [{ type: "text" as const, text: "ok" }] }
2497+
},
2498+
} as unknown as Client
2499+
const mcpWithEchoTool = Layer.succeed(
2500+
MCP.Service,
2501+
MCP.Service.of({
2502+
status: () => Effect.succeed({}),
2503+
clients: () => Effect.succeed({}),
2504+
instructions: () => Effect.succeed([]),
2505+
tools: () =>
2506+
Effect.succeed({
2507+
echo_say: { def: mcpEchoToolDef, client: mcpEchoClient },
2508+
}),
2509+
prompts: () => Effect.succeed({}),
2510+
resources: () => Effect.succeed({}),
2511+
resourceTemplates: () => Effect.succeed({}),
2512+
add: () => Effect.succeed({ status: { status: "disabled" as const } }),
2513+
connect: () => Effect.void,
2514+
disconnect: () => Effect.void,
2515+
getPrompt: () => Effect.succeed(undefined),
2516+
readResource: () => Effect.succeed(undefined),
2517+
startAuth: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
2518+
authenticate: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
2519+
finishAuth: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
2520+
removeAuth: () => Effect.void,
2521+
supportsOAuth: () => Effect.succeed(false),
2522+
hasStoredTokens: () => Effect.succeed(false),
2523+
getAuthStatus: () => Effect.succeed("not_authenticated" as const),
2524+
}),
2525+
)
2526+
2527+
const mcpIt = testEffect(makeHttp({ mcp: mcpWithEchoTool }))
2528+
2529+
mcpIt.instance("mcp tool ask publishes tool name and args (regression: #19549)", () =>
2530+
Effect.gen(function* () {
2531+
mcpEchoCalls.length = 0
2532+
const { llm } = yield* useServerConfig(providerCfg)
2533+
const events = yield* EventV2Bridge.Service
2534+
const seen = yield* Deferred.make<PermissionV1.Request>()
2535+
const unsub = yield* events.listen((event) => {
2536+
if (event.type === Permission.Event.Asked.type)
2537+
Deferred.doneUnsafe(seen, Effect.succeed(event.data as PermissionV1.Request))
2538+
return Effect.void
2539+
})
2540+
yield* Effect.addFinalizer(() => unsub)
2541+
2542+
const prompt = yield* SessionPrompt.Service
2543+
const permission = yield* Permission.Service
2544+
const sessions = yield* Session.Service
2545+
const session = yield* sessions.create({
2546+
title: "MCP wrapper ask",
2547+
permission: [{ permission: "*", pattern: "*", action: "ask" }],
2548+
})
2549+
2550+
yield* prompt.prompt({
2551+
sessionID: session.id,
2552+
agent: "build",
2553+
noReply: true,
2554+
parts: [{ type: "text", text: "say hi" }],
2555+
})
2556+
yield* llm.tool("echo_say", { greeting: "hi" })
2557+
yield* llm.text("done")
2558+
2559+
const fiber = yield* prompt.loop({ sessionID: session.id }).pipe(Effect.forkChild)
2560+
2561+
const request = yield* Deferred.await(seen).pipe(
2562+
Effect.timeoutOrElse({
2563+
duration: Duration.seconds(5),
2564+
orElse: () => Effect.fail(new Error("timed out waiting for permission.asked")),
2565+
}),
2566+
)
2567+
expect(request.permission).toBe("echo_say")
2568+
expect(request.patterns).toEqual(["echo_say"])
2569+
expect(request.always).toEqual(["echo_say"])
2570+
expect(request.metadata).toEqual({ greeting: "hi" })
2571+
2572+
yield* permission.reply({ requestID: request.id, reply: "once" })
2573+
2574+
const exit = yield* Fiber.await(fiber)
2575+
expect(Exit.isSuccess(exit)).toBe(true)
2576+
expect(mcpEchoCalls).toEqual([{ name: "say", arguments: { greeting: "hi" } }])
2577+
}),
2578+
)

0 commit comments

Comments
 (0)