Skip to main content
Glama

mcp-proxy

A hardening MCP proxy that sits in front of one or more upstream MCP servers and exposes only the tools a given profile is allowed to see and call.

One config file describes your real servers (GitHub, filesystem, Slack, …) and the profiles (reviewer, implementer, ci-bot, …). Each agent launches its own copy of the proxy with --profile <name> — or, in serve mode, a single shared HTTP server maps each connection to a profile via auth — and gets a filtered, enforced view of those servers, saving context tokens and preventing dangerous tool calls by construction.


Why mcp-proxy?

Official MCP servers expose all of their tools to every agent. The client fetches tools/list and injects every tool's schema into the prompt on every turn, burning context tokens. And a tool that's visible is a tool that can be called — there is no hard boundary.

mcp-proxy fixes both problems at once:

  • Token savings — a profile only advertises the tools you explicitly allow, so only those schemas ever enter the agent's context.

  • Hard guardrail — a tool that isn't allowed is neither listed nor callable: even a hallucinated call is rejected at execution time, not just hidden from the menu.


Related MCP server: Mavryn

Benefits

Benefit

How it helps

🔒 Fail-closed guardrail

block wins; unknown tools default to denied. Visibility and callability are kept in sync.

📉 Token savings

Filtered tools/list means smaller prompts and cheaper, more focused sessions.

👥 One config, many agents

Reviewer, implementer, and CI bot share the same servers block but get different profiles via --profile.

🧩 Multi-server aggregation

Merge several upstreams (stdio + HTTP) behind a single MCP endpoint.

🔐 Secrets stay out of the repo

${VAR} placeholders + .env; the loader fails fast on a missing variable.

♻️ Resilient

Auto-reconnect with exponential backoff; live tools/list_changed updates are re-filtered and propagated downstream.

🛡️ Argument validation

tools/call arguments are validated against the upstream inputSchema before forwarding.

📊 Observable

--verbose emits structured JSON-lines logs with per-request correlation ids; the shared server also exposes Prometheus /metrics.

🌐 Shared server mode

serve runs one Streamable HTTP server for many agents; per-connection auth maps tokens/headers to profiles.

🏷️ Collision-safe

Tools that share a name across servers are auto-prefixed (github__read_file), others keep bare names.


How it works

Architecture

flowchart TB
    subgraph agents["🤖 Agents (MCP clients)"]
        direction LR
        A1["reviewer agent<br/><code>--profile reviewer</code>"]
        A2["implementer agent<br/><code>--profile implementer</code>"]
    end

    subgraph proxy["mcp-proxy — one stdio process per agent"]
        direction TB
        D1["stdio transport"]
        D2["tool filter<br/>(allow/block · globs + regex)"]
        D3["call-time guardrail<br/>+ argument validation"]
        D4["upstream registry<br/>(discovery · reconnect · list_changed)"]
    end

    subgraph up["Upstream MCP servers"]
        direction LR
        U1["filesystem<br/>(stdio)"]
        U2["github<br/>(HTTP)"]
        U3["slack<br/>(HTTP)"]
    end

    A1 -->|"stdin/stdout"| D1
    A2 -->|"stdin/stdout"| D1
    D1 --> D2 --> D3 --> D4
    D4 -->|"spawn"| U1
    D4 -->|"connect"| U2
    D4 -->|"connect"| U3

Each agent spawns the proxy as a child process over stdio. The proxy connects to every upstream listed in the selected profile, fetches each tools/list, applies the profile's allow/block rules, and re-exposes only the surviving tools.

Request flow

sequenceDiagram
    autonumber
    participant A as Agent
    participant P as mcp-proxy
    participant U as Upstream MCP server

    A->>P: tools/list
    P->>U: tools/list (every upstream in profile)
    U-->>P: full tool set
    P->>P: filter + collision resolve
    P-->>A: allowed tools only

    A->>P: tools/call (allowed tool)
    P->>P: guardrail re-check<br/>+ schema validation
    P->>U: forward call
    U-->>P: result
    P-->>A: result

    A->>P: tools/call (blocked tool)
    P-->>A: ❌ rejected with error

    U-->>P: notifications/tools/list_changed
    P->>U: re-fetch tools/list
    P->>P: re-filter
    P-->>A: notifications/tools/list_changed

