@@ -10,6 +10,9 @@ import { Cause, Deferred, Duration, Effect, Exit, Fiber, Layer } from "effect"
1010import path from "path"
1111import { fileURLToPath } from "url"
1212import { 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"
1316import { Agent as AgentSvc } from "../../src/agent/agent"
1417import { BackgroundJob } from "@/background/job"
1518import { 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