Skip to main content
Glama

mcp-proxy

Ein härtender MCP-Proxy, der vor einem oder mehreren Upstream-MCP-Servern sitzt und nur die Tools offenlegt, die ein bestimmtes Profil sehen und aufrufen darf.

Eine Konfigurationsdatei beschreibt deine echten Server (GitHub, Dateisystem, Slack, …) und die Profile (Reviewer, Implementierer, CI-Bot, …). Jeder Agent startet seine eigene Kopie des Proxys mit --profile <name> – oder, im serve-Modus, bildet ein einzelner gemeinsamer HTTP-Server jede Verbindung per Authentifizierung auf ein Profil ab – und erhält eine gefilterte, erzwungene Sicht auf diese Server, was Kontext-Token spart und gefährliche Tool-Aufrufe von vornherein verhindert.


Warum mcp-proxy?

Offizielle MCP-Server legen alle ihrer Tools jedem Agenten offen. Der Client ruft tools/list ab und injiziert das Schema jedes Tools bei jedem Turn in den Prompt, was Kontext-Token verbrennt. Und ein Tool, das sichtbar ist, ist ein Tool, das aufgerufen werden kann – es gibt keine harte Grenze.

mcp-proxy löst beide Probleme gleichzeitig:

  • Token-Ersparnis – ein Profil bewirbt nur die Tools, die du explizit erlaubst, sodass nur diese Schemas jemals in den Kontext des Agenten gelangen.

  • Harte Leitplanke – ein Tool, das nicht erlaubt ist, wird weder gelistet noch aufrufbar: selbst ein halluzinierter Aufruf wird zur Ausführungszeit abgewiesen, nicht nur im Menü versteckt.


Related MCP server: Mavryn

Vorteile

Vorteil

Wie es hilft

🔒 Fail-closed-Leitplanke

block gewinnt; unbekannte Tools sind standardmäßig verweigert. Sichtbarkeit und Aufrufbarkeit bleiben synchron.

📉 Token-Ersparnis

Gefiltertes tools/list bedeutet kleinere Prompts und günstigere, fokussiertere Sitzungen.

👥 Eine Konfiguration, viele Agenten

Reviewer, Implementierer und CI-Bot teilen sich denselben servers-Block, erhalten aber über --profile unterschiedliche Profile.

🧩 Multi-Server-Aggregation

Mehrere Upstreams (stdio + HTTP) hinter einem einzigen MCP-Endpunkt zusammenführen.

🔐 Geheimnisse bleiben außerhalb des Repos

${VAR}-Platzhalter + .env; der Loader schlägt bei einer fehlenden Variable sofort fehl.

♻️ Resilient

Automatische Wiederverbindung mit exponentiellem Backoff; Live-tools/list_changed-Updates werden neu gefiltert und downstream weitergegeben.

🛡️ Argumentvalidierung

tools/call-Argumente werden vor der Weiterleitung gegen das Upstream-inputSchema validiert.

📊 Beobachtbar

--verbose erzeugt strukturierte JSON-Lines-Logs mit Korrelations-IDs pro Anfrage; der gemeinsame Server stellt außerdem Prometheus-/metrics bereit.

🌐 Gemeinsamer Server-Modus

serve betreibt einen Streamable-HTTP-Server für viele Agenten; Authentifizierung pro Verbindung bildet Tokens/Header auf Profile ab.

🏷️ Kollisionssicher

Tools mit demselben Namen über mehrere Server werden automatisch mit Präfix versehen (github__read_file), andere behalten ihre einfachen Namen.


So funktioniert es

Architektur

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

Jeder Agent startet den Proxy als Kindprozess über stdio. Der Proxy verbindet sich mit jedem im gewählten Profil aufgeführten Upstream, ruft jeweils tools/list ab, wendet die Allow/Block-Regeln des Profils an und legt nur die überlebenden Tools erneut offen.

Anfragefluss

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

Filterentscheidung

Ein Tool ist nur erlaubt, wenn es diese Präzedenzkette übersteht:

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 gewinnt immer. Muster sind Glob-Muster (read_*, {get,list}_*) oder Regexes (/.*delete.*/i). Ein Server, der in einem Profil nicht aufgeführt ist, legt keines seiner Tools offen.


Beispiel: drei Profile, live gemessen

Derselbe Proxy, gesteuert durch drei Profile, ausgeführt gegen den echten @modelcontextprotocol/server-filesystem-Upstream (14 Tools). Eine zweite Filesystem-Instanz vertrat den HTTP-GitHub-Server, damit die Demo kein Token benötigt – die Filterung pro Server verhält sich für jeden Upstream identisch.

# 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: ["**"] }

Gemessen über einen Live-tools/list-Handshake:

Profil

Offengelegte Tools

tools/list-Nutzlast

~Tokens

reviewer

5

2.926 Zeichen

~732

implementer

26

15.762 Zeichen

~3.941

noTools

0

2 Zeichen

~1

Tokens verwenden eine ~4-Zeichen-pro-Token-Heuristik; die tatsächliche Ersparnis ist die Schema-Oberfläche, die der Agent bei jedem Turn neu in den Kontext lädt.

Tools, die jedes Profil tatsächlich erhalten hat:

  • reviewer (schreibgeschützt, GitHub blockiert): read_file, list_directory, directory_tree, search_files, get_file_info

  • implementer (Deny-Liste, GitHub erlaubt): 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 (alles blockiert): (keine)

