Skip to content

Feature: let the MCP server act as its own authorization server (PAT as the login step), so CE works with header-less connector clients #201

Description

@smalyu

Feature: let the MCP server act as its own authorization server (PAT as the login step), so CE works with header-less connector clients

Summary

Connector-style MCP clients — Claude's "Add custom connector", ChatGPT connectors — accept only a URL plus optional OAuth client ID/secret. There is no field for custom headers. That makes the PAT transport at /http/api-key/mcp structurally unusable for this entire class of client: it needs Authorization: Bearer <pat> and X-Workspace-Slug, and the client has no way to send either.

On Plane CE the OAuth transport at /http/mcp doesn't work either, because it proxies to /auth/o/* endpoints CE doesn't have. So for a self-hosted CE instance the remote-transport matrix is empty:

Transport Mount CE status
OAuth HTTP /http/mcp upstream /auth/o/* → 404
OAuth SSE /sse same
PAT header /http/api-key/mcp works, but unreachable from connector clients (no header field)

This is the gap I'd like to propose closing inside this repo, without waiting on makeplane/plane#8782.

Evidence

Against a self-hosted CE instance (plane-aio-community:v1.4.1, host genericised):

$ for p in /auth/o/authorize-app/ /auth/o/token/ /auth/o/app-installation/ /api/v1/users/me/; do
    printf "%-30s " "$p"; curl -s -o /dev/null -w "%{http_code}\n" "https://plane.example.com$p"
  done
/auth/o/authorize-app/         404
/auth/o/token/                 404
/auth/o/app-installation/      404
/api/v1/users/me/              401

$ curl -s -X POST "https://plane.example.com/auth/o/token/" -d 'grant_type=authorization_code&code=x'
{"error": "Page not found."}

The 401 on /api/v1/users/me/ and the Django-shaped JSON 404 confirm the requests reach the API — the routes genuinely don't exist, it isn't a proxy misconfiguration. The CE proxy config forwards /auth/* to the API service, so there is nothing to fix on the deployment side.

For the record, this is unchanged in 0.3.0. Diffing 0.3.0 against 0.2.10: __main__.py, client.py, auth/plane_oauth_provider.py, auth/plane_header_auth_provider.py and storage.py are byte-identical; server.py only factors the shared middleware into _configured(). The 139 → 28 tool consolidation is unrelated to auth.

Proposal

Add a transport where the MCP server is the authorization server, and the login step is the user pasting their own Plane PAT, which the server validates against the instance and exchanges for a per-user, revocable token.

The pieces are already here:

  • Protocol plumbing — fastmcp 3.2.0 ships fastmcp/server/auth/providers/in_memory.py::InMemoryOAuthProvider with DCR, PKCE, auth codes, refresh and revocation. It is explicitly a test double (in-memory, authorize() auto-approves with no user interaction), so this isn't "just use it" — but the OAuth 2.1 surface doesn't need to be written from scratch.
  • PAT validation — PlaneHeaderAuthProvider._validate_api_key() already does exactly the needed check: GET /api/v1/users/me/ with x-api-key.
  • Durable storage — storage.py::build_token_store() already provides the Redis-backed store used by the OAuth proxy, so issued tokens have somewhere to live.
  • Redirect allowlist — DEFAULT_ALLOWED_REDIRECT_URIS in server.py already lists https://claude.ai/* and the ChatGPT connector callbacks.

Missing: a consent/login screen that takes a PAT (plus workspace slug, which today comes from X-Workspace-Slug or installations[0] and has no other source — see client.py::get_plane_client_context), and a provider that persists the PAT↔token binding.

Sketch of the flow:

  1. Client hits /http/self/mcp → 401 with WWW-Authenticate, discovery proceeds against this server's own metadata.
  2. Client registers via DCR (or the operator pastes a client ID/secret).
  3. /authorize renders a form: Plane PAT + workspace slug.
  4. Server validates the PAT via /api/v1/users/me/, stores the binding, issues code → token.
  5. Tool calls resolve the caller's own PAT; get_plane_client_context() already supports auth_method="api_key_header", so downstream code is unchanged.

Why this and not the alternatives

  • Waiting for [feature]: Add OAuth app registration to CE for MCP server support plane#8782 (OAuth apps in CE) — that's the right long-term fix, but it's Plane-side, still unshipped, and even once it lands it won't help anyone on an older CE. This proposal is independent of the Plane version and works against every CE that has /api/v1/users/me/.
  • Reverse proxy injecting the headers, with a secret in the URL path — works today (I have it working), but the URL becomes a permanent shared bearer credential: not revocable per user, and every write in Plane is attributed to whichever account owns the injected PAT.
  • An external IdP via the bundled fastmcp providers (GitHub/Google/WorkOS) — fixes who can reach the endpoint, but the Plane-side identity is still a single shared PAT, so attribution inside Plane is unchanged.

Only "server as AS with the user's own PAT" gives per-user identity and revocation on CE.

Related

Environment

  • plane-mcp-server 0.3.0 (and 0.2.10 — identical in the relevant modules)
  • Plane self-hosted CE, makeplane/plane-aio-community:v1.4.1
  • Client: Claude custom connector

Happy to send a PR if the direction is acceptable — the main thing I'd want steering on is whether a PAT-login AS is welcome in this repo at all, or whether CE auth is considered strictly a Plane-side concern.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    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