MCP Gateway
The MCP Gateway allows agents running in AgentVisor to access external MCP servers through a secure, policy-enforced interface.
This page covers the MCP Gateway - how agents access external MCP servers (like GitHub, Slack, or filesystem tools). For exposing AgentVisor agents as MCP tools to external clients, see MCP Transport.
For an overview of how gateways fit into AgentVisor's architecture, see Gateways. For a comparison with transports, see Transports.
What is MCP?
Model Context Protocol (MCP) is an open standard for connecting AI applications to external tools and data sources. MCP servers expose tools (functions) and resources (data) that AI agents can use.
AgentVisor's Gateway Approach
Unlike traditional MCP integrations where agents connect directly to MCP servers, AgentVisor's MCP Gateway places external MCP servers on the host side, completely isolated from agents:
┌──────────────────────────────────────────────────────────────────────┐
│ Host Environment │
│ ┌────────────────────────────────────────────────────────── ────────┐│
│ │ Guest Sandbox (network=none) ││
│ │ ┌────────────────┐ ┌───────────────────────────────────┐││
│ │ │ Agent Code │ │ Guest Runtime │││
│ │ │ │ │ │││
│ │ │ get_mcp_tools │───────▶│ gRPC to Host │││
│ │ │ call_tool │ │ │││
│ │ └────────────────┘ └───────────────┬───────────────────┘││
│ └────────────────────────────────────────────┼────────────────────┘│
│ │ Unix Socket │
│ ┌────────────────────────────────────────────▼────────────────────┐│
│ │ Host Runtime (MCP Gateway) ││
│ │ ┌───────────────┐ ┌───────────────┐ ┌──────────────────────┐││
│ │ │ Policy Engine │ │ MCP Manager │ │ External MCP Servers │││
│ │ │ │◀─│ │─▶│ • filesystem (stdio) │││
│ │ │ Check access │ │ Route calls │ │ • github (http) │││
│ │ │ per tool │ │ Inject creds │ │ • slack (http) │││
│ │ └───────────────┘ └───────────────┘ └──────────────────────┘││
│ └─────────────────────────────────────────────────────────────────┘│
└──────────────────────────────────────────────────────────────────────┘
The MCP Gateway acts as an intermediary: it is a client to upstream MCP servers and a service provider to agents running inside AgentVisor.
Security Model
The MCP Gateway provides four key security benefits:
1. Host-Side Configuration Isolation
External MCP server configuration (including credentials) lives exclusively on the host:
# agentvisor.yaml - never visible to agent code
mcp:
servers:
- name: github
transport: streamable_http
url: "https://api.githubcopilot.com/mcp/"
credentials:
type: bearer_token
source: env
env_var: GITHUB_TOKEN
The agent can call GitHub tools but never sees the GITHUB_TOKEN. Credentials are injected by the gateway when forwarding requests to the upstream MCP server.
2. Policy Enforcement
The MCP Gateway uses a two-tier policy model that separates coarse server-scope access from fine-grained per-item visibility.
Tier 1 — Server-scope check (coarse gate): Applied once per request before the upstream MCP server is contacted.
Agent calls: get_mcp_tools()
│
▼
Gateway checks: can principal invoke agentvisor:mcp:tool:list?
│
┌───────┴───────┐
▼ ▼
ALLOWED DENIED
│ │
Continue to Return error
per-item check (no tools)
Tier 2 — Per-item check (fine-grained): After the upstream tool/resource list is fetched, each item is checked individually. Items the principal cannot access are silently dropped before the response reaches the agent.
Tool list returned from upstream MCP server:
[delete_repo, list_issues, read_pr]
│
Per-tool policy check for each:
delete_repo → DENIED → dropped
list_issues → ALLOWED → included
read_pr → ALLOWED → included
│
▼
Agent receives: [list_issues, read_pr]
The full set of gateway operations:
| Operation | Tier | MRN Scope | Description |
|---|---|---|---|
agentvisor:mcp:tool:list | Server-scope | mrn:agentvisor:mcp:<server> | Gate entire tool list call |
agentvisor:mcp:tool:read | Per-item | mrn:agentvisor:mcp:<server>/<tool> | Per-tool visibility in list response |
agentvisor:mcp:tool:call | Server-scope | mrn:agentvisor:mcp:<server>/<tool> | Execute a specific tool |
agentvisor:mcp:resource:list | Server-scope | mrn:agentvisor:mcp:<server> | Gate entire resource list call |
agentvisor:mcp:resource:describe | Per-item | mrn:agentvisor:mcp:<server>/resource/<uri> | Per-resource visibility in list response |
agentvisor:mcp:resource:read | Server-scope | mrn:agentvisor:mcp:<server>/resource/<uri> | Read a specific resource |
If you write an explicit deny-by-default policy for MCP, include allow rules for both tiers:
agentvisor:mcp:tool:list— lets the agent call the list endpointagentvisor:mcp:tool:read— controls which individual tools appear in the response
The policies/domain.yml shipped by the langgraph/mcp-agent example template (agentvisor template create langgraph/mcp-agent) is a permissive onboarding example, not a production default — AgentVisor ships with no default policy. As shipped, it grants both tiers to any authenticated principal via the mrn:agentvisor:mcp:.* wildcard. Before production use, replace this wildcard with explicit allow rules scoped to the servers and tools each principal needs.
3. Automatic Discovery
Agents don't need to know which external MCP servers are available. They call get_mcp_tools() and receive all tools they're authorized to use:
from agentvisor.langchain import get_mcp_tools
# Automatically discovers all MCP tools from gateway configuration
tools = get_mcp_tools()
# Returns LangChain StructuredTool instances ready for use
graph = create_react_agent(model, tools)
4. Sandboxed Execution of stdio Servers
stdio MCP servers (e.g. npx -y @modelcontextprotocol/server-filesystem) download and execute arbitrary npm or PyPI packages at launch time. Without isolation, that code would run as a subprocess of the host container — in the host's full trust domain, with access to host environment variables, sockets, and credentials.
AgentVisor runs each stdio server in its own per-server sandbox, using the same isolation technology as the agent sandbox itself. The sandbox is chosen automatically based on the global guest.sandbox mode:
| Sandbox mode | MCP server launcher | Isolation |
|---|---|---|
gvisor | GVisorLauncher | gVisor kernel interception; runs as UID 1000, --network=host for npm/pypi pulls |
docker | DockerLauncher | Docker container with process, kernel, and filesystem hardening; bridge networking |
none | HostForkLauncher | No isolation — unsandboxed subprocess; warns once when the server starts (not on every tool call). Development only. |
Each sandboxed server runs as non-root (UID 1000) inside a dedicated mcp-tools rootfs (see The mcp-tools Sandbox Image below) with ephemeral tmpfs caches for npm/uv package downloads.
The sandbox isolates host filesystem, processes, and credentials — but does not restrict outbound network access from the MCP server. This is a deliberate limitation: gVisor's rootless mode does not support netstack, so --network=host is the only option that allows package managers (npm, uv) to download dependencies at startup.
If you need network constraints on a stdio MCP server, run the server externally and connect via streamable_http or sse transport instead, where external network controls can be applied.
stdio servers share a single connection across all principals — they cannot carry per-principal credentials. The gateway rejects principal_passthrough or token_exchange credentials on stdio servers at startup. Use bearer_token, api_key, or no credentials for stdio transports.
MRN Format
MCP resources use Manetu Resource Names (MRNs) for policy matching. Tools and resources
use different formats — a resource URI is percent-encoded into a single opaque segment
after a literal resource/ marker, since resource URIs can themselves contain : and /
(e.g. file:///path/to/file):
Tool: mrn:agentvisor:mcp:<server>/<tool>
Resource: mrn:agentvisor:mcp:<server>/resource/<percent-encoded-uri>
Examples:
• Tool call to filesystem server's read_file:
mrn:agentvisor:mcp:filesystem/read_file
• Resource read from github server (uri "repo://owner/repo/contents/path"):
mrn:agentvisor:mcp:github/resource/repo%3A%2F%2Fowner%2Frepo%2Fcontents%2Fpath
Policy Configuration
Define which MCP tools agents can access through the gateway in your policy:
resources:
# Allow all filesystem tools
- name: mcp-filesystem
selector:
- "mrn:agentvisor:mcp:filesystem/.*"
group: "mrn:agentvisor:resourcegroup:allowed"
# Allow specific GitHub tools only
- name: mcp-github-readonly
selector:
- "mrn:agentvisor:mcp:github/get_.*"
- "mrn:agentvisor:mcp:github/list_.*"
group: "mrn:agentvisor:resourcegroup:allowed"
# Block all other MCP operations
- name: mcp-default
selector:
- "mrn:agentvisor:mcp:.*"
group: "mrn:agentvisor:resourcegroup:denied"
resource-groups:
- mrn: "mrn:agentvisor:resourcegroup:allowed"
name: allowed
policy: *policy-allow
- mrn: "mrn:agentvisor:resourcegroup:denied"
name: denied
policy: *policy-deny
Environment Variables for stdio Servers
You can pass environment variables into a stdio MCP server's process with the env map. Values support $VAR and ${VAR} expansion against host environment variables, so secrets never need to be hard-coded in agentvisor.yaml:
mcp:
servers:
- name: my-server
transport: stdio
command: ["uvx", "my-mcp-server"]
env:
SERVER_URL: https://data.example.com # static literal
API_TOKEN: ${MY_SERVER_TOKEN} # expanded from host env at startup
DB_DSN: "postgres://${DB_USER}:${DB_PASS}@db.internal/app"
Expansion happens host-side before the process launches, so it works uniformly across all sandbox modes (gvisor, docker, none) — the gVisor and Docker launchers start from an empty environment and cannot inherit host variables via process inheritance.
| Syntax | Effect |
|---|---|
literal-value | Passed as-is |
$VAR or ${VAR} | Replaced with the host env var value |
$$ | Emits a literal $ |
When a referenced variable is not set in the host environment, AgentVisor logs a warning naming the variable and substitutes an empty string — startup continues. For secrets that require injection into HTTP requests rather than process env, prefer credentials: resolvers (see Credential Injection), which are more purpose-built for that use case.
External MCP Server Types
The MCP Gateway supports three transport types for connecting to external MCP servers:
| Transport | Description | Use Case |
|---|---|---|
stdio | Local process via stdin/stdout | Local tools (filesystem, git) |
sse | Server-Sent Events over HTTP | Legacy remote servers |
streamable_http | Bidirectional HTTP streaming | Modern remote servers (recommended) |
The mcp-tools Sandbox Image
stdio servers run inside a purpose-built mcp-tools rootfs that provides node/npx (for npm-based servers) and python/uvx (for PyPI-based servers). AgentVisor resolves the rootfs using the following priority order (first match wins):
mcp.tools_rootfs_path(AGENTVISOR_MCP_TOOLS_ROOTFS_PATH) — an explicit path to a pre-extracted directory on disk. Use this when you manage the rootfs yourself.- Baked rootfs at
/opt/agentvisor/mcp-tools-rootfs— present in images built withagentvisor build --bundle-mcp-toolsor extracted locally byagentvisor serve --bundle-mcp-tools. This is the recommended approach for production deployments. - On-demand image pull of
mcp.tools_image(AGENTVISOR_MCP_TOOLS_IMAGE) — pulled and cached automatically at host runtime startup, before any request is served, when neither of the above is available and at least one configured MCP server usesstdiotransport. Defaults to the version-matchedghcr.io/manetu/agentvisor/agentvisor-mcp-tools:<version>.
Fresh installs that don't use --bundle-mcp-tools fall through to option 3 and pull the image at startup — no manual setup required.
In gVisor mode the rootfs is extracted to disk and bind-mounted into the gVisor sandbox. In Docker mode the mcp-tools image is run directly as a container — no rootfs extraction step is needed.
Pre-installing Packages (prepull)
By default, a stdio server like npx -y @modelcontextprotocol/server-filesystem downloads its package on every cold start. In air-gapped environments or for faster startup, you can pre-install packages into the mcp-tools image at launch time using mcp.prepull in mav-agent-config.yaml:
# mav-agent-config.yaml
mcp:
prepull:
- manager: npm
package: "@modelcontextprotocol/server-filesystem"
- manager: pypi
package: mcp-server-fetch
| Field | Values | Effect |
|---|---|---|
manager: npm | any npm package name | npm install -g <package> run at image prep time |
manager: pypi | any PyPI package name | uv tool install <package> run at image prep time |
How it works:
agentvisor serve/run/execreadsmcp.prepullfrommav-agent-config.yamland pre-builds a customizedmcp-toolsrootfs before starting the runtime — no--bundle-mcp-toolsflag required.agentvisor build --bundle-mcp-toolsbakes the (unpopulated) base rootfs into the production image. To pre-install packages at build time, combine this with a CI step that readsmcp.prepull.- Package names are validated against a strict allowlist regex to prevent shell-injection attacks via Dockerfile
RUNlines.
For hosts without outbound internet access: combine mcp.prepull (pre-installs the package during image prep) with agentvisor build --bundle-mcp-tools (bakes the populated rootfs into the image) to produce a fully self-contained image that needs no network access at runtime.
Cross-references:
- CLI flags:
--bundle-mcp-tools/--mcp-tools-imageinagentvisor build - Configuration:
mcp.tools_rootfs_path/mcp.tools_image
Credential Injection
External MCP servers often require authentication. The MCP Gateway supports several credential injection methods:
| Type | Description | Connection Model |
|---|---|---|
bearer_token | Bearer token from environment variable | Shared (static) |
api_key | Custom header with API key | Shared (static) |
principal_passthrough | Forward caller's JWT to upstream MCP server | Per-principal (pooled) |
token_exchange | RFC 8693 token exchange for scoped tokens | Per-principal (pooled) |
MCP Gateway credential injection uses the same credential resolver system as HTTP proxy credential brokering. For details on how credentials are protected, see Credential Brokering.
Confidential Client Support
Token exchange supports confidential client authentication per RFC 6749 Section 2.3.1 (client_secret_basic). When client_id is configured, the gateway includes HTTP Basic authentication with the token exchange request:
credentials:
type: token_exchange
exchange_url: "https://auth.example.com/oauth/token"
audience: "service.example.com"
client_id: "agentvisor-client"
client_secret_env_var: "CLIENT_SECRET" # Recommended over inline secret
See the Configuration Reference for full details.
Connection Management
The MCP Gateway uses a hybrid connection model based on credential type:
-
Static credentials (
bearer_token,api_key, no auth): Connections to external MCP servers are created at startup and shared across all principals. These are efficient for servers with organization-wide credentials. -
Principal-bound credentials (
principal_passthrough,token_exchange): Each principal needs their own connection with their specific credentials. These connections are managed in an LRU (Least Recently Used) pool to balance resource usage with multi-tenant access.
Principal A ──┐
├──▶ Pool ──▶ External Server (per-principal connections)
Principal B ──┘
All Principals ──▶ Shared Client ──▶ External Server (static credentials)
The pool automatically:
- Creates connections on first use for each principal
- Caches connections for reuse across requests
- Evicts least-recently-used connections when full
- Closes idle connections after a configurable timeout
See the Configuration Reference for pool tuning options.
SDK Functions
The Python SDK provides two APIs for accessing external MCP servers through the gateway:
LangChain Integration (Recommended)
from agentvisor.langchain import get_mcp_tools
tools = get_mcp_tools() # All authorized MCP tools
tools = get_mcp_tools(server_name="filesystem") # From specific server
Low-Level API
from agentvisor import mcp
# List tools available through the gateway
tools = mcp.list_tools()
tools = mcp.list_tools(server_name="filesystem")
# Call a tool on an external MCP server
result = mcp.call_tool("filesystem", "read_file", {"path": "/data/file.txt"})
# List and read resources
resources = mcp.list_resources()
content = mcp.read_resource("filesystem", "file:///data/file.txt")
See the MCP SDK Reference for complete API documentation.
Troubleshooting
Tools Not Appearing
If get_mcp_tools() returns empty:
- Check gateway configuration: Verify servers are defined in
mcp.servers[]inagentvisor.yaml - Check server-scope policy: Ensure
agentvisor:mcp:tool:listis allowed for the caller's principal - Check per-item policy: If the server-scope check passes but no tools appear, per-item checks (
agentvisor:mcp:tool:read) may be denying all tools. Check that your policy allowsagentvisor:mcp:tool:readfor the specific tool MRNs. - Check server health: The external MCP server may have failed to connect
Enable debug logging:
AGENTVISOR_LOG_LEVEL=debug agentvisor serve ...
Tool Calls Denied
If tool calls return allowed: false:
- Check MRN: Verify the tool MRN matches your policy selector
- Check policy order: Deny rules may match before allow rules
- Check principal: The caller may not have access
External MCP Server Errors
For stdio servers:
- Check sandbox mode: with
--sandbox=gvisoror--sandbox=docker, the server runs inside anmcp-toolssandbox. Verify that themcp-toolsimage/rootfs is available (see The mcp-tools Sandbox Image). - Missing packages in the sandbox: if the server depends on a package that isn't pre-installed in the
mcp-toolsimage, it will be downloaded at runtime (requires internet access). For offline/air-gapped hosts, usemcp.prepullinmav-agent-config.yamlto pre-install it. - Command not found: verify that
commandinagentvisor.yamluses the correct invocation for the sandbox (e.g.["npx", "-y", "<package>"]for npm servers,["uvx", "<package>"]for PyPI servers). - With
--sandbox=none: the server runs as a host subprocess without isolation. Ensure the command is on$PATHon the host.
For HTTP-based servers (sse, streamable_http), diagnose connectivity directly rather than guessing:
- Run a staged connectivity check against the real endpoint, using the same config file the runtime loads:
This runsagentvisor mcp probe github --config agentvisor.yaml
connect→initialize(MCP handshake) →tools/listand reports the first stage that fails. - Read the failed stage's error classification code:
connection_refused,dns_error,tls_error,auth_error,timeout,credential_error, or theconnection_errorcatch-all. See Troubleshooting: Interpreting GET /ready for what each code means and how it's derived. - Check
GET /readyfor the live picture from the running server: a server that failed to connect at startup, or that degrades at runtime, appears undermcp.failed_endpoints[]with the same classification code, aphase(startupvsruntime), and asincetimestamp. - For the verbatim host-side error — hostnames, ports, and the underlying Go error, which the probe output and
GET /readydeliberately omit — enable debug logging for the MCP components:AGENTVISOR_LOG_LEVEL=info,mcp=debug,mcp.client=debug agentvisor serve ./my-agent
See Troubleshooting Guide: MCP Gateway Issues for connection pool exhaustion, idle timeout, and credential resolution failures.
Related Documentation
- Gateways Overview - How gateways fit into AgentVisor's architecture
- API Transports - How external clients access agents (opposite direction)
- MCP Transport Guide - Exposing agents as MCP tools to external clients
- MCP SDK Reference - Complete Python SDK API documentation
- Configuration Reference: MCP Gateway - Server, credential, and stdio sandbox configuration
- CLI Reference: agentvisor build -
--bundle-mcp-toolsand--mcp-tools-imageflags - Credential Brokering - How credentials are protected
- Troubleshooting Guide - Pool exhaustion, idle timeout, credential resolution, and
GET /ready