diff --git a/server/server.go b/server/server.go index 46e6d9c57..1ad16788f 100644 --- a/server/server.go +++ b/server/server.go @@ -46,6 +46,14 @@ type ToolHandlerMiddleware func(ToolHandlerFunc) ToolHandlerFunc // ToolFilterFunc is a function that filters tools based on context, typically using session information. type ToolFilterFunc func(ctx context.Context, tools []mcp.Tool) []mcp.Tool +// dynamicTools holds configuration for dynamic tool generation +type dynamicTools struct { + enabled bool + listFunc func(ctx context.Context, request mcp.ListToolsRequest) ([]mcp.Tool, error) + handlerFunc func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error) + validateFunc func(ctx context.Context, toolName string) bool +} + // ServerTool combines a Tool with its ToolHandlerFunc. type ServerTool struct { Tool mcp.Tool @@ -155,6 +163,7 @@ type MCPServer struct { tools map[string]ServerTool toolHandlerMiddlewares []ToolHandlerMiddleware toolFilters []ToolFilterFunc + dynamicTools *dynamicTools notificationHandlers map[string]NotificationHandlerFunc capabilities serverCapabilities paginationLimit *int @@ -288,6 +297,32 @@ func WithInstructions(instructions string) ServerOption { } } +func WithDynamicTools( + enabled bool, + listFunc func(ctx context.Context, request mcp.ListToolsRequest) ([]mcp.Tool, error), + handlerFunc func(ctx context.Context, request mcp.CallToolRequest) (*mcp.CallToolResult, error), + validateFunc func(ctx context.Context, toolName string) bool, +) ServerOption { + return func(s *MCPServer) { + // If enabled, all functions must be non-nil + if enabled && (listFunc == nil || handlerFunc == nil || validateFunc == nil) { + panic("WithDynamicTools: when enabled=true, all functions (listFunc, handlerFunc, validateFunc) must be non-nil") + } + + s.dynamicTools = &dynamicTools{ + enabled: enabled, + listFunc: listFunc, + handlerFunc: handlerFunc, + validateFunc: validateFunc, + } + + // Ensure capabilities.tool is not nil so that clients know tools exist + if enabled { + s.implicitlyRegisterToolCapabilities() + } + } +} + // NewMCPServer creates a new MCP server instance with the given name, version and options func NewMCPServer( name, version string, @@ -944,6 +979,44 @@ func (s *MCPServer) handleListTools( } } + // Add dynamic tools if enabled + if s.dynamicTools != nil && s.dynamicTools.enabled { + dynamicTools, err := s.dynamicTools.listFunc(ctx, request) + if err != nil { + return nil, &requestError{ + id: id, + code: mcp.INTERNAL_ERROR, + err: fmt.Errorf("dynamic tools list function failed: %w", err), + } + } + + if len(dynamicTools) > 0 { + // Create a map to merge tools properly + toolMap := make(map[string]mcp.Tool) + + // Add dynamic tools first (lowest priority) + for _, tool := range dynamicTools { + toolMap[tool.Name] = tool + } + + // Then add existing tools (they override dynamic tools) + for _, tool := range tools { + toolMap[tool.Name] = tool + } + + // Convert back to slice + tools = make([]mcp.Tool, 0, len(toolMap)) + for _, tool := range toolMap { + tools = append(tools, tool) + } + + // Sort again to maintain consistent ordering + sort.Slice(tools, func(i, j int) bool { + return tools[i].Name < tools[j].Name + }) + } + } + // Apply tool filters if any are defined s.toolFiltersMu.RLock() if len(s.toolFilters) > 0 { @@ -1006,6 +1079,18 @@ func (s *MCPServer) handleToolCall( s.toolsMu.RUnlock() } + // If still not found, try dynamic tool handler + if !ok && s.dynamicTools != nil && s.dynamicTools.enabled { + // Check if this tool is valid using the validation function + if s.dynamicTools.validateFunc(ctx, request.Params.Name) { + // Create a ServerTool with the dynamic handler + tool = ServerTool{ + Handler: s.dynamicTools.handlerFunc, + } + ok = true + } + } + if !ok { return nil, &requestError{ id: id,