@@ -10,6 +10,8 @@ import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
1010import path from "path"
1111import { fileURLToPath , pathToFileURL } from "url"
1212import { NamedError } from "@opencode-ai/core/util/error"
13+ import { PermissionV1 } from "@opencode-ai/core/v1/permission"
14+ import { dynamicTool , jsonSchema } from "ai"
1315import { Agent as AgentSvc } from "../../src/agent/agent"
1416import { BackgroundJob } from "@/background/job"
1517import { 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