Repository navigation
fix(plugin): match the resource identifier the server advertises, and write a real listing - #37
Conversation
… write a real listing The MCP server names its resource `…/mcp/v1` with no trailing slash, but the plugin shipped `…/mcp/v1/`. Claude Code derives the RFC 8707 `resource` parameter from the configured URL, so the slash produced a resource the authorization server does not recognize. Strip it at the three places that feed OAuth — the userConfig default, prewarm's own resolution (which was force-appending one), and the mcp_url otx seeds into user settings — while leaving normalizeMcpUrl alone, since the keychain and notice cache are keyed by that string and header-based clients never send `resource` at all. Then make the plugin submittable: a description that explains what the tools do rather than one line, a displayName, homepage pointing at docs that actually carry the privacy policy, a Privacy Policy section in the README, and a marketplace description. CI now runs `claude plugin validate --strict` on both manifests, because once the plugin is listed the review pipeline runs that same check on every commit it mirrors. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
What this PR doesFixes an OAuth resource-identifier mismatch by stripping the trailing slash from the plugin's MCP URL at the three seams that feed Claude Code's OAuth flow, and fleshes out the marketplace listing for submission.
Risk: low and contained — 9 files touched, 0 external call sites, 0.01% transitive symbol reach; main exposure is any user with a pre-existing slashed Flagged because it touches a contract (McpConfig) — callers elsewhere may be affected. ⚡ Blast radius9 files · 45 symbols touched · 0 call sites elsewhere in the codebase
Explore this PR's blast radius → · graph: 💰 Agent economics50 agent sessions reported in the last 30 days, but none matched this PR's commits. A session matches on the author email it reports and a commit landing within ~2h of it — so check the agent's email matches the commit author, and that the work was committed in the same sitting. Sessions → Full economics → · by CodeHub |
There was a problem hiding this comment.
✅ Approved
Solid fix for a real OAuth breakage. The root cause is clear — normalizeMcpUrl was appending a trailing slash, but the server advertises /mcp/v1 (no slash) as its OAuth resource identifier, so Claude Code was sending a resource parameter the authorization server didn't recognize. The fix is correct and the migration path (stripping a slashed value left by an older otx) is a nice touch.
The design decision to strip the slash only in writePluginMcpUrl rather than in normalizeMcpUrl is the right call — bearer-token clients never send resource, so changing the shared function would orphan keychain entries and notice-cache keys for no benefit. The comment explaining this is clear and will save someone from re-litigating it later.
One thing worth a follow-up (not blocking): normalizeMcpUrl still produces a trailing-slash URL, and that slashed value is what connect.ts:54 prints as "Endpoint" in the success output — even though writePluginMcpUrl strips it before writing to settings. So the displayed URL disagrees with what's actually in the file. A user who copies the printed endpoint to configure another tool gets the wrong form. Easy to fix by printing mcpUrl.replace(/\/+$/, "") in the success log, or by introducing a display-form helper.
Happy to see this merged as-is.
The default is already correct and otx strips a slash if one shows up, so warning about it in the config description and the README was explaining a rule nobody needs to know. Keep the code comments that justify the stripping, minus the restatement. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
🔄 Review Update
The new commit cleans up the user-facing copy around the trailing-slash behaviour — removing the warning from the The one outstanding item from my original review is still open though: That said, I flagged it as non-blocking originally, and this PR is otherwise solid. If you want to address it, a |
Same explanation removed from the config description and the plugin README in 1761d5a; this one was left behind. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
✅ Review Update — Approved
This commit finishes the docs cleanup — the trailing-slash explanation that was removed from The one non-blocking item I flagged (the This PR is otherwise solid and the core fix is correct. Happy to see it merged. |
The assertion diff already points at the slash when it fails, so the message only needs to say what it is asserting. The seam comment keeps the one point worth recording — why the stripping is not in normalizeMcpUrl — and drops the rest. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Fix OAuth resource identifier trailing-slash mismatch
🐛 Bug Fix · 🔧 Chore · 📝 Docs
The server advertises
…/mcp/v1(no trailing slash) as its RFC 8707 protected-resource identifier. Claude Code derives the OAuthresourceparameter from whatever URL it is configured with, so the previous slashed default caused sign-in to fail with an unrecognised resource. This PR strips the trailing slash everywhere the plugin'smcp_urlis written or resolved, and adds a CI job to validate both plugin manifests on every PR.Complexity
🟢 Low ·
9 files changed, 76 insertions(+), 19 deletions(-)One functional invariant (trailing slash) fixed consistently across three files. The only subtlety is the intentional asymmetry between
normalizeMcpUrl(still slashed, used for keychain/notice-cache keying) andwritePluginMcpUrl(no-slash, used for OAuth). Everything else is metadata and docs.Tests
🧪 The integration test is updated to assert the no-slash form; there is no isolated unit test for
writePluginMcpUrl's stripping behaviour, which is only covered end-to-end.Review focus
Pay particular attention to the following areas:
normalizeMcpUrldeliberately still produces a trailing slash (used for keychain/notice-cache keys);writePluginMcpUrlstrips it (used for OAuthresource). Verify no call site passesnormalizeMcpUrl's output into the plugin config path.prewarm.cjscanonical()silently strips a slashed URL already insettings.jsonon the next session start; confirm the server handles both forms identically for non-OAuth (bearer-token) requests.pluginjob runsnpm install -g @anthropic-ai/claude-codewithout a version pin; a breaking schema change in a future Claude Code release would block all PRs.