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
-
Deploy ghcr.io/metatool-ai/metamcp:latest (v2.4.22).
-
curl https://<host>/.well-known/oauth-authorization-server:
"grant_types_supported": ["authorization_code", "refresh_token"]
-
Add the instance as a custom connector in Claude.ai and complete OAuth. Tools list correctly.
-
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.ts — handleRefreshTokenGrant, 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.
Summary
The latest release — v2.4.22, which is also what
ghcr.io/metatool-ai/metamcp:latestcurrently serves (image created 2025-12-11) — advertisesrefresh_tokeningrant_types_supportedbut rejects that grant at the token endpoint, and issues no refresh token to reject it with.The implementation landed on
mainin 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_EXPIRYis 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 ownauthenticate/complete_authenticationpair. 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
Deploy
ghcr.io/metatool-ai/metamcp:latest(v2.4.22).curl https://<host>/.well-known/oauth-authorization-server:Add the instance as a custom connector in Claude.ai and complete OAuth. Tools list correctly.
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:No
refresh_tokenin the response, and the endpoint gates ongrant_type !== "authorization_code"— so a client that believes the advertised metadata and attempts a refresh getsunsupported_grant_type, then falls back to full re-auth.The schema agrees: in a database migrated by v2.4.22,
oauth_access_tokenshas no refresh-token column.Two side effects worth noting, both consistent with the above:
oauth_access_tokenstends to sit empty rather than accumulating stale rows — the introspection path callsdeleteAccessTokenonce a token is pastexpires_at.oauth_clientsgrows 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.ts—handleRefreshTokenGrant,REFRESH_TOKEN_EXPIRY = 7 * 24 * 3600, andrefresh_tokenin the token response.apps/backend/drizzle/0014_oauth_refresh_token.sql— addsrefresh_token,refresh_token_expires_atand 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 COLUMNplus an index), so the upgrade path looks low-risk.Happy to test a release candidate against a live multi-connector deployment.