@@ -10,6 +10,7 @@ import (
1010 "regexp"
1111 "strconv"
1212 "strings"
13+ "sync"
1314
1415 "github.com/altinity/altinity-mcp/pkg/clickhouse"
1516 "github.com/altinity/altinity-mcp/pkg/config"
@@ -25,7 +26,9 @@ type ClickHouseJWEServer struct {
2526 Config config.Config
2627 Version string
2728 // dynamic tools metadata for OpenAPI routing and schema
28- dynamicTools map [string ]dynamicToolMeta
29+ dynamicTools map [string ]dynamicToolMeta
30+ dynamicToolsMu sync.RWMutex
31+ dynamicToolsInit bool
2932}
3033
3134type dynamicToolParam struct {
@@ -75,8 +78,7 @@ func NewClickHouseMCPServer(cfg config.Config, version string) *ClickHouseJWESer
7578
7679 // Register tools, resources, and prompts
7780 RegisterTools (chJweServer )
78- // dynamic tools registered after static ones
79- registerDynamicTools (chJweServer )
81+ // dynamic tools registered lazily via EnsureDynamicTools
8082 RegisterResources (chJweServer )
8183 RegisterPrompts (chJweServer )
8284
@@ -414,19 +416,28 @@ func RegisterPrompts(srv AltinityMCPServer) {
414416 log .Info ().Int ("prompt_count" , 0 ).Msg ("ClickHouse prompts registered" )
415417}
416418
417- // registerDynamicTools discovers ClickHouse views and registers MCP/OpenAPI tools
418- func registerDynamicTools (s * ClickHouseJWEServer ) {
419+ // EnsureDynamicTools discovers ClickHouse views and registers MCP/OpenAPI tools
420+ func (s * ClickHouseJWEServer ) EnsureDynamicTools (ctx context.Context ) error {
421+ s .dynamicToolsMu .Lock ()
422+ defer s .dynamicToolsMu .Unlock ()
423+
424+ if s .dynamicToolsInit {
425+ return nil
426+ }
427+
419428 if len (s .Config .Server .DynamicTools ) == 0 {
420- return
429+ s .dynamicToolsInit = true
430+ return nil
421431 }
422432
423- ctx := context . WithValue ( context . Background (), "clickhouse_jwe_server" , s )
424- token := ""
433+ // Check if we have a valid client/token to proceed
434+ token := s . ExtractTokenFromCtx ( ctx )
425435 // Get ClickHouse client
426436 chClient , err := s .GetClickHouseClient (ctx , token )
427437 if err != nil {
428- log .Error ().Err (err ).Msg ("dynamic_tools: failed to get ClickHouse client" )
429- return
438+ // If we can't get a client (e.g. missing token when JWE enabled), we can't register dynamic tools yet
439+ // Return error so we retry later
440+ return fmt .Errorf ("dynamic_tools: failed to get ClickHouse client: %w" , err )
430441 }
431442 defer func () {
432443 if closeErr := chClient .Close (); closeErr != nil {
@@ -438,8 +449,7 @@ func registerDynamicTools(s *ClickHouseJWEServer) {
438449 q := "SELECT database, name, create_table_query, comment FROM system.tables WHERE engine='View'"
439450 result , err := chClient .ExecuteQuery (ctx , q )
440451 if err != nil {
441- log .Error ().Err (err ).Str ("query" , q ).Msg ("dynamic_tools: failed to list views" )
442- return
452+ return fmt .Errorf ("dynamic_tools: failed to list views: %w" , err )
443453 }
444454
445455 // compile regex rules
@@ -556,6 +566,9 @@ func registerDynamicTools(s *ClickHouseJWEServer) {
556566 log .Error ().Msg ("dynamic_tools: overlaps detected; conflicting views were skipped as per policy 'error on overlap'" )
557567 }
558568 log .Info ().Int ("tool_count" , dynamicCount ).Msg ("Dynamic ClickHouse view tools registered" )
569+
570+ s .dynamicToolsInit = true
571+ return nil
559572}
560573
561574func makeDynamicToolHandler (meta dynamicToolMeta ) server.ToolHandlerFunc {
@@ -830,11 +843,21 @@ func (s *ClickHouseJWEServer) OpenAPIHandler(w http.ResponseWriter, r *http.Requ
830843 case strings .HasSuffix (r .URL .Path , "/openapi/execute_query" ):
831844 s .handleExecuteQueryOpenAPI (w , r , token )
832845 case strings .Contains (r .URL .Path , "/openapi/" ) && r .Method == http .MethodPost :
846+ // Ensure dynamic tools are loaded
847+ if err := s .EnsureDynamicTools (r .Context ()); err != nil {
848+ log .Warn ().Err (err ).Msg ("Failed to ensure dynamic tools in OpenAPI handler" )
849+ }
850+
833851 // dynamic tool endpoint: /openapi/{tool}
834852 parts := strings .Split (r .URL .Path , "/openapi/" )
835853 if len (parts ) == 2 {
836854 tool := strings .Trim (parts [1 ], "/" )
837- if meta , ok := s .dynamicTools [tool ]; ok {
855+
856+ s .dynamicToolsMu .RLock ()
857+ meta , ok := s .dynamicTools [tool ]
858+ s .dynamicToolsMu .RUnlock ()
859+
860+ if ok {
838861 s .handleDynamicToolOpenAPI (w , r , token , meta )
839862 return
840863 }
@@ -847,6 +870,11 @@ func (s *ClickHouseJWEServer) OpenAPIHandler(w http.ResponseWriter, r *http.Requ
847870}
848871
849872func (s * ClickHouseJWEServer ) ServeOpenAPISchema (w http.ResponseWriter , r * http.Request ) {
873+ // Ensure dynamic tools are loaded
874+ if err := s .EnsureDynamicTools (r .Context ()); err != nil {
875+ log .Warn ().Err (err ).Msg ("Failed to ensure dynamic tools in ServeOpenAPISchema" )
876+ }
877+
850878 // Get host URL based on OpenAPI TLS configuration
851879 protocol := "http"
852880 if s .Config .Server .OpenAPI .TLS {
@@ -936,6 +964,10 @@ func (s *ClickHouseJWEServer) ServeOpenAPISchema(w http.ResponseWriter, r *http.
936964
937965 // add dynamic tool paths (POST)
938966 paths := schema ["paths" ].(map [string ]interface {})
967+
968+ s .dynamicToolsMu .RLock ()
969+ defer s .dynamicToolsMu .RUnlock ()
970+
939971 for toolName , meta := range s .dynamicTools {
940972 path := "/{jwe_token}/openapi/" + toolName
941973 // request body schema
0 commit comments