Give your agents fast access to your data lake
In this post, we combine the fast MCP server from the last post with the near-instantaneous queries on Parquet files from the post before that. This approach is not only fast, but good for data sovereignty and scale. With only 250 lines of code, we get a lightning-fast MCP server that puts Apache DataFusion behind a tool call, so an LLM can do analytics on enterprise-scale Parquet data without a notebook, a warehouse bill, or a single row of raw data entering the context window.
The basic approach is to create an MCP server in Rust, that makes use of the Rust interface to Apache DataFusion, to enable a tool that AI agents can use to query data in Parquet files, using SQL. This will be dramatically faster than using Python with data frames to make the same queries, and allows the analytics to happen close to the data, without ever leaving the premises, and without burning tokens.
You can view the code here.
The basic structure
The MCP server is virtually identical to the last post, except we expose three methods that act on the Parquet data in the data lake:
- tables lists the tables in the data lake directory (which is hard-coded for safety)
- schema takes one table name, and exposes the fields with their types and descriptions
- query takes an SQL query, and acts on one or more tables, returning the results
Note that the query cannot change anything, so the data is safe, and the queries can be restricted by number of rows, or be required to include an aggregation function, to avoid disclosing any information at the personal level (not done in this demo, but easy to add).
Tool definitions
The tool definitions should present no surprises, and are similar to those in the previous post. As in the previous example, we convert errors to the ErrorData format required for the MCP server to correctly act when things go wrong, an extra complexity since DataFusion returns its own error type.
Here is the definition of the query tool, which just takes a query and passes it to a separate function, which tries to run it:
// Tool for running a query on a table
#[tool(description = "Run a query on a table")]
async fn query(
&self,
params: Parameters<QueryRequest>,
) -> Result<Json<QueryResult>, ErrorData> {
let result = run_query(¶ms.0.sql)
.await
.map_err(|e| ErrorData::internal_error(e.to_string(), None))?;
Ok(Json(result))
}
The actual function to run the query is similar to the one in the DataFusion post, with a couple of key changes to make with as part of the MCP server:
// Execute a query, returning one JSON object per row of the result
async fn run_query(q: &str) -> Result<QueryResult> {
// Create a plan to run a SQL query
let ctx = session().await?;
let df = ctx.sql(q).await?;
// Execute and collect the results
let batches = df.collect().await?;
// Write the batches out as a JSON array, one object per row, so they
// can be serialized (raw record batches cannot be serialized)
let mut buffer = Vec::new();
let mut writer = WriterBuilder::new() // DataFusion feature
.with_explicit_nulls(true)
.build::<_, JsonArray>(&mut buffer);
writer.write_batches(&batches.iter().collect::<Vec<_>>())?;
writer.finish()?;
// Read the array back in as rows, and return as a QueryResult structure, defined above
let records =
serde_json::from_slice(&buffer).map_err(|e| DataFusionError::External(Box::new(e)))?;
Ok(QueryResult { records })
}
There are two key differences:
-
We need to convert any serialization errors from Serde errors, so they can be returned to the calling function. This is an artifact of our design decision to make the worker functions return DataFusion errors, rather than standard errors or MCP ones.
-
We need to convert the data that comes back from the DataFusion calls into data structures that we design, so they are serializable using Serde.
Note the import line near the top, that includes Result from DataFusion, not
MCP or the standard library:
use datafusion::error::{DataFusionError, Result};
Wrapping up
This is a promising approach to making enterprise data available to AI, which controls data that goes outside the corporate network. It addresses concerns about sovereignty, token economics, and infrastructure scalability. Tools like this also benefit from reusability that is enabled by the MCP architecture.