claude-code - 💡(How to fix) Fix Greptile MCP plugin: OAuth flow completes but tokens are not persisted, leaving plugin permanently unauthenticated

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

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.

Error Message

{"error":"invalid_grant","error_description":"...The authorization code has already been used."}

  1. Trace the persistence step after a successful greptile token exchange — does the write to Claude Code-credentials mcpOAuth fail with a swallowed exception?

Root Cause

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.

Fix Action

Fix / Workaround

Workarounds in use

Code Example

{
    "greptile": {
      "type": "http",
      "url": "https://api.greptile.com/mcp",
      "headers": {
        "Authorization": "Bearer ${GREPTILE_API_KEY}"
      }
    }
  }

---

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":{}}'

---

{
  "resource": "https://api.greptile.com/mcp",
  "authorization_servers": ["https://auth.greptile.com"],
  "scopes_supported": ["read", "write"],
  "bearer_methods_supported": ["header"]
}

---

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

---

$ 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
RAW_BUFFERClick to expand / collapse

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, no gitCommitSha recorded in installed_plugins.json)
  • Plugin .mcp.json (~/.claude/plugins/cache/claude-plugins-official/greptile/unknown/.mcp.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

  1. Install greptile@claude-plugins-official via /plugin.
  2. Confirm the only exposed tools are mcp__plugin_greptile_greptile__authenticate and mcp__plugin_greptile_greptile__complete_authentication.
  3. Invoke authenticate — Claude Code returns the OAuth URL.
  4. 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.
  5. Inspect the keychain (security find-generic-password -s "Claude Code-credentials" -w | jq .mcpOAuth) — no plugin:greptile:greptile|* entry exists.
  6. Inspect ~/.claude/mcp-needs-auth-cache.jsonplugin:greptile:greptile still flagged as needs-auth (no id).
  7. 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:greptile from mcp-needs-auth-cache.json doesn'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 no id.
  • macOS Keychain Claude Code-credentials (svc=Claude Code-credentials, acct=Claude) holds top-level mcpOAuth map 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 /mcp endpoint, just bypassing Claude Code's MCP transport entirely.
  • Plugin remains installed but unusable for tool invocations through the agent.

Suggested investigation paths

  1. Trace the persistence step after a successful greptile token exchange — does the write to Claude Code-credentials mcpOAuth fail with a swallowed exception?
  2. 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_in vs expires_at, scope formatting), that could cause a silent drop.
  3. The fact that Greptile only publishes oauth-protected-resource (and not oauth-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.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

claude-code - 💡(How to fix) Fix Greptile MCP plugin: OAuth flow completes but tokens are not persisted, leaving plugin permanently unauthenticated