Skip to content

Please cut a release: the OAuth refresh_token grant (f446ce3) has been on main, unreleased, since March #338

Description

@rafaelborja

Summary

The latest release — v2.4.22, which is also what ghcr.io/metatool-ai/metamcp:latest currently serves (image created 2025-12-11) — advertises refresh_token in grant_types_supported but rejects that grant at the token endpoint, and issues no refresh token to reject it with.

The implementation landed on main in f446ce3 (2026-03-25, "feat: implement OAuth refresh_token grant type"), whose own commit message describes exactly this case: letting "MCP clients (like Claude.ai Connectors) silently renew access tokens when they expire." Four months on, it has never appeared in a release, so every Docker deployment still forces a full interactive re-authorization every hour.

This is a request to tag a release (or publish an image) containing f446ce3 — not a bug report against main, where it is already fixed.

Impact

ACCESS_TOKEN_EXPIRY is 3600s. In the released build there is no refresh token, so an hour after authorizing, a Claude.ai connector can no longer reach the gateway. It stops listing the upstream tools and shows only its own authenticate / complete_authentication pair. With several connectors behind one MetaMCP instance, all of them fail together, and the only recovery is re-running the browser consent flow for each.

The failure is quiet from the operator's side: the MetaMCP container stays healthy, the upstream MCP servers stay healthy, and the endpoints answer correctly to anything holding a live token.

Reproduction

  1. Deploy ghcr.io/metatool-ai/metamcp:latest (v2.4.22).

  2. curl https://<host>/.well-known/oauth-authorization-server:

    "grant_types_supported": ["authorization_code", "refresh_token"]
  3. Add the instance as a custom connector in Claude.ai and complete OAuth. Tools list correctly.

  4. Wait an hour, then call any tool.

Observed: the connector reports that re-authorization is required and exposes only authenticate / complete_authentication.

Why, in the released build

From v2.4.22's bundled apps/backend/dist/index.js, the authorization-code path:

const accessToken = generateSecureAccessToken();
const expiresIn = 3600;
await oauthRepository.setAccessToken(accessToken, { /* ... */ });
res.json({ access_token: accessToken, token_type: "Bearer",
           expires_in: expiresIn, scope: codeData.scope });

No refresh_token in the response, and the endpoint gates on grant_type !== "authorization_code" — so a client that believes the advertised metadata and attempts a refresh gets unsupported_grant_type, then falls back to full re-auth.

The schema agrees: in a database migrated by v2.4.22, oauth_access_tokens has no refresh-token column.

Column     |           Type
-----------+--------------------------
access_token | text
client_id    | text
user_id      | text
scope        | text
expires_at   | timestamp with time zone
created_at   | timestamp with time zone

Two side effects worth noting, both consistent with the above:

  • oauth_access_tokens tends to sit empty rather than accumulating stale rows — the introspection path calls deleteAccessToken once a token is past expires_at.
  • oauth_clients grows without bound. Each re-add re-runs dynamic client registration; on my instance it reached 81 rows.

Already fixed on main

  • apps/backend/src/routers/oauth/token.tshandleRefreshTokenGrant, REFRESH_TOKEN_EXPIRY = 7 * 24 * 3600, and refresh_token in the token response.
  • apps/backend/drizzle/0014_oauth_refresh_token.sql — adds refresh_token, refresh_token_expires_at and an index.

Ask

Please cut a release including f446ce3 and publish the corresponding image. Self-hosters on the published tags currently have three options: run unreleased main, maintain a fork, or re-authorize every connector every hour.

If a release is not imminent, would you consider a patch release with just f446ce3 and migration 0014 on top of v2.4.22? The migration is additive (ADD COLUMN plus an index), so the upgrade path looks low-risk.

Happy to test a release candidate against a live multi-connector deployment.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions