Skip to main content
Glama
arpitjain310

warrant

by arpitjain310

warrant

ci

A remote MCP server that acts as a proper OAuth resource server: every tool call is authenticated against an external identity provider, authorized per tool, and scoped to the caller's own data.

Status: work in progress.


The problem

Most MCP server examples run over stdio and trust whoever launched them. On a URL, every request comes from a client you don't control, carrying a token issued by an authorization server you don't run. Each request has to answer:

  • Is the token genuine? Signature against the issuer's published keys, issuer, expiry.

  • Was it issued for this server? A token minted for a different API is refused (audience). Otherwise any service that sees a user's token can replay it here.

  • Can it call this tool? Read and write are separate scopes; a read-only token can't create.

  • Whose data? Every query is filtered by the token's subject. There is no unscoped read path.

The server never issues tokens. Login, consent and client registration stay with the authorization server; this side only verifies.

Related MCP server: Note App MCP Server

What it does

client ── POST /mcp + Bearer ──▶ JwtVerifier ──▶ scope gate ──▶ tool ──▶ NoteStore
  ▲                              JWKS, iss,      tool → scope   owner =    SQLite
  │                              aud, exp             │         token.sub
  ├── 401 + resource_metadata ◀── missing / invalid   │
  │   (discover the authorization server, log in)     │
  └── 403 insufficient_scope ◀────────────────────────┘
      (re-authorize with the added scope, retry)
  • Discovery — an unauthenticated request gets 401 with WWW-Authenticate: Bearer resource_metadata=.... The protected resource metadata (RFC 9728) names the authorization server and the baseline scope, notes:read, so a client can log in without per-server configuration.

  • Step-up — a call the token lacks scope for gets 403 with WWW-Authenticate: Bearer error="insufficient_scope", scope="...", listing everything that operation needs. The client re-authorizes and retries, so write access is granted when first used, not at login.

  • JwtVerifier (auth.py) — validates JWT access tokens (RFC 9068) against the issuer's JWKS: signature, iss, aud, exp, sub. RS256 only. An unknown key ID triggers one JWKS refetch (key rotation), with a cooldown so forged key IDs can't make it hammer the issuer.

  • Scope gate (server.py) — TOOL_SCOPES maps each tool to its scope. The HTTP gate reads the tool from the 2026-07-28 Mcp-Name header, which the SDK rejects if it disagrees with the body. A handler-tier check repeats it for requests without the header, and fails closed on a tool missing from the map.

  • NoteStore (store.py) — SQLite. Every method takes the owner.

Tools

Tool

Scope

Notes

search_notes(query, limit)

notes:read

Case-insensitive substring; empty query lists the most recent.

get_note(note_id)

notes:read

Another user's note reads as not found.

create_note(title, body)

notes:write

Every request also needs notes:read.

Explicit non-goals

  • Not an authorization server — no login, token issuance or client registration. Targets any OIDC provider that issues JWT access tokens.

  • Not a gateway — one server, its own tools.

  • Notes are a deliberately small domain; the auth layer is the point.

Protocol

Built on the official Python SDK (mcp 2.x) at MCP revision 2026-07-28: stateless, no sessions, each request carries its own protocol version. The SDK handles transport and the 401. Token verification, per-tool scopes, the 403 step-up challenge and per-user data scoping are here.

tests/test_oauth_flow.py runs the SDK's own OAuth client against the server and a stub authorization server: discovery, a notes:read login, a 403 on create_note, re-authorization for notes:read notes:write, and the retry.

Quickstart

python -m venv .venv && . .venv/bin/activate   # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
pytest -q
warrant --dev

--dev needs no identity provider. It runs a local issuer (signs tokens and serves its keys; not an authorization server) and prints a read token and a read+write token for user dev. Pass one as a bearer header from any MCP client, e.g. Claude Code:

claude mcp add --transport http warrant http://127.0.0.1:8000/mcp --header "Authorization: Bearer <token>"

With the read token, create_note gets the 403 step-up challenge. A static header can't re-authorize, so the call stops there.

Against an identity provider

The API (audience) identifier is the server's public /mcp URL, with notes:read and notes:write defined as its scopes:

warrant --issuer https://YOUR_TENANT/ --resource-url http://localhost:8000/mcp

Related MCP Connectors

Related MCP Servers

  • F
    license
    Not graded
    quality
    D
    maintenance
    A production-ready MCP server that authenticates agents via OAuth 2.1 Bearer tokens, validates JWTs with JWKS, enforces tool-level scopes and roles, and logs the full delegation chain.
    -
  • A
    license
    A
    quality
    D
    maintenance
    A compact MCP server demonstrating explicit tool boundaries, least-privilege discovery, execution-time authorization, destructive-action confirmation, and metadata-only audit logs using a local note store.
    3
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    A personal note store exposed as an MCP server. Enables any MCP-speaking assistant to create, search, list, and categorize notes, with per-client bearer tokens for author attribution.
    129 npm
    MIT