feat(mcp): read-only document tools, on a new v1 REST surface - #376
Merged
Conversation
Adds `document_list` and `document_get` under a new `documents:read` scope, so an agent can answer "what needs signing, and who hasn't signed it" without being able to act on the answer. WHY THIS STARTS WITH REST Every MCP tool wraps a `src/pages/api/v1/*` handler through `invokeV1`, and mcpTools.test.ts asserts on disk that each `v1Path` exists. There was no document handler — sign-off is tRPC-only — so the REST surface comes first. It has standalone value, and routing it through `createCaller` means the signer-or-owner rule is not re-implemented: there is still one authorization path, the one the UI uses. THE BOUNDARY IS UNCHANGED Both tools are readOnlyHint, so the existing "exposes no tool that can sign, spend or broadcast" assertion still lists exactly the two ballot write tools. Nothing here can create, edit, approve or sign a document — approval is a CIP-8 signature from a named wallet signer against a frozen snapshot, which is not reachable from this surface by construction. Bot keys are excluded twice over: `mcpScopesForBot` is an explicit allowlist that does not map `documents:read`, so the tools never appear for a bot, and both handlers reject bot JWTs anyway. Sign-off is a human accountability record and an automated identity has no standing in it. RESPONSES ARE A PROJECTION, NOT THE ROWS `summariseDocument` is an allowlist. A version can carry up to 512KB of inline base64, which is pure waste as model context and never what a caller asking about signatures wants — and because this output becomes model context, a new column on DocumentVersion must not be able to start flowing to a model silently. The outcome field is recomputed from the frozen snapshot with `evaluateThreshold`, so a caller sees the same rule the server enforces rather than a denormalised copy. Human wallet JWTs receive the new scope automatically, matching the existing rule that a wallet JWT grants what the signed-in user can already do in the UI. 1087 tests pass; tsc clean; next build exit 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
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.
Adds
document_listanddocument_getunder a newdocuments:readscope, so an agent can answer "what needs signing, and who hasn't signed it" — without being able to act on the answer.Why this starts with REST, not MCP
Every MCP tool wraps a
src/pages/api/v1/*handler throughinvokeV1, andmcpTools.test.tsasserts on disk that eachv1Pathexists. There was no document handler — sign-off is tRPC-only — so the REST surface had to come first.It has standalone value, and routing it through
createCallermeans the signer-or-owner rule is not re-implemented: there is still exactly one authorization path, the one the UI uses.The boundary is unchanged
The registry's stated boundary is that nothing reachable over MCP can sign, spend or broadcast. Both new tools are
readOnlyHint, so the existing assertion:still passes unchanged — I extended the order pin only, not the writable allowlist. Nothing here can create, edit, approve or sign a document: approval is a CIP-8 signature from a named wallet signer against a frozen snapshot, which is not reachable from this surface by construction.
Bot keys are excluded twice over.
mcpScopesForBotis an explicit allowlist that does not mapdocuments:read, so the tools never appear for a bot at all; and both handlers reject bot JWTs regardless. Sign-off is a human accountability record — every approval is a signature from a named person — and an automated identity has no standing in it.Responses are a projection, not the rows
summariseDocumentis an allowlist, for two reasons:DocumentVersionmust not be able to start flowing to a model silently.The
outcomefield is recomputed from the frozen snapshot viaevaluateThresholdrather than read off the row, so a caller sees the same rule the server enforces instead of a denormalised copy.Titles, descriptions and comments are included — they are the point of the feature — and they are user-authored strings, which is precisely why nothing on this surface can write or sign.
Scope grant
Human wallet JWTs receive
documents:readautomatically, matching the existing rule that a wallet JWT grants what the signed-in user can already do through the UI.CHALLENGE_SCOPESderives fromMCP_SCOPES, so discovery picks it up with no further change.tsc --noEmitclean; 1087 tests pass;next buildexit 0.🤖 Generated with Claude Code