- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- Build your first harnessUpdated
- Host optionsNew
- Send and show mediaUpdated
- Connect clientsUpdated
- Write a client in any languageNew
- Access and tenantsNew
- Run in the terminalUpdated
- Deploy a harnessUpdated
- Send inputs safelyUpdated
- Run a tool without a model
- Durable sessionsUpdated
- Run background work on workersNew
- List, rename, and fork sessionsUpdated
- Lifecycle and retentionNew
- Run side effects onceUpdated
- Wait for a personNew
- Build on the session logUpdated
- Control how a turn endsUpdated
- Store settings per threadUpdated
- Compact a long sessionUpdated
- Fork and reset a threadUpdated
- One log for a session treeUpdated
- Write a pluginUpdated
- Track usage and costUpdated
- Ask before risky tool callsUpdated
- Give your agent coding toolsUpdated
- Sandbox and extend the coding toolsUpdated
- Run the coding tools for many threadsNew
- Undo a turnUpdated
- Build a coding agentUpdated
- Delegate to coding agentsUpdated
- Switch between agent profilesUpdated
- Auth and connectorsUpdated
- Share a thread between peopleUpdated
- Connect model providersUpdated
- Build your own UIUpdated
- Code mode in a harnessUpdated
- Work until a goal is metUpdated
- Run agents from a harnessUpdated
- Hand off a conversationNew
- Add skillsUpdated
- Use MCP serversUpdated
- Give your agent a computerNew
- Use from any MCP clientUpdated
- Use a harness from an editor (ACP)Updated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Keep Keys on the ServerNew
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Tools
A host tool runs in your app, with your database and secrets. The agent in the sandbox calls it. The tool code does not run in the sandbox.
Pass your server tools to chat(). TanStack AI bridges them into the sandbox. The agent calls them like any MCP tool, and each tool runs on your host.
Bridge a tool into the sandbox#
- Define a server tool, the same as for any chat() call.
- Pass it in tools, next to withSandbox.
import { chat, toolDefinition } from '@tanstack/ai'
import { GROK_CLI_INSTALL_COMMAND, grokBuildText } from '@tanstack/ai-grok-build'
import {
createSecrets,
defineSandbox,
defineWorkspace,
githubRepo,
withSandbox,
} from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
import { z } from 'zod'
// Lives on your host. The sandbox never sees it.
const todos = [{ id: '1', userId: 'u1', title: 'Fix the login bug' }]
const getTodos = toolDefinition({
name: 'getTodos',
description: 'List the todos of a user.',
inputSchema: z.object({ userId: z.string() }),
}).server(async ({ userId }) => todos.filter((t) => t.userId === userId))
const repoSandbox = defineSandbox({
id: 'repo-agent',
provider: dockerSandbox({ image: 'node:22' }),
workspace: defineWorkspace({
source: githubRepo({ repo: 'owner/app' }),
setup: [GROK_CLI_INSTALL_COMMAND],
secrets: createSecrets({ XAI_API_KEY: process.env.XAI_API_KEY ?? '' }),
}),
})
const stream = chat({
threadId: 'thread-1',
adapter: grokBuildText('grok-build'),
messages: [{ role: 'user', content: 'Fix the bugs in the todos of user u1.' }],
tools: [getTodos],
middleware: [withSandbox(repoSandbox)],
})The agent can now call getTodos. The tool reads todos on your host and sends the result into the sandbox.
How it works#
The agent always has its own native tools (Bash, file edits, search). They run on the sandbox filesystem. Your chat() tools go over a host-side MCP tool proxy:
- The agent calls the tool by name, like any MCP tool.
- The call crosses the sandbox boundary to your host.
- The tool's execute() runs on your host, with its database handle, secrets, and closures.
- The result goes back into the sandbox as the tool output.
execute() never goes into the sandbox. Only the call and the result cross the boundary.
A random bearer token for each run protects the bridge. Only the agent of this run has that token, so only it can call your tools.
Note
This page is about your own tools, bridged back to your server. To give the agent third-party MCP servers that it calls directly, declare them on the workspace. See Provisioning. For server tools in general, see Server Tools.
Make sure the sandbox can reach the bridge#
The bridge is an HTTP endpoint that the sandbox calls back to. So the sandbox must be able to connect to your server.
| Where it runs | Host the sandbox calls | Setup |
|---|---|---|
| Local process | localhost | None. |
| Docker container (dockerSandbox) | host.docker.internal | None. |
| Docker Sandboxes (sbxSandbox) | host.docker.internal | A deny or ask allowlist must include localhost. sbxSandbox() adds it when it writes a real allowlist. |
| Deployed server (production) | A URL your provisioner advertises | Provide a bridge provisioner. |
| Cloud sandbox, driven from your laptop | Your laptop, which has no public URL | Tunnel the bridge with withNgrokBridge. |
Local process and Docker container#
The sandbox runs on the same machine as your server. It reaches it on localhost (local process) or host.docker.internal (Docker container). Bridged tools need no extra setup. If the HTTP listener cannot start, bridge startup rejects with the original Node.js error.
Docker Sandboxes (sbxSandbox)#
The guest still calls host.docker.internal. The host HTTP proxy changes that host to localhost before it checks sbx policy. If you write a deny or ask allowlist, allow localhost (or let sbxSandbox() add it). denyNetwork alone does not write that allowlist.
A deployed server (production)#
The default provisioner (nodeHttpBridgeProvisioner) binds loopback and tells the sandbox to call 127.0.0.1 (host.docker.internal for Docker). It does not read your public host from the request. So a remote sandbox cannot reach it, even from a deployed server.
Provide your own ToolBridgeProvisioner from a chat middleware with provideToolBridgeProvisioner(ctx, provisioner). Its provision() returns the bridge with a url the sandbox can reach, for example your server's public URL. withNgrokBridge from @tanstack/ai-sandbox/ngrok is a working example. Every call still needs the bearer token for the run, so a public endpoint is safe.
The Cloudflare package ships its own provisioner, so the edge setup needs no extra step. See Cloudflare.
A cloud sandbox, driven from your laptop#
A cloud provider (Daytona, Vercel, Sprites, and the others) runs a remote VM. In local dev, that VM cannot call your localhost, and your laptop has no public URL.
The @tanstack/ai-sandbox/ngrok subpath tunnels the bridge through ngrok:
- Install the optional peer dependency @ngrok/ngrok:
npm i @ngrok/ngrokpnpm add @ngrok/ngrokyarn add @ngrok/ngrokbun add @ngrok/ngrok- Set NGROK_AUTHTOKEN.
- Add withNgrokBridge after withSandbox(...):
import { chat } from '@tanstack/ai'
import { grokBuildText } from '@tanstack/ai-grok-build'
import { withSandbox } from '@tanstack/ai-sandbox'
import { withNgrokBridge } from '@tanstack/ai-sandbox/ngrok'
import { repoSandbox } from './sandbox'
import { getTodos } from './tools'
chat({
threadId: 'thread-1',
adapter: grokBuildText('grok-build'),
messages: [{ role: 'user', content: 'Fix the bugs in the todos of user u1.' }],
tools: [getTodos],
// Cloud provider in local dev: tunnel the bridge so the remote sandbox can
// reach it. Local process and Docker do not need this.
middleware: [withSandbox(repoSandbox), withNgrokBridge],
})withNgrokBridge is for local dev only. In production your deployed server is already reachable, so you ship without it.