Skip to content

fix(plugin): match the resource identifier the server advertises, and write a real listing - #37

Merged
Edwinbhart merged 4 commits into
devfrom
feat/plugin-directory-readiness
Aug 17, 2026
Merged

Edwinbhart merged 4 commits into
devfrom
feat/plugin-directory-readiness

Conversation

@Edwinbhart

@Edwinbhart Edwinbhart commented Aug 17, 2026 •

Copy link
Copy Markdown
Member

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 OAuth resource parameter 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's mcp_url is 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) and writePluginMcpUrl (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:

  • Two-form URL invariant — normalizeMcpUrl deliberately still produces a trailing slash (used for keychain/notice-cache keys); writePluginMcpUrl strips it (used for OAuth resource). Verify no call site passes normalizeMcpUrl's output into the plugin config path.
  • Self-heal of existing installs — prewarm.cjs canonical() silently strips a slashed URL already in settings.json on the next session start; confirm the server handles both forms identically for non-OAuth (bearer-token) requests.
  • Unpinned CI dependency — the new plugin job runs npm install -g @anthropic-ai/claude-code without a version pin; a breaking schema change in a future Claude Code release would block all PRs.

… 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>
@codehub-agent

codehub-agent Bot commented Aug 17, 2026 •

Copy link
Copy Markdown

What this PR does

Fixes 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.

  • Changes userConfig.mcp_url default in plugin.json from …/mcp/v1/ to …/mcp/v1 to match the resource string the server's .well-known/oauth-protected-resource metadata actually advertises.
  • Fixes resolveMcpUrl in prewarm.cjs, which previously force-appended a trailing slash (withSlash); it now strips one (canonical), also repairing values left by an older otx in existing user settings.
  • Changes writePluginMcpUrl in src/integrations/claude-code.ts to strip the slash on write, so otx install stops reinstating it; normalizeMcpUrl/buildMcpUrl and the keychain/notice-cache keying are deliberately left untouched to avoid orphaning stored tokens for header-auth clients.
  • Adds marketplace/plugin metadata (displayName, category, tags, expanded description, privacy-policy section) and a new CI job running claude plugin validate --strict on the manifests; bumps plugin version 0.3.0 → 0.4.0.

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 mcp_url in settings, which the strip logic is explicitly designed to repair.

Flagged because it touches a contract (McpConfig) — callers elsewhere may be affected.

⚡ Blast radius

9 files · 45 symbols touched · 0 call sites elsewhere in the codebase

symbol kind fan-in file
normalizePlugins Function 3 src/integrations/claude-code.ts
userSettingsPath Function 2 src/integrations/claude-code.ts
output Function 2 plugins/claude-code/bin/prewarm.cjs
fallbackContext Function 2 plugins/claude-code/bin/prewarm.cjs
promptHintContext Function 2 plugins/claude-code/bin/prewarm.cjs
git Function 2 plugins/claude-code/bin/prewarm.cjs

⚠️ Contracts touched (interfaces or fan-in ≥ 10): McpConfig, MarketplaceSource, ClaudeSettings, UserSettings — callers elsewhere may be affected.

Explore this PR's blast radius → · graph: workspace 365ee7cde142 · by CodeHub

💰 Agent economics

50 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

@opentrace opentrace Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ 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>
@opentrace

opentrace Bot commented Aug 17, 2026

Copy link
Copy Markdown

🔄 Review Update

1 new commit: 1761d5a

The new commit cleans up the user-facing copy around the trailing-slash behaviour — removing the warning from the plugin.json config description and the README paragraph that explained the OAuth resource identifier rule. The reasoning makes sense: since the default is already correct and otx silently repairs any stray slash, there's no reason to make users think about it.

The one outstanding item from my original review is still open though: connect.ts prints mcpUrl (from normalizeMcpUrl, which still produces a trailing-slash URL) as the "Endpoint" in the success output, while writePluginMcpUrl strips the slash before writing to settings. So the terminal output and the actual file disagree. It's a minor UX issue — a user who copies the printed endpoint to configure another tool gets the wrong form — but it's still there.

That said, I flagged it as non-blocking originally, and this PR is otherwise solid. If you want to address it, a mcpUrl.replace(/\/+$/, "") at the print site in connect.ts would do it — but happy to see this merged as-is if you'd rather track it separately.

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>
@opentrace

opentrace Bot commented Aug 17, 2026

Copy link
Copy Markdown

✅ Review Update — Approved

1 new commit: 005d527

This commit finishes the docs cleanup — the trailing-slash explanation that was removed from plugin.json and the README in the previous commit was still present in plugin-architecture.md, and now it's gone too. Consistent and clean.

The one non-blocking item I flagged (the connect.ts success output printing the trailing-slash form of the URL while the settings file gets the canonical form) is still there, but I called that out as non-blocking originally and again in my last follow-up. Since there's been no response either way, I'll leave it open rather than keep repeating myself — it's a minor UX rough edge, not a correctness issue.

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>
@Edwinbhart
Edwinbhart merged commit 9607a85 into dev Aug 17, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant