# How to Add Custom Tool Filters for Runtime Tool Registration in the GitHub MCP Server

> Learn to add custom tool filters for runtime tool registration in the GitHub MCP Server by implementing ToolFilter and attaching it to inventory Builder for efficient filtering.

- Repository: [GitHub/github-mcp-server](https://github.com/github/github-mcp-server)
- Tags: how-to-guide
- Published: 2026-02-16

---

**You can add custom tool filters by implementing the `ToolFilter` function signature and attaching it to the `inventory.Builder` using `WithFilter` before calling `Build()`, ensuring that both static and runtime tool registrations respect your filtering logic.**

The GitHub MCP Server manages tool discovery and registration through a centralized inventory system defined in [`pkg/inventory/builder.go`](https://github.com/github/github-mcp-server/blob/main/pkg/inventory/builder.go). Adding custom tool filters for runtime tool registration allows you to dynamically control which tools are exposed based on environment variables, feature flags, or tenant-specific policies.

## Understanding the Tool Filtering Architecture

The inventory system uses a builder pattern to construct an `Inventory` instance that contains the final set of available tools. Filtering occurs during the build phase, not during registration.

A **ToolFilter** is defined as:

```go
type ToolFilter func(context.Context, *ServerTool) (bool, error)

```

In [`pkg/inventory/builder.go`](https://github.com/github/github-mcp-server/blob/main/pkg/inventory/builder.go), the `Builder` struct collects filters in a slice. When you call `Build()`, the method iterates through every discovered tool and applies **all** registered filters. Only tools that receive `true` from every filter are added to the `AvailableTools` slice.

This design means that `RegisterAll` (static registration) and `EnableToolset` (dynamic registration) both automatically respect the filters because they operate on the pre-filtered `AvailableTools` collection.

## Creating a Custom Tool Filter

You can implement custom filtering logic by writing a function that matches the `ToolFilter` signature. Below are three common patterns used in production deployments.

### Name-Prefix Filter

Use this pattern to expose only tools related to specific GitHub domains (e.g., only repository tools).

```go
package main

import (
	"context"
	"strings"

	"github.com/github/github-mcp-server/pkg/inventory"
)

// keepOnlyPrefixed returns a filter that allows only tools with names
// starting with the specified prefix.
func keepOnlyPrefixed(prefix string) inventory.ToolFilter {
	return func(_ context.Context, st *inventory.ServerTool) (bool, error) {
		return strings.HasPrefix(st.Tool.Name, prefix), nil
	}
}

```

### Feature-Flag Filter

This pattern checks tool annotations to determine if a tool should be enabled based on runtime configuration.

```go
package main

import (
	"context"

	"github.com/github/github-mcp-server/pkg/inventory"
)

// makeFeatureFlagFilter creates a filter that keeps tools only if their
// FeatureFlagEnable annotation is empty or present in the enabled map.
func makeFeatureFlagFilter(enabled map[string]bool) inventory.ToolFilter {
	return func(_ context.Context, st *inventory.ServerTool) (bool, error) {
		ann := st.Tool.Annotations
		if ann == nil || ann.FeatureFlagEnable == "" {
			// No flag required, always allow
			return true, nil
		}
		return enabled[ann.FeatureFlagEnable], nil
	}
}

```

### Tenant-Based Filter

For multi-tenant deployments, you might restrict tools based on the specific tenant configuration.

```go
func buildInventoryForTenant(tenantID string, translator *github.Translator) (*inventory.Inventory, error) {
	// Define allowed tools per tenant
	allowedTools := map[string]map[string]bool{
		"tenantA": {"repo_list": true, "issue_create": true},
		"tenantB": {"repo_get": true},
	}[tenantID]

	tenantFilter := func(_ context.Context, st *inventory.ServerTool) (bool, error) {
		_, ok := allowedTools[st.Tool.Name]
		return ok, nil
	}

	builder := github.NewInventory(translator).
		WithFilter(tenantFilter).
		WithReadOnly(false)

	return builder.Build()
}

```

## Registering Filters with the Inventory Builder

Attach your custom filters using the `WithFilter` method before calling `Build()`. In [`internal/ghmcp/server.go`](https://github.com/github/github-mcp-server/blob/main/internal/ghmcp/server.go), the server construction demonstrates this pattern with the built-in scope filter:

```go
inventoryBuilder := github.NewInventory(translator).
    WithReadOnly(cfg.ReadOnly).
    WithToolsets(github.ResolvedEnabledToolsets(...)).
    WithTools(github.CleanTools(...)).
    WithFilter(github.CreateToolScopeFilter(cfg.TokenScopes)) // Custom filter added here

inv, err := inventoryBuilder.Build()
if err != nil {
    return nil, err
}

// Register only the filtered tools
inv.RegisterAll(ctx, mcpServer, deps)

```

The `WithFilter` method in [`pkg/inventory/builder.go`](https://github.com/github/github-mcp-server/blob/main/pkg/inventory/builder.go) appends your function to the internal `filters` slice:

```go
func (b *Builder) WithFilter(filter ToolFilter) *Builder {
    b.filters = append(b.filters, filter)
    return b
}

```

## How Runtime Registration Respects Filters

Dynamic tool registration occurs through the `enable_toolset` tool implemented in [`pkg/github/dynamic_tools.go`](https://github.com/github/github-mcp-server/blob/main/pkg/github/dynamic_tools.go). When a client calls this tool, the server invokes `deps.Inventory.ToolsForToolset(toolsetName)`, which returns tools from the pre-filtered `AvailableTools` collection.

Because filtering happens during `Build()` in [`pkg/inventory/builder.go`](https://github.com/github/github-mcp-server/blob/main/pkg/inventory/builder.go) (lines 71-96), the `ServerTool` instances stored in the inventory have already passed all custom filters. Consequently, when `EnableToolset` iterates through the toolset and calls `st.RegisterFunc(s, deps)` for each tool, it only registers tools that satisfy your custom logic.

This architecture ensures consistency between static registration (`RegisterAll` in [`pkg/inventory/registry.go`](https://github.com/github/github-mcp-server/blob/main/pkg/inventory/registry.go)) and dynamic registration, as both consume the same filtered `AvailableTools` slice.

## Advanced: Post-Build Filtering

If you cannot rebuild the inventory but need to apply additional constraints, you can manually filter during registration by iterating over `AvailableTools` and applying a secondary predicate:

```go
func registerWithExtraFilter(inv *inventory.Inventory, srv *mcp.Server, deps any,
    extra func(*inventory.ServerTool) bool) {

    for _, st := range inv.AvailableTools(context.Background()) {
        if extra(st) {
            st.RegisterFunc(srv, deps)
        }
    }
}

```

Use this approach only when the builder is inaccessible. The recommended pattern is always to add filters via `WithFilter` before `Build()` to ensure that `ToolsForToolset` and other inventory methods return consistent results.

## Summary

- **ToolFilter signature**: Implement `func(context.Context, *inventory.ServerTool) (bool, error)` to define custom logic that determines tool availability.
- **Attach early**: Use `Builder.WithFilter()` in [`pkg/inventory/builder.go`](https://github.com/github/github-mcp-server/blob/main/pkg/inventory/builder.go) before calling `Build()` to ensure filters apply to both static and dynamic registration paths.
- **Build-time execution**: Filters run once during `Inventory.Build()`; only tools passing all filters are stored in `AvailableTools`.
- **Runtime consistency**: Dynamic registration via `enable_toolset` in [`pkg/github/dynamic_tools.go`](https://github.com/github/github-mcp-server/blob/main/pkg/github/dynamic_tools.go) uses `ToolsForToolset()`, which returns pre-filtered tools, ensuring custom constraints are respected at runtime.
- **Reference implementation**: Study [`pkg/github/scope_filter.go`](https://github.com/github/github-mcp-server/blob/main/pkg/github/scope_filter.go) for a production example of filtering based on GitHub token scopes.

## Frequently Asked Questions

### When are custom tool filters executed during the server lifecycle?

Custom tool filters execute once during the `Inventory.Build()` phase, which occurs before the MCP server begins accepting connections. The builder iterates through every discovered tool, applies all registered filters in sequence, and only adds tools to the `AvailableTools` collection if every filter returns `true`. This build-time filtering ensures that both static registration (`RegisterAll`) and dynamic runtime registration (`EnableToolset`) operate on the same pre-validated set of tools.

### Can I apply different filters for different toolsets or tenants without restarting the server?

The standard inventory builder applies filters permanently during construction, but you can achieve per-tenant or per-toolset filtering by rebuilding the inventory for each context. Create a function that instantiates a new `Builder` with context-specific filters (such as the tenant-based example shown above), call `Build()`, and use the resulting inventory for that specific request or connection. While you cannot modify filters on a running inventory instance, the lightweight builder pattern makes reconstruction efficient for multi-tenant deployments.

### How do I debug which tools were filtered out during startup?

To audit filtering decisions, implement a logging wrapper around your custom filter that records the tool name and the boolean result before returning. Because `Inventory.Build()` in [`pkg/inventory/builder.go`](https://github.com/github/github-mcp-server/blob/main/pkg/inventory/builder.go) iterates through tools sequentially, you can also temporarily add debug prints inside the build loop or inspect the `AvailableTools` slice after `Build()` completes to compare against the full tool list. For production debugging, consider exposing a metrics endpoint that counts filtered versus available tools by category.

### Do custom filters affect the `enable_toolset` dynamic registration feature?

Yes, custom filters automatically affect dynamic registration because filtering occurs at inventory build time, not at registration time. When a client invokes the `enable_toolset` tool (implemented in [`pkg/github/dynamic_tools.go`](https://github.com/github/github-mcp-server/blob/main/pkg/github/dynamic_tools.go)), the server calls `ToolsForToolset()` on the inventory, which returns tools from the pre-filtered `AvailableTools` collection. Consequently, if your custom filter excluded a tool during `Build()`, that tool will not appear in any toolset and cannot be registered dynamically, ensuring consistent security and feature-gating across both static and runtime registration paths.