Skip to main content
Glama
vmhq

OpenRouter MCP Server

by vmhq

OpenRouter MCP Server

Remote MCP server (streamable HTTP, stateless JSON) that lets AI agents delegate tasks to cheaper models via the OpenRouter API, querying the catalog and live prices, with a cost policy configurable through a .env file.

What it does

  • Live catalog: queries OpenRouter's GET /api/v1/models (with a 5-minute cache) and exposes prices in USD per million tokens, context window, and tool-calling support.

  • Explicit delegation: the agent picks the model by looking at prices and delegates the task.

  • Automatic price-based delegation: the server picks the model based on a tier (economy / balanced / quality) using configurable price bands.

  • Policy via .env: max price caps, allowed/blocked model lists, default model, preferred providers.

  • Real cost: every delegation returns tokens used and estimated cost in USD.

  • No truncated answers: the server sizes the output budget itself and resumes answers that hit the token limit, so the calling agent never has to guess max_tokens (see Output budget).

Related MCP server: whichmodel-mcp

Installation

npm install
cp .env.example .env   # edit and set your OPENROUTER_API_KEY
npm run build
npm start              # listens on http://localhost:3000/mcp

For development with auto-reload: npm run dev.

Docker

A multi-arch image (linux/amd64, linux/arm64) is built automatically by GitHub Actions and published to GHCR:

ghcr.io/vmhq/openrouter-mcp-server

Available tags: latest (main branch), vX.Y.Z / X.Y (releases), main, and sha-<commit>.

Docker Compose

services:
  openrouter-mcp:
    image: ghcr.io/vmhq/openrouter-mcp-server:latest
    container_name: openrouter-mcp
    restart: unless-stopped
    ports:
      - "3000:3000"
    env_file:
      - .env
    volumes:
      # Persists OAuth state (registered clients, token hashes)
      - ./data:/app/data
    healthcheck:
      test: ["CMD", "wget", "-qO-", "http://localhost:3000/health"]
      interval: 30s
      timeout: 5s
      retries: 3
docker compose up -d

Note: the container runs as the unprivileged node user. Make sure the mounted ./data directory is writable by UID 1000 (chown -R 1000:1000 ./data), otherwise OAuth state cannot be persisted.

Example .env

# ============================================================
# OpenRouter MCP Server - environment variables
# Copy this file to .env and fill in the values.
# ============================================================

# --- Required ---
# Your OpenRouter API key (https://openrouter.ai/keys)
OPENROUTER_API_KEY=sk-or-v1-...

# --- HTTP server ---
# Port where the MCP endpoint is exposed (http://host:PORT/mcp)
PORT=3000
# Optional token protecting the remote server. If set, MCP clients must send
# "Authorization: Bearer <token>". Strongly recommended if the server is
# reachable outside localhost.
MCP_AUTH_TOKEN=

# --- Interactive OAuth with PocketID (for AI agents like Claude) ---
# Public URL of this server (e.g. https://mcp.example.com). Required so the
# OAuth metadata and callback point to the right URL behind a reverse proxy.
MCP_PUBLIC_URL=
# When all three POCKETID_* variables are set, the /oauth/authorize flow
# delegates the human login to your PocketID instance (passkey).
# In PocketID: create an OIDC client and register this callback:
#   <MCP_PUBLIC_URL>/oauth/callback
# Restrict who can sign in with the OIDC client's allowed groups in PocketID.
# The static MCP_AUTH_TOKEN keeps working in parallel for machine-to-machine
# access.
POCKETID_ISSUER=
POCKETID_CLIENT_ID=
POCKETID_CLIENT_SECRET=
# Optional OIDC scopes (space-separated). Default: "openid profile email".
# POCKETID_SCOPES=openid profile email
# Path of the file where OAuth state is persisted (registered clients, codes
# and token hashes). Default: ./data/oauth-state.json
# MCP_OAUTH_STATE_PATH=./data/oauth-state.json
# OAuth access token lifetime, in seconds. Default: 2592000 (30 days).
# MCP_OAUTH_TOKEN_TTL_S=2592000

# --- Optional OpenRouter attribution (rankings) ---
APP_URL=
APP_TITLE=OpenRouter MCP Server

# --- Delegation policy ---
# Default model when the agent doesn't specify one in openrouter_delegate_task
DEFAULT_MODEL=

# Price caps (USD per million tokens). Models above them are rejected with an
# explanatory error. Empty = no limit.
MAX_PROMPT_PRICE_PER_M=
MAX_COMPLETION_PRICE_PER_M=

# Comma-separated control lists. Accept exact ids ("openai/gpt-4.1-mini") or
# provider prefixes ("openai/"). Empty ALLOWED_MODELS = all allowed (except
# the blocked ones).
ALLOWED_MODELS=
BLOCKED_MODELS=

# Allow free models (price 0)? They usually have strict rate limits.
ALLOW_FREE_MODELS=true

# Preferred providers for automatic selection (openrouter_auto_delegate)
PREFERRED_PROVIDERS=openai,anthropic,google,meta-llama,mistralai,deepseek,qwen,x-ai,amazon

# "Blended" price caps (70% prompt + 30% completion, USD/M tokens) for each
# tier of the automatic selection.
TIER_ECONOMY_MAX_PRICE=0.5
TIER_BALANCED_MAX_PRICE=3
TIER_QUALITY_MAX_PRICE=15

# Model catalog cache, in seconds
MODELS_CACHE_TTL_SECONDS=300

# --- Output budget (truncated-answer control) ---
# The calling agent should not have to guess max_tokens: the server derives a
# budget from each model's context window and per-request output cap, and
# resumes answers that get cut off. These knobs bound that behaviour.

# Completion budget used when the agent does not pass max_tokens.
DEFAULT_MAX_TOKENS=4096
# Floor applied on reasoning models, whose budget is consumed by internal
# chain-of-thought before any visible text is produced.
REASONING_MIN_MAX_TOKENS=2000
# Hard ceiling for a single delegation, summed across auto-continuations.
# This is the real cost guard: it bounds how much a runaway answer can spend.
MAX_OUTPUT_TOKENS=32000
# How many times an answer cut off by the token limit may be auto-resumed.
# Set to 0 to disable auto-continuation server-wide.
MAX_CONTINUATIONS=3
# Max characters returned inline in one tool result. Longer answers are
# returned as a first page plus a response_id for openrouter_fetch_response.
MAX_RESPONSE_CHARS=25000

Environment variables

See .env.example — the main ones:

Variable

Description

OPENROUTER_API_KEY

Required. Your key from https://openrouter.ai/keys

PORT

HTTP port (default 3000)

MCP_AUTH_TOKEN

If set, clients must send Authorization: Bearer <token>. Effectively mandatory if you expose the server outside localhost.

MCP_PUBLIC_URL

