Skip to content

Feature: MCP OAuth 2.1 PKCE Authentication for HTTP Transport #497

@teknium1

Description

@teknium1

Overview

Our MCP client (tools/mcp_tool.py) currently supports HTTP transport authentication only via static headers (e.g., Authorization: Bearer sk-...). A growing number of MCP servers — including ml.ink (#495), and potentially future services — use OAuth 2.1 with PKCE for authentication instead of (or in addition to) static API keys.

This enhancement would add OAuth 2.1 PKCE flow support to our MCP HTTP transport, allowing Hermes Agent to authenticate with MCP servers that require interactive OAuth without manual token management.

Discovered during: Research into ml.ink integration (#495). Confirmed the OAuth discovery endpoint works at https://mcp.ml.ink/.well-known/oauth-authorization-server and dynamic client registration succeeds at /oauth/register.


Research Findings

How MCP OAuth 2.1 PKCE Works

The MCP specification supports OAuth 2.1 with PKCE as a standard authentication mechanism for HTTP transport. The flow:

  1. Discovery — Client checks <server>/.well-known/oauth-authorization-server for endpoints
  2. Dynamic Client Registration — Client registers at the registration_endpoint (one-time)
  3. Authorization Code Flow
    • Client generates a code verifier + S256 challenge
    • Opens browser to authorization_endpoint with PKCE challenge
    • User authenticates and grants permissions
    • Server redirects to callback URL with authorization code
  4. Token Exchange — Client exchanges code + verifier at token_endpoint for access + refresh tokens
  5. Token Usage — Client adds Authorization: Bearer <access_token> to MCP requests
  6. Token Refresh — Client uses refresh_token to get new tokens when expired

ml.ink's OAuth Implementation (Confirmed)

{
    "authorization_endpoint": "https://mcp.ml.ink/oauth/authorize",
    "code_challenge_methods_supported": ["S256"],
    "grant_types_supported": ["authorization_code", "refresh_token"],
    "issuer": "https://mcp.ml.ink",
    "registration_endpoint": "https://mcp.ml.ink/oauth/register",
    "response_types_supported": ["code"],
    "token_endpoint": "https://mcp.ml.ink/oauth/token",
    "token_endpoint_auth_methods_supported": ["none"]
}

Dynamic client registration was tested and works:

curl -X POST https://mcp.ml.ink/oauth/register \
  -H "Content-Type: application/json" \
  -d '{"client_name": "hermes-agent", "redirect_uris": ["http://localhost:3000/callback"]}'
# Returns: {"client_id": "hermes-agent", ...}

Current State in Hermes Agent

What we have:

  • MCP HTTP transport in tools/mcp_tool.py supports static headers config:
    mcp_servers:
      remote_api:
        url: "https://my-mcp-server.example.com/mcp"
        headers:
          Authorization: "Bearer sk-..."
  • This works for API key auth but not OAuth flows

What we lack:

  • No OAuth discovery (/.well-known/oauth-authorization-server)
  • No PKCE code challenge generation
  • No browser-based authorization redirect
  • No token storage/refresh logic
  • No dynamic client registration

Implementation Plan

This is a Tool Enhancement (not a Skill)

This modifies tools/mcp_tool.py — the existing MCP client tool — to support an additional authentication method. Per CONTRIBUTING.md: this is core tool code that handles auth flows, token management, and credential storage, which all require deterministic processing logic.

What We'd Need

  1. OAuth discovery — Check for /.well-known/oauth-authorization-server on first connection to URL-based MCP servers
  2. Dynamic client registration — Register hermes-agent as an OAuth client if needed
  3. PKCE flow — Generate code verifier/challenge, open browser for auth, handle callback
  4. Token storage — Store access/refresh tokens securely (e.g., ~/.hermes/mcp-tokens/<server>.json)
  5. Token refresh — Automatically refresh expired tokens before MCP requests
  6. Config syntax — New auth option in config.yaml:
    mcp_servers:
      ink:
        url: "https://mcp.ml.ink/mcp"
        auth: oauth  # triggers OAuth PKCE flow on first use

Phased Rollout

Phase 1: Basic OAuth PKCE

  • OAuth discovery endpoint detection
  • PKCE code generation (S256)
  • Browser-based authorization (open URL, listen on localhost callback)
  • Token exchange and storage at ~/.hermes/mcp-tokens/
  • Automatic Bearer header injection on MCP requests
  • auth: oauth config option

Phase 2: Token lifecycle

  • Automatic token refresh using refresh_token grant
  • Token expiry detection and proactive refresh
  • Graceful re-auth if refresh token is revoked
  • CLI command: hermes mcp auth <server> for manual re-authentication

Phase 3: Headless/gateway support

  • Device code flow for headless environments (Telegram/Discord gateway)
  • Or: prompt user to visit URL and paste back auth code
  • Token sharing across CLI and gateway sessions

Pros & Cons

Pros

  • Unlocks OAuth MCP servers — ml.ink, and any future MCP server using OAuth, becomes seamlessly accessible
  • Better security — OAuth tokens can be scoped and revoked; static API keys are all-or-nothing
  • Standards-compliant — OAuth 2.1 PKCE is the MCP specification's recommended auth flow
  • User convenience — No need to manually generate and paste API keys

Cons / Risks

  • Complexity — OAuth PKCE adds ~200-300 lines of code to mcp_tool.py, plus a localhost HTTP server for the callback
  • Browser dependency — CLI environments need webbrowser.open() which may not work in all contexts (SSH, Docker, headless)
  • Gateway challenge — Telegram/Discord users can't easily do browser-based OAuth. Fallback to API key auth is necessary.
  • Token storage security — Tokens stored on disk need appropriate permissions (chmod 600)

Open Questions

  1. Priority — API key auth works for ml.ink and most MCP servers. Is OAuth support urgent, or can it wait?
  2. Headless flow — For gateway/SSH users, should we support device code flow, or just fall back to API key auth?
  3. Callback server — Should the localhost callback be a simple http.server handler, or use a more robust approach?
  4. Token storage location~/.hermes/mcp-tokens/ vs ~/.hermes/auth.json (where we already store Nous Portal OAuth tokens)?

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions