docs/deployment/sandboxed-agents.mdx
This guide is for deployments where an agent runs inside an isolated container, subprocess, or remote worker and still needs MCP access. In that setup, the sandbox itself becomes part of your trust boundary.
The core recommendation is simple: use FastMCP as the capability boundary. Run a remote FastMCP server, authenticate the sandbox with short-lived scoped credentials, and keep privileged credentials on the server side.
This pattern is useful when:
If you are building a local desktop integration, STDIO and normal local configuration may be enough. This guide is for cases where the sandbox is isolated enough that secret distribution, credential lifetimes, and privilege boundaries become part of the design.
A desktop MCP client usually runs on a developer's machine and launches local servers with configuration the developer controls. A sandboxed agent is different:
That means convenience patterns that are acceptable locally become risky in sandboxes. Passing a GitHub token, database password, or cloud credentials directly into the sandbox creates a secret distribution problem you do not need to have.
The safer approach is to make your FastMCP server the only component with privileged access and let the sandbox call it over MCP.
Use this shape by default:
flowchart LR
A["Sandboxed agent"] -->|"short-lived token"| B["FastMCP server"]
B --> C["internal APIs"]
B --> D["databases"]
B --> E["other MCP servers"]
The sandbox gets:
The FastMCP server does the privileged work:
The key design rule is simple:
<Tip> Give the sandbox capabilities, not credentials. </Tip>With that boundary in place, the next questions are how the sandbox connects, how the server verifies and authorizes it, and how you design the tools the sandbox is allowed to call.
For sandboxes, prefer a remote HTTP server over a local STDIO server.
STDIO is still excellent for local development, but a remote HTTP server is usually the better production boundary for sandboxed agents because:
This means the sandbox should connect as a client:
from fastmcp import Client
from fastmcp.client.auth import BearerAuth
client = Client(
"https://sandbox-tools.example.com/mcp",
auth=BearerAuth("short-lived-sandbox-token"),
)
And your FastMCP server should run remotely:
from fastmcp import FastMCP
mcp = FastMCP("Sandbox Tools")
if __name__ == "__main__":
mcp.run(transport="http", host="0.0.0.0", port=8000)
For production transport setup, see HTTP Deployment.
For sandboxed agents, it is usually cleaner to issue credentials for the sandbox session than to place long-lived upstream credentials directly inside the container.
In practice, that usually means issuing a short-lived bearer token for each sandbox, run, or tenant and validating it on your FastMCP server with a token verifier.
from fastmcp import FastMCP
from fastmcp.server.auth.providers.jwt import JWTVerifier
auth = JWTVerifier(
jwks_uri="https://auth.example.com/.well-known/jwks.json",
issuer="https://auth.example.com",
audience="sandbox-mcp",
)
mcp = FastMCP("Sandbox Tools", auth=auth)
The token should identify the sandbox's scope. Depending on your system, it may represent a job, a tenant, a run, or a user-authorized session. Useful claims often include:
Avoid shared static tokens across many sandboxes. If one sandbox token leaks, you want the blast radius to be small and the lifetime to be short.
Token verification is only one half of the boundary. Authorization still belongs on the FastMCP server: use scopes, claims, middleware, or custom auth checks to decide which tools and resources that sandbox can actually access.
For example, you can verify the token globally and still require a narrower scope on a specific tool:
from fastmcp import FastMCP
from fastmcp.server.auth import require_scopes
from fastmcp.server.auth.providers.jwt import JWTVerifier
auth = JWTVerifier(
jwks_uri="https://auth.example.com/.well-known/jwks.json",
issuer="https://auth.example.com",
audience="sandbox-mcp",
)
mcp = FastMCP("Sandbox Tools", auth=auth)
@mcp.tool(auth=require_scopes("write:summary"))
def write_summary(content: str) -> str:
return f"Stored summary with {len(content)} characters"
For validation patterns, see Token Verification. For policy enforcement, see Authorization.
The sandbox should not need:
Instead, expose MCP tools that perform privileged work on the server side.
Good sandbox-facing tools tend to look like this:
get_recent_updateswrite_summaryfetch_repo_contextpublish_review_commentThese tools describe the capability the sandbox needs, not the low-level credentialed action required to perform it.
That distinction matters. A tool like write_summary lets the server decide where and how to persist the summary. A tool like run_sql or call_internal_api pushes privilege and policy into the sandbox where they are much harder to control.
Sandboxed agents behave best when those tools are narrow and structured:
from fastmcp import FastMCP
mcp = FastMCP("Sandbox Tools")
@mcp.tool
def write_summary(content: str) -> str:
"""Store the final summary for the current run."""
return f"Stored summary with {len(content)} characters"
@mcp.tool
def publish_review_comment(pr_number: int, body: str) -> str:
"""Queue a review comment for a specific pull request."""
return f"Queued comment for PR #{pr_number}"
These are easier to audit, easier to authorize, and easier for agents to use reliably than a broad catch-all tool like mutate_state(kind: str, payload: dict).
Narrow tools also let you express different policies per tool instead of creating one large privileged escape hatch.
If the sandbox needs access to other MCP servers or internal systems, put FastMCP in front of them instead of forwarding secrets into the sandbox.
This is where proxying becomes useful. Your public-facing FastMCP server can authenticate the sandbox, then forward allowed capabilities to upstream systems with stronger credentials.
Typical examples:
If the upstream system is itself an MCP server, FastMCP's proxy support is a natural fit. See MCP Proxy.
If your sandboxed agent is configured through mcp.json, keep that configuration minimal. Point it at the remote FastMCP server and pass only the values the sandbox actually needs.
{
"mcpServers": {
"sandbox-tools": {
"url": "https://sandbox-tools.example.com/mcp",
"transport": "http"
}
}
}
In many systems, authentication is injected by the launcher or environment rather than hardcoded in mcp.json. That is usually the right tradeoff for sandboxes. Avoid baking long-lived credentials directly into generated config files, and avoid treating mcp.json as the place where secret material should live.
That is all this section needs to do: tell the sandbox where the server lives. Keep auth and secret handling elsewhere.
For configuration details, see MCP.json.
The same few mistakes show up again and again in sandboxed deployments:
Each of these works at first. Each becomes painful once you have multiple tenants, multiple jobs, or an incident that requires revoking access quickly.
Before shipping a sandbox-facing FastMCP server, check these:
If you adopt those defaults, sandbox support stops being a special case and becomes a normal deployment pattern: isolated workers talk to a constrained FastMCP surface, and the server handles the privileged parts centrally.