Public URL of the server (e.g. https://mcp.example.com); required for the OAuth flow behind a reverse proxy

POCKETID_ISSUER / POCKETID_CLIENT_ID / POCKETID_CLIENT_SECRET

Enable interactive OAuth login by delegating authentication to your PocketID instance (see below)

DEFAULT_MODEL

Model used by openrouter_delegate_task when the agent doesn't specify one

MAX_PROMPT_PRICE_PER_M / MAX_COMPLETION_PRICE_PER_M

Price ceiling (USD/M tokens); more expensive models are rejected

ALLOWED_MODELS / BLOCKED_MODELS

Comma-separated lists: exact ids or prefixes (openai/)

ALLOW_FREE_MODELS

Allow free models (default true)

TIER_*_MAX_PRICE

Combined price ceilings (0.7·input + 0.3·output) for each tier of the automatic selection

DEFAULT_MAX_TOKENS

Completion budget when the agent doesn't pass max_tokens (default 4096)

REASONING_MIN_MAX_TOKENS

Budget floor for reasoning models (default 2000)

MAX_OUTPUT_TOKENS

Ceiling for one delegation across all continuations (default 32000) — the real cost guard

MAX_CONTINUATIONS

How many times a cut-off answer is auto-resumed (default 3; 0 disables it)

MAX_RESPONSE_CHARS

Inline size limit of a tool result before paging kicks in (default 25000)

Exposed tools

Tool

Description

openrouter_list_models

Lists models with live prices; filters by text, price, context, tool-calling; sort by price/context/recency; paginated

openrouter_get_model

Full detail of a model + whether the .env policy allows it

openrouter_delegate_task

Delegates a task to a specific model; returns response, tokens, and estimated cost

openrouter_auto_delegate

The server picks the model by price tier (economy/balanced/quality) and delegates

openrouter_fetch_response

Reads the remaining pages of a delegated answer too large to return inline

openrouter_check_credits

Usage and limits of the configured API key

Typical agent flow: openrouter_list_models (or directly openrouter_auto_delegate with the economy tier) → delegate the task → use the response, knowing how much it cost.

Important: the delegated model does not see the agent's conversation; the task (task) must be self-contained, with all the necessary context.

Connecting an agent

Claude Code:

claude mcp add --transport http openrouter http://localhost:3000/mcp

With an auth token:

claude mcp add --transport http openrouter http://YOUR_HOST:3000/mcp --header "Authorization: Bearer YOUR_TOKEN"

Any MCP client: point it at the POST /mcp endpoint with the "streamable HTTP" transport. There's a GET /health endpoint for monitoring.

claude.ai (remote connector): requires a public HTTPS URL — deploy the server on a VPS behind a reverse proxy (Caddy/nginx) or use a tunnel (e.g. cloudflared tunnel). With OAuth enabled (see below), add the connector pointing to https://YOUR_HOST/mcp and leave the advanced OAuth Client ID/Secret fields empty: the server publishes OAuth metadata and supports Dynamic Client Registration, so Claude registers itself and obtains its token automatically when you click Authorize.

OAuth with PocketID

The server implements full OAuth 2.1 for AI agents (Claude, Cursor, …): it acts as the authorization server towards MCP clients (RFC 7591 Dynamic Client Registration + PKCE S256 + issuing its own tokens, with RFC 8414/9728 metadata) and delegates the human login to your PocketID instance via OIDC (passkey).

Flow: the MCP client receives a 401 with WWW-Authenticate → discovers the metadata at /.well-known/oauth-protected-resource → registers at /oauth/register → opens /oauth/authorize in the browser → the user signs in to PocketID with their passkey → PocketID returns to /oauth/callback → the server issues its own code and the client exchanges it at /oauth/token for an access token (30 days by default).

Setup:

  1. In PocketID, create a new OIDC client.

  2. Register the callback: <MCP_PUBLIC_URL>/oauth/callback.

  3. Restrict who can sign in using the OIDC client's allowed groups in PocketID.

  4. Copy the Client ID and Client Secret into POCKETID_CLIENT_ID / POCKETID_CLIENT_SECRET, and set the PocketID base URL in POCKETID_ISSUER.

  5. Set MCP_PUBLIC_URL to the server's public HTTPS URL.

If the POCKETID_* variables are not set, the interactive /oauth/authorize flow shows an error; the static MCP_AUTH_TOKEN bearer keeps working in parallel for machine-to-machine access (curl, Codex, etc.).

OAuth state (registered clients, one-time codes, and SHA-256 hashes of the tokens — never the plaintext tokens) is persisted to ./data/oauth-state.json (configurable via MCP_OAUTH_STATE_PATH). If the connector fails after a restart with wiped state, remove it in Claude and add it again so it re-registers.

Output budget (no more truncated answers)

Picking max_tokens is the calling agent's most common failure mode: too low and the answer is cut off mid-sentence, and on reasoning models the whole budget goes to hidden chain-of-thought so the answer comes back empty. The server takes that decision away from the agent.

  1. The budget is derived, not guessed. If the call omits max_tokens, the server starts from DEFAULT_MAX_TOKENS and clamps it to what the model actually accepts: the provider's per-request output cap (top_provider.max_completion_tokens) and the room left in the context window after the prompt. A prompt that leaves no room for an answer is reported as such instead of returning a stub.

  2. Reasoning models get a floor. On a model that supports reasoning, a budget below REASONING_MIN_MAX_TOKENS is raised automatically — that is what makes a "write one line" delegation come back empty.

  3. Cut-off answers are resumed. If the model stops with finish_reason: "length", the server feeds the partial answer back, asks it to continue exactly where it stopped, and stitches the pieces together — up to MAX_CONTINUATIONS rounds and MAX_OUTPUT_TOKENS in total. Pass auto_continue: false to opt out per call.

  4. An empty reasoning answer is retried, once, bigger. If the first attempt produced only hidden tokens, the server retries with a much larger budget before giving up.

  5. Whatever happens is reported. The result always carries truncated, continuations and human-readable notes (e.g. "raised max_tokens from 300 to 2000"), so a still-incomplete answer is never mistaken for a complete one.

  6. Large answers are paged, not clipped. An answer over MAX_RESPONSE_CHARS is returned as a first page plus a response_id; the agent pulls the rest with openrouter_fetch_response instead of having its client clip the tool result. Stored answers live in memory for 30 minutes.

max_tokens remains available as a deliberate cost/length cap — it is just no longer something the agent has to get right.

The answer itself is returned as plain text with a compact metadata footer, rather than JSON-escaped inside the payload; the full structured data is still available in structuredContent.

Development

npm run dev        # tsx watch
npm run typecheck  # tsc over src/ and test/
npm test           # typecheck + node:test suite
npm run build      # emit dist/

How openrouter_auto_delegate picks a model

  1. Filters the catalog by the .env policy and the call's requirements (require_tools, min_context, text output).

  2. Computes the combined price per model: 0.7·input_price + 0.3·output_price (USD/M tokens).

  3. Depending on the tier, searches within its price band (falling back to the neighboring band if empty):

    • economy (≤ $0.5/M by default): the cheapest.

    • balanced ($0.5–$3/M): the cheapest in the middle band.

    • quality ($3–$15/M): the highest-priced within the ceiling (price as a proxy for capability, without reaching flagship models).

  4. Prefers providers from PREFERRED_PROVIDERS, and reports in the response the chosen model, the reasoning, and the discarded alternatives.

Security

  • The OpenRouter API key lives only in the server's .env; it is never exposed to agents.

  • The .env file is in .gitignore.

  • If the port is reachable from outside, set MCP_AUTH_TOKEN and serve behind HTTPS.

A
license - permissive license
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

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

Related MCP Servers

View all related MCP servers

Related MCP Connectors

  • SaaS intelligence for AI agents. 5 unified tools cover 1,000+ services with 91-96% token savings.

  • See, price, and control every tool call your AI agents make: policy checks, cost, and audit tools.

  • Human-as-a-Service for AI agents. Delegate tasks that need a real human, get results via API.

View all MCP Connectors

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/vmhq/openrouter-mcp-server'

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