Skip to content

Add per-tool jq response filters for gateway tool results#5273

Merged
lpcox merged 5 commits into
mainfrom
copilot/reduce-token-bloat-in-jq-filter
May 8, 2026
Merged

Add per-tool jq response filters for gateway tool results#5273
lpcox merged 5 commits into
mainfrom
copilot/reduce-token-bloat-in-jq-filter

Conversation

Copilot AI commented May 7, 2026

Copy link
Copy Markdown
Contributor

Some GitHub MCP tool responses carry large repeated fields that dominate token usage, notably list_code_scanning_alerts where rule.help repeats full CWE help text on every alert. This change adds a gateway-side, per-tool jq filter so responses can be trimmed before they reach the agent and before payload preview/schema generation runs.

  • What changed

    • Added tool_response_filters to per-server config for both TOML and stdin JSON configs.
    • Filters are keyed by backend tool name and contain a jq expression applied to that tool’s response payload.
  • Config + fail-fast validation

    • jq expressions are parsed/compiled during config validation.
    • Invalid or empty filters fail startup/config load instead of surfacing at request time.
  • Response pipeline

    • Applied configured filters in the jq middleware before:
      • inline tool response serialization
      • large-payload storage / preview generation
      • schema extraction
    • Preserves existing metadata and follow-up content handling while rewriting the primary JSON payload.
  • Result

    • Enables targeted response shaping for high-volume tools without changing the upstream MCP server.
    • Keeps the behavior declarative and server-local in gateway config.

Example:

