Skip to content

Commit 68a9077

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 55738fe commit 68a9077

1 file changed

Lines changed: 93 additions & 2 deletions

File tree

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

Lines changed: 93 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
1010
import path from "path"
1111
import { fileURLToPath, pathToFileURL } from "url"
1212
import { NamedError } from "@opencode-ai/core/util/error"
13+
import { PermissionV1 } from "@opencode-ai/core/v1/permission"
14+
import { dynamicTool, jsonSchema } from "ai"
1315
import { Agent as AgentSvc } from "../../src/agent/agent"
1416
import { BackgroundJob } from "@/background/job"
1517
import { Command } from "../../src/command"
@@ -163,7 +165,7 @@ const blockingProcessor = Layer.succeed(
163165
}),
164166
)
165167

166-
function makePrompt(input?: { processor?: "blocking" }) {
168+
function makePrompt(input?: { processor?: "blocking"; mcp?: Layer.Layer<MCP.Service> }) {
167169
const deps = Layer.mergeAll(
168170
Session.defaultLayer,
169171
Snapshot.defaultLayer,
@@ -176,7 +178,7 @@ function makePrompt(input?: { processor?: "blocking" }) {
176178
Config.defaultLayer,
177179
ProviderSvc.defaultLayer,
178180
lsp,
179-
mcp,
181+
input?.mcp ?? mcp,
180182
FSUtil.defaultLayer,
181183
BackgroundJob.defaultLayer,
182184
status,
@@ -2324,3 +2326,92 @@ noLLMServer.instance(
23242326
}),
23252327
30_000,
23262328
)
2329+
2330+
// MCP wrapper permission payload — see packages/opencode/src/session/tools.ts.
2331+
// The wrapper must forward the tool name as the permission key/pattern and the
2332+
// raw tool arguments as metadata so SDK consumers can reason about the call.
2333+
const mcpWithEchoTool = Layer.succeed(
2334+
MCP.Service,
2335+
MCP.Service.of({
2336+
status: () => Effect.succeed({}),
2337+
clients: () => Effect.succeed({}),
2338+
tools: () =>
2339+
Effect.succeed({
2340+
echo_say: dynamicTool({
2341+
description: "echo back the greeting",
2342+
inputSchema: jsonSchema({
2343+
type: "object",
2344+
properties: { greeting: { type: "string" } },
2345+
required: ["greeting"],
2346+
additionalProperties: false,
2347+
}),
2348+
execute: async () => ({ content: [{ type: "text" as const, text: "ok" }] }),
2349+
}),
2350+
}),
2351+
prompts: () => Effect.succeed({}),
2352+
resources: () => Effect.succeed({}),
2353+
add: () => Effect.succeed({ status: { status: "disabled" as const } }),
2354+
connect: () => Effect.void,
2355+
disconnect: () => Effect.void,
2356+
getPrompt: () => Effect.succeed(undefined),
2357+
readResource: () => Effect.succeed(undefined),
2358+
startAuth: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
2359+
authenticate: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
2360+
finishAuth: () => Effect.die("unexpected MCP auth in prompt-effect tests"),
2361+
removeAuth: () => Effect.void,
2362+
supportsOAuth: () => Effect.succeed(false),
2363+
hasStoredTokens: () => Effect.succeed(false),
2364+
getAuthStatus: () => Effect.succeed("not_authenticated" as const),
2365+
}),
2366+
)
2367+
2368+
const mcpIt = testEffect(Layer.mergeAll(TestLLMServer.layer, makePrompt({ mcp: mcpWithEchoTool })))
2369+
2370+
mcpIt.instance("mcp tool ask publishes tool name and args (regression: #19549)", () =>
2371+
Effect.gen(function* () {
2372+
const { llm } = yield* useServerConfig(providerCfg)
2373+
const events = yield* EventV2Bridge.Service
2374+
const seen = yield* Deferred.make<PermissionV1.Request>()
2375+
const unsub = yield* events.listen((event) => {
2376+
if (event.type === Permission.Event.Asked.type)
2377+
Deferred.doneUnsafe(seen, Effect.succeed(event.data as PermissionV1.Request))
2378+
return Effect.void
2379+
})
2380+
yield* Effect.addFinalizer(() => unsub)
2381+
2382+
const prompt = yield* SessionPrompt.Service
2383+
const permission = yield* Permission.Service
2384+
const sessions = yield* Session.Service
2385+
const session = yield* sessions.create({
2386+
title: "MCP wrapper ask",
2387+
permission: [{ permission: "*", pattern: "*", action: "ask" }],
2388+
})
2389+
2390+
yield* prompt.prompt({
2391+
sessionID: session.id,
2392+
agent: "build",
2393+
noReply: true,
2394+
parts: [{ type: "text", text: "say hi" }],
2395+
})
2396+
yield* llm.tool("echo_say", { greeting: "hi" })
2397+
yield* llm.text("done")
2398+
2399+
const fiber = yield* prompt.loop({ sessionID: session.id }).pipe(Effect.forkChild)
2400+
2401+
const request = yield* Deferred.await(seen).pipe(
2402+
Effect.timeoutOrElse({
2403+
duration: Duration.seconds(5),
2404+
orElse: () => Effect.fail(new Error("timed out waiting for permission.asked")),
2405+
}),
2406+
)
2407+
expect(request.permission).toBe("echo_say")
2408+
expect(request.patterns).toEqual(["echo_say"])
2409+
expect(request.always).toEqual(["echo_say"])
2410+
expect(request.metadata).toEqual({ greeting: "hi" })
2411+
2412+
yield* permission.reply({ requestID: request.id, reply: "once" })
2413+
2414+
const exit = yield* Fiber.await(fiber)
2415+
expect(Exit.isSuccess(exit)).toBe(true)
2416+
}),
2417+
)

0 commit comments

Comments
 (0)