AWS Builder Center
Give Your Agent a Data Lake and Interactive Charts

Give Your Agent a Data Lake and Interactive Charts

An MCP server that hands an agent a sandboxed Python workbench over Athena and returns interactive, provenance-stamped charts right in the chat.

Giving an agent a data lake it can actually explore — and charts a human can actually use

Most "chat with your data" demos stop at the same place: the agent writes some SQL, gets back a wall of numbers, and summarizes them in prose. That is useful for a one-shot question. It falls apart the moment the work gets real — when the agent needs to discover what's in the lake before it can ask a good question, and when the human needs to see the result, not read a paragraph about it.
The MCP server I'll walk through here is built around those two failure points. It's a self-contained Model Context Protocol (MCP)  server that gives an agent a sandboxed Python environment wired to an AWS data lake — Amazon Athena  and the AWS Glue Data Catalog  over Amazon S3  — and returns not just answers but interactive, provenance-stamped Plotly  charts rendered right in the chat.
Here it is answering a plain-English question against a well-production table — discovering the schema, running the query, and handing back an interactive chart in one turn:
The MCP server answering a natural-language question against a data lake and returning an interactive chart
This post is about two things that fall out of that design:
  1. Rich agent ↔ data-lake interaction — how the server lets an agent discover and analyze a lake the way a competent analyst would, instead of guessing at table names and firing off blind queries.
  2. A better user experience — why handing the chart back as a live, interactive UI beats both "here are the numbers in text" and "let the agent redraw a picture in its own plotting tool."
If you've read my earlier post on giving an agent an HPC cluster through MCP tools , this is the same philosophy pointed at analytics: the agent only ever calls well-shaped tools; the tools hold the credentials and do the privileged, dangerous work.

1. Rich agent ↔ data-lake interaction

The problem with giving an agent a bare SQL connection

The naive version of "agent + data lake" is a single run_sql(query) tool. It sounds clean, but it strands the agent. It has no idea which databases exist, what the tables are called, which columns are partition keys, or how much data a careless SELECT * will scan. So it hallucinates table names, writes queries that fail, and burns turns recovering. Worse, when a query does run, the raw boto3  / AWS SDK for pandas  surface is a minefield of IAM and workgroup configuration that has nothing to do with the actual analysis.
This server treats the agent like an analyst who just got handed credentials to an unfamiliar warehouse. It gives it the three things an analyst needs: a map, a safe workbench, and a house style for asking questions.

A map: discovery before analysis

The server ships a describe_datalake tool and two documentation resources — a data-catalog overview and a writing-analysis-code guide — plus an analysis-guidance prompt. These aren't decoration. MCP lets a server expose resources  and prompts  as first-class citizens alongside tools , so the agent can orient itself before it writes a single query. It can read which databases are queryable, learn the conventions, and only then form a plan.
That ordering — discover, then analyze — is what separates a tool call that works on the first try from one that fails three times on TABLE_NOT_FOUND.

A safe workbench: a real Python sandbox, not a query box

Here's the key design choice. The core tool, run_analysis, doesn't take a SQL string. It takes Python code, and runs it inside an Amazon Bedrock AgentCore  Code Interpreter  sandbox:
1
2
3
df = query_athena("SELECT region, SUM(revenue) rev FROM sales GROUP BY 1")
fig = px.bar(df, x="region", y="rev")
publish_chart(fig, title="Revenue by region")
Giving the agent a Turing-complete workbench instead of a single query is what makes rich interaction possible. In one tool call the agent can run several queries, join and reshape results in pandas , compute a derived metric, decide the next query based on what the last one returned, and emit multiple charts. That is the actual shape of analysis — iterative, multi-step, conditional — and a bare run_sql tool can't express it.
The sandbox is primed before the agent's code ever runs. A Python preamble is prepended to every execution and does the unglamorous work the agent shouldn't have to think about:
  • Injects two helpers. query_athena(sql, database=None) runs a query and returns a DataFrame; publish_chart(figure, title) emits a chart. The agent writes analysis, not plumbing.
  • Enforces least privilege, invisibly. Every Athena call is routed through a managed workgroup  with the CTAS fallback disabled, so queries can't quietly try to write to a bucket the sandbox role can't reach, or demand Glue permissions it doesn't have. The guardrails hold whether or not the model follows instructions.
  • Captures provenance automatically. Every query_athena call records its SQL, the Athena query-execution ID , row count, and bytes scanned. The agent gets this for free — it never has to remember to track where a number came from.

Session reuse: the lake stays warm across a conversation

The sandbox session is cached per conversation. The first run_analysis in a chat pays a one-time cost — spinning up the interpreter and pip installing the AWS SDK for pandas and Plotly — and every follow-up reuses the warm session. That means a DataFrame computed in one turn can still be in memory for the next, and the back-and-forth of real exploration — "now break that down by month," "filter to the top 5 and re-plot" — flows naturally instead of starting from cold each time.
The net effect: the agent interacts with the lake the way a skilled human would — look around first, run a query, react to it, run another, build something up — and the dangerous parts (IAM, workgroups, result-bucket routing) are handled for it rather than exposed to it.

2. A richer user experience than text — or redrawn plots

Now the second half: what the human gets back. There are three ways an agent can hand a chart-worthy result to a user, and they are not equal.

Option A — text alone

The agent runs the query and types out "Revenue was highest in US-East at $4.2M, followed by EU-West at $3.1M…". For two rows, fine. For a trend across 30 days, a distribution, or anything with more than a handful of points, prose is a lossy compression of the data. The user can't see the shape, can't spot the outlier, can't hover to read an exact value. The thing that makes a chart a chart is gone.

Option B — the agent redraws the plot in its own tool

Better: the agent takes the numbers and reconstructs a chart using the chat client's native plotting — an artifact, a generated image, a code block the client renders. This is where most setups land today, and it has real problems:
  • A second lossy hop. The agent has to re-serialize the data into another tool's format. Numbers get rounded, rows get dropped to fit a token budget, and the chart is now a redrawing of the data rather than the data itself.
  • No provenance. The picture floats free of the query that produced it. If a stakeholder asks "where did this number come from?", the answer is buried in a tool call three messages up — if it's anywhere at all.
  • Inconsistent and unstyled. Every redraw is a fresh improvisation. Colors, branding, and axis conventions drift from chart to chart.

Option C — return a live, interactive UI (what this does)

run_analysis returns its result bound to an MCP App : a self-contained, interactive UI the host renders inline in the chat. The MCP UI extension lets a server ship an HTML/JS bundle addressed by a ui:// URI; a tool points its metadata at that bundle, and the host renders the UI with the tool's structured result. Here that UI is a chart carousel.
Why this is strictly better than Option B:
The real figure travels, not a redrawing. The sandbox builds a Plotly figure and serializes it with fig.to_json(). That exact figure — every data point — rides back in the tool's structured content and is handed to the carousel, which renders it with the real Plotly library. There is no second lossy hop and no re-plotting. What the human sees is what the analysis produced, fully interactive: hover for values, zoom, pan, toggle series.
Provenance is attached to the picture. Every chart carries the SQL, the Athena query-execution ID, row count, and bytes scanned that produced it — and the carousel renders a "Query provenance" panel right under the chart. The human can see exactly which query drew the bar they're looking at. The chart isn't a claim about the data; it's an auditable view of it. This is the thing text and redrawn plots both throw away, and it's what makes a chart trustworthy enough to forward to someone who wasn't in the conversation.
Consistent, branded styling with zero agent effort. The preamble registers a Plotly template  — a colorblind-safe, validated palette with a subtle brand mark — and makes it the default and embeds it on every published figure. Every chart from every run looks like it came from the same system, and the agent never has to think about color or layout. Compare that to Option B, where styling is re-improvised on every redraw.
Big results stay usable instead of overwhelming. Query result tables can be large. The server caps what it ships inline to the first page and persists the full result in S3, addressed by its own resource URI. When the user clicks "Next" in the table view, the carousel asks the host to read the next page over MCP — host-proxied, so the UI never makes a direct network call and the sandbox's deny-by-default content security policy  stays locked down. The user gets to page through thousands of rows interactively without any of it bloating the chat transcript or the model's context.
One result, many views, in a single frame. Charts and per-query tables land in the same carousel; the user arrows left/right (or uses the keyboard) through every figure and every result table from the run. It's one coherent artifact in the chat, not a scattered trail of images and number dumps.

How it fits together

Data flow: the agent calls run_analysis(python); the AgentCore Code Interpreter sandbox runs the code against Athena/Glue over the S3 data lake, captures provenance, and persists figures and tables to S3; structured content is handed to an MCP App chart carousel rendered in the chat, which pages large tables back from S3 over MCP
The agent gets a workbench rich enough to explore a lake like an analyst. The human gets back the real, interactive, provenance-stamped result — not a paragraph about it, and not a lossy redrawing. The two goals reinforce each other: because the sandbox captures provenance and builds real figures as a side effect of normal analysis, the rich UI costs the agent nothing to produce.
That's the whole idea — move the richness to both ends of the conversation at once, and let the plumbing in the middle stay invisible.

Related reading

Any opinions in this article are those of the individual author and may not reflect the opinions of AWS.
Enjoyed reading this content? Let the author know!

Your likes, comments, shares, and saves help creators reach more builders.

Loading recommendations

Loading article