HTTP Streamable transport enables remote MCP server access with SSE streaming, session management, and resumability.
Use HTTP transport when you need:
- Remote server access (not just localhost)
- Multiple concurrent clients
- Load balancing across MCP servers
- Integration with reverse proxies (nginx, cloudflare)
- Stateful sessions
Use stdio transport when you need:
- Local desktop MCP client
- Simple single-client setup
- Maximum security (no network exposure)
Best for: Testing, development, single-user servers
# Set environment
export MCP4_TRANSPORT=http
export MCP4_HOST=127.0.0.1 # localhost only (secure default)
export MCP4_PORT=3003
export MCP4_API_TOKEN=your_token
export MCP4_API_BASE_URL=https://api.example.com
export MCP4_PROFILE_PATH=./mcp-profile.json
# Start server
npm startCLI alternative:
export MCP4_API_TOKEN=your_token
npx mcp4openapi \
--transport http \
--host 127.0.0.1 \
--port 3003 \
--api-base-url https://api.example.com
--profile-path ./mcp-profile.jsonNote: MCP4_OPENAPI_SPEC_PATH is optional when the selected profile includes openapi_spec_path. If a profile does not provide it, you must set MCP4_OPENAPI_SPEC_PATH or --openapi-spec-path. In HTTP profile routing mode, MCP4_OPENAPI_SPEC_PATH acts as a global fallback for profiles that omit openapi_spec_path.
Server will log:
{"timestamp":"...","level":"info","message":"HTTP transport started","host":"127.0.0.1","port":3003}
All clients share the same MCP4_API_TOKEN from environment.
Best for: Production, multiple users with different tokens
# Allow network access
export MCP4_TRANSPORT=http
export MCP4_HOST=0.0.0.0
export MCP4_PORT=3003
export MCP4_API_BASE_URL=https://api.example.com
# Note: No MCP4_API_TOKEN in environment
# Configure allowed origins (for corporate networks)
export MCP4_ALLOWED_ORIGINS="example.com,*.company.com,192.168.1.0/24,10.0.0.0/8,2001:db8::/32"
# Optional: Enable heartbeat for proxy keepalive
export MCP4_HEARTBEAT_ENABLED=true
export MCP4_HEARTBEAT_INTERVAL_MS=30000 # 30 seconds
npx mcp4openapiCLI alternative:
npx mcp4openapi \
--transport http \
--host 0.0.0.0 \
--port 3003 \
--api-base-url https://api.example.com \
--allowed-origins "example.com,*.company.com,192.168.1.0/24,10.0.0.0/8,2001:db8::/32" \
--heartbeat-enabled true \
--heartbeat-interval-ms 30000Each client sends their own token in Authorization: Bearer <token> header during initialization. Alternatively, clients can use OAuth authorization flow for API with OAuth support.
Security Warning: When binding to 0.0.0.0, ensure firewall protection, configure MCP4_ALLOWED_ORIGINS, and use HTTPS reverse proxy. Server will log warning if MCP4_ALLOWED_ORIGINS is not configured.
Enable profile-specific routes to serve multiple profiles from one HTTP server:
export MCP4_TRANSPORT=http
export MCP4_HTTP_PROFILE_ROUTING=true
export MCP4_HTTP_PROFILE_INDEX=true
export MCP4_ALLOW_PROFILES=gitlab,github
export MCP4_PROFILES_DIR=./profiles
export MCP4_HOST=127.0.0.1
export MCP4_PORT=3003
npx mcp4openapiCLI alternative:
npx mcp4openapi \
--transport http \
--http-profile-routing true \
--http-profile-index true \
--allow-profiles gitlab,github \
--profiles-dir ./profiles \
--host 127.0.0.1 \
--port 3003Routes:
POST /profile/:profileId/mcpGET /profile/:profileId/mcpDELETE /profile/:profileId/mcp- Legacy alias:
POST|GET|DELETE /profile/:profileId/sse - Optional HTML profile index:
GET /(whenMCP4_HTTP_PROFILE_INDEX=true)- Keeps current API endpoint display semantics (env/default source)
- Can show admin-supplied raw HTML descriptions from
MCP4_PROFILES_DESCRIPTIONin the detail card before the profile description - When tenant config is available for a profile, shows tenant availability and tenant list per profile
- Includes interactive tenant picker for supported remote snippet formats and injects
X-Mcp4-Tenant-Idinto copied snippet output - Includes a per-profile tool catalog with interactive builders for
X-Mcp4-ToolsandX-Mcp4-Params - While tool or parameter filtering is active, only snippet variants with verified custom-header support remain visible
- Picker includes an explicit "no tenant" option that keeps snippet headers unchanged
- For
mask:tenant selection, picker also injects exampleX-Mcp4-Api-Base-Urlwith wildcard parts replaced by<your-part> - In
Local stdiomode, tenant selection injects tenant API base URL into snippet env config for supported local snippet formats - In
Local stdiomode, active tool/parameter filters are translated into localmcp4openapiCLI arguments (--tool-filter-allow-names,--tool-filter-allow-categories,--param-filter) instead of hiding supported local snippets - Profiles that use
auth.type: "session-cookie"are shown only inLocal stdiosnippets because remote HTTP initialization does not accept upstream login/password via request headers
Default profile behavior:
- If
MCP4_PROFILE_PATH(or--profile-path) is set,/mcpand/ssestay available. - If no default profile is configured,
/mcpis not registered and you must use/profile/:profileId/mcp.
Allowlist controls (only when routing is enabled):
MCP4_ALLOW_PROFILES: Comma-separated profile ids/names/aliases that can be routed.MCP4_ALLOW_PROFILES_REGEX: Regex pattern that can match profile ids/names/aliases.MCP4_HIDDEN_PROFILES: Comma-separated profile ids/names/aliases to hide from the index page (profiles remain fully functional).
Profile index admin descriptions:
MCP4_PROFILES_DESCRIPTION: Optional JSON object mappingprofileId,profileName, or alias to an HTML snippet rendered in the HTML detail card before the profile's own description.- Parsed once at startup and resolved against the loaded profile catalog.
- Startup fails fast on invalid JSON, non-object payloads, non-string values, duplicate keys resolving to the same profile, or values longer than
10000characters. - Keys that do not match any loaded profile are ignored.
- The HTML is rendered only for the HTML index response on
GET /; the JSON profile index omits this field. - The content is rendered as raw HTML, so it must be treated as trusted administrator input.
Example:
export MCP4_HTTP_PROFILE_ROUTING=true
export MCP4_HTTP_PROFILE_INDEX=true
export MCP4_PROFILES_DESCRIPTION='{"gitlab":"<p><strong>Internal:</strong> Use SSO token.</p>","gl":"<p>Alias-based entry also works.</p>"}'OAuth and metadata endpoints are scoped per profile when routing is enabled:
/.well-known/oauth-protected-resource/mcp->/profile/:profileId/.well-known/oauth-protected-resource/mcp/.well-known/oauth-authorization-server->/profile/:profileId/.well-known/oauth-authorization-server/oauth/authorize->/profile/:profileId/oauth/authorize/oauth/token->/profile/:profileId/oauth/token/oauth/register->/profile/:profileId/oauth/register/oauth/callback->/profile/:profileId/oauth/callback
Root protected resource metadata also supports a resource query parameter for profile selection:
/.well-known/oauth-protected-resource/mcp?resource=http://host/profile/:profileId/mcp
If no default profile is configured, use the resource query parameter to resolve metadata.
You can configure per-session tenant selection using either:
MCP4_HTTP_TENANTS_FILE=/path/to/tenants.jsonMCP4_HTTP_TENANTS_JSON={...}
api_base_url selector formats:
- exact:
https://team-a.example.com/api - mask:
mask:https://grafana.*.security.*.ops.iszn.cz/api
Required tenant scoping:
profile_ids: required non-empty array of profile ids where the tenant is active
Header selectors (initialize request only):
X-Mcp4-Tenant-Id: select tenant bytenant_id.X-Mcp4-Api-Base-Url: select tenant by exact or mask selector.
Deterministic resolution order at initialization:
X-Mcp4-Tenant-Id- exact match for
X-Mcp4-Api-Base-Url mask:match forX-Mcp4-Api-Base-Url
Mask selector behavior:
- For exact selectors,
X-Mcp4-Tenant-IdorX-Mcp4-Api-Base-Urlis sufficient. - For mask selectors, concrete URL selection requires
X-Mcp4-Api-Base-Url. X-Mcp4-Tenant-Idfor mask entries is optional guard; when both headers are provided, they must resolve to the same tenant.
Session immutability:
- On non-initialize requests, selector headers are optional.
- If provided, they must match the stored session tenant selection (tenant id and concrete base URL).
- Mismatch returns
400 ValidationError. - If no tenant headers are sent, tenant override is skipped and profile-level config is used.
Security and validation rules:
- Tenant base URLs are allowlist-only; unknown selectors are rejected.
httpsis required by default (httponly withMCP4_HTTP_TENANTS_ALLOW_HTTP=true).- Credentials, query, and fragment are rejected in selectors.
defaulttenant property is not supported.mask:grammar:- wildcard
*is allowed only as a full hostname label - literal hostname labels must match
[a-z0-9-]+ - wildcard
*is allowed in path only as a full path segment - one
*path segment matches exactly one concrete path segment
- wildcard
- Startup fail-fast collision checks:
- exact vs exact with incompatible auth
- exact vs mask intersection
- mask vs mask intersection
- Runtime ambiguity guard: if one concrete URL matches multiple mask tenants, request fails with
400 ValidationError.
Tenant config example (exact + mask):
{
"version": 1,
"tenants": [
{
"tenant_id": "team-a",
"profile_ids": ["grafana", "grafana-optimized"],
"api_base_url": "https://team-a.example.com/api",
"auth_mode": "token",
"auth": { "type": "bearer", "value_from_env": "TEAM_A_TOKEN" }
},
{
"tenant_id": "grafana-security",
"profile_ids": ["grafana-security"],
"api_base_url": "mask:https://grafana.*.security.*.ops.iszn.cz/api",
"auth_mode": "token",
"auth": { "type": "bearer", "value_from_env": "GRAFANA_SECURITY_TOKEN" }
}
]
}If you run behind a reverse proxy that sets X-Forwarded-For, enable Express trust proxy so rate limiting and OAuth flows work correctly:
export MCP4_TRUST_PROXY=1This implementation follows MCP Specification 2025-03-26 for Streamable HTTP transport.
Source: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports
- Single MCP endpoint (
/mcp) for POST + GET - JSON-RPC request/notification/response handling
- Batch requests (JSON-RPC arrays)
- SSE streaming responses
- Session management (
Mcp-Session-Idheader) - Resumability (
Last-Event-IDheader) - Origin validation (DNS rebinding protection)
- Session termination (DELETE endpoint)
- Accept header validation
Purpose: Client sends JSON-RPC messages to server
Headers:
Content-Type: application/json(required)Accept: application/jsonortext/event-stream(required)Mcp-Session-Id: <session-id>(required except for initialization)Authorization: Bearer <token>orX-API-Token: <token>(required for initialization if not using env var)- If the active profile uses
auth.type: "custom-header", clients can send the configured header name (for exampleX-N8N-API-KEY) with the token instead ofAuthorizationorX-API-Token. - The header name comes from the profile auth configuration, so it can vary per profile.
- Supports various token formats: GitLab (
glpat-...), YouTrack (perm:...), generic tokens - Flexible whitespace handling (extra spaces are trimmed)
- If the active profile uses
auth.type: "session-cookie", upstream authentication is handled by the server using the profile'ssession_cookie_configcredentials, so an initialization token header is not required unless another auth method is active for that profile.
- If the active profile uses
X-Mcp4-Params: <filter>(optional)X-Mcp4-Tools: <tool-filter>(optional)- If sent during initialization, the server stores the normalized header value in the session.
- Subsequent requests may omit the header, but if provided it must match the session value or the server returns
400.
Parameter Filtering header format:
- Comma-separated list of
key=valueitems - Control keys (no value):
_allow_list: for list operations, allow omitting the filtered key and allow any value if the key is present._allow_read: for read operations, allow omitting the filtered key and allow any value if the key is present.- Control keys do not relax modify operations (write remains constrained by the allowed set).
- Values containing spaces or commas must be percent-encoded
- Key pattern:
^[A-Za-z0-9_][A-Za-z0-9_-]{0,63}$
Example:
X-Mcp4-Params: project_id=123, project_id=456, _allow_read
Global baseline:
MCP4_PARAM_FILTERuses the same syntax and applies process-wide.- If both
MCP4_PARAM_FILTERandX-Mcp4-Paramsare set, the session header may only narrow the global baseline. - Conflicting overlaps fail during session initialization.
Tool filtering header format:
- Comma-separated list of tool names or regex entries
- Regex entries must be prefixed with
regex:and are auto-anchored unless already wrapped with^and$ - Max entries default is 100, max entry length is 255 characters
- Control keywords (tools categories, session initialization only):
_allow_list: allow tools detected as list category (GET without path params)_allow_read: allow tools detected as read category (GET with path params)- These keywords are only allowed during session initialization. The server stores the normalized header value in the session. Subsequent requests may omit the header, but if provided it must match the session value or the server returns
400. - Other
_allow_*keywords are rejected with an error suggestingX-Mcp4-Params.
Important: In X-Mcp4-Tools, _allow_list/_allow_read control which tools are available (tool categories). In X-Mcp4-Params, _allow_list/_allow_read control parameter filtering behavior for list/read operations.
Example:
X-Mcp4-Tools: get_user, list_users, regex:read_.*
Regex patterns are validated for length, nested quantifiers, and alternations with quantifiers.
Request Body:
- Single JSON-RPC request/notification/response
- Or array (batch) of requests/notifications/responses
Response:
- HTTP 200 with JSON response (if
Accept: application/json) - HTTP 200 with SSE stream (if
Accept: text/event-stream) - HTTP 202 (no body) for notification-only messages
- HTTP 400/404/500 for errors
Example - Initialize:
curl -X POST http://localhost:3003/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Authorization: Bearer your_gitlab_token" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"clientInfo": {
"name": "my-client",
"version": "1.0.0"
}
}
}'Alternative with X-API-Token header:
curl -X POST http://localhost:3003/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "X-API-Token: your_gitlab_token" \
-d '{...}'Response:
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"protocolVersion": "2025-03-26",
"serverInfo": {
"name": "mcp4openapi",
"version": "0.2.9"
},
"capabilities": {
"tools": {}
}
}
}Response Headers:
Mcp-Session-Id: <generated-session-id>
Example - List Tools:
SESSION_ID="<session-id-from-init>"
curl -X POST http://localhost:3003/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Mcp-Session-Id: $SESSION_ID" \
-d '{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/list",
"params": {}
}'Example - Call Tool:
curl -X POST http://localhost:3003/mcp \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Mcp-Session-Id: $SESSION_ID" \
-d '{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "manage_project_badges",
"arguments": {
"action": "list",
"project_id": "123"
}
}
}'Purpose: Open Server-Sent Events stream for server-initiated messages
Headers:
Accept: text/event-stream(required)Mcp-Session-Id: <session-id>(required)Last-Event-ID: <event-id>(optional, for resuming)
Response:
- HTTP 200 with SSE stream (
Content-Type: text/event-stream) - HTTP 400/404/405 for errors
Example:
curl -N -H "Accept: text/event-stream" \
-H "Mcp-Session-Id: $SESSION_ID" \
http://localhost:3003/mcpSSE Format:
id: 1234567890123
data: {"jsonrpc":"2.0","method":"notification","params":{}}
id: 1234567890124
data: {"jsonrpc":"2.0","method":"another","params":{}}
Heartbeat (if enabled):
:ping
Purpose: Explicitly terminate session and cleanup resources
Headers:
Mcp-Session-Id: <session-id>(required)
Response:
- HTTP 204 (no content) on success
- HTTP 400/404 for errors
Example:
curl -X DELETE http://localhost:3003/mcp \
-H "Mcp-Session-Id: $SESSION_ID"Purpose: Check server health and session count
Response:
{
"status": "ok",
"sessions": 5
}Example:
curl http://localhost:3003/health
⚠️ Deprecated: This endpoint is maintained for backward compatibility only. Use/mcpendpoints instead.
For clients expecting the /sse endpoint, the server provides a deprecated alias that logs warnings and delegates to the /mcp endpoints.
Supported methods: POST, GET, DELETE
All headers, request/response formats, and behavior are identical to /mcp endpoints.
Example - Initialize (deprecated):
curl -X POST http://localhost:3003/sse \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-H "Authorization: Bearer your_token" \
-d '{
"jsonrpc": "2.0",
"id": 1,
"method": "initialize",
"params": {
"protocolVersion": "2025-03-26",
"clientInfo": {
"name": "my-client",
"version": "1.0.0"
}
}
}'Response: Same as /mcp POST
Mcp-Session-Id: <generated-session-id>
Example - Open SSE Stream (deprecated):
SESSION_ID="<session-id-from-init>"
curl -N -H "Accept: text/event-stream" \
-H "Mcp-Session-Id: $SESSION_ID" \
http://localhost:3003/sseExample - Terminate Session (deprecated):
curl -X DELETE http://localhost:3003/sse \
-H "Mcp-Session-Id: $SESSION_ID"Migration: Replace /sse with /mcp in all client code. No other changes required.
- Created: On initialization (POST with
method: "initialize") - Active: Session ID in
Mcp-Session-Idheader - Expired: After
MCP4_SESSION_TIMEOUT_MSof inactivity (default: 30 minutes) - Terminated: Explicit DELETE or server shutdown
export MCP4_SESSION_TIMEOUT_MS=1800000 # 30 minutes (default)
export MCP4_SESSION_TIMEOUT_MS=3600000 # 1 hour
export MCP4_SESSION_TIMEOUT_MS=600000 # 10 minutesBehavior:
- Activity tracked on every request
- Expired sessions automatically cleaned up (every 1 minute)
- Expired session requests return HTTP 404
OAuth Sessions:
OAuth sessions with refresh tokens have extended timeouts to avoid forcing users to re-authenticate after periods of inactivity:
- Default OAuth session timeout: 24 hours (configurable via
MCP4_OAUTH_SESSION_TIMEOUT_MS) - Unlimited timeout: Set
MCP4_OAUTH_SESSION_TIMEOUT_MS=0to never expire OAuth sessions - Automatic token refresh: Access tokens are automatically refreshed before expiration (60 seconds before by default, configurable via
MCP4_OAUTH_REFRESH_THRESHOLD_MS)
Why extended timeout for OAuth?
- OAuth access tokens are short-lived (15-60 minutes) for security
- Refresh tokens allow automatic renewal without user intervention
- Extended session timeout prevents unnecessary re-authentication after idle periods
- Users don't need to restart MCP in their IDE when tokens expire
Configuration:
# OAuth session timeout: 24 hours (default)
export MCP4_OAUTH_SESSION_TIMEOUT_MS=86400000
# Unlimited OAuth session timeout (never expire)
export MCP4_OAUTH_SESSION_TIMEOUT_MS=0
# Refresh token 60 seconds before expiration (default)
export MCP4_OAUTH_REFRESH_THRESHOLD_MS=60000Sessions store:
- Session ID (crypto-secure UUID)
- Creation timestamp
- Last activity timestamp
- Active SSE streams (for resumability)
When MCP4_TOKEN_KEY is configured, the gateway wraps OAuth tokens in encrypted envelopes so MCP
clients can survive arbitrary gateway restarts (for example k8s pod evictions) without re-running
the OAuth browser flow.
mcp4.v1.<base64url(12-byte-nonce + AES-256-GCM-ciphertext + 16-byte-tag)>
- Algorithm: AES-256-GCM with a fresh 12-byte random nonce per token.
- Additional Authenticated Data (AAD): the profile_id as UTF-8 bytes. This binds an envelope to exactly one profile and prevents cross-profile replay.
- Payload (encrypted): IdP access_token, IdP refresh_token (optional), expiry, OAuth client_id, scopes, profile_id, issued-at timestamp, and an optional OAuth client registration snapshot (creg) so the client does not need to re-register on restart.
client_secretis NEVER embedded - DCR clients are public PKCE clients without one.
The gateway returns an envelope as access_token in the /oauth/token response only when ALL of:
MCP4_TOKEN_KEYis configured at startup.- The IdP returned a
refresh_token(envelopes without a refresh path provide no recovery benefit, so plain access_token is returned instead).
If encryption fails for any reason, the gateway logs a warn and falls back to the plain IdP access_token - the response shape is unchanged. Clients ALWAYS work, with or without envelopes.
- The MCP client stores the
mcp4.v1.*token from the OAuth response. - The gateway is restarted (k8s rolling deploy, OOM kill, etc.) and all in-memory state is lost.
- The client reconnects and re-presents the same envelope on the next MCP
initializerequest. - The gateway detects the
mcp4.v1.prefix, decrypts usingMCP4_TOKEN_KEYand the request-profile_id (as AAD). On success, it rehydrates the session: refresh_token, expiry, client_id, scopes, and (if creg is present) the OAuth client registration in memory. - If the access token is already expired, the existing refresh-token path silently exchanges it for a fresh access token in the next request - the client sees no auth challenge.
- 64-char hex string: decoded directly as 32 raw bytes (AES-256 key).
- Anything else: SHA-256(value) yields 32 bytes - any passphrase works.
- Whitespace around the value is trimmed before derivation (k8s ConfigMap newline tolerance).
- Unset: plain-token mode is active and a startup warn is logged. Behavior matches earlier releases byte-for-byte.
If your IdP issues rotating refresh tokens AND the gateway restarts after at least one in-session
token refresh, the client still holds the original envelope with the now-stale rt. Re-auth is
required in that case (same as today). For non-rotating refresh tokens, zero-reauth across
arbitrary restarts is supported.
- The AES-GCM auth tag IS the integrity signature - any tamper to nonce, ciphertext, or tag
produces a
nulldecrypt result, NOT a partial recovery. - profile_id is bound as AAD AND post-decrypt-validated - even an attacker who possessed the symmetric key could not present a profile-A envelope to profile-B.
- All decrypt failures are silent (debug log only) - the session falls back to plain-bearer treatment without a 500 or 401.
Resume SSE streams after network disconnection.
- Server: Assigns unique
idto each SSE event - Client: Tracks last received event ID
- Reconnect: Client sends
Last-Event-IDheader - Server: Replays missed events (last 100 per stream)
Initial connection:
curl -N -H "Accept: text/event-stream" \
-H "Mcp-Session-Id: $SESSION_ID" \
http://localhost:3003/mcpResume after disconnect:
LAST_EVENT_ID="1234567890123"
curl -N -H "Accept: text/event-stream" \
-H "Mcp-Session-Id: $SESSION_ID" \
-H "Last-Event-ID: $LAST_EVENT_ID" \
http://localhost:3003/mcpServer replays events with id > 1234567890123.
Keep SSE connections alive through reverse proxies.
export MCP4_HEARTBEAT_ENABLED=true
export MCP4_HEARTBEAT_INTERVAL_MS=30000 # 30 secondsWhy: Proxies (nginx, cloudflare) timeout idle connections
How: Sends :ping\n\n comments (ignored by clients)
Default: Disabled (enable only if needed)
Purpose: Prevent DNS rebinding attacks
Behavior:
- Validates
Originheader for non-localhost requests - Always allows:
localhost,127.0.0.1, configuredMCP4_HOST - Additionally allows: Origins in
MCP4_ALLOWED_ORIGINS(if configured) - Rejects: Other origins with HTTP 403
Default Configuration:
MCP4_ALLOWED_ORIGINSis empty by default- Server binds to
localhost(127.0.0.1) by default - Warning logged if binding to non-localhost with empty
MCP4_ALLOWED_ORIGINS
Supported Formats:
# Exact hostname
export MCP4_ALLOWED_ORIGINS="example.com,api.example.com"
# Wildcard subdomain (*.domain.com)
export MCP4_ALLOWED_ORIGINS="*.company.com" # Matches: api.company.com, web.company.com
# IPv4 CIDR range (for corporate networks)
export MCP4_ALLOWED_ORIGINS="192.168.1.0/24" # Matches: 192.168.1.1 - 192.168.1.254
export MCP4_ALLOWED_ORIGINS="10.0.0.0/8" # Matches: 10.0.0.0 - 10.255.255.255
# IPv6 CIDR range
export MCP4_ALLOWED_ORIGINS="2001:db8::/32" # Matches: 2001:db8:: - 2001:db8:ffff:ffff:ffff:ffff:ffff:ffff
# Combination (comma-separated)
export MCP4_ALLOWED_ORIGINS="example.com,*.company.com,192.168.1.0/24,10.0.0.0/8,2001:db8::/32"Examples:
# Allow specific subdomain
MCP4_ALLOWED_ORIGINS="api.company.com"
# Allow all company subdomains
MCP4_ALLOWED_ORIGINS="*.company.com"
# Allow branch offices (private networks)
MCP4_ALLOWED_ORIGINS="192.168.1.0/24,192.168.2.0/24,192.168.3.0/24"
# Allow entire corporate /8 network
MCP4_ALLOWED_ORIGINS="10.0.0.0/8"
# Allow IPv6 segment
MCP4_ALLOWED_ORIGINS="2001:db8::/32"
# Mixed: public domains + private networks
MCP4_ALLOWED_ORIGINS="example.com,*.company.com,192.168.0.0/16,10.0.0.0/8,2001:db8::/32"Skip: Requests to localhost hostname always allowed without additional configuration
Default: Server binds to 127.0.0.1 (localhost only)
export MCP4_HOST=127.0.0.1 # Secure (default)
export MCP4_HOST=0.0.0.0 # Network access (use with caution!)Security Warning: When binding to non-localhost address without MCP4_ALLOWED_ORIGINS configured, server logs warning. Always set MCP4_ALLOWED_ORIGINS when exposing server to network or bind to localhost.
Default MCP4_ALLOWED_ORIGINS: Empty (no origins allowed except localhost).
- Localhost first: Use
127.0.0.1unless remote access needed - HTTPS reverse proxy: Use traefik/nginx/caddy with TLS for remote access
- Firewall: Restrict port access to trusted IPs
- Strong tokens: Use cryptographically secure API tokens
- Monitor sessions: Check
/healthendpoint regularly
server {
listen 443 ssl http2;
server_name mcp.example.com;
ssl_certificate /path/to/cert.pem;
ssl_certificate_key /path/to/key.pem;
location / {
proxy_pass http://127.0.0.1:3003;
proxy_http_version 1.1;
# SSE support
proxy_set_header Connection '';
chunked_transfer_encoding off;
proxy_buffering off;
proxy_cache off;
# Headers
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 3600s; # 1 hour for SSE
}
}Enable heartbeat to prevent proxy timeouts:
export MCP4_HEARTBEAT_ENABLED=trueCause: Session expired or never initialized
Solution: Initialize first, check MCP4_SESSION_TIMEOUT_MS
# Check timeout
echo $MCP4_SESSION_TIMEOUT_MS
# Increase if needed
export MCP4_SESSION_TIMEOUT_MS=3600000 # 1 hourCause: Origin validation rejected request Solution: Check Origin header, use allowed origin
# Check logs for rejected origin
# Add origin to allowlist or use localhostCause: Proxy timing out SSE stream Solution: Enable heartbeat
export MCP4_HEARTBEAT_ENABLED=true
export MCP4_HEARTBEAT_INTERVAL_MS=30000Cause: Binding to localhost only Solution: Bind to network interface
export MCP4_HOST=0.0.0.0 # or specific IPWarning: Ensure firewall protection!
Enable metrics for production observability:
export MCP4_METRICS_ENABLED=true
export MCP4_METRICS_PATH=/metrics # Optional, default: /metrics
npm startMetrics endpoint:
curl http://localhost:3003/metricsAvailable metrics:
# HTTP metrics
mcp_http_requests_total{method,path,status,profile_id,tenant_id}
mcp_http_request_duration_seconds{method,path,status,profile_id,tenant_id}
# Session metrics
mcp_sessions_active{profile_id,tenant_id}
mcp_sessions_created_total{profile_id,tenant_id}
mcp_sessions_destroyed_total{profile_id,tenant_id}
# Tool call metrics
mcp_tool_calls_total{tool,status,profile_id,tenant_id}
mcp_tool_call_duration_seconds{tool,status,profile_id,tenant_id}
mcp_tool_call_errors_total{tool,error_type,profile_id,tenant_id}
# API call metrics (to backend)
mcp_api_calls_total{operation,status,profile_id,tenant_id}
mcp_api_call_duration_seconds{operation,status,profile_id,tenant_id}
mcp_api_call_errors_total{operation,error_type,profile_id,tenant_id}
Prometheus scrape config:
scrape_configs:
- job_name: 'mcp-server'
static_configs:
- targets: ['<your-mcp-server-host>']
MCP4_METRICS_PATH: '/metrics'
scrape_interval: 15sGrafana dashboard ideas:
- Request rate & latency (p50, p95, p99)
- Active sessions over time
- Tool call success rate
- Backend API error rate
- Session timeout rate
curl http://localhost:3003/healthResponse:
{
"status": "ok",
"sessions": 3
}Monitor sessions count to detect leaks or issues.
JSON format (for log aggregation):
export MCP4_LOG_FORMAT=json
npm startConsole format (for debugging):
export MCP4_LOG_FORMAT=console
npm startDefault rate limit applies to all API operations:
MCP4_HTTP_RATE_LIMIT_MAX_REQUESTS=600 # per minute
MCP4_HTTP_RATE_LIMIT_WINDOW_MS=60000 # 60 secondsDefault: 600 requests/minute per API token
Override rate limits for specific operations in your profile:
{
"http_client": {
"rate_limit": {
"max_requests_per_minute": 600,
"overrides": {
"postApiV4ProjectsIdIssues": {
"max_requests_per_minute": 10
},
"deleteApiV4ProjectsIdIssuesIssueIid": {
"max_requests_per_minute": 5
}
}
}
}
}How it works:
- Rate limits are enforced per API token
- Token bucket algorithm allows bursts
- 429 responses trigger automatic retry with backoff
Different operation types should have different limits:
| Operation Type | Recommended Limit | Reason |
|---|---|---|
| Read (GET) | 120-600 req/min | Low abuse risk |
| Write (POST, PUT) | 10-20 req/min | Prevent spam |
| Delete | 5-10 req/min | Destructive operations |
| Batch | 1-5 req/min | Resource intensive |
Why per-endpoint limits:
- Prevents spam (e.g., mass issue creation)
- Protects against DoS attacks
- Enforces API quotas
- Allows burst traffic for reads
Responses include rate limit information:
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 573
X-RateLimit-Reset: 1634567890When rate limited (429 response):
- Automatic retry: HTTP client retries with exponential backoff
- Backoff schedule: 1s → 2s → 4s
- Max attempts: 3 (configurable in profile)
{
"http_client": {
"retry": {
"max_attempts": 3,
"backoff_ms": [1000, 2000, 4000],
"retry_on_status": [429, 502, 503, 504]
}
}
}HTTP transport supports multiple concurrent clients with separate sessions.
Tested: 100+ concurrent sessions Limit: System resources (memory, file descriptors)
Expired sessions cleaned every 60 seconds.
Memory: ~1KB per session (approx)
Recommendation: Monitor with /health endpoint
Each stream buffers last 100 messages for resumability.
Memory: ~10KB per active stream (approx) Recommendation: Close unused streams
See EXAMPLE-GITLAB.md for complete curl-based examples with GitLab API.
- README.md - Project overview
- PROFILE-GUIDE.md - Creating profiles
- MCP Specification - Official spec