Skip to content

Config-driven tool visibility filtering to reduce LLM token usage #90

@cjimti

Description

@cjimti

Problem

The MCP server announces 25-32 tools via tools/list, consuming significant tokens in the LLM client context. Deployments that only need a subset of registered tools (e.g., only Trino tools) pay the full token cost for all tool descriptions on every request.

Proposed Solution

Add a config-driven tools: allow/deny filter implemented as MCP middleware that filters tools/list responses. This is a visibility filter (reduces token usage), not a security boundary — persona-based auth continues to gate tools/call.

Config Syntax

New top-level tools: section using allow/deny glob patterns (consistent with existing persona ToolRulesDef syntax):

tools:
  allow:
    - "trino_*"
    - "datahub_search"
    - "datahub_get_entity"
    - "platform_info"
  deny:
    - "*_delete_*"

Semantics:

  • No tools: section = all tools visible (backward compatible)
  • allow set = only matching tools pass
  • deny set = matching tools excluded
  • Both set = allow first, then deny removes from that set
  • Empty allow: [] with deny = all tools pass, then deny removes matches

Implementation Approach

Middleware

New file pkg/middleware/mcp_visibility.go with three functions:

  • MCPToolVisibilityMiddleware(allow, deny []string) — middleware factory, takes slices directly (decoupled from config types)
  • filterToolVisibility(allow, deny []string, method string, result mcp.Result) — extracted for unit testability
  • isToolVisible(name string, allow, deny []string) bool — core filter using filepath.Match (same as persona/filter.go)

Follows the same pattern as mcpapps.ToolMetadataMiddleware — intercept tools/list response, type-assert to *mcp.ListToolsResult, filter listResult.Tools.

Wiring

Added as the very last AddReceivingMiddleware call in finalizeSetup() (after mcpapps), making it the absolute outermost middleware. Only registered when patterns are configured:

if len(p.config.Tools.Allow) > 0 || len(p.config.Tools.Deny) > 0 {
    p.mcpServer.AddReceivingMiddleware(
        middleware.MCPToolVisibilityMiddleware(p.config.Tools.Allow, p.config.Tools.Deny),
    )
}

Updated execution order:

Tool visibility → Apps metadata → Auth/Authz → Audit → Rules → Enrichment → handler

Design Decisions

  1. Middleware over registration-time filtering — toolkits call server.AddTool() directly and the MCP SDK has no RemoveTool. Response-time filtering is cleaner and handles dynamically registered tools.
  2. No tools/call blocking — visibility only filters tools/list. Persona auth already gates tools/call. Goal is token reduction, not security.
  3. Conditional registration — middleware only added when patterns are configured, avoiding overhead for the common case.
  4. filepath.Match error handling — invalid glob patterns silently treated as non-matches (consistent with persona filter).

Files to Create/Modify

File Action Description
pkg/platform/config.go Modify Add ToolsConfig struct and Tools field to Config
pkg/middleware/mcp_visibility.go Create Middleware with MCPToolVisibilityMiddleware, filterToolVisibility, isToolVisible
pkg/middleware/mcp_visibility_test.go Create Table-driven unit tests for filter logic
pkg/middleware/middleware_chain_test.go Modify Integration test TestMiddlewareChain_ToolVisibility
pkg/platform/platform.go Modify Wire middleware in finalizeSetup() after mcpapps block
pkg/platform/config_test.go Modify YAML parsing test for tools: section
configs/platform.yaml Modify Commented example for tools: section

Acceptance Criteria

  • make verify passes
  • Config with no tools: section behaves identically to today (backward compatible)
  • Config with tools.allow: ["trino_*"] only shows trino tools in tools/list
  • Config with tools.deny: ["*_delete_*"] hides delete tools
  • Integration test proves end-to-end filtering through real MCP server wiring

Edge Cases

Edge Case Expected Behavior
No tools: section All tools visible (backward compatible)
allow: ["*"] All tools pass allow, then deny applies
allow: [] with deny: ["s3_*"] All tools pass, then deny removes s3 tools
Invalid glob pattern filepath.Match error → treated as non-match, silently skipped
Empty tools list from tools/list Passes through, no error
Tool registered after middleware setup Filtered at response time, so dynamically registered tools are also filtered
Interaction with persona filtering Independent — persona gates tools/call, visibility gates tools/list
Interaction with mcpapps metadata Visibility runs outermost; denied tools removed entirely including _meta.ui metadata

Test Plan

  • isToolVisible unit tests: no rules, allow-only, deny-only, allow+deny, invalid patterns, exact match, wildcard, multiple patterns
  • filterToolVisibility unit tests: non-tools/list passthrough, nil result, non-ListToolsResult type, empty tools, actual filtering
  • Integration test: Wire real mcp.Server + AddReceivingMiddleware, register 4 tools, add visibility middleware with allow: ["trino_*"], call session.ListTools(), assert only trino tools appear
  • Config parsing test: YAML with tools: section deserializes correctly

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions