MCP Gateway
@vengtoo/mcp-gateway is a drop-in proxy that sits between an MCP client (Claude Code, Cursor, VS Code, GitHub Copilot) and any Model Context Protocol server. It intercepts every tool call and checks authorization before forwarding it to the downstream server.
Because it runs as a proxy, you don't modify the MCP server: point your client at the gateway instead of the server directly. This is the right choice when you're connecting an agent to servers you don't own or can't change.
Why
An agent connected to an MCP server can call any tool it's exposed: read your database, delete files, execute arbitrary SQL. The gateway puts a policy enforcement point in front of those tools, so every call is authorized before it executes and every invocation is logged.
How it works
The gateway runs as an MCP server: over stdio by default (a local subprocess), or over HTTP for a remote, shared deployment (see Transports). Either way, your client talks to it exactly as it would talk to the real server; the gateway spawns the downstream server, forwards the tools it exposes, and gates each call.
Every decision is fail-closed: if the authorization backend is unreachable or returns an error, the call is denied, not allowed through.
Two modes
Configure either cloudUrl or agentUrl in your gateway.config.json: the gateway connects to whichever is set.
| Mode | Decision point | When to use |
|---|---|---|
| Cloud | Vengtoo Cloud (api.vengtoo.com) evaluating your managed policies | Centrally managed policies, shared across deployments, full Decision Log |
| Local | Vengtoo Agent on localhost:8181 | Offline, self-contained, no cloud account needed |
Transports
The two modes above decide where authorization is evaluated. Transports decide how callers reach the gateway: the two are independent, so any transport pairs with either mode.
| Transport | Shape | When to use |
|---|---|---|
| stdio (default) | Local subprocess speaking MCP over stdin/stdout | A single desktop agent (Claude Code, Cursor, Claude Desktop). No network surface. |
| HTTP | Streamable HTTP server at a URL | A remote agent, or many concurrent agents, sharing one governed gateway. |
stdio
The default. Nothing to configure: the client launches the gateway as a child process. All the examples below use it unless noted.
HTTP (remote gateway)
Serve the gateway over HTTP so agents can reach it at a URL. Enable it with --http (or "transport": "http" in the config):
npx -y @vengtoo/mcp-gateway --config gateway.config.json --http --port 8808
{
"vengtoo": { "cloudUrl": "https://pdp.vengtoo.com/access/v1/evaluation", "apiKey": "vgt_..." },
"subject": "agent:prod-assistant",
"transport": "http",
"http": {
"port": 8808,
"host": "0.0.0.0",
"path": "/mcp",
"authTokens": ["<caller-token>"],
"allowedHosts": ["gateway.example.com"]
},
"servers": { "database": { "command": "node", "args": ["./db-server.js"] } }
}
The gateway runs the MCP Streamable HTTP transport in stateful mode: the client's first request (initialize) mints a session, returned in the mcp-session-id header, that every follow-up request carries. Many agents can hold sessions concurrently against one gateway, all governed by the same policies and sharing one pool of downstream servers.
An unauthenticated GET /healthz liveness endpoint is always served (returns {"status":"ok"}) for load balancers and readiness probes.
Securing a public endpoint
The HTTP listener is hardened for public exposure: bounded request bodies, connection timeouts (slowloris protection), and a bounded, idle-evicted session table.
The most important guard: the gateway refuses to bind a non-loopback interface (e.g. 0.0.0.0) unless you configure caller auth (http.callers or http.authTokens); otherwise it exits at startup rather than silently exposing an open MCP endpoint. Callers present Authorization: Bearer <token>; requests without a valid token get 401. To knowingly run an open endpoint (e.g. behind your own auth proxy), set VENGTOO_GATEWAY_ALLOW_UNAUTHENTICATED=true.
Set http.allowedHosts / http.allowedOrigins to enable DNS-rebinding protection: the transport then rejects requests whose Host/Origin header isn't listed.
Per-caller identity
One HTTP gateway can serve many agents, each authorized as itself. Map each bearer token to a subject with http.callers:
"http": {
"port": 8808,
"host": "0.0.0.0",
"callers": [
{ "token": "<token-a>", "subject": "agent:claude" },
{ "token": "<token-b>", "subject": "agent:cursor" }
]
}
Each call and audit record is then attributed to that caller's subject and evaluated against its own policies, not one shared identity. A session is bound to the token that opened it, so a different caller presenting another caller's mcp-session-id gets a 401. Also settable via VENGTOO_GATEWAY_HTTP_CALLERS="<token>=agent:claude,<token>=agent:cursor".
http.authTokens remains for the simpler case: anonymous tokens that grant access but run as the config's global subject. In open mode (no tokens configured, loopback or ALLOW_UNAUTHENTICATED), calls also run as the global subject.
Per-caller rate-limiting at the gateway is still on the roadmap; today rate limits are enforced cloud-side, per subject and action.
Quick start (cloud mode)
1. Create a gateway.config.json
{
"vengtoo": {
"cloudUrl": "https://pdp.vengtoo.com/access/v1/evaluation",
"apiKey": "vgt_..."
},
"subject": "agent:ai-assistant",
"servers": {
"database": {
"command": "node",
"args": ["./my-database-mcp-server.js"]
}
}
}
The API key can also be supplied via VENGTOO_API_KEY.
2. Point your MCP client at the gateway
claude mcp add --transport stdio vengtoo-gateway -- \
npx -y @vengtoo/mcp-gateway --config /path/to/gateway.config.json
Your agent now calls tools through the gateway, and every call is authorized against your Vengtoo policies before it executes. Tool inventory, decisions, and subjects are visible in the Vengtoo console.
Local mode (no cloud account)
Install and start the Vengtoo Agent locally, then scaffold a policy for your tools:
npx -y @vengtoo/mcp-gateway --config ./gateway.config.json --generate-policy ./policy.rego
The gateway connects to each downstream server, discovers all tools, and generates a starter policy file classified by trust level: read-only tools allowed, write operations requiring explicit approval, destructive operations blocked by default:
package vengtoo.mcp
default allow := false
# LOW trust — allowed for all approved agents
allow if { input.resource.name == "database__query" }
allow if { input.resource.name == "database__list_tables" }
# MEDIUM trust — allowed for specific agents
allow if {
input.resource.name == "database__execute"
input.subject.id in {"agent:cursor", "agent:claude"}
}
# HIGH trust — uncomment to allow (destructive operations)
# allow if {
# input.resource.name == "database__drop_table"
# input.subject.id == "agent:claude"
# }
Review the file, adjust the rules, and start the agent:
vengtoo-agent --policy ./policy.rego
Then use the same gateway.config.json with agentUrl instead of cloudUrl:
{
"vengtoo": {
"agentUrl": "http://localhost:8181"
},
"subject": "agent:dev-assistant",
"servers": {
"database": {
"command": "node",
"args": ["./my-database-mcp-server.js"]
}
}
}
For centralized policy management, audit logs, and multi-agent visibility, point the gateway at Vengtoo Cloud instead: swap agentUrl for cloudUrl and add your API key.
Tool governance
Beyond per-call policy checks, the gateway enforces the governance state each tool holds in the Vengtoo console. Every call is checked against this state before any policy evaluation, and the gateway fails closed.
| State | What it means | Gateway behavior |
|---|---|---|
| Approved | Reviewed; current schema matches the approved baseline | Proceeds to policy evaluation |
| Pending review | Discovered but never approved: a new tool is not trusted on first sight | Denied with tool_pending_review until an admin approves it |
| Drifted | Schema changed from the approved baseline | Denied (drift block) until re-approved |
| Blocked | Explicitly blocked by an admin | Denied |
State is synced from the cloud as tools change and re-seeded on startup, so a restart never silently makes a held or blocked tool callable again. Approve or block tools from the MCP Governance section of the console; the change reaches the gateway on the next sync.
The gateway reports only facts about tools: names, schemas, and what changed. It never scores trust or drift severity: those judgments are computed in the Vengtoo cloud from the reported facts. This keeps the gateway, the most exposed component, unable to influence a security decision even if a downstream server it proxies is compromised.
Human-in-the-loop approvals
A policy can require a human to approve an action before it runs (set on the policy in the console). When an agent calls such a tool, the gateway blocks the call and waits: it polls Vengtoo until the request is approved (the call then proceeds and returns the tool's result), denied, or the approval window lapses.
To keep the call from outliving your MCP client's request timeout, the gateway blocks for at most vengtoo.approvalMaxWaitMs (default 50s). If approval hasn't arrived by then, the tool returns a "still awaiting approval" message and the agent can re-invoke to resume waiting on the same pending request: no duplicate approval is created.
Configuration
| Field | Type | Required | Description |
|---|---|---|---|
vengtoo.cloudUrl | string | * | Vengtoo Cloud evaluation URL (cloud mode). Env: VENGTOO_CLOUD_URL |
vengtoo.agentUrl | string | * | URL of a local Vengtoo Agent (local mode). Env: VENGTOO_AGENT_URL |
vengtoo.apiKey | string | API key from the Vengtoo console. Env: VENGTOO_API_KEY | |
vengtoo.clientId | string | OAuth2 client ID: alternative to apiKey when using client credentials | |
vengtoo.clientSecret | string | OAuth2 client secret. Use together with clientId | |
vengtoo.timeoutMs | number | Authorization request timeout in ms (default: 10000) | |
vengtoo.blockOnDrift | boolean | Local mode only: block any tool whose schema drifts from the approved baseline (default: false) | |
vengtoo.approvalMaxWaitMs | number | Max time a call blocks awaiting human approval before returning "still pending" (default: 50000) | |
subject | string | yes | Identity of the agent making tool calls. Env: VENGTOO_SUBJECT |
subjectType | string | Subject type (default: "agent") | |
resourceType | string | Resource type for authorization checks (default: "mcp_tool") | |
servers | object | yes | Map of downstream MCP servers to proxy |
transport | string | Caller transport: "stdio" (default) or "http". Env: VENGTOO_GATEWAY_TRANSPORT | |
http | object | HTTP transport settings (used when transport is "http"), see below |
* Provide either cloudUrl (cloud mode) or agentUrl (local mode).
Each entry under servers:
| Field | Type | Required | Description |
|---|---|---|---|
command | string | yes | Command to spawn the MCP server |
args | string[] | Command arguments | |
env | object | Additional environment variables |
The http block (used when transport is "http"):
| Field | Type | Default | Description |
|---|---|---|---|
port | number | 8808 | TCP port to listen on. Env: VENGTOO_GATEWAY_PORT / PORT |
host | string | 127.0.0.1 | Interface to bind; 0.0.0.0 accepts remote connections. Env: VENGTOO_GATEWAY_HOST |
path | string | /mcp | URL path of the MCP endpoint. Env: VENGTOO_GATEWAY_PATH |
callers | object[] | Per-caller identity: { token, subject } pairs: each token authorizes as its own subject. Env: VENGTOO_GATEWAY_HTTP_CALLERS (token=subject,…) | |
authTokens | string[] | Anonymous bearer tokens (grant access, run as the global subject). Env: VENGTOO_GATEWAY_HTTP_TOKENS (comma-separated) | |
allowedHosts | string[] | Enables DNS-rebinding protection; rejects requests with an unlisted Host header | |
allowedOrigins | string[] | Enables DNS-rebinding protection; rejects requests with an unlisted Origin header |
Set VENGTOO_GATEWAY_ALLOW_UNAUTHENTICATED=true to bind a non-loopback interface without any caller auth (callers or authTokens); otherwise the gateway refuses to start (see Securing a public endpoint).
Audit forwarding
Every tool call, allowed or denied, is recorded. Records are always written as structured JSON to stderr, and forwarded to Vengtoo by default: in cloud mode to <cloudUrl>/v1/agent-logs/ingest, in local mode to <agentUrl>/v1/agent-logs/ingest. Forwarding activates automatically whenever the gateway holds credentials (the ingest endpoint authenticates the batch and derives the tenant from your API key). Events are batched and flushed on an interval.
| Field | Type | Default | Description |
|---|---|---|---|
audit.forward | boolean | true | Set false to suppress network forwarding and keep audit stderr-only |
audit.forwardUrl | string | Override the ingest endpoint. Defaults to the cloud/agent ingest URL for the configured mode | |
audit.tenantId | string | Optional. Attribution comes from the API key; a matching value is fine, a mismatched one is rejected |
CLI flags
| Flag | Description |
|---|---|
--config <path> | Path to the gateway config file (default: ./gateway.config.json) |
--http | Serve over HTTP instead of stdio |
--port <n> | HTTP listen port (default: 8808) |
--host <h> | HTTP bind interface (default: 127.0.0.1) |
--path <p> | HTTP endpoint path (default: /mcp) |
--list-tools | List all tools from the configured downstream servers and exit |
--generate-policy [path] | Discover tools and generate a trust-classified starter policy (default: policy.rego) |
MCP client setup
Point your client at the gateway instead of the downstream server. The examples below use the default stdio transport, where the client launches the gateway as a subprocess. If you run the gateway over HTTP instead, point your client at its URL (e.g. http://gateway.example.com:8808/mcp) using the client's HTTP/streamable transport, and supply the bearer token if one is configured.
Claude Code
claude mcp add --transport stdio vengtoo-gateway -- \
npx -y @vengtoo/mcp-gateway --config /path/to/gateway.config.json
Cursor
Add to .cursor/mcp.json:
{
"mcpServers": {
"vengtoo-gateway": {
"command": "npx",
"args": [
"-y",
"@vengtoo/mcp-gateway",
"--config",
"/path/to/gateway.config.json"
]
}
}
}
Claude Desktop
Add to ~/Library/Application Support/Claude/claude_desktop_config.json:
{
"mcpServers": {
"vengtoo-gateway": {
"command": "npx",
"args": [
"-y",
"@vengtoo/mcp-gateway",
"--config",
"/path/to/gateway.config.json"
]
}
}
}
VS Code / GitHub Copilot
Add to .vscode/mcp.json:
{
"servers": {
"vengtoo-gateway": {
"type": "stdio",
"command": "npx",
"args": [
"-y",
"@vengtoo/mcp-gateway",
"--config",
"/path/to/gateway.config.json"
]
}
}
}
Source
github.com/vengtoo/mcp-gateway: Apache-2.0 licensed.