feat(039): role-scope MCP tools to the hub user role#119
Merged
Conversation
Embedded OAuth 2.1 authorization server so the Hub can be added as a Claude Desktop / claude.ai custom connector and via `claude mcp add`: RFC 7591 dynamic client registration, PKCE S256, RFC 8414/9728 discovery, rotating refresh tokens with family-revocation reuse detection, consent anchored on the existing NextAuth login, grants management under Settings → Connections. Shared-secret auth retained for headless clients. New tools: find_users, list_claude_users, get_claude_cost_dashboard, get_budget_report, list_license_assignments, list_invoices, get_copilot_analytics. list_ai_tools gains license utilization, get_user_cost_profile suggests near-matches, all tools carry readOnlyHint annotations. Spec artifacts (brainstorm, plan, implementation notes) in specs/038-mcp-v2/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Reuse: shared formatDate, clientIp() helper in rate-limit, date-fns month math, Copilot connection/range helpers shared between the two Copilot tools, token prefixes exported from the OAuth store. Simplification: consent actions share validateConsentSubmission, authorize params built from one AUTHORIZE_PARAM_KEYS list, boolean isValidScopeRequest, dropped unused exports and the hiddenFields alias. Efficiency: explicit column selects, single joined query in rotateRefreshToken, parallel insert+prune in issueAuthCode, leaner active-budget lookup. Alignment: list_claude_users now excludes the sync-lock sentinel user and matches the dashboard's Default Workspace label; tools.test asserts readOnlyHint on every tool. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n notes Control experiment: deploying main (6bd1dac) from the same machine fails with identical 60s static-generation timeouts on /reports, /reports/budget and /settings/license-templates, so the red Vercel check on PR #118 is a preview-DB/build-environment issue, not introduced by this branch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- requestOrigin: forwarded headers can be comma-separated lists in multi-hop proxy chains; take the first (client-facing) entry so the OAuth issuer/discovery URLs stay valid. Unit test added. - RevokeConnectionButton: router.refresh() after a successful revoke, matching the settings-page convention (scheduled-jobs-table, ingestion-filters-section), so the grants table updates immediately. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two concurrent refresh requests presenting the same token could both pass the pre-checks and both revoke the row unconditionally, leaving two live successors in one family. The revoke UPDATE is now conditional on revokedAt IS NULL with a row-count check; the race loser is treated like a replay (family revoked, invalid_grant). Unit test added. Addresses Copilot review pass 2 on PR #118. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- Token endpoint rate-limit branch returns 429 (matching /register) so clients back off instead of treating throttling as a permanent error. - isAllowedRedirectUri rejects URLs with an empty hostname. - clientIp treats empty/whitespace forwarded headers as absent so the rate-limit key falls back to x-real-ip / "unknown". - consumeAuthCode binds the client id into the atomic consume predicate, so a code presented by the wrong client is refused without being burned for its rightful owner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes the open question from spec 038: every authenticated MCP credential no longer sees all org-wide data. The live users.role rides the existing verifyAccessToken join into AuthInfo.extra; a new pure module (src/lib/mcp/access.ts) classifies the 14 tools into admin-only (11, denied to viewers with a shared isError message), self-scoped (cost profile + license assignments, pinned to the token owner), and viewer-safe catalog (list_ai_tools without utilization). Shared secret stays admin-equivalent; unknown roles fail closed to viewer. No schema changes, no new packages. Spec, plan (md + html), research, contracts, quickstart, tasks and implementation notes in specs/039-mcp-role-scoping/. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates the Hub’s MCP server authorization model so tool access is enforced by the live hub user role (admin vs viewer) bound to the credential, preventing viewer OAuth tokens from accessing org-wide read-only admin data while preserving shared-secret/admin behavior.
Changes:
- Propagates
users.rolethroughverifyAccessToken→verifyMcpToken→AuthInfo.extra.roleon every request (live enforcement). - Introduces
src/lib/mcp/access.tsto centralize role parsing, admin gating, and viewer self-scoping (with consistent denial messages). - Reclassifies MCP tools into admin-only vs self-scoped vs viewer-safe catalog (with
list_ai_toolsstripping utilization for viewers and skipping the aggregate query).
Reviewed changes
Copilot reviewed 22 out of 22 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| tests/unit/oauth/store.test.ts | Updates token verification tests to include the live role field. |
| tests/unit/mcp/tools.test.ts | Adds an exhaustive access-class partition and viewer/admin/shared-secret behavior matrix for all tools. |
| tests/unit/mcp/data.test.ts | Updates listAiToolsData tests for the new { includeUtilization } option and viewer-safe omission behavior. |
| tests/unit/mcp/auth.test.ts | Verifies verifyMcpToken emits extra.role (and shared-secret is explicitly admin-equivalent). |
| tests/unit/mcp/access.test.ts | New unit tests for role parsing, admin gating, and self-scope email resolution semantics. |
| src/lib/oauth/store.ts | Extends VerifiedAccessToken + verifyAccessToken projection to include live users.role. |
| src/lib/mcp/tools.ts | Wires role-aware enforcement into tool handlers (adminOnly wrapper, self-scoping, viewer-safe catalog). |
| src/lib/mcp/data.ts | Adds listAiToolsData({ includeUtilization }) and omits utilization fields when false. |
| src/lib/mcp/auth.ts | Adds extra.role for OAuth tokens and explicit { role: "admin" } for shared-secret auth. |
| src/lib/mcp/access.ts | New pure module: callerFromAuthInfo, adminOnly, resolveSelfEmail, and shared denial messages. |
| specs/039-mcp-role-scoping/tasks.md | Documents the implementation task breakdown and verification checkpoints. |
| specs/039-mcp-role-scoping/spec.md | Feature spec defining requirements, scenarios, and success criteria for role scoping. |
| specs/039-mcp-role-scoping/research.md | Design decisions and alternatives (enforcement seam, denial shape, self-scope semantics). |
| specs/039-mcp-role-scoping/quickstart.md | Local verification instructions (unit tests + manual probe guidance). |
| specs/039-mcp-role-scoping/plan.md | Implementation plan tying code changes to requirements and test strategy. |
| specs/039-mcp-role-scoping/implementation-plan.html | HTML rendition of the implementation plan for stakeholders. |
| specs/039-mcp-role-scoping/implementation-notes.html | Running notes documenting decisions, tradeoffs, and verification gates. |
| specs/039-mcp-role-scoping/data-model.md | Describes the in-memory caller model and access-class mapping. |
| specs/039-mcp-role-scoping/contracts/tool-authorization.md | External behavior contract for tool authorization/denial result shapes. |
| specs/039-mcp-role-scoping/checklists/requirements.md | Spec quality checklist confirming completeness and testability. |
| specs/038-mcp-v2/implementation-notes.html | Records the open question as resolved and points to spec 039 artifacts. |
| CLAUDE.md | Updates repo meta to include spec 039’s stack/context entry. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes the open question recorded in spec 038's implementation notes: MCP tool access now respects the hub user role bound to the credential, instead of every authenticated token seeing all org-wide read-only data. Follow-up to #118.
What changed
verifyAccessTokenprojectsusers.roleover its existing join →AuthInfo.extra.role. The role is re-read on every request, so demoting a user takes effect on their existing tokens immediately (no propagation delay, zero extra queries).src/lib/mcp/access.ts:callerFromAuthInfo(anything but literal"admin"fails closed to viewer),adminOnly(...)wrapper,resolveSelfEmail(...)identity pinning (trim + case-insensitive).isErrortool result naming the required role; the data function is never invoked; descriptions carry a "Requires an admin-role token." discovery hint.get_user_cost_profile(emailnow optional, defaults to the token owner) andlist_license_assignments(viewers pinned to their own email) — a foreign email is refused, never silently substituted.list_ai_toolsomitsactiveAssignments/maxLicenses/licenseUtilizationPctfor viewers (mirrors the/toolspage) and skips the count aggregate query.extra: { role: "admin" }, spec-034 judgment); its org-wide behaviour is unchanged.No schema changes, no new packages, no route changes. Full spec/plan/research/contract/notes in
specs/039-mcp-role-scoping/(incl. HTML implementation plan + implementation notes).Testing
tests/unit/mcp/access.test.ts(new, 23 tests) +tools.test.tsextended to a 55-test deny/serve matrix across viewer/admin/shared-secret; the expected-tool list is derived from the access-class partition, so an unclassified future tool fails the registration test.pnpm typecheckandpnpm lintclean.🤖 Generated with Claude Code