MCP GuardMCP Guard

How It Works

Understanding MCP Guard's security architecture and code execution flow.

Architecture Overview

┌─────────────────────────────────────────────────────────────────────┐
│                          Your IDE (Cursor, Claude Code, etc.)        │
│                                                                      │
│  ┌──────────────────┐    ┌─────────────────────────────────────┐   │
│  │   AI Agent       │───▶│         MCP Guard Server            │   │
│  │                  │    │  ┌─────────────────────────────┐    │   │
│  │  Generates code  │    │  │   Code Validation Layer     │    │   │
│  │  to call MCP     │    │  │   • Blocks eval(), require() │    │   │
│  │  tools           │    │  │   • Blocks process access    │    │   │
│  └──────────────────┘    │  └─────────────────────────────┘    │   │
│                          │                 │                    │   │
│                          │                 ▼                    │   │
│                          │  ┌─────────────────────────────┐    │   │
│                          │  │   Cloudflare Worker Isolate  │    │   │
│                          │  │   • Network: deny by default │    │   │
│                          │  │     allowlist optional       │    │   │
│                          │  │   • No filesystem access     │    │   │
│                          │  │   • No env variables         │    │   │
│                          │  │   • Memory/CPU limited       │    │   │
│                          │  └─────────────────────────────┘    │   │
│                          │                 │                    │   │
│                          │                 ▼ (Service Binding)  │   │
│                          │  ┌─────────────────────────────┐    │   │
│                          │  │   MCP Server (e.g. GitHub)   │    │   │
│                          │  │   • API keys stored safely   │    │   │
│                          │  │   • Network access allowed   │    │   │
│                          │  └─────────────────────────────┘    │   │
│                          └─────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────────┘

Security Layers

Layer 1: Code Validation (Pre-Execution)

Before any code runs, MCP Guard validates it for dangerous patterns:

// BLOCKED patterns:
require('fs')           // No require()
eval('code')            // No eval()
process.env.SECRET      // No process access
import('module')        // No dynamic imports
__dirname               // No path access
new Function('code')    // No Function constructor
child_process           // No system execution

Layer 2: V8 Isolate Sandboxing (Runtime)

Code executes in a Cloudflare Workers V8 isolate:

  • Fresh environment per execution
  • No Node.js APIs (fs, child_process, etc.)
  • No global state persists between runs
  • Memory limited (configurable, default 128MB)
  • CPU limited (configurable execution timeout)

Layer 3: Network Isolation

// Worker configuration
{
  // Default: deny all outbound requests
  globalOutbound: null
 
  // Optional (per MCP): when network enabled, globalOutbound set to FetchProxy Service Binding
  // FetchProxy enforces host allowlist and optional localhost access
}

By default, the isolate has zero network capability. It cannot:

  • Make HTTP requests
  • Open WebSocket connections
  • Access any external resources

Three-Layer Network Access Enforcement (when network enabled via settings):

  1. Layer 1: Runtime Isolation - Dynamic workers ALWAYS have globalOutbound: null. globalThis.fetch does NOT exist natively.

  2. Layer 2: Module-Level Fetch Wrapper - When network enabled, generateWorkerCode() injects a wrapper that:

    • Uses Object.defineProperty to define globalThis.fetch
    • Adds X-MCPGuard-Allowed-Hosts and X-MCPGuard-Allow-Localhost headers
    • Delegates to env.FETCH_PROXY.fetch() Service Binding
  3. Layer 3: FetchProxy Service Binding - Runs in parent Worker (has network access):

    • Reads allowlist from headers
    • Enforces rules (exact match, wildcard *.github.com, localhost blocking)
    • Returns 403 JSON error if blocked, otherwise proxies via parent's fetch()

If you enable Network Access for a specific guarded MCP, MCP Guard uses this three-layer approach to allow controlled fetch() while enforcing a host allowlist (and optional localhost access).

Layer 4: Binding-Based Access Control

The only way to interact with MCP tools is through Service Bindings:

// Inside the isolate, code can only call:
await mcp.search_repositories({ query: 'cloudflare' })
 
// This calls a Service Binding, NOT a network request
// The binding is the ONLY way to communicate with MCPs

Layer 5: Credential Isolation

  • API keys are never exposed to the isolate
  • Credentials are managed by the MCP Guard server
  • MCP bindings handle authentication transparently
  • Even if code tried to access credentials, they don't exist in scope

Code Execution Flow

Step by Step

