@@ -339,7 +339,8 @@ func (n *SamNode) handleFindRemoteTools(ctx context.Context, req *mcp.CallToolRe
339339 if params .ToolName != "" && params .PeerID == "" && n .Discovery != nil {
340340 n .Discovery .Ensure (api .ServiceType_SERVICE_TYPE_MCP , params .ToolName )
341341 if provs := n .Discovery .Providers (api .ServiceType_SERVICE_TYPE_MCP , params .ToolName ); len (provs ) > 0 {
342- rows = gossipToolRows (provs , params .ToolName , params .ServiceName )
342+ candidates := gossipToolRows (provs , params .ToolName , params .ServiceName )
343+ rows = n .verifyGossipToolRows (ctx , candidates )
343344 if len (rows ) > 0 {
344345 return marshalToolRows (rows )
345346 }
@@ -419,6 +420,98 @@ func gossipToolRows(provs []samdiscovery.Provider, toolName, serviceNameFilter s
419420 return rows
420421}
421422
423+ // verifyGossipToolRows treats unsolicited announcements as routing hints, not
424+ // authorization evidence. Each candidate is confirmed through the existing
425+ // authenticated MCP session and exact tools/list response before it is exposed.
426+ func (n * SamNode ) verifyGossipToolRows (ctx context.Context , candidates []remoteToolRow ) []remoteToolRow {
427+ return verifyGossipToolRowsWithFetcher (ctx , candidates , n .fetchRemoteToolDescription )
428+ }
429+
430+ const (
431+ gossipVerificationMaxConcurrent = 8
432+ gossipVerificationTimeout = 5 * time .Second
433+ )
434+
435+ func verifyGossipToolRowsWithFetcher (
436+ ctx context.Context ,
437+ candidates []remoteToolRow ,
438+ fetchDescription func (context.Context , peer.ID , string ) (* remoteToolDescription , error ),
439+ ) []remoteToolRow {
440+ if len (candidates ) == 0 {
441+ return nil
442+ }
443+
444+ type verificationResult struct {
445+ row remoteToolRow
446+ ok bool
447+ }
448+ results := make ([]verificationResult , len (candidates ))
449+ jobs := make (chan int )
450+ workerCount := len (candidates )
451+ if workerCount > gossipVerificationMaxConcurrent {
452+ workerCount = gossipVerificationMaxConcurrent
453+ }
454+
455+ var wg sync.WaitGroup
456+ for range workerCount {
457+ wg .Add (1 )
458+ go func () {
459+ defer wg .Done ()
460+ for {
461+ select {
462+ case <- ctx .Done ():
463+ return
464+ case index , ok := <- jobs :
465+ if ! ok {
466+ return
467+ }
468+ candidate := candidates [index ]
469+ pid , err := peer .Decode (candidate .PeerID )
470+ if err != nil {
471+ logger .Debugf ("[find_remote_tools] invalid gossip peer %q skipped: %v" , candidate .PeerID , err )
472+ continue
473+ }
474+ requestCtx , cancel := context .WithTimeout (ctx , gossipVerificationTimeout )
475+ description , err := fetchDescription (requestCtx , pid , candidate .ToolName )
476+ cancel ()
477+ if err != nil {
478+ logger .Debugf ("[find_remote_tools] unverified gossip candidate %s on %s skipped: %v" , candidate .ToolName , pid , err )
479+ continue
480+ }
481+ results [index ] = verificationResult {
482+ ok : true ,
483+ row : remoteToolRow {
484+ PeerID : description .PeerID ,
485+ ToolName : description .ToolName ,
486+ Description : description .Description ,
487+ Labels : candidate .Labels ,
488+ },
489+ }
490+ }
491+ }
492+ }()
493+ }
494+
495+ sendCandidates:
496+ for index := range candidates {
497+ select {
498+ case jobs <- index :
499+ case <- ctx .Done ():
500+ break sendCandidates
501+ }
502+ }
503+ close (jobs )
504+ wg .Wait ()
505+
506+ rows := make ([]remoteToolRow , 0 , len (results ))
507+ for _ , result := range results {
508+ if result .ok {
509+ rows = append (rows , result .row )
510+ }
511+ }
512+ return rows
513+ }
514+
422515// filterRowsByToolName keeps rows whose namespaced tool name ends in the
423516// bare tool name; error rows (no tool listing) are dropped from targeted
424517// lookups.
@@ -592,66 +685,73 @@ type DescribeRemoteToolParams struct {
592685 ToolName string `json:"tool_name" jsonschema:"Namespaced server name as returned by find_remote_tools (e.g. 'mcp://code-reviewer/review_pr'). Required."`
593686}
594687
595- // handleDescribeRemoteTool implements the describe_remote_tool client-facing tool.
596- func (n * SamNode ) handleDescribeRemoteTool (ctx context.Context , req * mcp.CallToolRequest , params DescribeRemoteToolParams ) (* mcp.CallToolResult , any , error ) {
597- if params .PeerID == "" {
598- return nil , nil , fmt .Errorf ("peer_id is required" )
599- }
600- if params .ToolName == "" {
601- return nil , nil , fmt .Errorf ("tool_name is required" )
602- }
603-
604- pid , err := peer .Decode (params .PeerID )
688+ func (n * SamNode ) fetchRemoteToolDescription (ctx context.Context , pid peer.ID , toolName string ) (* remoteToolDescription , error ) {
689+ serviceName , actualToolName , err := api .SplitToolName (toolName )
605690 if err != nil {
606- return nil , nil , fmt .Errorf ("invalid peer_id: %w" , err )
607- }
608-
609- serviceName , actualToolName , err := api .SplitToolName (params .ToolName )
610- if err != nil {
611- return nil , nil , err
691+ return nil , err
612692 }
613693 if serviceName == "system://" + api .CatalogTarget {
614- return nil , nil , fmt .Errorf ("cannot describe system catalog tools via describe_remote_tool" )
694+ return nil , fmt .Errorf ("cannot describe system catalog tools via describe_remote_tool" )
615695 }
616696 n .preparePeerAddrs (ctx , pid )
617697
618698 session , cleanup , err := n .ConnectMCPSession (ctx , pid , serviceName , nil )
619699 if err != nil {
620- return nil , nil , err
700+ return nil , err
621701 }
622702 defer cleanup ()
623703
624704 listRes , err := session .ListTools (ctx , nil )
625705 if err != nil {
626- return nil , nil , err
706+ return nil , err
627707 }
628708 if listRes == nil {
629- return nil , nil , fmt .Errorf ("list tools response was nil" )
709+ return nil , fmt .Errorf ("list tools response was nil" )
630710 }
631711
632- for _ , t := range listRes .Tools {
633- if t == nil {
712+ for _ , tool := range listRes .Tools {
713+ if tool == nil {
634714 continue
635715 }
636- if t .Name == actualToolName {
637- payload := remoteToolDescription {
716+ if tool .Name == actualToolName {
717+ return & remoteToolDescription {
638718 PeerID : pid .String (),
639- ToolName : params .ToolName ,
640- Description : t .Description ,
641- InputSchema : t .InputSchema ,
642- OutputSchema : t .OutputSchema ,
643- }
644- data , err := json .Marshal (payload )
645- if err != nil {
646- return nil , nil , err
647- }
648- return & mcp.CallToolResult {
649- Content : []mcp.Content {& mcp.TextContent {Text : string (data )}},
650- }, nil , nil
719+ ToolName : toolName ,
720+ Description : tool .Description ,
721+ InputSchema : tool .InputSchema ,
722+ OutputSchema : tool .OutputSchema ,
723+ }, nil
651724 }
652725 }
653726
654- return nil , nil , fmt .Errorf ("tool not found on peer" )
727+ return nil , fmt .Errorf ("tool not found on peer" )
728+ }
729+
730+ // handleDescribeRemoteTool implements the describe_remote_tool client-facing tool.
731+ func (n * SamNode ) handleDescribeRemoteTool (ctx context.Context , req * mcp.CallToolRequest , params DescribeRemoteToolParams ) (* mcp.CallToolResult , any , error ) {
732+ if params .PeerID == "" {
733+ return nil , nil , fmt .Errorf ("peer_id is required" )
734+ }
735+ if params .ToolName == "" {
736+ return nil , nil , fmt .Errorf ("tool_name is required" )
737+ }
738+
739+ pid , err := peer .Decode (params .PeerID )
740+ if err != nil {
741+ return nil , nil , fmt .Errorf ("invalid peer_id: %w" , err )
742+ }
743+
744+ payload , err := n .fetchRemoteToolDescription (ctx , pid , params .ToolName )
745+ if err != nil {
746+ return nil , nil , err
747+ }
748+ data , err := json .Marshal (payload )
749+ if err != nil {
750+ return nil , nil , err
751+ }
752+ return & mcp.CallToolResult {
753+ Content : []mcp.Content {& mcp.TextContent {Text : string (data )}},
754+ }, nil , nil
655755}
656756
657757// CheckConnectivityParams defines the parameters for the check_connectivity tool.
0 commit comments