feat(mcp): let MCP sessions turn just-in-time access on or off, and revoke access grants by vsai12 · Pull Request #21626 · bytebase/bytebase · GitHub
Skip to content

feat(mcp): let MCP sessions turn just-in-time access on or off, and revoke access grants - #21626

Merged
vsai12 merged 12 commits into
mainfrom
feat/mcp-journey1-writes
Oct 21, 2026
Merged

vsai12 merged 12 commits into
mainfrom
feat/mcp-journey1-writes

Conversation

@vsai12

@vsai12 vsai12 commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

feat(mcp): let MCP sessions turn just-in-time access on or off, and revoke access grants

Part of BYT-9292 user journey 1, the last of the three PRs in plans/2026-10-13-mcp-decision-matrix.md (rows 9 and 10), split in two: this one, then masking exemptions. #21588 and #21593 are merged.

An agent can now file an access request and, after the user confirms, approve it. Two dead ends remain on that path. In a project with just-in-time access off, the request is refused, and only the console can turn the setting on. And once the work is done, the agent cannot revoke the grant it was given. Both close for users who hold the permissions: admins, DBAs and project owners hold bb.projects.update and bb.accessGrants.revoke; a developer's or member's agent still can do neither.

What changes:

  • A new tool, set_just_in_time_access(project, enabled), turns one project's just-in-time access on or off after the user confirms in an MCP form, the same elicitation decide_issue uses. The form has three lines: the change and who makes it, the project ID on a line of its own (Project: "hr"), and what the change does:

    • on: "Active grants that have not expired apply again at once."
    • off: "Active grants here stop applying (not revoked); new requests are refused."

    Both are what the code does: with the setting off, preCheckAccess ignores grants for queries and exports, and CreateAccessGrant refuses new requests. A project already in the requested state is reported as unchanged, without asking.

  • The confirmation covers the exact write. The form's request state carries a digest of the UpdateProject request the tool will send, signed with the workspace, user, client, procedure and project. A client resends the call with its answer, so an answer that comes back with other arguments is stale, and nothing changes.

  • UpdateProject moves from EXCLUDED to WRITE, but only set_just_in_time_access can use it. The tool attaches the confirmation to its internal request, and the gate serves the call only when the procedure and project match. call_api cannot set a context value, so it is refused, pointed to the tool for this setting and to the console for every other.

  • Both directions go through the form. Turning just-in-time access off is a tightening, which the decision matrix would serve without a prompt. Serving it apart would take a second path through call_api for one value of one field.

  • RevokeAccessGrant moves from EXCLUDED to WRITE. It removes access, so it needs no confirmation. bb.accessGrants.revoke still applies, and the handler refuses anything but an active grant.

  • A confirmation expires after ten minutes. The signed request state now carries when the user was asked, under the MAC. A just-in-time write's digest repeats for the same project and value, so without a bound an accepted answer could be replayed whenever the setting flipped back. An expired answer is reported stale, not asked again, because an older client gets one second round per call. decide_issue gets the same bound.

  • decide_issue's round trip moves into a shared helper (tool_confirm.go): the client-capability check, the form, the signed request state, the answer check and its outcome. Each tool keeps its own wording. decide_issue's tests pass unchanged on the commit that moves it (a6108db7d8). The confirmed value's Issue field becomes Resource, and one gate refusal serves ApproveIssue, RejectIssue and UpdateProject.

Copy: the Access policy page's "Request access grants" row adds "Revoke an active grant" and "Turn a project's "Allow access grants" on or off, after the user confirms". The Read-write summary reads "request and revoke access". Five locales.

Not in this change

  • Concurrent writes can still lose a change. The confirmation covers the request the tool sends. The read and the write that follow are not atomic. UpdateProject writes the project's whole settings object, so another settings change that lands in the same moment can be lost. The console has the same race; BOT-153 tracks fixing it once for every writer.
  • UpdateProject now appears in search_api, as ApproveIssue does since feat(mcp): let MCP sessions approve and reject issues after the user confirms #21593. A call_api call on it is refused and names the tool.
  • No new audit field. These writes reach an MCP session only through the confirmed tool, so an MCP-delegated audit row for UpdateProject already means confirmed.
  • A request state signed before this upgrade no longer verifies, because the signing purpose lost its tool name. That call is asked again.
  • Masking exemptions: the next PR.

Size

+1012/−211 lines in 27 files: production Go +467/−153, of which about 100 lines move from tool_decide.go to tool_confirm.go (198 lines; tool_jit.go is 194); tests +514/−27; the rest is copy in five locales, the ladder doc, two proto annotations and generated code. Masking exemptions are split into the next PR to keep this one reviewable.

Tests and verification

  • set_just_in_time_access: turns it on with the exact update and the confirmation the gate admits; says what turning it off does; asks nothing when already set; reports a setting changed while asking as unchanged; changes nothing on decline, cancel or an unticked box; refuses a client that cannot show the form, with the settings link; an answer resent with the other value is stale, and one for another project is asked again; refuses an invalid project; reports a refused update in the server's words; tells a client without forms that nothing needs changing when the setting is already set, and refuses its unasked answer; the form at 80 and 120 columns in the model of Claude Code's dialog: the longest project ID (63 characters) shows whole, and two IDs that differ only at the end (production-analytics-platform-west and -east) stay distinguishable.
  • Confirmation lifetime: an accepted decide_issue answer signed eleven minutes earlier decides nothing; the signed state rejects another digest, another time or a missing signature.
  • Gate: UpdateProject refused without the confirmation, served with it, refused for another project or another procedure, and still held to the ceiling. RevokeAccessGrant and UpdateProject pinned as WRITE.
  • Live, in TestMCPGrantIssuesAndAccessRequests: after the just-in-time refusal, call_api UpdateProject is refused, the session turns the setting on through the tool, files the request, has it approved through decide_issue, then revokes the grant through call_api.
  • Release test (matrix step 0.3): a workspace with no saved MCP setting resolves to Read-write, pinned in the store (TestMCPSettingsWithNoRowAreReadWrite). Read-write serving a WRITE method, Read-only, Disabled and a new workspace's Read-only were already pinned.
  • Local gates: buf format, buf lint and buf generate; gofmt; go vet; golangci-lint (no findings in the files this PR changes); the server build; pnpm --dir frontend fix and pnpm --dir frontend test (582 files, 6,595 tests; the frontend has not changed since); comment share 16%; and the full Go suite as CI runs it at ba5d0e2c0d: 121 packages ok, none failed.
  • Pre-review: 16 confirmed findings, 15 fixed in 6d672ac3d8 and ba5d0e2c0d, one declined (pre-checking archived or default projects, which would copy the server's refusals into the tool); the recheck of the fix delta cleared.

Release note

MCP sessions in Read-write mode can turn a project's just-in-time access on or off with the new set_just_in_time_access tool, after the user confirms the change in their MCP client, and can revoke an active access grant. Bytebase permissions still apply.

Receipts

Claim Receipt
With the setting off, grants do not apply to queries or exports backend/api/v1/sql_service.go:215 (preCheckAccess), called at :309 (Query) and :1183 (Export)
New requests are refused with it off backend/api/v1/access_grant_service.go:158
UpdateProject writes the whole settings object backend/api/v1/project_service.go:296, backend/store/project.go:301
Only the tool sets the confirmation backend/api/mcp/tool_jit.go (setJustInTimeAccessOnAnswer), tool_decide.go; call_api passes the session's own context, tool_call.go:74

🤖 Generated with Claude Code

vsai12 and others added 10 commits October 20, 2026 02:20
The form, the signed request state, the answer check and its outcome
move to tool_confirm.go and take any write that names its procedure and
resource, so the next confirmed tools reuse them. decide_issue's tests
pass unchanged. The signing purpose loses its tool name; a request state
signed before an upgrade no longer verifies, so that call is asked again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MCPConfirmedDecision.Issue becomes Resource, and the gate's refusal takes
the procedure, a way to read the resource from the request, the reason
and the way out, so the next confirmed writes reuse it. ApproveIssue and
RejectIssue keep their wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
RevokeAccessGrant moves from EXCLUDED to WRITE, so an agent that
requested access can revoke the grant when the work is done. It removes
access, so it needs no confirmation; bb.accessGrants.revoke still
applies, and the handler refuses anything but an active grant. The
ladder doc, the Access policy row and the Read-write summary name it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…the user confirms

A new tool, set_just_in_time_access, writes one project's
allow_just_in_time_access through UpdateProject after the user confirms
in an MCP form. The form names the project and what the change does to
active grants. The confirmation's digest is the exact update, so an
answer resent with other arguments is stale. UpdateProject moves to
WRITE, and the gate serves it only with the confirmation the tool
attaches, so call_api cannot change a project. Both directions go
through the form: turning it off is a tightening, but serving it apart
would take a second path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new WRITE method reaches every Read-write session at once, including
workspaces from before the MCP setting, which have no row. Only the
store's default said so; this test removes the row and makes a write.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iscovery

set_just_in_time_access parses its arguments the way decide_issue does,
so the parse error reaches the result (nilerr). UpdateProject is now
WRITE, so search_api lists it, as it lists ApproveIssue; call_api meets
the gate's denial naming set_just_in_time_access.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…oors

The wiring test counted three non-door entries; UpdateProject makes a
fourth. Naming them keeps a new entry from slipping past the count.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A signed request state carried no time, and a just-in-time write's
digest repeats for the same project and value, so an accepted answer
could be replayed whenever the setting flipped back. The state now
carries when the user was asked, under the MAC, and an answer older than
ten minutes is reported stale rather than asked again: an older client
gets one second round per call. decide_issue gets the same bound.

The client-capability check and the signed-answer check move into the
helper, so each tool keeps only its own wording. set_just_in_time_access
checks the client after its already-in-state answer, so a client that
cannot show a form is told nothing needs changing. New tests: a refused
update is reported in the server's words, an unasked answer from a
client without forms is refused, an expired confirmation decides
nothing, and the form's first line shows a 63-character project ID whole
at 120 columns.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fault in the store

The wiring test now checks that every refusal-table entry is a door it
drives or one it names, not only their count. The no-saved-setting
default moves from a booted server to the store, where it lives; the gate
tests already cover Read-write serving a WRITE method. The confirmation's
type doc keeps one word for what the user confirms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-21T04:40:15.744875Z f676636 New commits
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

The latest Buf updates on your PR. Results from workflow CI / lint-protos (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed⏩ skippedOct 21, 2026, 5:05 AM

The question line put a long fixed prefix before the project, so at 80
columns production-analytics-platform-west and -east looked the same.
The ID now has a line of its own, "Project: <id>"; a resource ID is at
most 63 characters, so it always fits the 74 columns Claude Code shows.
The dialog test checks the longest ID whole and two IDs that differ only
at the end at both widths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f676636275

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread docs/design/mcp-capability-ladder.md Outdated
Only UpdateProject is served through set_just_in_time_access;
CreateAccessGrant and RevokeAccessGrant are ordinary WRITE methods.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@vsai12
vsai12 merged commit 222f2d3 into main Oct 21, 2026
9 checks passed
@vsai12
vsai12 deleted the feat/mcp-journey1-writes branch October 21, 2026 05:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants