@@ -366,6 +366,204 @@ func TestDirectModeHandler_DestructiveToolNeedsDestructivePermission(t *testing.
366366 assert .Contains (t , result .Content [0 ].(mcp.TextContent ).Text , "destructive" )
367367}
368368
369+ func TestRequiredPermissionForDirectTool_MapsAnnotationsToAuthPermissions (t * testing.T ) {
370+ readOnly := true
371+ write := false
372+ destructive := true
373+
374+ tests := []struct {
375+ name string
376+ annotations * config.ToolAnnotations
377+ want string
378+ }{
379+ {
380+ name : "nil annotations default to read" ,
381+ want : auth .PermRead ,
382+ },
383+ {
384+ name : "read only hint maps to read" ,
385+ annotations : & config.ToolAnnotations {
386+ ReadOnlyHint : & readOnly ,
387+ },
388+ want : auth .PermRead ,
389+ },
390+ {
391+ name : "read only false maps to write" ,
392+ annotations : & config.ToolAnnotations {
393+ ReadOnlyHint : & write ,
394+ },
395+ want : auth .PermWrite ,
396+ },
397+ {
398+ name : "destructive hint maps to destructive" ,
399+ annotations : & config.ToolAnnotations {
400+ DestructiveHint : & destructive ,
401+ },
402+ want : auth .PermDestructive ,
403+ },
404+ {
405+ name : "destructive hint takes precedence over read only hint" ,
406+ annotations : & config.ToolAnnotations {
407+ ReadOnlyHint : & readOnly ,
408+ DestructiveHint : & destructive ,
409+ },
410+ want : auth .PermDestructive ,
411+ },
412+ }
413+
414+ for _ , tt := range tests {
415+ t .Run (tt .name , func (t * testing.T ) {
416+ assert .Equal (t , tt .want , requiredPermissionForDirectTool (tt .annotations ))
417+ })
418+ }
419+ }
420+
421+ func TestSetDirectToolPermissions_DefensivelyCopiesMap (t * testing.T ) {
422+ proxy := & MCPProxyServer {}
423+ toolName := FormatDirectToolName ("github" , "get_issue" )
424+ perms := map [string ]string {
425+ toolName : auth .PermRead ,
426+ }
427+
428+ proxy .setDirectToolPermissions (perms )
429+ perms [toolName ] = auth .PermDestructive
430+
431+ got , ok := proxy .lookupDirectToolPermission (toolName )
432+ require .True (t , ok )
433+ assert .Equal (t , auth .PermRead , got )
434+ }
435+
436+ func TestFilterDirectModeToolsForAuth_DoesNotMutateInputSlice (t * testing.T ) {
437+ proxy := & MCPProxyServer {}
438+ allowed := FormatDirectToolName ("github" , "get_issue" )
439+ denied := FormatDirectToolName ("gitlab" , "get_issue" )
440+ tools := []mcp.Tool {
441+ {Name : allowed },
442+ {Name : denied },
443+ }
444+ original := append ([]mcp.Tool (nil ), tools ... )
445+
446+ proxy .setDirectToolPermissions (map [string ]string {
447+ allowed : auth .PermRead ,
448+ denied : auth .PermRead ,
449+ })
450+
451+ ctx := auth .WithAuthContext (context .Background (), & auth.AuthContext {
452+ Type : auth .AuthTypeAgent ,
453+ AgentName : "test-agent" ,
454+ AllowedServers : []string {"github" },
455+ Permissions : []string {auth .PermRead },
456+ })
457+
458+ filtered := proxy .filterDirectModeToolsForAuth (ctx , tools )
459+
460+ assert .Equal (t , []string {allowed }, directToolNamesForTest (filtered ))
461+ assert .Equal (t , original , tools )
462+ }
463+
464+ func TestFilterDirectModeToolsForAuth_AgentServerAndPermissionScope (t * testing.T ) {
465+ proxy := & MCPProxyServer {}
466+
467+ githubRead := FormatDirectToolName ("github" , "get_issue" )
468+ githubWrite := FormatDirectToolName ("github" , "create_issue" )
469+ githubDestroy := FormatDirectToolName ("github" , "delete_repo" )
470+ gitlabRead := FormatDirectToolName ("gitlab" , "get_issue" )
471+
472+ proxy .setDirectToolPermissions (map [string ]string {
473+ githubRead : auth .PermRead ,
474+ githubWrite : auth .PermWrite ,
475+ githubDestroy : auth .PermDestructive ,
476+ gitlabRead : auth .PermRead ,
477+ })
478+
479+ tools := []mcp.Tool {
480+ {Name : githubRead },
481+ {Name : githubWrite },
482+ {Name : githubDestroy },
483+ {Name : gitlabRead },
484+ }
485+
486+ agentCtx := auth .WithAuthContext (context .Background (), & auth.AuthContext {
487+ Type : auth .AuthTypeAgent ,
488+ AgentName : "test-agent" ,
489+ AllowedServers : []string {"github" },
490+ Permissions : []string {auth .PermRead , auth .PermWrite },
491+ })
492+
493+ filtered := proxy .filterDirectModeToolsForAuth (agentCtx , tools )
494+
495+ assert .Equal (t , []string {githubRead , githubWrite }, directToolNamesForTest (filtered ))
496+ }
497+
498+ func TestFilterDirectModeToolsForAuth_NonAgentUnchanged (t * testing.T ) {
499+ proxy := & MCPProxyServer {}
500+ tools := []mcp.Tool {
501+ {Name : FormatDirectToolName ("github" , "get_issue" )},
502+ {Name : FormatDirectToolName ("gitlab" , "get_issue" )},
503+ }
504+
505+ assert .Equal (t , tools , proxy .filterDirectModeToolsForAuth (context .Background (), tools ))
506+
507+ adminCtx := auth .WithAuthContext (context .Background (), auth .AdminContext ())
508+ assert .Equal (t , tools , proxy .filterDirectModeToolsForAuth (adminCtx , tools ))
509+ }
510+
511+ func TestFilterDirectModeToolsForAuth_FailsClosedOnMissingPermissionMetadata (t * testing.T ) {
512+ proxy := & MCPProxyServer {}
513+
514+ visible := FormatDirectToolName ("github" , "get_issue" )
515+ missing := FormatDirectToolName ("github" , "unknown" )
516+ proxy .setDirectToolPermissions (map [string ]string {
517+ visible : auth .PermRead ,
518+ })
519+
520+ ctx := auth .WithAuthContext (context .Background (), & auth.AuthContext {
521+ Type : auth .AuthTypeAgent ,
522+ AgentName : "test-agent" ,
523+ AllowedServers : []string {"github" },
524+ Permissions : []string {auth .PermRead },
525+ })
526+
527+ filtered := proxy .filterDirectModeToolsForAuth (ctx , []mcp.Tool {
528+ {Name : visible },
529+ {Name : missing },
530+ })
531+
532+ assert .Equal (t , []string {visible }, directToolNamesForTest (filtered ))
533+ }
534+
535+ func TestFilterDirectModeToolsForAuth_KeepsNonDirectTools (t * testing.T ) {
536+ proxy := & MCPProxyServer {}
537+
538+ direct := FormatDirectToolName ("github" , "get_issue" )
539+ nonDirect := "retrieve_tools"
540+ proxy .setDirectToolPermissions (map [string ]string {
541+ direct : auth .PermRead ,
542+ })
543+
544+ ctx := auth .WithAuthContext (context .Background (), & auth.AuthContext {
545+ Type : auth .AuthTypeAgent ,
546+ AgentName : "test-agent" ,
547+ AllowedServers : []string {"github" },
548+ Permissions : []string {auth .PermRead },
549+ })
550+
551+ filtered := proxy .filterDirectModeToolsForAuth (ctx , []mcp.Tool {
552+ {Name : direct },
553+ {Name : nonDirect },
554+ })
555+
556+ assert .Equal (t , []string {direct , nonDirect }, directToolNamesForTest (filtered ))
557+ }
558+
559+ func directToolNamesForTest (tools []mcp.Tool ) []string {
560+ names := make ([]string , 0 , len (tools ))
561+ for _ , tool := range tools {
562+ names = append (names , tool .Name )
563+ }
564+ return names
565+ }
566+
369567func TestDirectModeHandler_NoAuthContext (t * testing.T ) {
370568 logger , _ := zap .NewDevelopment ()
371569 proxy := & MCPProxyServer {
0 commit comments