Put a boundary around your agent's MCP servers

The AgentBound sandbox runs each MCP server in its own container and grants it only the files, variables and hosts you approve. AgentSpy is a research preprint.

Release status

Only AgentBound has released code.

Quick start

This example uses the Python wrapper for the OpenAI Agents SDK, which adds manifest checks and consent prompts to the Docker sandbox.

Prerequisites

  • Python 3.10 or later
  • Docker, installed and running
  • uv (optional)
  • An OpenAI API key for the example agent

Install

uv init
uv add openai-agents
uv add \
  https://github.com/GuardiAgent/python-mcp-sandbox-openai-sdk.git

Walk-through

The example runs the official Filesystem MCP server in the sandbox and asks an agent to list a directory.

import os

from agents import Agent, Runner

from mcp_sandbox_openai_sdk import (
    FSAccess,
    MCPManifest,
    MCPServers,
    Permission,
    Registry,
    SandboxedMCPStdio,
)

manifest = MCPManifest( 
    name="Filesystem Server",
    description="A server that accesses the local filesystem and allows interaction with all sorts of files.",
    registry=Registry.NPM,
    package_name="@modelcontextprotocol/server-filesystem",
    permissions=[
        Permission.MCP_AC_FILESYSTEM_READ,
        Permission.MCP_AC_FILESYSTEM_WRITE,
        Permission.MCP_AC_FILESYSTEM_DELETE,
    ],
)


async def main():
    async with MCPServers(
        SandboxedMCPStdio( 
            manifest=manifest,
            runtime_args=[os.path.abspath("./")],
            runtime_permissions=[FSAccess(os.path.abspath("./"))],
        )
    ) as servers:
        agent = Agent(
            name="MCP Sandbox Test",
            model="gpt-5-mini",
            mcp_servers=servers,
        )

        prompt = f"""
            Read the files in the {os.path.abspath("./")} directory and
            list all the found files and directories of the first level.
            Do not descend recursively.
            Use the provided mcp servers to access the filesystem.
        """
        result = await Runner.run(
            agent,
            input=prompt,
        )
        print(result.final_output)


if __name__ == "__main__":
    import asyncio

    print("Execute Sandbox Demo")
    asyncio.run(main())
  • Lines 14 to 24 declare the manifest. Few servers ship one yet, so you write it yourself or load a JSON file with MCPManifest.from_file(path).
  • Lines 29 to 33 use SandboxedMCPStdio in place of the SDK's MCPServerStdio. It rejects runtime permissions the manifest does not cover, asks the user to approve the rest, and turns the approved ones into mounts, an egress allow list and environment variables of the container.

The consent prompt looks like this:

Runtime Permission: File System Access Permission
Allow access to file system path: .../example (read: True, write: False)
Do you want to give the agent the requested access? (yes/no):

FSAccess is read-only by default, so the prompt shows write: False although the manifest declares write access. If the user answers no, the directory is not mounted.

The security manifest

An MCP server declares the kinds of access it needs in a JSON manifest, modeled on Android app permissions.
{
  "name": "Filesystem Server",
  "description": "A server that accesses the local filesystem and allows interaction with all sorts of files.",
  "registry": "npm",
  "package_name": "@modelcontextprotocol/server-filesystem",
  "permissions": [ 
    "mcp.ac.filesystem.read",
    "mcp.ac.filesystem.write",
    "mcp.ac.filesystem.delete"
  ]
}

The registry (npm or pypi) decides which container image starts. The permissions name kinds of access, not resources: mcp.ac.filesystem.write means the server writes to some directory, and the user grants a specific one at launch.

The policy is default-deny: a capability the manifest does not declare cannot be granted. In the prototype, only capabilities with a runtime permission class are enforced at a fine grain, and a read-write mount also allows deletion.

Capability vocabulary (Table 2 of the AgentBound paper)
Capability
Filesystem
mcp.ac.filesystem.readRead files or directoriesPython SDK: FSAccess
mcp.ac.filesystem.writeWrite or create filesPython SDK: FSAccess(write=True)
mcp.ac.filesystem.deleteDelete files or directories
System
mcp.ac.system.env.readRead environment variablesPython SDK: EnvironmentVariable
mcp.ac.system.env.writeSet environment variables
mcp.ac.system.execExecute OS commands
mcp.ac.system.processList, kill or interact with processes
Network
mcp.ac.network.clientOutgoing network accessPython SDK: DomainPort, HostPort
mcp.ac.network.serverAccept incoming connections
mcp.ac.network.bluetoothUse Bluetooth connections
Peripherals
mcp.ac.peripheral.cameraCapture images or video
mcp.ac.peripheral.microphoneRecord audio
mcp.ac.peripheral.speakerPlay audio
mcp.ac.peripheral.screen.captureScreen capture
Others
mcp.ac.locationAccess location data
mcp.ac.notifications.postShow system notifications
mcp.ac.clipboard.readRead clipboard contents
mcp.ac.clipboard.writeWrite to clipboard

