Skip to content
Anysite Blog· 7 min read

Filter, Aggregate, and Export MCP Data Without Burning Context Tokens

Filter, Aggregate, and Export MCP Data Without Burning Context Tokens

You search LinkedIn for 200 software engineers. The MCP server fetches them. Then all 200 profiles — names, headlines, locations, profile links — land in the LLM's context at once.

If you only wanted the ones in San Francisco who are open to work, you still paid to load the rest. If you wanted to know how the profiles split across locations, the LLM had to calculate that from raw data sitting in its context. At scale, this isn't just expensive. It's a structural ceiling.

The Anysite MCP Server v2 introduced a different model. Every execute() call stores results in a server-side data cache. Filtering, aggregation, and sorting happen server-side. Only the results you asked for come back to the LLM. The 200 profiles stay in the cache. What changes is how much of them you ever need to see.

The Context Window Tax

Context windows are finite. They're also shared. Every token spent holding raw data is a token unavailable for reasoning, instructions, and conversation history.

A single LinkedIn profile in structured JSON runs to roughly 800-1,500 tokens depending on how complete it is. Fetch 200 profiles and you've consumed somewhere between 160,000 and 300,000 tokens before asking a single question. For models with 200K token limits, that's most of the window. For models with smaller limits, it exceeds them entirely.

This shapes what you can do in a single MCP session. Want to see how the profiles split by location? The LLM reads each profile, extracts the location, holds all 200 values in working context, then tallies them. It works. But you're using a language model as a spreadsheet, and paying language model prices to do it.

The bottleneck isn't intelligence. It's logistics.

The Cache Architecture

Every execute() call automatically stores its results in a data cache on the Anysite servers. The call returns a cache_key — a reference to that stored dataset — along with the first page of results.

The cache persists for 7 days. During that time, query_cache and export_data can operate on the stored dataset without making new API requests and without returning all the raw data to the LLM.

execute() → stores to data cache → returns cache_key + first page
query_cache(cache_key, ...) → queries data cache → returns only results
export_data(cache_key, ...) → packages cached dataset → returns download URL

The distinction matters: query_cache is not re-fetching from LinkedIn or Twitter or wherever the data came from. It's querying an already-collected dataset that lives on Anysite's servers. One API call collects the data. Everything after that is analysis on the copy.

MCP server-side data analysis architecture: execute → data cache → query_cache → export_data

Filtering

The most common use case: collect broadly, then narrow down.

query_cache(cache_key, conditions=[
  {"field": "location", "op": "contains", "value": "San Francisco"},
  {"field": "open_to_work", "op": "=", "value": true}
])

This returns only profiles where location contains "San Francisco" and the person is marked open to work. The rest of the dataset stays in the cache, untouched and unread.

Supported operators: =, !=, >, <, >=, <=, contains, not_contains. Multiple conditions use AND logic — every condition must match for a record to be included.

A few practical patterns:

Employer in headline: Find profiles that mention a specific employer.

{"field": "headline", "op": "contains", "value": "Anthropic"}

Exclusion filter: Remove records you don't want.

{"field": "headline", "op": "not_contains", "value": "Recruiter"}

Keyword in text: Surface profiles mentioning a specific technology.

{"field": "headline", "op": "contains", "value": "machine learning"}

The filtered results come back to the LLM. The discarded records do not.

Aggregation

Aggregation answers analytical questions about a dataset without requiring the LLM to hold the dataset in context.

Number of distinct locations across all collected profiles:

query_cache(cache_key, aggregate={"field": "location", "op": "uniq"})

Supported functions: sum, avg, min, max, count, uniq.

count returns the total number of records in the dataset. uniq counts distinct values in a field — useful for understanding how many unique companies, locations, or industries appear in a dataset.

None of these operations require the LLM to see individual records. The calculation runs server-side. The LLM receives a single number.

Group By

Group by lets you break aggregations down by a categorical field. The pattern: aggregate a metric, grouped by a dimension.

Profiles per location:

query_cache(cache_key,
  aggregate={"field": "name", "op": "count"},
  group_by="location"
)

How many are open to work:

query_cache(cache_key,
  aggregate={"field": "name", "op": "count"},
  group_by="open_to_work"
)

The result is a table: one row per unique value of the group-by field, with the aggregate value for each group. You can group by any field in the dataset — location, a boolean flag, or any other structured attribute in the response.

This is the difference between "how many profiles did we collect?" and "how are those profiles distributed across locations?" Both questions answer from the same cache. Neither requires loading the full dataset.

Sorting and Pagination

Sort by any field in the dataset, with an optional limit:

query_cache(cache_key, sort_by="name", sort_order="asc", limit=10)

This returns the first 10 profiles by name. The LLM sees 10 records, not 200.

Combine sorting with filtering to get precise subsets:

query_cache(cache_key,
  conditions=[{"field": "location", "op": "contains", "value": "New York"}],
  sort_by="name",
  sort_order="asc",
  limit=20
)

The first 20 New York profiles by name, from a 200-profile dataset. A filter and a sort applied server-side. Twenty records returned to context.

For large result sets, use get_page to paginate through cached results:

get_page(cache_key, offset=20)

A Complete Workflow

Here's how the pieces fit together in practice. The goal: find ML engineers in the Bay Area from a broad LinkedIn search, see where the rest are based, then export the collected dataset for a spreadsheet.

Step 1: Collect broadly.

execute("linkedin", "search", "search_users", {"keywords": "machine learning engineer", "count": 200})

Returns cache_key + first 10 profiles. 200 profiles now in the data cache.

Step 2: Filter to Bay Area.

query_cache(cache_key, conditions=[
  {"field": "location", "op": "contains", "value": "Bay Area"}
])

Returns the subset of Bay Area profiles. No new API call. No re-fetching from LinkedIn.

Step 3: Check the distribution.

query_cache(cache_key,
  aggregate={"field": "name", "op": "count"},
  group_by="location"
)

Reveals where the ML engineers in the dataset are based.

Step 4: Get 15 Bay Area profiles that are open to work.

query_cache(cache_key,
  conditions=[
    {"field": "location", "op": "contains", "value": "Bay Area"},
    {"field": "open_to_work", "op": "=", "value": true}
  ],
  sort_by="name",
  sort_order="asc",
  limit=15
)

Up to fifteen profiles, sorted by name. Ready to read.

Step 5: Export the dataset.

export_data(cache_key, "csv")

export_data writes the whole collected dataset, not the filtered view — apply the Bay Area filter again in the spreadsheet, or narrow the execute() query itself.

All five steps operate on the same collected dataset. One API call to LinkedIn. The rest is analysis.

Exporting Datasets

export_data packages the full cached dataset as a downloadable file. Three formats: JSON, CSV, and JSONL.

export_data(cache_key, "csv")

The tool returns a download URL. The file stays available for 7 days — the same TTL as the cache. Download it into a spreadsheet, load it into a database, pass the URL to a reporting tool.

CSV works for spreadsheets and most BI tools. JSON preserves nested data structures from the original response. JSONL (newline-delimited JSON) works best for large datasets that need to be processed line by line or streamed into a database.

Exporting is particularly useful when analysis needs to exceed what any MCP session can handle. A 200-profile dataset in a spreadsheet can be filtered, pivoted, and charted without token constraints.

Best Practices

Collect broadly, filter narrowly. Use larger count values in execute() — the cache is cheap. Filtering server-side is cheaper than making multiple narrow API calls, and you can slice the same dataset multiple ways without re-collecting.

Filter before reading into context. If you know you only want a subset of a dataset, apply query_cache before asking the LLM to analyze individual records. There's no reason to load 200 records when you want 15.

Aggregate server-side. If the question is statistical — averages, counts, distributions — let the server calculate it. The LLM receives a number, not a dataset.

Export for external analysis. When the analysis requires a tool better suited to tabular data — a spreadsheet, a SQL query, a BI dashboard — export the dataset and work there. The MCP session is for discovery and exploration, not for being a calculation engine.

Know when to graduate. MCP is built for exploration: ad-hoc queries, conversational analysis, one-off research. When a workflow becomes routine — weekly competitive reports, daily prospect refreshes, ongoing talent pipeline tracking — it belongs in the CLI as a scheduled pipeline. The same Anysite engine powers both. The difference is the interface and whether a human needs to be involved.

What This Changes

The practical effect of server-side analysis is that the size of a dataset stops being the constraint on what you can analyze. You can collect 500 records, filter to 30, aggregate across all 500, and export the full set — in the same session, from the same cache.

Context windows expand over time. But the architectural principle holds regardless: analysis that runs on the data source is more efficient than analysis that requires the data to travel through context. For data-heavy MCP workflows, query_cache is the difference between what fits and what doesn't.


Full documentation for query_cache and export_data is at docs.anysite.io.