The wrapper sed-patches the two host-wide OAuth 2.1 discovery route registrations in MetaMCP's compiled backend bundle so spec-compliant MCP clients do not force an interactive login flow on public endpoints. See README for full rationale (RFC 9728, MCP Authorization spec 2026-03-26 revision) and the diagnostic evidence. Guarded with grep pre/post-conditions so a future upstream release that renames or restructures these routes fails the build loudly rather than silently shipping an unpatched image.
3.6 KiB
metamcp-patched
A thin wrapper around ghcr.io/metatool-ai/metamcp
that hides the host-wide OAuth 2.1 discovery endpoints so spec-compliant MCP
clients don't force an interactive login on public endpoints.
The problem this fixes
MetaMCP unconditionally publishes RFC 9728 protected-resource metadata and RFC 8414 authorization-server metadata at:
/.well-known/oauth-protected-resource/.well-known/oauth-authorization-server
Both documents describe the whole MetaMCP host as OAuth-protected — even if every configured endpoint is public. Per the MCP Authorization spec (2026-03-26 revision), a spec-compliant client — Claude.ai's remote-MCP integration, Claude Desktop, and the reference SDKs — probes these paths on connect. When it sees them, it kicks off the OAuth flow (with dynamic client registration and PKCE) before calling any tool. Users get an interactive login page even when trying to connect to an endpoint that doesn't require auth.
The route mounts live in a compiled bundle with no env-var gate — the
line app.use(oauth_default) in apps/backend/dist/index.js pulls the
metadata router in unconditionally. There is no admin toggle for it in
the shipping build.
This repo produces a wrapper image that sed-patches the two discovery route registrations to bogus paths, so Express falls through to its default 404 for those requests. Nothing else in MetaMCP changes.
Endpoints that DO enforce auth still 401 correctly; they just can't be consumed via OAuth dynamic-discovery clients any more (consume them via a bearer token in the MCP client's config instead).
Using the pre-built image
# docker-compose.yml
services:
metamcp:
image: git.jpaul.io/justin/metamcp-patched:latest
pull_policy: always
# ... rest of your existing metamcp service config unchanged
Or build from source locally
services:
metamcp:
image: metamcp-patched:local
pull_policy: never
build:
context: https://git.jpaul.io/justin/metamcp-patched.git
# or a local clone / vendored path
Bump upstream at any time with:
docker compose build --pull metamcp && docker compose up -d metamcp
The Dockerfile has grep guards that fail the build loudly if a future
upstream release renames or restructures the OAuth routes, so a bad bump
never silently ships an unpatched image.
Verify it worked
UA="Mozilla/5.0 (X11; Linux x86_64) Firefox/120.0"
# want 404, was 200 pre-patch
curl -sSo /dev/null -w "%{http_code}\n" -H "User-Agent: $UA" \
"https://<your-metamcp-host>/.well-known/oauth-protected-resource"
curl -sSo /dev/null -w "%{http_code}\n" -H "User-Agent: $UA" \
"https://<your-metamcp-host>/.well-known/oauth-authorization-server"
# want 200 (unchanged) — proves you didn't collaterally break the endpoint
curl -sSo /dev/null -w "%{http_code}\n" -H "User-Agent: $UA" \
-H "Accept: application/json, text/event-stream" \
-X POST -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{},"clientInfo":{"name":"diag","version":"1.0"}}}' \
"https://<your-metamcp-host>/metamcp/<a-public-endpoint>/mcp"
Delete this repo the day upstream ships a toggle
The right fix belongs in MetaMCP itself — either an env var like
DISABLE_OAUTH_DISCOVERY=true, or per-endpoint RFC 9728 metadata
scoping so public endpoints don't advertise. Track upstream at
https://github.com/metatool-ai/metamcp.
License
MIT for the wrapper contents (Dockerfile, workflow, docs). The base image and everything it contains remain under their upstream licenses.