To draft manifests for existing servers, the paper's AgentManifestGen reads a server's source code; responding maintainers judged 80.9% of its manifests correct (§4.1.3). The prompts are in the replication package.

Runtime permissions

Runtime permissions turn the capabilities of a manifest into concrete resources, and the user approves each one at launch. The sandbox does not check how a server uses a resource it was granted.
  • FSAccess(path, container_path=None, read=True, write=False)

    Mounts one file or directory, read-only unless write=True.

    Manifest must declare: mcp.ac.filesystem.read, plus mcp.ac.filesystem.write when write=True

  • EnvironmentVariable(name)

    Copies one variable, such as an API key, from your environment into the container.

    Manifest must declare: mcp.ac.system.env.read

  • DomainPort(domain, port)

    Allows outbound TCP to a domain and port. The domain is resolved to IPv4 addresses once, at startup.

    Manifest must declare: mcp.ac.network.client

  • HostPort(host, port)

    Allows outbound TCP to an IPv4 address and port.

    Manifest must declare: mcp.ac.network.client

Each resource gets its own permission, and the manifest must declare every capability the list uses.

runtime_permissions=[
    FSAccess(os.path.abspath("./docs")),              # read-only
    FSAccess(os.path.abspath("./out"), write=True),   # read-write
    EnvironmentVariable("API_KEY"),
    DomainPort("api.example.com", 443),
]

Container images

The wrapper starts ghcr.io/guardiagent/mcp-sandbox-npm or ghcr.io/guardiagent/mcp-sandbox-pypi, depending on the registry in the manifest. You can also extend the base image into your own runtime image.

The entry point installs the package before it sets the iptables rules, so installation runs without network restrictions. The server then starts as mcpuser, and the rules drop all traffic except loopback, established connections, DNS to the allowed resolvers and outbound TCP to the ALLOWED_EGRESS entries.

Configuration variables of the sandbox container
VariableMeaning
RUNTIMEpypi or npm.
PACKAGEThe MCP server package to install from that registry.
EXEThe command that starts the server. Set automatically unless PRE_INSTALLED is set.
PRE_INSTALLEDSkip installation and run a server already in the image or mounted at /sandbox (set by DevMCPManifest). Requires EXE.
ALLOWED_EGRESShost:port or ip:port entries for outbound TCP, filled from the approved DomainPort and HostPort permissions. Empty by default.
EXTRA_DNSResolvers to allow in addition to those in /etc/resolv.conf.
ALLOW_LOOPBACKLoopback traffic. 1 by default, 0 disables it.

DNS stays reachable. Queries to every resolver in /etc/resolv.conf and EXTRA_DNS are allowed even with an empty ALLOWED_EGRESS, so a server can encode data in lookups. Restrict the resolvers at the host or network level if that matters.

Checklist from our papers

Practices that follow from the AgentBound and AgentSpy results, with the paper sections they draw on.
  1. Deny egress by default

    Leave ALLOWED_EGRESS empty and grant network permissions only to the servers that need them. Expect to write such lists: 83.1% of 296 popular MCP servers needed outbound access.

    AgentBound, §3.3, §4.1.2

  2. Keep credentials out of reach

    Grant single variables with EnvironmentVariable and never mount ~/.ssh or a home directory. In the paper's SSH-key test, the full sandbox kept the key unreadable.

    AgentBound, §4.2, attacks A.1 to A.3

  3. Prefer read-only mounts

    Keep FSAccess at write=False unless the server has to write. A read-write mount also allows deletion.

    AgentBound, §3.1, §3.3

  4. Audit untrusted skills before production

    Run the agent with the skill several times in a disposable VM and check the system calls of the agent and every process it starts. In the AgentSpy paper, deterministic rules flagged 14 of 18 executed skill-injection attacks and raised no alert in the 32 runs without executed injected behavior.

    AgentSpy, §3.5.1, §4.3

  5. Do not rely on the trajectory alone

    The trajectory, the agent's own record, has one entry per action: in the AgentSpy running example, a single step hid 35 connections to Crossref. Keep system call and network records for agents with real access.

    AgentSpy, §3.4.1