1- import type {
2- McpHttpServerConfig ,
3- McpServerConfig ,
4- } from "@anthropic-ai/claude-agent-sdk" ;
5- import { Client } from "@modelcontextprotocol/sdk/client/index.js" ;
6- import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js" ;
7- import type { Tool } from "@modelcontextprotocol/sdk/types.js" ;
1+ import type { McpServerStatus , Query } from "@anthropic-ai/claude-agent-sdk" ;
82import { Logger } from "../../../utils/logger.js" ;
93
104export interface McpToolMetadata {
@@ -13,86 +7,74 @@ export interface McpToolMetadata {
137
148const mcpToolMetadataCache : Map < string , McpToolMetadata > = new Map ( ) ;
159
10+ const PENDING_RETRY_INTERVAL_MS = 1_000 ;
11+ const PENDING_MAX_RETRIES = 10 ;
12+
1613function buildToolKey ( serverName : string , toolName : string ) : string {
1714 return `mcp__${ serverName } __${ toolName } ` ;
1815}
1916
20- function isHttpMcpServer (
21- config : McpServerConfig ,
22- ) : config is McpHttpServerConfig {
23- return config . type === "http" && typeof config . url === "string" ;
24- }
25-
26- async function fetchToolsFromHttpServer (
27- _serverName : string ,
28- config : McpHttpServerConfig ,
29- ) : Promise < Tool [ ] > {
30- const transport = new StreamableHTTPClientTransport ( new URL ( config . url ) , {
31- requestInit : {
32- headers : config . headers ?? { } ,
33- } ,
34- } ) ;
35-
36- const client = new Client ( {
37- name : "twig-metadata-fetcher" ,
38- version : "1.0.0" ,
39- } ) ;
40-
41- try {
42- await client . connect ( transport ) ;
43- const result = await client . listTools ( ) ;
44- return result . tools ;
45- } finally {
46- await client . close ( ) . catch ( ( ) => { } ) ;
47- }
48- }
49-
50- function extractToolMetadata ( tool : Tool ) : McpToolMetadata {
51- return {
52- readOnly : tool . annotations ?. readOnlyHint === true ,
53- } ;
17+ function delay ( ms : number ) : Promise < void > {
18+ return new Promise ( ( resolve ) => setTimeout ( resolve , ms ) ) ;
5419}
5520
5621export async function fetchMcpToolMetadata (
57- mcpServers : Record < string , McpServerConfig > ,
22+ q : Query ,
5823 logger : Logger = new Logger ( { debug : false , prefix : "[McpToolMetadata]" } ) ,
5924) : Promise < void > {
60- const fetchPromises : Promise < void > [ ] = [ ] ;
25+ let retries = 0 ;
26+
27+ while ( retries <= PENDING_MAX_RETRIES ) {
28+ let statuses : McpServerStatus [ ] ;
29+ try {
30+ statuses = await q . mcpServerStatus ( ) ;
31+ } catch ( error ) {
32+ logger . error ( "Failed to fetch MCP server status" , {
33+ error : error instanceof Error ? error . message : String ( error ) ,
34+ } ) ;
35+ return ;
36+ }
37+
38+ const pendingServers = statuses . filter ( ( s ) => s . status === "pending" ) ;
39+
40+ for ( const server of statuses ) {
41+ if ( server . status !== "connected" || ! server . tools ) {
42+ continue ;
43+ }
44+
45+ let readOnlyCount = 0 ;
46+ for ( const tool of server . tools ) {
47+ const toolKey = buildToolKey ( server . name , tool . name ) ;
48+ const readOnly = tool . annotations ?. readOnly === true ;
49+ mcpToolMetadataCache . set ( toolKey , { readOnly } ) ;
50+ if ( readOnly ) readOnlyCount ++ ;
51+ }
52+
53+ logger . info ( "Fetched MCP tool metadata" , {
54+ serverName : server . name ,
55+ toolCount : server . tools . length ,
56+ readOnlyCount,
57+ } ) ;
58+ }
6159
62- for ( const [ serverName , config ] of Object . entries ( mcpServers ) ) {
63- if ( ! isHttpMcpServer ( config ) ) {
64- continue ;
60+ if ( pendingServers . length === 0 ) {
61+ return ;
6562 }
6663
67- const fetchPromise = fetchToolsFromHttpServer ( serverName , config )
68- . then ( ( tools ) => {
69- const toolCount = tools . length ;
70- const readOnlyCount = tools . filter (
71- ( t ) => t . annotations ?. readOnlyHint === true ,
72- ) . length ;
73-
74- for ( const tool of tools ) {
75- const toolKey = buildToolKey ( serverName , tool . name ) ;
76- mcpToolMetadataCache . set ( toolKey , extractToolMetadata ( tool ) ) ;
77- }
78-
79- logger . info ( "Fetched MCP tool metadata" , {
80- serverName,
81- toolCount,
82- readOnlyCount,
83- } ) ;
84- } )
85- . catch ( ( error ) => {
86- logger . error ( "Failed to fetch MCP tool metadata" , {
87- serverName,
88- error : error instanceof Error ? error . message : String ( error ) ,
89- } ) ;
64+ retries ++ ;
65+ if ( retries > PENDING_MAX_RETRIES ) {
66+ logger . warn ( "Gave up waiting for pending MCP servers" , {
67+ pendingServers : pendingServers . map ( ( s ) => s . name ) ,
9068 } ) ;
69+ return ;
70+ }
9171
92- fetchPromises . push ( fetchPromise ) ;
72+ logger . info ( "Waiting for pending MCP servers" , {
73+ pendingServers : pendingServers . map ( ( s ) => s . name ) ,
74+ retry : retries ,
75+ } ) ;
76+ await delay ( PENDING_RETRY_INTERVAL_MS ) ;
9377 }
94-
95- await Promise . all ( fetchPromises ) ;
9678}
9779
9880export function isMcpToolReadOnly ( toolName : string ) : boolean {
0 commit comments