Skip to main content

MCP Gateway

The MCP Gateway allows agents running in AgentVisor to access external MCP servers through a secure, policy-enforced interface.

Terminology Note

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:

OperationTierMRN ScopeDescription
agentvisor:mcp:tool:listServer-scopemrn:agentvisor:mcp:<server>Gate entire tool list call
agentvisor:mcp:tool:readPer-itemmrn:agentvisor:mcp:<server>/<tool>Per-tool visibility in list response
agentvisor:mcp:tool:callServer-scopemrn:agentvisor:mcp:<server>/<tool>Execute a specific tool
agentvisor:mcp:resource:listServer-scopemrn:agentvisor:mcp:<server>Gate entire resource list call
agentvisor:mcp:resource:describePer-itemmrn:agentvisor:mcp:<server>/resource/<uri>Per-resource visibility in list response
agentvisor:mcp:resource:readServer-scopemrn:agentvisor:mcp:<server>/resource/<uri>Read a specific resource
Policy authoring for two-tier filtering

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 endpoint
  • agentvisor: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 modeMCP server launcherIsolation
gvisorGVisorLaunchergVisor kernel interception; runs as UID 1000, --network=host for npm/pypi pulls
dockerDockerLauncherDocker container with process, kernel, and filesystem hardening; bridge networking
noneHostForkLauncherNo 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.

Network access

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 + principal-bound credentials

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.

SyntaxEffect
literal-valuePassed 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:

TransportDescriptionUse Case
stdioLocal process via stdin/stdoutLocal tools (filesystem, git)
sseServer-Sent Events over HTTPLegacy remote servers
streamable_httpBidirectional HTTP streamingModern 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):

  1. 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.
  2. Baked rootfs at /opt/agentvisor/mcp-tools-rootfs — present in images built with agentvisor build --bundle-mcp-tools or extracted locally by agentvisor serve --bundle-mcp-tools. This is the recommended approach for production deployments.
  3. 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 uses stdio transport. Defaults to the version-matched ghcr.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.

gVisor vs Docker

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
FieldValuesEffect
manager: npmany npm package namenpm install -g <package> run at image prep time
manager: pypiany PyPI package nameuv tool install <package> run at image prep time

How it works:

  • agentvisor serve/run/exec reads mcp.prepull from mav-agent-config.yaml and pre-builds a customized mcp-tools rootfs before starting the runtime — no --bundle-mcp-tools flag required.
  • agentvisor build --bundle-mcp-tools bakes the (unpopulated) base rootfs into the production image. To pre-install packages at build time, combine this with a CI step that reads mcp.prepull.
  • Package names are validated against a strict allowlist regex to prevent shell-injection attacks via Dockerfile RUN lines.
Offline/air-gapped deployments

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:

Credential Injection

External MCP servers often require authentication. The MCP Gateway supports several credential injection methods:

TypeDescriptionConnection Model
bearer_tokenBearer token from environment variableShared (static)
api_keyCustom header with API keyShared (static)
principal_passthroughForward caller's JWT to upstream MCP serverPer-principal (pooled)
token_exchangeRFC 8693 token exchange for scoped tokensPer-principal (pooled)
info

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:

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:

  1. Check gateway configuration: Verify servers are defined in mcp.servers[] in agentvisor.yaml
  2. Check server-scope policy: Ensure agentvisor:mcp:tool:list is allowed for the caller's principal
  3. 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 allows agentvisor:mcp:tool:read for the specific tool MRNs.
  4. 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:

  1. Check MRN: Verify the tool MRN matches your policy selector
  2. Check policy order: Deny rules may match before allow rules
  3. Check principal: The caller may not have access

External MCP Server Errors

For stdio servers:

  1. Check sandbox mode: with --sandbox=gvisor or --sandbox=docker, the server runs inside an mcp-tools sandbox. Verify that the mcp-tools image/rootfs is available (see The mcp-tools Sandbox Image).
  2. Missing packages in the sandbox: if the server depends on a package that isn't pre-installed in the mcp-tools image, it will be downloaded at runtime (requires internet access). For offline/air-gapped hosts, use mcp.prepull in mav-agent-config.yaml to pre-install it.
  3. Command not found: verify that command in agentvisor.yaml uses the correct invocation for the sandbox (e.g. ["npx", "-y", "<package>"] for npm servers, ["uvx", "<package>"] for PyPI servers).
  4. With --sandbox=none: the server runs as a host subprocess without isolation. Ensure the command is on $PATH on the host.

For HTTP-based servers (sse, streamable_http), diagnose connectivity directly rather than guessing:

  1. Run a staged connectivity check against the real endpoint, using the same config file the runtime loads:
    agentvisor mcp probe github --config agentvisor.yaml
    This runs connectinitialize (MCP handshake) → tools/list and reports the first stage that fails.
  2. Read the failed stage's error classification code: connection_refused, dns_error, tls_error, auth_error, timeout, credential_error, or the connection_error catch-all. See Troubleshooting: Interpreting GET /ready for what each code means and how it's derived.
  3. Check GET /ready for the live picture from the running server: a server that failed to connect at startup, or that degrades at runtime, appears under mcp.failed_endpoints[] with the same classification code, a phase (startup vs runtime), and a since timestamp.
  4. For the verbatim host-side error — hostnames, ports, and the underlying Go error, which the probe output and GET /ready deliberately 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.