[servers.github]
command = "docker"
args = ["run", "--rm", "-i", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server:latest"]

[servers.github.tool_response_filters]
list_code_scanning_alerts = "map(del(.rule.help))"
list_secret_scanning_alerts = "map(del(.push_protection_bypassed_by))"

That allows responses like:

[
  {
    "number": 101,
    "rule": {
      "id": "go/sql-injection",
      "help": "very long repeated CWE help text"
    }
  }
]

to be rewritten as:

[
  {
    "number": 101,
    "rule": {
      "id": "go/sql-injection"
    }
  }
]

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • example.com
    • Triggering command: /tmp/go-build302098926/b509/launcher.test /tmp/go-build302098926/b509/launcher.test -test.testlogfile=/tmp/go-build302098926/b509/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true ache/go/1.25.9/x-p ache/go/1.25.9/xgoogle.golang.org/grpc/internal/metadata x_amd64/asm (dns block)
    • Triggering command: /tmp/go-build3754274398/b513/launcher.test /tmp/go-build3754274398/b513/launcher.test -test.testlogfile=/tmp/go-build3754274398/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s e=/t�� t0 m0s ash -errorsas -ifaceassert -nilfunc 098926/b514/importcfg -ato�� k/gh-aw-mcpg/gh-aw-mcpg/internal/logger/sanitizego1.25.9 k/gh-aw-mcpg/gh-aw-mcpg/internal/logger/sanitize-c=4 /opt/hostedtoolcache/go/1.25.9/x64/pkg/tool/linu-nolocalimports -errorsas -ifaceassert -nilfunc /opt/hostedtoolcache/go/1.25.9/x64/pkg/tool/linu/tmp/go-build2478948471/b264/_testmain.go (dns block)
    • Triggering command: /tmp/go-build2307936139/b513/launcher.test /tmp/go-build2307936139/b513/launcher.test -test.testlogfile=/tmp/go-build2307936139/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s know�� known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libobject-926daa94a00ee327.rlib known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libmemchr-48d5b0db80402653.rlib known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libaddr2line-3367f26bd486b29d.rlib known-linux-gnu/bash known-linux-gnu//usr/bin/runc known-linux-gnu/--version ive.12123747d8da05ed-cgu.00.rcgu.o ive.�� l/middleware/jqschema_test.go ive.12123747d8da05ed-cgu.03.rcgu.o x_amd64/compile ive.12123747d8dabash ive.12123747d8da--norc ive.12123747d8da--noprofile x_amd64/compile (dns block)
  • invalid-host-that-does-not-exist-12345.com
    • Triggering command: /tmp/go-build302098926/b491/config.test /tmp/go-build302098926/b491/config.test -test.testlogfile=/tmp/go-build302098926/b491/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true ternal/wasmdebug-p ternal/wasmdebuggoogle.golang.org/grpc/metadata x_amd64/compile (dns block)
    • Triggering command: /tmp/go-build2478948471/b264/config.test /tmp/go-build2478948471/b264/config.test -test.testlogfile=/tmp/go-build2478948471/b264/testlog.txt -test.paniconexit0 -test.timeout=10m0s 0989�� uf@v1.36.11/reflect/protoreflect-errorsas uf@v1.36.11/reflect/protoreflect-ifaceassert x_amd64/vet --gdwarf-5 ernal/x -o x_amd64/vet rtcf�� 1.80.0/internal/-errorsas 1.80.0/internal/-ifaceassert 64/pkg/tool/linu-nilfunc --gdwarf-5 --64 -o 64/pkg/tool/linu-tests (dns block)
    • Triggering command: /tmp/go-build2144274392/b264/config.test /tmp/go-build2144274392/b264/config.test -test.testlogfile=/tmp/go-build2144274392/b264/testlog.txt -test.paniconexit0 -test.timeout=10m0s .o .o .o .o .o .o 69327bc0f.build_0fe53110ba95802fc3eedafe42b5f4ea0d304545:internal/config/validation.go lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libstd-46d936097e8c5b85.rlib lib/�� lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libobject-926daa94a00ee327.rlib lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libmemchr-48d5b0db80402653.rlib lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libaddr2line-3367f26bd486b29d.rlib lib/rustlib/x86_/bin/sh lib/rustlib/x86_-c lib/rustlib/x86_echo " test-serena - Run Serena MCP Server tests (direct connection)" 05ed-cgu.00.rcgu.o (dns block)
  • nonexistent.local
    • Triggering command: /tmp/go-build302098926/b509/launcher.test /tmp/go-build302098926/b509/launcher.test -test.testlogfile=/tmp/go-build302098926/b509/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true ache/go/1.25.9/x-p ache/go/1.25.9/xgoogle.golang.org/grpc/internal/metadata x_amd64/asm (dns block)
    • Triggering command: /tmp/go-build3754274398/b513/launcher.test /tmp/go-build3754274398/b513/launcher.test -test.testlogfile=/tmp/go-build3754274398/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s e=/t�� t0 m0s ash -errorsas -ifaceassert -nilfunc 098926/b514/importcfg -ato�� k/gh-aw-mcpg/gh-aw-mcpg/internal/logger/sanitizego1.25.9 k/gh-aw-mcpg/gh-aw-mcpg/internal/logger/sanitize-c=4 /opt/hostedtoolcache/go/1.25.9/x64/pkg/tool/linu-nolocalimports -errorsas -ifaceassert -nilfunc /opt/hostedtoolcache/go/1.25.9/x64/pkg/tool/linu/tmp/go-build2478948471/b264/_testmain.go (dns block)
    • Triggering command: /tmp/go-build2307936139/b513/launcher.test /tmp/go-build2307936139/b513/launcher.test -test.testlogfile=/tmp/go-build2307936139/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s know�� known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libobject-926daa94a00ee327.rlib known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libmemchr-48d5b0db80402653.rlib known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libaddr2line-3367f26bd486b29d.rlib known-linux-gnu/bash known-linux-gnu//usr/bin/runc known-linux-gnu/--version ive.12123747d8da05ed-cgu.00.rcgu.o ive.�� l/middleware/jqschema_test.go ive.12123747d8da05ed-cgu.03.rcgu.o x_amd64/compile ive.12123747d8dabash ive.12123747d8da--norc ive.12123747d8da--noprofile x_amd64/compile (dns block)
  • slow.example.com
    • Triggering command: /tmp/go-build302098926/b509/launcher.test /tmp/go-build302098926/b509/launcher.test -test.testlogfile=/tmp/go-build302098926/b509/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true ache/go/1.25.9/x-p ache/go/1.25.9/xgoogle.golang.org/grpc/internal/metadata x_amd64/asm (dns block)
    • Triggering command: /tmp/go-build3754274398/b513/launcher.test /tmp/go-build3754274398/b513/launcher.test -test.testlogfile=/tmp/go-build3754274398/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s e=/t�� t0 m0s ash -errorsas -ifaceassert -nilfunc 098926/b514/importcfg -ato�� k/gh-aw-mcpg/gh-aw-mcpg/internal/logger/sanitizego1.25.9 k/gh-aw-mcpg/gh-aw-mcpg/internal/logger/sanitize-c=4 /opt/hostedtoolcache/go/1.25.9/x64/pkg/tool/linu-nolocalimports -errorsas -ifaceassert -nilfunc /opt/hostedtoolcache/go/1.25.9/x64/pkg/tool/linu/tmp/go-build2478948471/b264/_testmain.go (dns block)
    • Triggering command: /tmp/go-build2307936139/b513/launcher.test /tmp/go-build2307936139/b513/launcher.test -test.testlogfile=/tmp/go-build2307936139/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s know�� known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libobject-926daa94a00ee327.rlib known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libmemchr-48d5b0db80402653.rlib known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libaddr2line-3367f26bd486b29d.rlib known-linux-gnu/bash known-linux-gnu//usr/bin/runc known-linux-gnu/--version ive.12123747d8da05ed-cgu.00.rcgu.o ive.�� l/middleware/jqschema_test.go ive.12123747d8da05ed-cgu.03.rcgu.o x_amd64/compile ive.12123747d8dabash ive.12123747d8da--norc ive.12123747d8da--noprofile x_amd64/compile (dns block)
  • this-host-does-not-exist-12345.com
    • Triggering command: /tmp/go-build302098926/b518/mcp.test /tmp/go-build302098926/b518/mcp.test -test.testlogfile=/tmp/go-build302098926/b518/testlog.txt -test.paniconexit0 -test.timeout=10m0s -test.v=true experimental.go internal.go x_amd64/compile -p crypto/ecdh -lang=go1.25 x_amd64/compile -I /opt/hostedtoolcache/go/1.25.9/x-p cfg x_amd64/vet --gdwarf-5 --64 -o x_amd64/vet (dns block)
    • Triggering command: /tmp/go-build3754274398/b522/mcp.test /tmp/go-build3754274398/b522/mcp.test -test.testlogfile=/tmp/go-build3754274398/b522/testlog.txt -test.paniconexit0 -test.timeout=10m0s -ato�� -bool -buildtags bash -errorsas -ifaceassert -nilfunc /opt/hostedtoolcache/go/1.25.9/x--revs -ato�� -bool -buildtags x_amd64/link -errorsas -ifaceassert -nilfunc x_amd64/link (dns block)
    • Triggering command: /tmp/go-build2307936139/b522/mcp.test /tmp/go-build2307936139/b522/mcp.test -test.testlogfile=/tmp/go-build2307936139/b522/testlog.txt -test.paniconexit0 -test.timeout=10m0s know�� known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libobject-926daa94a00ee327.rlib known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libmemchr-48d5b0db80402653.rlib known-linux-gnu/lib/rustlib/x86_64-REDACTED-linux-gnu/lib/libaddr2line-3367f26bd486b29d.rlib known-linux-gnu/runc jvhawl2nskseooqp--version z8tylr32cgwnziux.0lr3v9a.rcgu.o ruczucboywd9kbz6.0lr3v9a.rcgu.o aiea�� l/server/tool_registry.go p1agk9if6se1ud54.0lr3v9a.rcgu.o fc88gtczrpthgg1u.0lr3v9a.rcgu.o 6uc4ma8f604u2vi6bash 5vmlrtxer9j2lnp9--norc zucrirrh7hnf4z1l--noprofile o4qlefxtp7qruodt.0lr3v9a.rcgu.o (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI and others added 2 commits May 7, 2026 23:54
Agent-Logs-Url: https://github.com/github/gh-aw-mcpg/sessions/7aabaf6a-7e6d-4110-a807-60dc34e41348

Co-authored-by: lpcox <15877973+lpcox@users.noreply.github.com>
Copilot AI changed the title [WIP] Reduce token bloat in jq response filters for list_code_scanning_alerts Add per-tool jq response filters for gateway tool results May 8, 2026
Copilot AI requested a review from lpcox May 8, 2026 00:01
Copilot finished work on behalf of lpcox May 8, 2026 00:01
@lpcox lpcox marked this pull request as ready for review May 8, 2026 02:12
Copilot AI review requested due to automatic review settings May 8, 2026 02:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a per-backend-tool jq “response shaping” feature to the gateway so large/redundant fields can be trimmed from tool results before inline return, payload storage/preview, and schema extraction—primarily to reduce token usage for high-volume GitHub MCP tools.

Changes:

  • Introduces tool_response_filters per server (TOML + stdin JSON) and validates filters at config load by parsing/compiling jq.
  • Wires tool registration to wrap handlers with a filter-aware jq/payload middleware when a filter is configured.
  • Adds middleware + config tests for filter compilation and basic filtering behavior.
Show a summary per file
File Description
internal/server/tool_registry.go Looks up per-server per-tool filter config and chooses filtered vs unfiltered middleware wrapper.
internal/middleware/jqschema.go Adds jq filter compilation/execution helpers and applies filtering in the tool result middleware pipeline.
internal/middleware/jqschema_test.go Adds unit tests for applying filters and for filtered inline responses.
internal/config/validation.go Validates tool_response_filters by parsing/compiling jq during config validation.
internal/config/validation_test.go Tests valid/invalid/empty jq filter validation behavior.
internal/config/config_stdin.go Adds stdin JSON support for tool_response_filters and passes it into ServerConfig.
internal/config/config_core.go Adds ToolResponseFilters to ServerConfig and validates it on TOML load.
internal/config/config_core_test.go Adds TOML load test coverage for tool_response_filters.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 8/8 changed files
  • Comments generated: 2

Comment on lines 207 to 236
// stripExtensionFieldsForValidation returns a copy of the raw JSON with known gateway
// extension fields removed, so the copy can be validated against the upstream MCP Gateway
// schema. These fields are gateway-specific additions that are not part of the upstream
// schema definition, so they must be removed before schema validation to prevent spurious
// "additional properties" errors.
//
// Fields stripped:
// - Top-level "guards": gateway-specific guard configuration
// - Per-server "guard": reference to a named guard
// - Per-server "auth": upstream authentication configuration (OIDC etc.)
//
// Note: "guard-policies" and "registry" are already injected into the upstream schema
// by fetchAndFixSchema, so they do not need to be stripped here.
func stripExtensionFieldsForValidation(data []byte) ([]byte, error) {
var config map[string]interface{}
if err := json.Unmarshal(data, &config); err != nil {
return nil, err
}

// Strip top-level "guards" extension field
delete(config, "guards")

// Strip per-server "guard" and "auth" extension fields
if servers, ok := config["mcpServers"].(map[string]interface{}); ok {
for _, server := range servers {
if serverMap, ok := server.(map[string]interface{}); ok {
delete(serverMap, "guard")
delete(serverMap, "auth")
delete(serverMap, "tool_response_filters")
}
Comment on lines +289 to +304
}

filteredData, filterErr := applyCompiledToolResponseFilter(ctx, filterCode, data)
if filterErr != nil {
logger.LogWarn("payload", "Failed to apply tool response filter, returning original response: tool=%s, queryID=%s, error=%v",
toolName, queryID, filterErr)
return result, data
}

filteredResult, convertErr := mcp.ConvertToCallToolResult(filteredData)
if convertErr != nil {
logger.LogWarn("payload", "Failed to rebuild filtered tool response, returning original response: tool=%s, queryID=%s, error=%v",
toolName, queryID, convertErr)
return result, data
}

@lpcox

lpcox commented May 8, 2026

Copy link
Copy Markdown
Collaborator

@copilot address the review feedback

Copilot AI and others added 2 commits May 8, 2026 04:13

Copilot AI commented May 8, 2026

Copy link
Copy Markdown
Contributor Author

@copilot address the review feedback

Addressed in 2a4880b, with final cleanup in f7d01f5. The filter now handles MCP text-envelope payloads by parsing/filtering the first text JSON payload and rewriting both the outbound content and backing data consistently, and I updated the config_stdin comment to document stripping tool_response_filters.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • example.com
    • Triggering command: /tmp/go-build741625722/b513/launcher.test /tmp/go-build741625722/b513/launcher.test -test.testlogfile=/tmp/go-build741625722/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build741625722/b408/vet.cfg @v1.43.0/semconv/v1.40.0/otelconv/metric.go -buildtags x_amd64/vet -errorsas -ifaceassert -nilfunc x_amd64/vet /usr�� --version _cgo_.o x_amd64/vet go --global 64/pkg/tool/linu-bool x_amd64/vet (dns block)
  • invalid-host-that-does-not-exist-12345.com
    • Triggering command: /tmp/go-build2707338352/b258/config.test /tmp/go-build2707338352/b258/config.test -test.testlogfile=/tmp/go-build2707338352/b258/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build2707338352/b240/vet.cfg go --global t@users.noreply.-lang=go1.25 committer.email (dns block)
    • Triggering command: /tmp/go-build3558144198/b258/config.test /tmp/go-build3558144198/b258/config.test -test.testlogfile=/tmp/go-build3558144198/b258/testlog.txt -test.paniconexit0 -test.timeout=10m0s conf�� 64/src/runtime/cgo ls13/cast.go x_amd64/compile copilot.originalbash (dns block)
  • nonexistent.local
    • Triggering command: /tmp/go-build741625722/b513/launcher.test /tmp/go-build741625722/b513/launcher.test -test.testlogfile=/tmp/go-build741625722/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build741625722/b408/vet.cfg @v1.43.0/semconv/v1.40.0/otelconv/metric.go -buildtags x_amd64/vet -errorsas -ifaceassert -nilfunc x_amd64/vet /usr�� --version _cgo_.o x_amd64/vet go --global 64/pkg/tool/linu-bool x_amd64/vet (dns block)
  • slow.example.com
    • Triggering command: /tmp/go-build741625722/b513/launcher.test /tmp/go-build741625722/b513/launcher.test -test.testlogfile=/tmp/go-build741625722/b513/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build741625722/b408/vet.cfg @v1.43.0/semconv/v1.40.0/otelconv/metric.go -buildtags x_amd64/vet -errorsas -ifaceassert -nilfunc x_amd64/vet /usr�� --version _cgo_.o x_amd64/vet go --global 64/pkg/tool/linu-bool x_amd64/vet (dns block)
  • this-host-does-not-exist-12345.com
    • Triggering command: `/tmp/go-build741625722/b522/mcp.test /tmp/go-build741625722/b522/mcp.test -test.testlogfile=/tmp/go-build741625722/b522/testlog.txt -test.paniconexit0 -test.timeout=10m0s /tmp/go-build741625722/b430/vet.cfg -unreachable=false ayloads

Agent-Logs-Url: https:/-nolocalimports x_amd64/vet go ough x_amd64/compile x_amd64/vet --ve��` (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot finished work on behalf of lpcox May 8, 2026 04:18
@lpcox lpcox merged commit 5ed5d5f into main May 8, 2026
16 checks passed
@lpcox lpcox deleted the copilot/reduce-token-bloat-in-jq-filter branch May 8, 2026 13:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[gateway] Per-tool jq response filters to reduce token bloat (e.g., list_code_scanning_alerts rule.help)

3 participants