Filtering large MCP tool responses with jq
Large API responses consume valuable LLM context. When an API returns hundreds of user records, the LLM must process every field of every record, even when only a few specific details are needed. This wastes context space and slows down response generation.
The context window problem
Consider a contact management API. A tool that calls GET /contacts
might return 100 contacts, each with 20 fields (name, email, phone, address, company, etc.). That’s 2,000 fields of data loaded into the LLM’s context, when the user might only need email addresses.
Traditional solutions involve manually filtering response data in the MCP server code. This creates a tradeoff: Either include all data (wasting context) or exclude fields (making data inaccessible when needed).
What is jq?
jq is a lightweight command-line JSON processor. Like sed
for JSON data, jq slices, filters, and transforms structured data with minimal syntax. It’s particularly effective for extracting specific values from complex API responses.
Dynamic response filtering in MCP
Response filtering allows the LLM to apply jq syntax to transform API responses based on the response schema. The LLM dynamically selects only the information needed to answer each query.
Implementation with FastMCP
Here’s how to implement jq filtering in a FastMCP tool:
import subprocess
import json
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("Contact Manager")
@mcp.tool()
def get_contacts(jq_filter: str | None = None) -> str:
"""Retrieve contact information.
Args:
jq_filter: Optional jq syntax to filter the response
"""
# Fetch contacts from your API
response = fetch_contacts_from_api()
# Apply jq filter if provided
if jq_filter:
response = apply_jq_filter(response, jq_filter)
return json.dumps(response)
def apply_jq_filter(data: dict, filter_expr: str) -> dict:
"""Apply a jq filter to JSON data."""
try:
# Convert data to JSON string
json_input = json.dumps(data)
# Run jq command
result = subprocess.run(
['jq', filter_expr],
input=json_input,
capture_output=True,
text=True,
check=True
)
return json.loads(result.stdout)
except subprocess.CalledProcessError as e:
return {"error": f"Invalid jq filter: {e.stderr}"}
except json.JSONDecodeError:
return {"error": "Failed to parse jq output"}
With this implementation, the LLM can apply jq filters dynamically:
# Extract only name and email fields
get_contacts(jq_filter='.contacts[] | {name, email}')
# Filter for active users only
get_contacts(jq_filter='.data[] | select(.status == "active")')
# Transform arrays of objects
get_contacts(jq_filter='.results | map({id, company_name})')
How it works
By adding an optional jq_filter
parameter to your tool, the LLM can provide jq syntax to filter the response before processing it.
For example, when asked “What are the email addresses of active customers?”, the LLM can:
- Call
get_contacts
withjq_filter='.contacts[] | select(.status == "active") | .email'
- The tool applies the filter using the
jq
command-line tool - Only the filtered results are returned to the LLM
- The LLM processes a fraction of the original response size
This approach maintains full API access while dramatically reducing context consumption. The LLM chooses what data to retrieve based on each specific query.
Related reading
For more strategies to optimize MCP tools, see Why less is more for MCP.
Last updated on