Filter decision

A tool is allowed only if it survives this precedence chain:

flowchart LR
    T["tool name"] --> B{"matches a<br/><code>block</code> pattern?"}
    B -- "yes" --> DENY["🔒 DENY"]
    B -- "no" --> A{"matches an<br/><code>allow</code> pattern?"}
    A -- "yes" --> OK["✅ ALLOW"]
    A -- "no" --> D["fallback:<br/>server <code>default</code><br/>→ profile <code>default</code><br/>→ <code>block</code>"]
    D --> F{"fallback is <code>allow</code>?"}
    F -- "yes" --> OK
    F -- "no" --> DENY

block always wins. Patterns are globs (read_*, {get,list}_*) or regexes (/.*delete.*/i). A server omitted from a profile exposes none of its tools.


Example: three profiles, measured live

The same proxy driven through three profiles, run against the real @modelcontextprotocol/server-filesystem upstream (14 tools). A second filesystem instance stood in for the HTTP GitHub server so the demo needs no token — per-server filtering behaves identically for any upstream.

# mcp-proxy.yaml (demo)
version: 1
servers:
  filesystem:
    type: stdio
    command: npx
    args: ["-y", "@modelcontextprotocol/server-filesystem", "C:/data"]
  github:                     # HTTP in real life; filesystem stand-in in this demo
    type: http
    url: https://api.github.com/mcp
    headers: { Authorization: "${GITHUB_TOKEN}" }

profiles:
  reviewer:
    default: block
    servers:
      filesystem:
        allow: ["read_file", "list_directory", "search_files", "directory_tree", "get_file_info"]
      github:
        block: ["**"]          # GitHub fully disabled for this agent

  implementer:
    default: allow
    servers:
      filesystem:
        block: ["/.*delete.*/i", "remove_*", "edit_file", "write_file"]
      github: {}               # all GitHub tools allowed

  noTools:
    default: block
    servers:
      filesystem: { block: ["**"] }
      github: { block: ["**"] }

Measured over a live tools/list handshake:

Profile

Tools exposed

tools/list payload

~tokens

reviewer

5

2,926 chars

~732

implementer

26

15,762 chars

~3,941

noTools

0

2 chars

~1

Tokens use a ~4 chars/token heuristic; the real saving is the schema surface the agent re-loads into context on every turn.

Tools each profile actually received:

  • reviewer (read-only, GitHub blocked): read_file, list_directory, directory_tree, search_files, get_file_info

  • implementer (deny-list, GitHub allowed): filesystem__read_file, github__read_file, filesystem__read_text_file, github__read_text_file, filesystem__read_media_file, github__read_media_file, filesystem__read_multiple_files, github__read_multiple_files, filesystem__create_directory, github__create_directory, filesystem__list_directory, github__list_directory, filesystem__list_directory_with_sizes, github__list_directory_with_sizes, filesystem__directory_tree, github__directory_tree, filesystem__move_file, github__move_file, filesystem__search_files, github__search_files, filesystem__get_file_info, github__get_file_info, filesystem__list_allowed_directories, github__list_allowed_directories, write_file, edit_file

  • noTools (everything blocked): (none)

Two details worth noting:

  • Collision auto-prefixingread_file exists on both servers, so it becomes filesystem__read_file and github__read_file. But write_file/edit_file keep their bare names because they're blocked on filesystem, leaving github as the only source.

  • An empty profile view is validnoTools (or any profile with block: ["**"], or a server simply omitted) exposes zero tools; the agent still connects, it just has nothing to call.


Quick start

1. Install and build

npm install
npm run build          # compiles TypeScript to dist/

2. Put secrets in .env (never in the config)

cp .env.example .env   # then fill in your tokens

3. Write mcp-proxy.yaml

version: 1

servers:
  filesystem:
    type: stdio
    command: npx
    args: ["-y", "@modelcontextprotocol/server-filesystem", "C:/repo"]
    env:
      ROOT: "C:/repo"

  github:
    type: http
    url: https://api.github.com/mcp
    headers:
      Authorization: "${GITHUB_TOKEN}"   # env-var reference, not a literal secret

profiles:
  reviewer:                 # read-only, fail-closed
    description: "Read-only agent"
    default: block
    servers:
      filesystem:
        allow: ["read_file", "list_directory", "directory_tree", "get_file_info"]
      github:
        allow: ["get_*", "list_*", "search_*"]

  implementer:              # deny-list, fail-open minus dangerous ops
    description: "Full access minus destructive ops"
    default: allow
    servers:
      filesystem:
        block: ["/.*delete.*/i", "edit_file", "write_file"]
      github:
        block: ["merge_pull_request", "delete_*"]

defaultProfile: reviewer

4. Run

node dist/cli/index.js --profile reviewer
# add --verbose for structured debug logging
node dist/cli/index.js --profile reviewer --verbose

Profile precedence: --profile > MCP_PROFILE > defaultProfile.


Configuration reference

servers — upstream MCP servers

stdio (spawned as a child process):

filesystem:
  type: stdio
  command: npx
  args: ["-y", "@modelcontextprotocol/server-filesystem", "C:/repo"]
  env: { ROOT: "C:/repo" }
  prefix: fs__          # optional: override collision-prefix namespace

http (Streamable HTTP):

github:
  type: http
  url: https://api.github.com/mcp
  headers:
    Authorization: "${GITHUB_TOKEN}"
  prefix: gh__          # optional

profiles — named tool views

profiles:
  my-profile:
    description: "..."           # optional
    default: allow               # allow | block (fallback when no rule matches)
    servers:
      github:
        allow: ["get_*"]         # optional allow-list
        block: ["delete_*"]      # optional block-list (always wins)
        default: block           # optional per-server fallback override
      # filesystem omitted → none of its tools are exposed

http — Streamable HTTP downstream (serve mode)

Optional top-level block that turns the proxy into a shared HTTP server serving many agents from one process. See Shared server (HTTP).

http:
  host: 0.0.0.0             # default 127.0.0.1
  port: 3000                # default 3000
  path: /mcp                # MCP endpoint (default /mcp)
  metricsPath: /metrics     # Prometheus metrics (default /metrics)
  healthPath: /health       # liveness (default /health)
  readyPath: /ready         # readiness (default /ready)
  auth:
    header: authorization   # selector header (default authorization)
    scheme: Bearer          # optional prefix to strip
    tokens:                 # token -> profile map (values may use ${VAR})
      tok-reviewer: reviewer
      tok-impl: implementer
    defaultProfile: reviewer # optional fallback (fail-closed without it)

When tokens is set, the scheme-stripped header value is looked up in the map. Without tokens, the stripped header value is used directly as the profile name. A missing/unknown selector falls back to defaultProfile, then is rejected (401/403) if none applies.

Secrets

${VAR} placeholders are resolved from the environment (or .env) at load time. The YAML holds only the variable name, so it's safe to commit. A missing variable makes the loader fail fast — no silently-empty headers.


Wire it to your agent

The proxy is an MCP server over stdio. Point your agent at the proxy entrypoint instead of the real server, passing the profile flag.

// .mcp.json — reviewer agent
{
  "mcpServers": {
    "proxy": {
      "command": "node",
      "args": ["C:/Dev/mcp-proxy/dist/cli/index.js", "--profile", "reviewer"]
    }
  }
}
// .mcp.json — implementer agent (same proxy, different profile)
{
  "mcpServers": {
    "proxy": {
      "command": "node",
      "args": ["C:/Dev/mcp-proxy/dist/cli/index.js", "--profile", "implementer"]
    }
  }
}

Each agent gets its own stdio process, so profiles are fully isolated per agent and credentials never cross a process boundary.

Shared server (HTTP)

For a central deployment, run serve to expose one Streamable HTTP server that many agents share. Each connection is mapped to a profile from its auth header:

node dist/cli/index.js serve --config mcp-proxy.yaml
# options: --host, --port (override http.host/http.port)

Endpoints:

Path

Purpose

/mcp

Streamable HTTP MCP endpoint (session-per-connection)

/health

Liveness — always 200 once the process is up

/ready

Readiness — 200 only when every profile's upstreams are connected

/metrics

Prometheus text metrics (tools listed/called/blocked, latency, upstream state)

Per-connection profile resolution is fail-closed: a connection with no usable selector is rejected (401) unless http.auth.defaultProfile is set, and a selector that maps to an unknown profile is rejected (403).

Client config for a shared deployment (any Streamable-HTTP-capable client):

// .mcp.json — reviewer agent (token maps to the `reviewer` profile)
{
  "mcpServers": {
    "proxy": {
      "type": "http",
      "url": "https://proxy.example.com/mcp",
      "headers": { "Authorization": "Bearer ${PROXY_TOKEN}" }
    }
  }
}
// .mcp.json — implementer agent (same server, different token/profile)
{
  "mcpServers": {
    "proxy": {
      "type": "http",
      "url": "https://proxy.example.com/mcp",
      "headers": { "Authorization": "Bearer ${PROXY_TOKEN_IMPL}" }
    }
  }
}

The Copilot coding agent reads the repository's .mcp.json; for other agents use their native MCP server field (see context/AGENT-SETUP.md and context/VENDOR-AGENTS.md).


Observability

Run with --verbose to emit structured JSON-lines logs to stderr (keeping the MCP stdio channel on stdout clean):

{"timestamp":"2026-08-23T17:22:26.976Z","level":"info","message":"connected to upstream","server":"filesystem","tools":14}
{"timestamp":"2026-08-23T17:22:26.980Z","level":"debug","message":"tools/call","correlationId":"42","tool":"read_file","server":"filesystem"}

Every tools/list and tools/call entry carries the MCP request's correlationId, so a single request can be traced across the proxy and its upstreams.

To see the context cost of a profile, compare the tool count and tools/list payload size across profiles (see the measured example above): fewer advertised tools means fewer schemas injected into the prompt each turn.

In serve mode, scrape /metrics for Prometheus counters, gauges, and histograms: mcp_proxy_tools_listed_total, mcp_proxy_tools_called_total, mcp_proxy_tools_blocked_total, mcp_proxy_tool_call_duration_seconds, and mcp_proxy_upstream_connections (all labelled by profile/server/tool).


Resilience

  • Auto-reconnect — if an upstream (especially a spawned stdio process) dies, the proxy reconnects with exponential backoff (500ms → 15s cap, infinite retries).

  • Live tool updates — when an upstream emits notifications/tools/list_changed, the proxy re-fetches, re-filters, and forwards the change downstream, so agents always see an accurate tool list.

  • Argument validationtools/call arguments are checked against the upstream's inputSchema before forwarding; invalid calls are rejected locally.


Development

npm run typecheck    # tsc --noEmit
npm test             # vitest (unit + integration + filesystem smoke)
npm run build        # tsc → dist/

More

Maintenance

ActivityMaintained
ResponsivenessSyncing

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Self-hosted MCP proxy and aggregation platform. Register multiple upstream MCP servers and expose them through a single unified endpoint with namespace routing, multi-transport support (HTTP/SSE, stdio, OpenAPI→MCP), per-tool overrides, and a web admin UI.
    16
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Centralized MCP control plane that proxies multiple upstream MCP servers with tool namespacing, filtering, policy enforcement, audit logging, and health checks.
    16
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    An authorizing reverse proxy for MCP servers that enforces per-call policy rules on tool arguments with audit logging, dry-run, and rate limiting.
    Apache 2.0
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables serving multiple MCP toolkits behind one server with capability-based access control, so different callers see and can call only the tools they are authorized for, over stdio or streamable HTTP with bearer-token auth.
    MIT

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DawidNowak/mcp-proxy'

If you have feedback or need assistance with the MCP directory API, please join our Discord server