Skip to content

Add Enabled function and WithFilter to registry for flexible tool filtering #1618

@SamMorrowDrums

Description

@SamMorrowDrums

Summary

Add two generic mechanisms to the registry package for flexible tool filtering without coupling to specific domain concepts:

  1. Enabled function on ServerTool - Allows tools to self-filter based on request context
  2. WithFilter method on Builder - Allows cross-cutting filters to be applied to all tools

Motivation

Consumers of the registry (like github-mcp-server-remote) need to filter tools based on various conditions:

  • User permissions and policies
  • Request context (e.g., which client is calling)
  • Feature flags with complex logic (AND/OR combinations)
  • Runtime availability of dependencies

Currently, consumers must do this filtering externally before passing tools to the registry, which leads to complex orchestration code. By adding generic filter hooks, consumers can encapsulate their domain-specific logic while the registry remains agnostic.

Implementation

1. Add Enabled field to ServerTool struct in pkg/registry/tool.go:

type ServerTool struct {
    Tool              mcp.Tool
    Toolset           ToolsetMetadata
    HandlerFunc       HandlerFunc
    FeatureFlagEnable string
    FeatureFlagDisable string
    
    // Enabled is an optional function called at build/filter time to determine
    // if this tool should be available. If nil, the tool is considered enabled
    // (subject to FeatureFlagEnable/FeatureFlagDisable checks).
    // The context carries request-scoped information for the consumer to use.
    // Returns (enabled, error). On error, the tool should be treated as disabled.
    Enabled func(ctx context.Context) (bool, error)
}

2. Add ToolFilter type and WithFilter method to Builder in pkg/registry/registry.go:

// ToolFilter is a function that determines if a tool should be included.
// Returns true if the tool should be included, false to exclude it.
type ToolFilter func(ctx context.Context, tool *ServerTool) (bool, error)

// WithFilter adds a filter function that will be applied to all tools.
// Multiple filters can be added and are evaluated in order.
// If any filter returns false or an error, the tool is excluded.
func (b *Builder) WithFilter(filter ToolFilter) *Builder {
    b.filters = append(b.filters, filter)
    return b
}

3. Update isToolEnabled in the builder to check these in order:

  1. Check tool's own Enabled function (if set)
  2. Check FeatureFlagEnable (existing behavior)
  3. Check FeatureFlagDisable (existing behavior)
  4. Apply builder filters (new)

4. Add a context-aware method to get filtered tools:

// FilteredTools returns tools filtered by the Enabled function and builder filters.
// This should be called per-request with the request context.
func (r *Registry) FilteredTools(ctx context.Context) ([]ServerTool, error)

Example Usage

// Tool with self-filtering logic
tool := registry.ServerTool{
    Tool:    myTool,
    Toolset: myToolset,
    HandlerFunc: myHandler,
    Enabled: func(ctx context.Context) (bool, error) {
        // Consumer's domain-specific logic here
        user := mypackage.UserFromContext(ctx)
        return user != nil && user.HasPermission("use_tool"), nil
    },
}

// Builder with cross-cutting filter
reg := registry.NewBuilder().
    SetTools(tools).
    WithFilter(func(ctx context.Context, tool *registry.ServerTool) (bool, error) {
        // Cross-cutting concern like scope filtering
        return !shouldHideForCurrentAuth(ctx, tool), nil
    }).
    Build()

// Get filtered tools for a request
enabledTools, err := reg.FilteredTools(ctx)

Testing

Add tests for:

  • Enabled function returning true/false/error
  • WithFilter with single and multiple filters
  • Interaction between Enabled, feature flags, and filters
  • FilteredTools method with various filter combinations

Backwards Compatibility

  • All new fields are optional with nil/empty defaults
  • Existing code continues to work unchanged
  • FeatureFlagEnable/FeatureFlagDisable remain fully functional

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions