Enable tool-search synthesis

This guide walks through enabling tool-search synthesis. Use it when:

  • Your upstream MCP servers expose many tools (50+) and your agents waste tokens on tool definitions in every prompt
  • The upstream doesn’t natively support a tools/search method
  • You want a single virtual mcp_search tool to appear in tools/list instead of the full upstream catalogue

Behavior change: synthesize mode is a behavioral override. Agents already coded against specific tool names will continue to call those names directly and will not benefit. Agents that read tools/list and react to what they find will need to call mcp_search first to discover tool names. See Tool-search tradeoffs before enabling in production.

Prerequisites

  • mcpgw vNEXT
  • One or more upstreams configured under upstreams:

Steps

1. Enable synthesize mode

Edit mcpgw.yaml:

tool_search:
  enabled: true
  mode: synthesize
  refresh_interval: 60s     # how often to pull tools/list from each upstream
  max_results: 20            # hard cap on results returned by mcp_search

tool_search.enabled: true with mode: synthesize is the only supported combination that changes gateway behavior. mode: passthrough (the default) is a no-op and preserves prior tools/list behavior.

2. Restart (mode is not hot-reloadable)

mcpgw reads tool_search.mode once at startup. SIGHUP refreshes the index but does not switch between passthrough and synthesize. Restart for any mode change.

# Docker
docker restart mcpgw

# systemd
systemctl restart mcpgw

# direct process (sends SIGTERM, process must restart via supervisor)
kill "$(pgrep mcpgw)"

3. Verify the index populated

On startup, the refresher attempts one initial tools/list call to each configured upstream. Check stderr or journald for the result:

  • Failure: the log contains toolindex: initial refresh failed with an err= field naming the cause. The gateway continues running but mcp_search returns empty results until a later refresh cycle succeeds.
  • Success: no log line is emitted for a successful refresh — the refresher is silent on the happy path.

To inspect the live index state, run mcpgw stats (if exposed in your deployment) or tail the process logs for subsequent toolindex: refresh failed warnings at the configured refresh_interval.

4. Verify tools/list returns the stub

curl -s -X POST https://gw.acme.com/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | jq

Expected response:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "mcp_search",
        "description": "Search available tools by keyword. Returns ranked matches with full definitions. Call this with {query: \"...\"} before tools/call when you don't know the exact tool name.",
        "inputSchema": {
          "type": "object",
          "properties": {
            "query": {
              "type": "string",
              "description": "Keywords to search for in tool names and descriptions."
            },
            "limit": {
              "type": "integer",
              "description": "Maximum number of results to return."
            }
          },
          "required": ["query"]
        }
      }
    ]
  }
}

If you see the upstream’s full tool list instead, confirm tool_search.enabled: true and mode: synthesize in the running config, then restart.

curl -s -X POST https://gw.acme.com/mcp \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"mcp_search","arguments":{"query":"read","limit":5}}}' | jq

Expected: a result.content[0].text containing a JSON array of matching tool definitions pulled from the index. Each element has the same shape as a single tool in a tools/list response — name, description, and inputSchema.

If the array is empty, see Troubleshooting below.

6. Audit log

Synthesized responses appear in audit.jsonl with action values synthesize_list and synthesize_search. Filter:

jq 'select(.action | startswith("synthesize"))' audit.jsonl

A synthesize_search line looks like:

{
  "ts": "2026-05-09T14:23:01.123Z",
  "session_id": "claude-desktop-3f2a",
  "method": "tools/call",
  "tool_name": "mcp_search",
  "action": "synthesize_search",
  "latency_ms": 1
}

Troubleshooting

SymptomCauseFix
tools/list returns the upstream’s full tool listtool_search.enabled: false or mode: passthrough, or config was not reloadedConfirm config; restart (SIGHUP alone does not switch modes)
mcp_search returns an empty results arrayIndex hasn’t populated yet, or all upstream refreshes failedCheck logs for toolindex: initial refresh failed or toolindex: refresh failed; verify upstreams are reachable from the gateway host
Audit shows synthesize_search but calls to discovered tools return no_routeThe discovered tool belongs to an upstream not covered by your routes: configAdd a matching route entry, or set default_upstream to a catch-all upstream
Mode change doesn’t take effect after SIGHUPSIGHUP refreshes only the index, not the routing policyRestart for mode changes
Agent ignores mcp_search and calls tool names directlyAgent was trained on or configured with explicit tool namesThose calls pass through to the upstream unchanged — synthesize mode does not intercept them. Token savings do not apply; see tradeoffs

Disabling

Set tool_search.enabled: false and restart. The gateway reverts to forwarding tools/list directly to the upstream.