Here's exactly what happens when the AI generates and executes code:

  1. User Request: AI calls call_mcp with TypeScript code

  2. Validation: MCP Guard validates code for dangerous patterns

    • If blocked patterns found → Error returned immediately
    • If safe → Continue to execution
  3. Worker Generation: MCP Guard generates a Worker script containing:

    • User code embedded directly
    • MCP binding stubs (functions that call env.MCP.callTool())
    • Console output capture
    • Metrics tracking
  4. Isolate Creation: Wrangler spawns a fresh V8 isolate:

    • globalOutbound: null (default: no network)
    • optional per-MCP allowlist may enable outbound fetch()
    • Memory and CPU limits applied
    • Service Binding injected for MCP access
  5. Code Execution: User code runs in the isolate:

    • Can call MCP tools via generated stubs
    • Cannot access network, filesystem, or environment
    • Console output captured
  6. MCP Tool Calls: When code calls an MCP tool:

    await mcp.search_repositories({ query: 'test' })
    • Stub calls env.MCP.callTool('search_repositories', input)
    • Service Binding receives the call (runs in parent Worker)
    • Parent Worker calls Node.js RPC server via localhost
    • RPC server uses MCP SDK to call actual MCP process
    • Results flow back through the chain
  7. Results Return:

    • Console output returned to AI
    • Metrics recorded (execution time, MCP calls made)
    • Isolate disposed (no state persists)

Service Bindings Architecture

The key to MCP Guard's security is the Service Binding pattern:

┌─────────────────────────────────┐
│     Dynamic Worker (Isolated)    │
│     • globalOutbound: null (default) │
│     • fetch() denied by default      │
│     • optional allowlist enables fetch() │
│                                  │
│  await mcp.toolName(args)        │
│         │                        │
│         ▼ (Service Binding call) │
└─────────────────────────────────┘

              │ (Native RPC, no network)

┌─────────────────────────────────┐
│     MCPBridge (Parent Worker)    │
│     • Has network access         │
│     • Receives binding calls     │
│                                  │
│  fetch('http://localhost/rpc')   │
│         │                        │
└─────────────────────────────────┘

              │ (localhost only)

┌─────────────────────────────────┐
│     Node.js RPC Server           │
│     • MCP SDK Client             │
│     • Manages credentials        │
│                                  │
│  client.callTool(name, args)     │
│         │                        │
└─────────────────────────────────┘

              │ (stdio)

┌─────────────────────────────────┐
│     MCP Server Process           │
│     (e.g. GitHub MCP)            │
└─────────────────────────────────┘

Key benefits:

  • Dynamic workers use native Service Binding calls (not fetch)
  • True network isolation for user code
  • Only parent Worker can reach the RPC server
  • Credentials never exposed to user code

Why This Architecture?

Traditional MCP (Insecure)

AI → MCP Server → Direct access to everything
                  • Filesystem ⚠️
                  • Network ⚠️
                  • Environment ⚠️
                  • System ⚠️

MCP Guard (Secure)

AI → Code → Validation → Isolate → Service Binding → MCP
            ✅ Blocked    ✅ No      ✅ Controlled
               patterns      access    communication

The isolation boundary ensures that even if the AI generates malicious code:

  1. Dangerous patterns are blocked before execution
  2. The isolate cannot access system resources
  3. Communication is limited to approved MCP bindings
  4. Credentials remain hidden from the executing code

Limitations

OAuth MCPs Cannot Be Guarded

Important Limitation

MCP servers that require OAuth authentication cannot be guarded by MCP Guard.

Why? OAuth authentication creates a trust relationship between the IDE and the MCP provider:

┌─────────────────┐     OAuth Flow      ┌─────────────────┐
│  Cursor/Claude  │◄──────────────────►│  OAuth Provider  │
│      IDE        │    (tokens bound    │  (Atlassian,    │
│                 │     to IDE)         │   Google, etc)  │
└────────┬────────┘                     └─────────────────┘

         │ Tokens stored in IDE

┌─────────────────┐
│    MCP Server   │
│  (uses IDE's    │
│   OAuth token)  │
└─────────────────┘

When MCP Guard intercepts communication:

  • It cannot access the IDE's OAuth tokens
  • The MCP server rejects requests without valid tokens
  • Re-authentication through MCP Guard would require client registration with every OAuth provider

Solution: Use OAuth MCPs directly through your IDE without guarding. MCP Guard detects OAuth requirements and displays a "Cannot Guard" indicator.

MCPs that work with MCP Guard:

  • API key authenticated MCPs (GitHub with PAT, etc.)
  • Command-based local MCPs (filesystem, git, etc.)
  • URL-based MCPs with header authentication
  • Any MCP that doesn't require OAuth