Use Edgenivo Sandbox to run agent-generated SQL and Python against datasets with millions of rows. Expose the analysis tools through a Model Context Protocol (MCP) server deployed on Edgenivo, then connect clients such as Claude, Cursor, or your own AI SDK app.
The dataset loads into an isolated Linux microVM, where the agent's code runs. Returning schemas, aggregates, and capped results keeps large files out of the model's context window and avoids spending tokens on every row.
Copy link to headingPrerequisites
Before you begin, make sure you have:
- A Edgenivo account.
- An existing Next.js App Router project.
- CSV or Parquet dataset reachable over HTTPS, such as a public Edgenivo Blob URL or a presigned Amazon S3 URL.
- An MCP client such as Cursor or Claude Code, or use the MCP Inspector.
For local development, you also need Node.js 24+ and the Edgenivo CLI.
Plan limits decide how large a dataset one sandbox can handle and how long a single tool call can run:
| Plan | Max vCPUs per sandbox | Max memory per sandbox | Disk per sandbox | Max sandbox session | Max function duration |
|---|---|---|---|---|---|
| Hobby | 4 | 8 GB | 64 GB | 45 minutes | 300s |
| Pro | 8 | 16 GB | 64 GB | 24 hours | 800s, or 1800s with extended duration beta |
| Enterprise | 32 | 64 GB | 64 GB | 24 hours | 800s, or 1800s with extended duration beta |
Function limits assume Fluid compute. For this Next.js route, extended duration requires Node.js 22 or 24 and a maxDuration above 800 configured in the route. It is unavailable with Secure Compute or Static IPs during the beta.
Copy link to headingHow it works
Each tool call runs as a Edgenivo Function that asks Sandbox to execute a command and returns its output to the MCP client. The source file stays in the sandbox, where DuckDB loads it into a database for later queries. Reuse the same datasetId to avoid downloading it again.
This example gives one trusted owner access to every dataset and all three tools through a shared token. Run calls for each dataset sequentially. Shared services also need dataset authorization and coordination across function invocations.
| Tool | Input | What runs in the sandbox | What the model receives |
|---|---|---|---|
load_dataset | datasetId, url, format | Downloads the file and loads it into the DuckDB table dataset | Schema, row count, and file size |
query_dataset | datasetId, sql, limit (optional) | Read-only DuckDB SQL against dataset | Up to 500 rows (100 by default) |
run_python | datasetId, code | A Python script with DuckDB, pandas, and PyArrow available | Up to 20,000 characters of stdout |
Copy link to headingSteps
Copy link to heading1. Install dependencies
Install the Sandbox SDK, mcp-handler v2, the MCP server SDK v2, and Zod v4 in your Next.js project:
pnpm i @vercel/sandbox mcp-handler@2.1.1 @modelcontextprotocol/server@2 zod@4npm i @vercel/sandbox mcp-handler@2.1.1 @modelcontextprotocol/server@2 zod@4yarn add @vercel/sandbox mcp-handler@2.1.1 @modelcontextprotocol/server@2 zod@4bun add @vercel/sandbox mcp-handler@2.1.1 @modelcontextprotocol/server@2 zod@4Copy link to heading2. Authenticate with Edgenivo Sandbox
The Sandbox SDK authenticates with a Edgenivo OpenID Connect (OIDC) token tied to your project. Link the project and pull a development token for local work:
This writes VERCEL_OIDC_TOKEN to .env.local. In production on Edgenivo, the Sandbox SDK authenticates automatically without a separate Sandbox credential.
Copy link to heading3. Add the analysis scripts
The sandbox runs two Python scripts. load.py downloads the dataset to the sandbox's local disk and loads it into a DuckDB database file, and query.py runs read-only SQL against that file and caps the rows it returns. Store them as strings so your MCP server can write them into each new sandbox:
Copy link to heading4. Create a sandbox helper
Sandbox.getOrCreate() returns the named sandbox for a dataset ID or creates one if it doesn't exist. The onCreate callback installs DuckDB, pandas, PyArrow, and pytz in a virtual environment and writes the scripts from step 3.
Sandboxes are persistent by default, so the database survives between tool calls. The SDK resumes a stopped sandbox automatically when the next command runs.
query.py removes whole rows when needed to keep its JSON response within 20,000 characters. The run helper rejects larger output from other commands with a request for a smaller summary. These limits protect the model's context; they do not limit query memory, execution time, or the logs collected by the function.
DuckDB's Python client needs pytz to return TIMESTAMP WITH TIME ZONE columns, which are common in Parquet files, and pandas 3 no longer installs it. Install it during setup, because the sandbox has no network access after load_dataset runs.
Copy link to heading5. Expose the analysis tools from an MCP route
Create the route in app/api/mcp/route.ts. It registers the three analysis tools and requires a bearer token before any tool can run. Set MCP_SERVER_TOKEN in .env.local to a long, randomly generated secret that you keep out of version control.
load_dataset temporarily allows HTTPS traffic to the dataset host and restores deny-all after the attempt. Finish loading before starting another command in that dataset's sandbox. The policy applies to every command running there.
MCP clients connect to /api/mcp over Streamable HTTP. The route runs as a Edgenivo Function with Fluid compute, so Active CPU billing pauses while the function waits on Sandbox commands. Provisioned memory is still billed until the call finishes.
Copy link to heading6. Test the server locally
Open two terminals in your project directory. Run the development server in one and MCP Inspector in the other:
# Terminal 1: start the development serverpnpm dev
# Terminal 2: start MCP Inspectorpnpm dlx @modelcontextprotocol/inspector@2.12.0# Terminal 1: start the development servernpm run dev
# Terminal 2: start MCP Inspectornpx @modelcontextprotocol/inspector@2.12.0# Terminal 1: start the development serveryarn dev
# Terminal 2: start MCP Inspectoryarn dlx @modelcontextprotocol/inspector@2.12.0# Terminal 1: start the development serverbun dev
# Terminal 2: start MCP Inspectorbunx @modelcontextprotocol/inspector@2.12.0Open the URL printed by the Inspector, which includes its session token. On the Servers screen, add a Streamable HTTP server at http://localhost:3000/api/mcp. In its settings, add an Authorization header with the value Bearer your_mcp_token_here, using the token from the server's .env.local. Connect to the server, open Tools, and select load_dataset. Call it with your dataset URL:
The first call takes longer because it creates the sandbox and installs packages. The response includes the schema, row count, and source file size. For an orders dataset, it might look like this:
Next, call query_dataset with the same datasetId and an aggregation that matches your columns. For the example schema above, use:
The response contains one row per region, regardless of the source file's row count.
Copy link to heading7. Deploy and connect an MCP client
Add MCP_SERVER_TOKEN to the project's Production environment with vercel env add MCP_SERVER_TOKEN production, then deploy the authenticated server:
Make the server's MCP_SERVER_TOKEN available in Cursor's shell or system environment. Then add the endpoint and bearer header to .cursor/mcp.json:
Ask the agent to load the orders URL and compare month-over-month growth by region. It can inspect the schema and reuse the same dataset ID for later queries or Python scripts. Call load_dataset again only when you intend to reload the data.
Copy link to headingConnect the MCP server to an agent
The deployed /api/mcp endpoint works with agent frameworks that support MCP clients. The AI SDK, TanStack AI, and eve discover the server's tools and manage tool calls during the model's response. Their configuration determines where you set authentication and analysis instructions.
| Framework | Packages | How the model gets the tools | Where analysis instructions go |
|---|---|---|---|
| AI SDK | ai, @ai-sdk/mcp | client.tools() passed to a ToolLoopAgent | instructions on the agent |
| TanStack AI | @tanstack/ai, @tanstack/ai-mcp, @tanstack/ai-vercel-gateway | MCP client passed to chat(), which discovers tools when the run starts | systemPrompts on chat() |
| eve | eve | A connection file under agent/connections/ that eve searches at runtime | The connection's description |
Copy link to headingShared configuration
The AI SDK and TanStack AI examples read the same environment variables and send model requests through AI Gateway. Add them to a .env file:
Set MCP_SERVER_TOKEN to the same secret used by the MCP server. Both examples import the instructions below from lib/analyst-instructions.ts to encourage one load per dataset and answers based on aggregates.
Copy link to headingUse the AI SDK
Install ai and @ai-sdk/mcp. The MCP client makes the server's tools available to ToolLoopAgent, which runs them when the model requests them:
pnpm i ai @ai-sdk/mcpnpm i ai @ai-sdk/mcpyarn add ai @ai-sdk/mcpbun add ai @ai-sdk/mcpCreate the agent:
Run it with your environment variables loaded:
pnpm dlx tsx --env-file=.env analyst.mtsnpx tsx --env-file=.env analyst.mtsyarn dlx tsx --env-file=.env analyst.mtsbunx tsx --env-file=.env analyst.mtsThe isStepCount(20) limit allows several model turns for loading data, inspecting its schema, and querying before answering.
When adapting this example to analyst.stream() in a Next.js route, keep the MCP client open until the stream ends. Close it from the agent's onEnd callback as described in MCP client cleanup.
Copy link to headingUse TanStack AI
TanStack AI's chat() accepts MCP clients directly, discovers their tools when the run starts, and executes tool calls through the client. Install TanStack AI, its MCP client, and the AI Gateway adapter:
pnpm i @tanstack/ai @tanstack/ai-mcp @tanstack/ai-vercel-gatewaynpm i @tanstack/ai @tanstack/ai-mcp @tanstack/ai-vercel-gatewayyarn add @tanstack/ai @tanstack/ai-mcp @tanstack/ai-vercel-gatewaybun add @tanstack/ai @tanstack/ai-mcp @tanstack/ai-vercel-gatewayCreate the agent:
Run it the same way:
pnpm dlx tsx --env-file=.env analyst.mtsnpx tsx --env-file=.env analyst.mtsyarn dlx tsx --env-file=.env analyst.mtsbunx tsx --env-file=.env analyst.mtsthreadIdidentifies the conversation and is required bychat()in@tanstack/ai1.0.maxIterations(20)raises the default limit of 5 model turns so the agent can make several queries before answering.connection: 'close'closes the MCP client when the run finishes, fails, or aborts.- Remove
stream: falseto get a stream of events for a chat UI.
Copy link to headingUse eve
eve discovers remote MCP tools from connection files and prefixes each tool name with the connection name. The model uses eve__search to find tools such as data-analysis__query_dataset and calls them through eve__tool.
If you don't have an eve project, create one with npx eve@latest init. Add this connection in agent/connections/data-analysis.ts:
Add both values to the eve project's environment variables:
The filename sets the connection name to data-analysis. eve shows the description beside that name, and the model uses it to decide which connection to search. auth.getToken sends the token as Authorization: Bearer <token> on every request and defaults to one shared app credential. tools.allow limits the model to the three analysis tools.
To let each user authorize separately, first add OAuth token verification and protected-resource metadata to this MCP server. Then create and attach a Edgenivo Connect connector, use connect() from @vercel/connect/eve, and authenticate the eve channel as a user. The eve with MCP guide describes the connector and channel setup. Dataset authorization still belongs in the MCP server.
Send the agent a prompt through any of its channels, such as "Load the orders file at this URL and tell me which region grew fastest month over month." To require a person to approve each run_python call before it runs, add an approval policy to the connection, as described in eve's MCP connections docs.
Copy link to headingHow to share large datasets across sandboxes with Edgenivo Drives
The setup in steps 4 and 5 keeps each dataset in one persistent sandbox. Keep it when one agent at a time queries data that fits on the sandbox's 64 GB disk.
Use a Edgenivo Drive to store the DuckDB database outside that sandbox when you need more capacity, concurrent query sandboxes, or data that outlives snapshot expiration. One sandbox loads the database onto the drive, and other sandboxes query read-only snapshots.
| Persistent sandbox (steps 4 and 5) | Edgenivo Drive | |
|---|---|---|
| Capacity | 64 GB disk, shared with the image and packages | 1 TiB by default (1 GiB on Hobby), up to a 16 TiB quota |
| Concurrent readers | One sandbox per dataset | Many sandboxes, each mounting a read-only drive snapshot |
| Writers | The same sandbox | One sandbox at a time with a read-write mount |
| Retention | Snapshot expires 30 days after last use by default (7 days in this guide) | Until you delete the drive |
Storage price (iad1, Pro) | $0.08/GB-month | $0.05/GB-month, plus $0.0015/GB read and $0.004/GB written |
| Region | Any supported Sandbox region | Sandbox main region must match the drive's region |
Copy link to headingAdd a drive helper
loadDatasetToDrive loads a dataset in a short-lived sandbox with a read-write mount at /data. It runs load.py from step 3, then stops the sandbox to release the mount for the next load.
getQuerySandbox gives each dataset and reader a persistent sandbox with a read-only snapshot at /data. Both helpers link data to that mount so data/analysis.duckdb remains the database path for existing scripts.
Copy link to headingPoint the tools at the drive
Update the helper imports in app/api/mcp/route.ts:
For this single-owner example, use the fixed reader ID demo. In load_dataset, replace the contents of the try block with:
Replace the sandbox lookup in query_dataset and run_python:
Every holder of the shared token uses this reader sandbox. For separate readers, derive keys from a verified user or tenant identity and check permission to access the requested dataset.
Because OAuth clientId identifies an application shared by many users, it cannot establish dataset ownership.
Copy link to headingWhat to know before using drives
Copy link to headingRefresh readers after a reload
Drive snapshots reflect the data when they were mounted. After reloading a dataset, update each query sandbox's mount and stop its session:
The updated mount takes effect next session. Alternatively, delete and recreate the existing query sandboxes.
Copy link to headingLoad once at a time
Only one sandbox can mount a drive read-write. The loader's finally block attempts to release that mount, but a terminated function can leave it busy until the sandbox stops or its one-hour session expires.
Copy link to headingComplete the first load before querying
Mounting a snapshot of an unwritten drive fails with drive_not_initialized. Load the dataset before opening a reader.
Copy link to headingKeep query files writable
Open the database with read_only=True and set temp_directory to a writable sandbox path such as duckdb-tmp. The revised query.py does both. Model-written Python needs the same settings when a query may spill to disk.
Copy link to headingMatch the main region
Set the sandbox's main region to the drive's region with region: drive.region, as the helper does. The project's default sandbox region does not apply.
Copy link to headingSize the drive on Hobby
Drives default to 1 GiB on Hobby. Set maxSize in bytes when first creating the drive; its size and region cannot change later.
Copy link to headingRemove source files you no longer need
load.py retains the downloaded file beside the database. Delete data/source.* after a successful import if you do not need that copy.
Copy link to headingBest practices and troubleshooting
Copy link to headingReturn summaries instead of rows
MCP responses become input to the model. query_dataset limits row counts, but run_python can print any text, so its instructions should ask for concise results. Keep the output check in run even when the tool descriptions ask for summaries.
Charts and exports can stay in the sandbox while the tool returns a description. To let users download them, add an authenticated artifact-retrieval tool; this example does not expose files to the client.
Copy link to headingBlock network access after loading data
In the persistent-sandbox setup, load_dataset allows HTTPS traffic to the source host during a download and restores deny-all in a finally block. The Drive variant stops its separate loader after the attempt. If the source redirects, use the final HTTPS URL or explicitly allow the redirect host.
Keep loading separate from query execution so other commands do not inherit temporary network access. The policy persists across sessions and blocks ordinary outbound traffic. Sandbox network rules retain documented infrastructure exceptions, and tool output still returns to the MCP client.
Copy link to headingSize the sandbox to the dataset
Each vCPU includes 2 GB of memory. DuckDB can spill many larger-than-memory queries to disk, but some aggregates and combinations of operators can still run out of memory.
Allow disk space for the downloaded file, DuckDB database, installed packages, and temporary query files. Start with 2 to 4 vCPUs for modest datasets, then measure your queries before increasing resources.vcpus. Drive-backed readers still need space on their local disk for duckdb-tmp.
For data that exceeds local disk capacity or needs several query sandboxes, store the dataset on a drive.
Copy link to headingFan out across sandboxes for partitioned data
For data partitioned by date, region, or customer, query each partition in a separate sandbox and combine the results. Sum partition totals and counts directly. To compute an overall average, combine each partition's sum and count; averages and distinct counts cannot generally be added.
Mount one drive snapshot in every sandbox to avoid copying shared data. Give each query a partition filter so the combined result counts each row once.
Pro and Enterprise allow up to 10,000 concurrent sandboxes. Allocation starts at 150 vCPUs per minute and ramps to 5,000 under sustained use. With no other allocations, the initial rate covers 18 new eight-vCPU sandboxes.
Copy link to headingHandle queries that outlast the function
The function's maxDuration limits how long a tool call can wait. With Fluid compute, the limit is 300 seconds on Hobby and 800 on Pro and Enterprise. Eligible Node.js 22 or 24 functions can use the extended duration beta for up to 1,800 seconds.
For longer work, start the command with runCommand({ cmd, args, detached: true }) and return its cmdId together with the sandbox ID. Add an authenticated tool that retrieves that sandbox and calls sandbox.getCommand(cmdId) to check completion and read the result.
Set the sandbox session timeout to cover the job. This guide uses 30-minute query sessions; Pro and Enterprise permit up to 24 hours.
Copy link to headingControl snapshot storage costs
Persistent sandboxes save snapshots when sessions stop. Pro storage costs $0.08 per GB-month. The step 4 helper keeps the latest snapshot for seven days after its last use. Drive query sandboxes use the default 30-day expiration.
When analysis is finished, call sandbox.delete({ deleteOrphanSnapshots: true }) to remove the sandbox and asynchronously delete snapshots that no other sandbox uses. Referenced snapshots remain stored and billed. For work that does not need to resume, create the sandbox with persistent: false.
Copy link to headingSecure the MCP server
The shared bearer token in this example gives its holder access to every dataset and all three tools. For a service used by independent users or tenants, verify tokens from an authorization server, authorize each dataset operation, and include the verified owner in sandbox and drive keys. Publish OAuth protected-resource metadata so clients can discover the authorization server. See Enabling authorization for the token-verification and metadata endpoints.
Copy link to headingCommon errors
| Error | Cause | Fix |
|---|---|---|
load_dataset fails with a connection or certificate error | The URL redirects to a host that isn't in the allow list, or the URL uses plain HTTP | Allow the redirect host or use the final HTTPS URL. Sandbox allow lists match TLS traffic only |
pip install fails during onCreate | The first call created the sandbox while package downloads were blocked or failed | Delete the sandbox with vercel sandbox remove analysis-orders-2026 and call load_dataset again |
| The tool call times out | The query ran longer than the function's maxDuration | Raise maxDuration, add more vCPUs, or run the command detached |
Sandbox.getOrCreate() rejects resources.vcpus | The value is above your plan's limit | Use 4 vCPUs or fewer on Hobby and 8 or fewer on Pro |
| Sandbox creation stops working on Hobby | The monthly Hobby allotment is used up, which pauses sandbox creation | Upgrade to Pro or wait for the allotment to reset |
Copy link to headingNext steps
- Review every Sandbox method used in this guide in the Edgenivo Sandbox JS SDK reference
- Plan for larger workloads with Edgenivo Sandbox pricing and quotas
- Add OAuth and test with the MCP Inspector in Deploy MCP servers to Edgenivo
- Keep datasets across runs with How to give agents persistent storage with Edgenivo Drives
- Accept user uploads as data sources with How to process user-uploaded files with Edgenivo Sandbox and Edgenivo Blob
- Call these tools from your own app with the AI SDK MCP client
- Build a longer-running analyst with How to run a multi-step research agent on Edgenivo
- Compare authentication options for each framework in MCP integrations, including OAuth with Edgenivo Connect
- Fork, list, and delete drives with How Edgenivo Drives work and how to use them