feat(mcp): profile-level read-only enforcement (KLA-409)#22
Merged
Conversation
Closes the misconfiguration vector that --read-only alone leaves open:
an operator who provisioned a JumpCloud OAuth client with read-only API
scope and bound it 1:1 to a jc profile could still accidentally start
'jc mcp serve' against that profile *without* --read-only and have
mutation tools advertised in ListTools. Calls would then fail noisily
at the JumpCloud API layer with 403s, polluting the audit trail.
Adds a profile-level role field (auth_profile_role: read_only) that
the MCP server honors at startup:
- config: ProfileRole(profile), IsReadOnlyProfile(), SetProfileRole
with validation. Only ProfileRoleReadOnly ("read_only") is accepted.
- mcp serve: applyProfileRole() helper coerces readOnly=true when the
active profile carries the role; emits a one-line stderr warning if
the operator hadn't already passed --read-only; and rejects the start
with a clear error if --read-only=false was passed explicitly.
- jc auth status: surfaces a 'Profile Role' line (and JSON field) when
the active profile has a role set.
Note: KLA-409's original Parts A (allow/block lists) and B (--read-only
flag) were already shipped in earlier work; this is just the remaining
Slice C (profile-level safety net).
Tests: 4 new config tests, 4 new applyProfileRole helper tests, 2 new
auth status output tests. All package suites green.
Doc update for docs/AUTH.md will follow once PR #21 (KLA-410) lands;
this PR is code-only to avoid a stacked-PR conflict.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
4 tasks
jrennichjc
approved these changes
Apr 28, 2026
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.
Summary
Closes KLA-409 — Slice C (the only remaining piece; allow/block lists and
--read-onlyalready shipped).Adds a profile-level role field (
auth_profile_role: read_only) so a profile bound to a read-only JumpCloud OAuth client can't accidentally be started in mutation-capable mode:ProfileRole(profile),IsReadOnlyProfile(),SetProfileRole()with validation. OnlyProfileRoleReadOnly("read_only") is accepted; unknown values are rejected at set-time so typos don't silently fail to enforce anything.jc mcp serve— newapplyProfileRole()helper coercesreadOnly = truewhen the active profile carries the role, emits a one-line stderr warning if the operator hadn't already passed--read-only, and rejects the start with a clear error if--read-only=falsewas passed explicitly.jc auth status— surfaces aProfile Roleline (andprofile_roleJSON field) when set; omits it otherwise to keep output clean for the 99% case.Why
Today an operator can set up the right pattern (read-only OAuth client → dedicated jc profile → MCP-server use only) but still misconfigure the start command and end up advertising mutation tools that will only fail at the JumpCloud API layer with 403s. That's noisy, confusing in audit logs, and defeats the point of having a read-only credential. This adds a startup-time gate that closes the loop.
Behavior matrix
--read-onlyflagread_onlytrue; warning on stderrread_onlytrue(explicit)read_onlyfalse(explicit)Test plan
go build ./...cleango test ./...— full suite passingSetProfileRoleround-trip, invalid-role rejectionapplyProfileRolecover all branches of the matrix aboveProfile RolelineFollow-up
docs/AUTH.md(which lands in #21 / KLA-410) will get a small section on the profile role once that PR merges. This PR is code-only to avoid a stacked-PR conflict.🤖 Generated with Claude Code
Note
Medium Risk
Medium risk because it changes
jc mcp servestartup behavior (new coercion/warnings and a hard error for incompatible flags) and introduces new config fields that affect runtime enforcement.Overview
Adds a new profile-level role (
profiles.<name>.auth_profile_role) with validation helpers (ProfileRole,IsReadOnlyProfile,SetProfileRole) to support aread_onlyprofile mode.Updates
jc mcp serveto enforce read-only profiles viaapplyProfileRole(): it forces--read-onlyon when the active profile is read-only (emitting a stderr warning if not explicitly set) and fails fast if the operator explicitly passes--read-only=false.Extends
jc auth statusto include the profile role in both human output and JSON (profile_role), omitting it when unset; adds targeted unit tests for the new status output, role helpers, and enforcement matrix.Reviewed by Cursor Bugbot for commit 9a3dc17. Bugbot is set up for automated code reviews on this repo. Configure here.