14 LAUNCHES IN 14 DAYS

SEE WHAT’S NEW

MCP vs SQL for Data Agents: Why Cross-Source Analytics Needs Real Joins

MCP vs SQL for Data Agents: Why Cross-Source Analytics Needs Real Joins

MCP vs SQL for Data Agents: Why Cross-Source Analytics Needs Real Joins

All Posts

MCP suits context and actions. For numbers across Stripe, PostHog and HubSpot, a data agent needs SQL joins. When to use each, and why it matters.

Ka Ling Wu

Co-Founder & CEO, Upsolve AI

10 min

Title card for Upsolve Data Plane, Feature 07 of 14 in The Complete Agent Stack

The Complete Agent Stack · Part 03 of 14 · Ontology layer

This guide is one part of a 14-part series on what a production data agent needs, from the data model it reads to the agent that keeps it accurate. Each part stands on its own, and they build on each other in order.

← Previous: Part 02: Row-Level Security for AI Agents · Next: Part 04: AI Agent Context Versioning →

A team connects Stripe, PostHog and HubSpot to an AI agent through three MCP servers and asks a reasonable question: which acquisition channel brought in customers who pay the most and use the product most often? The agent calls each server, receives three piles of records, and tries to match them. The answer comes back formatted and confident, and nobody can tell whether the matching was right.

This guide explains why that pattern fails for analytics, what cross-source SQL looks like when there is no warehouse yet, and how to decide which questions belong to MCP and which belong to SQL. The companion guide, MCP Servers for Data Agents, covers the other half: connecting MCP servers for context and actions.

Key Takeaways

  • Direct tool calls route data through the model. Anthropic's engineers note that with standard MCP tool calling, every intermediate result passes through the model, and one workflow fell from 150,000 tokens to 2,000 when the data was handled in code instead.

  • Context is a hard budget. Anthropic restricts tool responses in Claude Code to 25,000 tokens by default, which is small next to a few years of charges, events and contacts.

  • Models are unreliable join engines. In an ACL 2025 workshop study, duplicate entities cut one model's accuracy on sum questions from 41% to 20%, and models rarely flagged the duplicates.

  • Access control in MCP is per server. The MCP specification makes authorisation optional and defines it for each server separately, so a row-level rule that spans sources has to live in the layer the agent queries through.

The 45-second version:


What Is the Difference Between MCP and SQL for Analytics?

Diagram of The Complete Agent Stack: four layers and 14 features, with Data Plane highlighted in the ontology layer

The Model Context Protocol (MCP) is an open standard for connecting AI applications to external systems. A server exposes tools, which the specification describes as functions the model can invoke, such as "querying databases, calling APIs, or performing computations" (MCP specification: Tools). The client calls a tool, the server returns content, and that content goes back to the model.

SQL works the other way round. The agent writes one declarative query, a database engine executes it, and the engine does the filtering, joining and aggregation. The model only sees the result set, which for an analytical question is usually a few dozen rows.

Both are useful to a data agent. The difference is where the computation happens. With MCP, the model is the place where data from different tools meets. With SQL, the engine is. For a question like "what is net revenue by channel over two years", that difference decides whether the number can be trusted.

Why Does Stitching MCP Results Break Down for Analytics?

MCP servers are built to let an agent operate a product: search a workspace, read a record, create a ticket. Analytics asks for something different, which is the combination of many records from many systems into one correct number. Four problems appear when an agent tries to do that through tool calls.

Every intermediate result passes through the context window

Anthropic's engineering team describes the default pattern plainly: "Every intermediate result must pass through the model" (Anthropic, Code execution with MCP). In their example, restructuring a workflow so that data stayed in an execution environment reduced token use from 150,000 to 2,000. The same post warns that with large documents or complex data structures, models "may be more likely to make mistakes when copying data between tool calls."

Size limits make this worse for analytics. Anthropic restricts tool responses in Claude Code to 25,000 tokens by default and notes that agents have limited context while "computer memory is cheap and abundant" (Anthropic, Writing effective tools for agents). A multi-year analysis either gets truncated, paginated across many calls, or summarised before it arrives. Each of those choices changes the answer. Long contexts also reduce accuracy, a problem covered in our guide to context rot.

The model becomes the join engine

Once results from Stripe, PostHog and HubSpot sit in context, the model has to do the work a query planner normally does: match customer IDs to emails, normalise case, drop duplicates, align time zones, and sum the right column. Research on tabular reasoning suggests models are weak at exactly this work.

Wolff and Hulsebos tested several models on realistic table variations and found that duplicate entities cut Llama 3.1's accuracy on sum questions from 41% to 20% on 2K-token tables, and that models acknowledged duplicates far less often than missing values (Wolff and Hulsebos, ACL 2025 TRL Workshop). The broader MMTU benchmark, with more than 28,000 questions across 25 table tasks, found that even GPT-5 scored only around 69% (Xing et al., MMTU). Duplicates and mismatched keys are normal in SaaS data, so an agent joining sources in context meets these conditions routinely.

The provider decides what you can ask

An MCP server exposes what its provider chose to expose. Stripe's server, for example, offers read and write access to API methods such as listing customers, charges and invoices, plus a separate analytics tool in preview (Stripe, MCP documentation). That design suits operating a Stripe account. A question that joins those records to product events still requires the agent to page through API responses and combine them itself, and any table or field the server does not expose is out of reach.

Permissions stop at each server's edge

The MCP specification makes authorisation optional and defines it per server: a remote server acts as an OAuth resource server, and tokens must be issued for that specific server (MCP specification: Authorization). That is sound security design, and it means each connection has its own credential. A team either shares one credential with every user of the agent, or asks each user to authenticate against each source. Neither option expresses a rule such as "this customer's users only see their own account's rows in every source." That rule needs a layer above the sources, which is the subject of the sibling guide on row-level security for AI agents.

Analysts have noticed the same gap. Atlan reports a Gartner analyst's prediction that 60% of agentic analytics projects relying solely on MCP will fail for lack of a consistent semantic layer (Atlan).

What Does Cross-Source SQL Look Like Without a Warehouse?

Joining sources with SQL does not require a warehouse project first. Federated query engines are built for it. In Trino, for example, each data source is configured as a catalog, and you can query several catalogs "even within the same SQL query" using fully qualified names such as catalog.schema.table (Trino documentation: Concepts).

For a data agent, the pattern looks like this. Connectors sync or expose tables from each SaaS tool. Those tables sit in one data model, where they are described and their join keys are recorded. The agent writes SQL against that model, and the engine runs it. The illustrative query below answers the channel question from the introduction.

-- Illustrative only: table and column names vary by connector
WITH revenue AS (
  SELECT lower(c.email) AS email,
         SUM(ch.amount - ch.amount_refunded) / 100.0 AS net_revenue
  FROM stripe.charges ch
  JOIN stripe.customers c ON c.id = ch.customer
  WHERE ch.status = 'succeeded'
    AND ch.created >= current_date - INTERVAL '24' MONTH
  GROUP BY lower(c.email)
),
activity AS (
  SELECT lower(person_email) AS email,
         COUNT(DISTINCT date(event_time)) AS active_days_90d
  FROM posthog.events
  WHERE event_time >= current_date - INTERVAL '90' DAY
  GROUP BY lower(person_email)
)
SELECT h.original_source AS channel,
       COUNT(*) AS customers,
       SUM(r.net_revenue) AS net_revenue,
       AVG(a.active_days_90d) AS avg_active_days
FROM hubspot.contacts h
JOIN revenue r ON r.email = lower(h.email)
LEFT JOIN activity a ON a.email = lower(h.email)
GROUP BY h.original_source
ORDER BY net_revenue DESC

-- Illustrative only: table and column names vary by connector
WITH revenue AS (
  SELECT lower(c.email) AS email,
         SUM(ch.amount - ch.amount_refunded) / 100.0 AS net_revenue
  FROM stripe.charges ch
  JOIN stripe.customers c ON c.id = ch.customer
  WHERE ch.status = 'succeeded'
    AND ch.created >= current_date - INTERVAL '24' MONTH
  GROUP BY lower(c.email)
),
activity AS (
  SELECT lower(person_email) AS email,
         COUNT(DISTINCT date(event_time)) AS active_days_90d
  FROM posthog.events
  WHERE event_time >= current_date - INTERVAL '90' DAY
  GROUP BY lower(person_email)
)
SELECT h.original_source AS channel,
       COUNT(*) AS customers,
       SUM(r.net_revenue) AS net_revenue,
       AVG(a.active_days_90d) AS avg_active_days
FROM hubspot.contacts h
JOIN revenue r ON r.email = lower(h.email)
LEFT JOIN activity a ON a.email = lower(h.email)
GROUP BY h.original_source
ORDER BY net_revenue DESC

-- Illustrative only: table and column names vary by connector
WITH revenue AS (
  SELECT lower(c.email) AS email,
         SUM(ch.amount - ch.amount_refunded) / 100.0 AS net_revenue
  FROM stripe.charges ch
  JOIN stripe.customers c ON c.id = ch.customer
  WHERE ch.status = 'succeeded'
    AND ch.created >= current_date - INTERVAL '24' MONTH
  GROUP BY lower(c.email)
),
activity AS (
  SELECT lower(person_email) AS email,
         COUNT(DISTINCT date(event_time)) AS active_days_90d
  FROM posthog.events
  WHERE event_time >= current_date - INTERVAL '90' DAY
  GROUP BY lower(person_email)
)
SELECT h.original_source AS channel,
       COUNT(*) AS customers,
       SUM(r.net_revenue) AS net_revenue,
       AVG(a.active_days_90d) AS avg_active_days
FROM hubspot.contacts h
JOIN revenue r ON r.email = lower(h.email)
LEFT JOIN activity a ON a.email = lower(h.email)
GROUP BY h.original_source
ORDER BY net_revenue DESC

-- Illustrative only: table and column names vary by connector
WITH revenue AS (
  SELECT lower(c.email) AS email,
         SUM(ch.amount - ch.amount_refunded) / 100.0 AS net_revenue
  FROM stripe.charges ch
  JOIN stripe.customers c ON c.id = ch.customer
  WHERE ch.status = 'succeeded'
    AND ch.created >= current_date - INTERVAL '24' MONTH
  GROUP BY lower(c.email)
),
activity AS (
  SELECT lower(person_email) AS email,
         COUNT(DISTINCT date(event_time)) AS active_days_90d
  FROM posthog.events
  WHERE event_time >= current_date - INTERVAL '90' DAY
  GROUP BY lower(person_email)
)
SELECT h.original_source AS channel,
       COUNT(*) AS customers,
       SUM(r.net_revenue) AS net_revenue,
       AVG(a.active_days_90d) AS avg_active_days
FROM hubspot.contacts h
JOIN revenue r ON r.email = lower(h.email)
LEFT JOIN activity a ON a.email = lower(h.email)
GROUP BY h.original_source
ORDER BY net_revenue DESC

Three properties make this better than the tool-call version. The engine processes every row, so nothing is truncated to fit a context window. The joins are explicit, so a reviewer can see that email is the key and that refunds are subtracted. The query can be rerun next month and will produce a comparable number.

The query is also only as good as the model underneath it. If HubSpot holds two contacts with the same email, the join double-counts revenue. The data model is where a team records that contacts must be deduplicated first, which tables are the source of truth, and what "net revenue" means. Those definitions are covered in the guides on data models for AI agents and the semantic layer for AI.

When Should a Data Agent Use MCP, and When SQL?

The two approaches answer different kinds of request, and most production agents need both. The table summarises the trade-offs.

Dimension

MCP tool calls, one server per source

SQL across a modelled data plane

Best for

Actions and supporting context: save to Notion, open a ticket, read a doc

Numbers you need to trust across one or many sources

Where data is combined

In the model's context window

In a query engine

Data reachable

Whatever each provider exposes as tools

The tables and fields you choose to model

Volume

Bounded by tool output limits and pagination

Full history, multi-month or multi-year

Verifiability

Hard to audit how records were matched

The SQL can be read, rerun and tested

Access control

Per server, per credential

One set of row-level rules across sources

Setup

Paste a server URL and authenticate

Connect sources, then describe and model tables

A simple rule follows from the table. If the request ends in a number, a chart or a ranking, it belongs in SQL. If it ends in a document, a ticket, a message or a lookup of unstructured context, it belongs in MCP. Many good workflows use both in sequence: the agent answers the question with SQL, then uses an MCP server to write the analysis where the team works.

How Upsolve Approaches MCP vs SQL for Data Agents

Many Upsolve customers want their agent to answer questions across tools like GitHub, PostHog and Stripe, and not all of them have a warehouse ready. Upsolve agents could already reach those tools through MCP connections, which work well for actions and context. For analytics, Serguei Balanovich, Upsolve, described the limit in the Upsolve Data Plane demo: "It's going to be very difficult for an agent to create an in-memory database where it can do proper joins between all these different sources, and it's very prone to error."

The Data Plane connects hundreds of sources into one Upsolve data model, with no warehouse required. Connectors include Stripe, PostHog, HubSpot, GitHub, Google and Facebook Ads, Google Sheets, CSVs, BigQuery and ClickHouse, so teams that already have a warehouse can join it with their SaaS tools. The workflow has three steps:

  1. Connect sources. Pick the connectors you need.

  2. Model them together. Add only the tables you need to one data model, then annotate and describe them, so you expose exactly the tables and fields you want.

  3. Query with real SQL. The agent writes SQL across sources for multi-month and multi-year analysis that you can verify.

Because Data Plane tables sit inside the data model, the same row-level security applies to them as to everything else. The demo shows PostHog and Stripe tables added to a project's data model, where the same global data security rules can then be configured on them. Global and custom rules apply to every connected source, so there is no need to expose a source to every user through one shared connection or ask each user to re-authenticate against each source.

MCP stays part of the stack. Upsolve agents connect to MCP servers for actions and supporting context, as described in the companion guide on MCP servers for data agents. If your agent will answer in Claude rather than in your product, the guide on data analysis in Claude with MCP covers that route. The division of labour is the same in both: MCP for doing things, the Data Plane for numbers.

