Greptile MCP plugin: OAuth flow completes but tokens are not persisted, leaving plugin permanently unauthenticated
Greptile MCP plugin: OAuth flow completes but tokens are not persisted, leaving plugin permanently unauthenticated
Summary
The official greptile@claude-plugins-official plugin's OAuth flow runs end-to-end (browser consent succeeds, authorization code is consumed by Claude Code's localhost callback), but the resulting tokens are not written to the keychain Claude Code-credentials mcpOAuth map. The plugin therefore stays in "needs auth" state indefinitely, exposing only the two placeholder tools (authenticate, complete_authentication) and never the real ones (list_merge_requests, search_custom_context, trigger_code_review, etc.).
Other plugins using the same OAuth-via-RFC9728-discovery pattern (sentry-mcp, posthog, intercom, klaviyo, semrush) persist tokens correctly. Only greptile exhibits this failure.
Environment
- Claude Code:
2.1.146 - macOS:
26.4.1(Darwin 25.4.0) - Plugin:
greptile@claude-plugins-official(scope: user, installed 2026-04-28, nogitCommitSharecorded ininstalled_plugins.json) - Plugin
.mcp.json(~/.claude/plugins/cache/claude-plugins-official/greptile/unknown/.mcp.json):
``json``
{
"greptile": {
"type": "http",
"url": "https://api.greptile.com/mcp",
"headers": {
"Authorization": "Bearer ${GREPTILE_API_KEY}"
}
}
}
What works (rules out server-side problem)
Greptile's MCP endpoint responds correctly to both static Bearer auth (using a personal API key) and OAuth Bearer tokens (using the access token from a completed flow):
curl -X POST https://api.greptile.com/mcp \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-H "Accept: application/json, text/event-stream" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
Returns HTTP 200 with 12 real tools (list_custom_context, get_custom_context, search_custom_context, list_merge_requests, list_pull_requests, get_merge_request, list_merge_request_comments, list_code_reviews, get_code_review, trigger_code_review, search_greptile_comments, create_custom_context).
The static Bearer header in .mcp.json appears to be ignored by Claude Code in favor of OAuth discovery — Greptile publishes RFC 9728 protected-resource metadata at https://api.greptile.com/.well-known/oauth-protected-resource:
{
"resource": "https://api.greptile.com/mcp",
"authorization_servers": ["https://auth.greptile.com"],
"scopes_supported": ["read", "write"],
"bearer_methods_supported": ["header"]
}
(Note: bearer_methods_supported: ["header"] here — the same method .mcp.json configures — but Claude Code still routes through OAuth.)
What fails
After a full OAuth flow that the browser experiences as successful (Allow clicked at auth.greptile.com, redirect to http://localhost:<port>/callback?code=...&state=... lands and the localhost listener responds), the keychain Claude Code-credentials entry's mcpOAuth map contains zero plugin:greptile:greptile|* entries. Same map shows healthy entries for the working plugins:
mcpOAuth keys after greptile OAuth flow completes:
- klaviyo|802946b484e2b2ec
- semrush|0d62b1893a361fc0
- plugin:sentry-mcp:sentry|800cb29a3ac61727
- posthog|b66af36bf9e35c01
- plugin:sentry-mcp-experimental:sentry|5af721fb944eb408
- intercom|51f5eb6147df6f63
# ← no plugin:greptile:greptile entry
~/.claude/mcp-needs-auth-cache.json simultaneously shows plugin:greptile:greptile with only a timestamp (no id field, unlike the authenticated claude.ai Gmail/Google Drive/Google Calendar entries which carry id: mcpsrv_*).
The localhost listener appears to have consumed the code — Greptile's token endpoint confirms it when probed with the same code from outside Claude Code:
$ curl -X POST https://auth.greptile.com/oauth2/token \
-d "grant_type=authorization_code" \
-d "code=<the same code the browser delivered to localhost callback>" \
-d "redirect_uri=http://localhost:<port>/callback" \
-d "client_id=<dynamically registered client_id>"
{"error":"invalid_grant","error_description":"...The authorization code has already been used."}
HTTP 400
So: Claude Code did exchange the code for tokens (code is consumed), but the tokens never landed in the keychain entry where every other working OAuth plugin's tokens live. Some failure between successful token exchange and persistence is silent.
Steps to reproduce
- Install
greptile@claude-plugins-officialvia/plugin. - Confirm the only exposed tools are
mcp__plugin_greptile_greptile__authenticateandmcp__plugin_greptile_greptile__complete_authentication. - Invoke
authenticate— Claude Code returns the OAuth URL. - Open URL in browser, click Allow. Browser redirects to
http://localhost:<port>/callback?.... Whether it loads a success page or shows "connection refused", the authorization code itself is consumed by Claude Code's listener. - Inspect the keychain (
security find-generic-password -s "Claude Code-credentials" -w | jq .mcpOAuth) — noplugin:greptile:greptile|*entry exists. - Inspect
~/.claude/mcp-needs-auth-cache.json—plugin:greptile:greptilestill flagged as needs-auth (noid). - Restart Claude Code. Plugin still exposes only the two auth placeholder tools. The 12 real tools never appear.
What we ruled out
- Stale needs-auth cache entry — clearing
plugin:greptile:greptilefrommcp-needs-auth-cache.jsondoesn't help; it's re-populated on next probe. - Stale OAuth state — wiping any existing
plugin:greptile:greptile|*keychain entry before re-running the flow doesn't help; new tokens are still not persisted. - Invalid API key / wrong server — direct curl with either a static API key or the OAuth access token returns 200 + 12 tools. The server side is healthy.
- Browser callback never arrived — Greptile reports "code already used" on subsequent token exchange attempts with the same code, proving Claude Code's listener DID consume it.
- Reproduces fresh after each Claude Code restart — at least four full OAuth cycles attempted across three restarts, identical outcome.
Diagnostic data referenced
~/.claude/plugins/cache/claude-plugins-official/greptile/unknown/.mcp.json— plugin's HTTP config (above).~/.claude/mcp-needs-auth-cache.json— needs-auth gating cache; greptile entry has timestamp but noid.- macOS Keychain
Claude Code-credentials(svc=Claude Code-credentials,acct=Claude) holds top-levelmcpOAuthmap keyed by<serverName>|<hash>; greptile entry absent post-flow. - Greptile RFC 9728 metadata:
GET https://api.greptile.com/.well-known/oauth-protected-resource→ 200 with the JSON shown above. - Greptile OAuth metadata:
GET https://api.greptile.com/.well-known/oauth-authorization-server→ 404 (separate observation — Greptile only publishes the protected-resource doc, not the auth-server metadata doc).
Workarounds in use
- Direct REST/curl via static API key stored in macOS Keychain (
svc=GREPTILE_API_KEY,acct=greptile), exported to env via~/.zshenv. Works perfectly against the same/mcpendpoint, just bypassing Claude Code's MCP transport entirely. - Plugin remains installed but unusable for tool invocations through the agent.
Suggested investigation paths
- Trace the persistence step after a successful greptile token exchange — does the write to
Claude Code-credentialsmcpOAuthfail with a swallowed exception? - Compare the JSON shape of greptile's token response with sentry/posthog/intercom — if greptile's response includes/omits a field the persistence layer requires (e.g.,
expires_invsexpires_at,scopeformatting), that could cause a silent drop. - The fact that Greptile only publishes
oauth-protected-resource(and notoauth-authorization-server) may be tripping a code path that expects both documents.
Happy to provide additional logs or run targeted commands — please point at what you'd like instrumented.
This issue has 3 comments on GitHub. Read the full discussion on GitHub ↗