Zwei Details, die erwähnenswert sind:

  • Kollisions-Autoprefixread_file existiert auf beiden Servern, wird also zu filesystem__read_file und github__read_file. Aber write_file/edit_file behalten ihre einfachen Namen, weil sie auf filesystem blockiert sind, sodass github die einzige Quelle bleibt.

  • Eine leere Profilansicht ist gültignoTools (oder jedes Profil mit block: ["**"], oder ein einfach weggelassener Server) legt null Tools offen; der Agent verbindet sich weiterhin, hat aber nichts aufzurufen.


Schnellstart

1. Installieren und bauen

npm install
npm run build          # compiles TypeScript to dist/

2. Geheimnisse in .env ablegen (niemals in der Konfiguration)

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

3. mcp-proxy.yaml schreiben

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. Ausführen

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

Profil-Präzedenz: --profile > MCP_PROFILE > defaultProfile.


Konfigurationsreferenz

servers – Upstream-MCP-Server

stdio (als Kindprozess gestartet):

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 – benannte Tool-Ansichten

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-Modus)

Optionaler Top-Level-Block, der den Proxy in einen gemeinsamen HTTP-Server verwandelt, der viele Agenten aus einem Prozess bedient. Siehe Gemeinsamer 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)

Wenn tokens gesetzt ist, wird der um das Schema bereinigte Header-Wert in der Map nachgeschlagen. Ohne tokens wird der bereinigte Header-Wert direkt als Profilname verwendet. Ein fehlender/unbekannter Selektor fällt auf defaultProfile zurück und wird dann abgewiesen (401/403), wenn keines zutrifft.

Geheimnisse

${VAR}-Platzhalter werden beim Laden aus der Umgebung (oder .env) aufgelöst. Das YAML enthält nur den Variablen-Namen, ist also sicher zu committen. Eine fehlende Variable lässt den Loader sofort fehlschlagen – keine stillen leeren Header.


An deinen Agenten anbinden

Der Proxy ist ein MCP-Server über stdio. Richte deinen Agenten auf den Proxy-Einstiegspunkt statt auf den echten Server aus und übergib das Profil-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"]
    }
  }
}

Jeder Agent erhält seinen eigenen stdio-Prozess, sodass Profile pro Agent vollständig isoliert sind und Anmeldedaten niemals eine Prozessgrenze überschreiten.

Gemeinsamer Server (HTTP)

Für eine zentrale Bereitstellung führe serve aus, um einen Streamable-HTTP-Server bereitzustellen, den viele Agenten teilen. Jede Verbindung wird über ihren Auth-Header auf ein Profil abgebildet:

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

Endpunkte:

Pfad

Zweck

/mcp

Streamable-HTTP-MCP-Endpunkt (Sitzung pro Verbindung)

/health

Liveness – immer 200, sobald der Prozess läuft

/ready

Readiness – 200 nur, wenn die Upstreams jedes Profils verbunden sind

/metrics

Prometheus-Textmetriken (gelistete/aufgerufene/blockierte Tools, Latenz, Upstream-Zustand)

Die Profilauflösung pro Verbindung ist fail-closed: Eine Verbindung ohne verwendbaren Selektor wird abgewiesen (401), sofern nicht http.auth.defaultProfile gesetzt ist, und ein Selektor, der auf ein unbekanntes Profil abgebildet wird, wird abgewiesen (403).

Client-Konfiguration für eine gemeinsame Bereitstellung (jeder Streamable-HTTP-fähige 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}" }
    }
  }
}

Der Copilot-Coding-Agent liest die .mcp.json des Repos; für andere Agenten verwende deren natives MCP-Server-Feld (siehe context/AGENT-SETUP.md und context/VENDOR-AGENTS.md).


Beobachtbarkeit

Führe mit --verbose aus, um strukturierte JSON-Lines-Logs auf stderr auszugeben (wodurch der MCP-stdio-Kanal auf stdout sauber bleibt):

{"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"}

Jeder Eintrag von tools/list und tools/call trägt die correlationId der MCP-Anfrage, sodass eine einzelne Anfrage über den Proxy und seine Upstreams hinweg verfolgt werden kann.

Um die Kontextkosten eines Profils zu sehen, vergleiche die Tool-Anzahl und die tools/list- Nutzlastgröße über Profile hinweg (siehe das gemessene Beispiel weiter oben): weniger beworbene Tools bedeutet weniger Schemas, die bei jedem Turn in den Prompt injiziert werden.

Im serve-Modus scrappe /metrics für Prometheus-Counter, -Gauges und -Histogramme: mcp_proxy_tools_listed_total, mcp_proxy_tools_called_total, mcp_proxy_tools_blocked_total, mcp_proxy_tool_call_duration_seconds und mcp_proxy_upstream_connections (alle mit den Labels profile/server/tool).


Resilienz

  • Automatische Wiederverbindung – wenn ein Upstream (insbesondere ein gestarteter stdio-Prozess) stirbt, verbindet sich der Proxy mit exponentiellem Backoff neu (500 ms → 15 s Obergrenze, unbegrenzte Wiederholungen).

  • Live-Tool-Updates – wenn ein Upstream notifications/tools/list_changed ausgibt, ruft der Proxy erneut ab, filtert neu und gibt die Änderung downstream weiter, sodass Agenten immer eine korrekte Tool-Liste sehen.

  • Argumentvalidierungtools/call-Argumente werden vor der Weiterleitung gegen das inputSchema des Upstreams geprüft; ungültige Aufrufe werden lokal abgewiesen.


Entwicklung

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

Mehr

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