Frequently Asked Questions

Can MCP be used for data analytics?

It works for narrow questions about a single system, such as looking up one customer's subscription status. MCP becomes unreliable when an answer needs many records from several sources, because each tool result returns to the model, which then has to match and aggregate the records in its context window. Tool output limits, such as Claude Code's 25,000-token default, also cap how much history an agent can see in one call. For trend, cohort or revenue analysis across sources, an agent should write SQL that a query engine executes, and use MCP for supporting context and actions.

Do I need a data warehouse to build a data agent across SaaS tools?

No. A warehouse is one way to bring sources together, but federated query engines and data plane products can expose tables from tools like Stripe, PostHog and HubSpot directly to SQL. What you do need is a data model: descriptions of each table, the join keys between sources, and definitions for metrics such as net revenue. A team can start with a handful of SaaS tables, get an agent answering accurately, and introduce a warehouse later if volume or transformation needs grow. The two approaches can also run together.

Why do AI agents make mistakes when joining data from multiple MCP servers?

The model is doing work that a database engine is designed for. It must match identifiers across systems, remove duplicates, handle missing values and sum the right fields, all from text in its context window. Research on tabular reasoning shows accuracy drops when tables contain duplicates or missing values, and models often fail to mention the problem. Large results also get truncated or paginated, so the model may join incomplete data without knowing it. The output still looks precise, which makes these errors hard to spot.

What is a data plane for AI agents?

A data plane is the layer that connects an agent's data sources and executes its queries. It brings tables from warehouses, databases and SaaS applications into one place the agent can query with SQL, under one set of definitions and access rules. The agent decides what to ask, and the data plane does the joining and aggregation. This keeps large intermediate results out of the model's context, makes every answer traceable to a query, and lets row-level security apply consistently across all connected sources.

Does row-level security work across multiple data sources?

It can, if the rules live above the sources. MCP authorisation is defined per server, so each connection carries its own credential and permissions. A rule such as "users from this customer only see their own rows" cannot span several MCP servers by itself. When source tables sit in one data model, row-level rules can filter every query the agent runs, whichever source a table came from. In Upsolve, global and custom rules apply to Data Plane tables in the same way they apply to warehouse tables.

Should I replace my MCP servers with SQL connectors?

No. Keep MCP servers for what they do well: reading documents and tickets, and taking actions such as creating a Notion page or a Linear issue. Move analytical questions, anything that ends in a number, chart or ranking, to SQL over a modelled data plane. In that combination, the agent computes the answer with a verifiable query, then uses an MCP server to publish the analysis where the team will read it.

Sources

  1. Model Context Protocol: Specification, Tools. https://modelcontextprotocol.io/specification/2026-07-28/server/tools

  2. Model Context Protocol: Specification, Authorization. https://modelcontextprotocol.io/specification/2026-07-28/basic/authorization

  3. Anthropic Engineering (Jones and Kelly): Code execution with MCP: Building more efficient agents. https://www.anthropic.com/engineering/code-execution-with-mcp

  4. Anthropic Engineering (Aizawa): Writing effective tools for agents, with agents. https://www.anthropic.com/engineering/writing-tools-for-agents

  5. Wolff and Hulsebos, ACL 2025 Table Representation Learning Workshop: How well do LLMs reason over tabular data, really? https://arxiv.org/abs/2505.07453

  6. Xing et al.: MMTU: A Massive Multi-Task Table Understanding and Reasoning Benchmark. https://arxiv.org/abs/2506.05587

  7. Stripe Documentation: Model Context Protocol (MCP). https://docs.stripe.com/mcp

  8. Trino Documentation: Concepts (catalogs, connectors and multi-catalog queries). https://trino.io/docs/current/overview/concepts.html

  9. Atlan: What Is a Semantic Layer for AI Agents? (reporting Gartner's MCP prediction). https://atlan.com/know/ai-agent/semantic-layer-for-ai-agents/

The Data Plane is part of The Complete Agent Stack series, which covers the ontology, harness, distribution and improvement loop behind accurate data agents. To try cross-source SQL on your own tools, start free at upsolve.ai with 2,000 free AI credits, or talk to our team.

Try Upsolve for Embedded Dashboards & AI Insights

Embed dashboards and AI insights directly into your product, with no heavy engineering required.

Fast setup

Built for SaaS products

30‑day free trial

See Upsolve in Action

Launch customizable dashboards and AI‑powered insights inside your app, fast and with minimal engineering effort. No code.

Follow us

Related Articles

Stop answering the same 10 questions today.

The Platform for Accurate, Reliable, and Trustworthy AI Analytics.

Agent Studio for Data Teams. Encode context. Deploy agents. Deliver clarity.

© 2026 Upsolve AI, Inc.  

|