feat(mcp): split the library and automation surfaces read from write - #705
Open
cpellan561 wants to merge 2 commits into
Open
feat(mcp): split the library and automation surfaces read from write#705cpellan561 wants to merge 2 commits into
cpellan561 wants to merge 2 commits into
Conversation
Previewhttps://derive-pr-705.derive-to.workers.dev Deployed from It shares production's database — sign in with your real account, and treat anything you change here as changed for real. It has no routes, no cron, no queue consumer and no OG renderer, so it cannot serve derive.to, run scheduled work, or write images onto real artifacts. Unlike production it serves artifact HTML on its own origin (that is what makes frame-side changes visible here). Storage is still sandboxed away, but untrusted HTML and the sign-in form share a hostname — treat this URL as you would any link: don't type a password into it because a page asked you to. |
`organize` switched behaviour on which arguments arrived: no short_ids read the
workspace vocabulary, short_ids alone inspected artifacts, and add/remove/set/
collection/state wrote — including `state:'deleted'`, a permanent delete. A tool
containing that has to declare destructiveHint, so browsing a tag vocabulary
prompted the user exactly as hard as destroying an artifact. `automate` had the
same shape with `list` sitting alongside create/run_now/record.
Now:
browse_library readOnly the reads
organize write tags + collections, honestly non-destructive
shelve destructive retire / restore / delete, the one hard verb
list_automations readOnly the read
automate write create / run_now / record / create_context
Annotation-honouring clients auto-approve read-only tools, so the common path
stops interrupting. That is the reason to do this; MCP directory review happens
to require the same split, which is how it surfaced.
BEHAVIOUR IS UNCHANGED for everything that was already reachable. Each pair is
one handler with two or three schemas over it — the handler still routes on which
arguments arrived, so every guard, cascade and response shape is the code that
was already there. Worth knowing while reading the diff: most of it is
reindentation from extracting that handler, not new logic.
The one deliberate behaviour change: `automate` now REFUSES `action:"list"`
rather than merely not advertising it. Each tool passes the action set it allows,
so the split is enforced server-side. A split that exists only in a description
is not a split — a caller with a cached schema would otherwise still reach the
read through the tool that declares itself a write.
Two things deliberately preserved:
- `shelve` keeps BOTH directions on one `state` parameter. Retire and restore are
the same decision read two ways, and the response still hands back its own undo
— now naming `shelve` rather than `organize`.
- `list_automations` keeps the owner gate. It sits above the action branch, so
listing automations always required owner, and splitting a surface is not the
moment to quietly widen a permission.
mcp.ts's design rule needed amending rather than leaving stale: "a new capability
is a parameter, not a new tool" still holds, with one carve-out now stated there
— a parameter may not carry a tool across the read/write line, because
annotations are per-tool and clients gate on them. The header also said TEN tools
while serving twelve, before this change.
Surface budget 11000 -> 11450 (tool descriptions 3200 -> 3650). Measured 3571 +
7636 = 11207: +532 characters, roughly 130 tokens against a surface already
costing ~2.6k, and the param budget absorbed the duplicated workspace/short_ids
text without moving. The rationale is at SURFACE_BUDGET, the number the file says
is the one that matters. This is the first raise that buys something — fewer
approval prompts and annotations that are true — rather than paying for a new
capability.
`wanted("organize")` now gates all three library tools and `wanted("automate")`
both automation tools, so a caller naming either still gets a coherent set.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s are Two description changes, both aimed at the same class of reader: something scanning this surface cold, with no access to the code, deciding whether it behaves. `stage target:'api'` mints a bearer and said it was "a REAL bearer for REST". The token is spent against endpoints the CALLER composes, so that left the one question a caller holding a credential most needs answered — which REST? — to inference. It now names the Derive REST API and points at its reference. Read uncharitably, an undocumented credential-minting tool with a freeform target is indistinguishable from a catch-all request tool. The server instructions said core skills were "a resource". Telling a model to fetch procedure before it acts is, in outline, the shape of a prompt-injection vector, and the distinction that makes ours legitimate is that the resources are this server's own. That was implied by the URI scheme and by nothing else, so the sentence now says it: "CORE SKILLS are this server's own resources". The same sentence got shorter by dropping the redundant second way to say it. Both fit the existing budgets, no ceiling raised: tool descriptions 3604/3650, instructions 2345/2400, total surface 11240/11450.
cpellan561
force-pushed
the
feat/split-read-write
branch
from
August 13, 2026 01:35
e7170b0 to
b23d221
Compare
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.
organizeswitched behaviour on which arguments arrived: noshort_idsread theworkspace vocabulary,
short_idsalone inspected artifacts, andadd/remove/set/collection/state wrote, including
state:'deleted', a permanentdelete. A tool containing that has to declare
destructiveHint, so browsing a tagvocabulary prompted the user exactly as hard as destroying an artifact.
automatehad the same shape, with
listsitting alongside create/run_now/record.browse_libraryorganizeshelvelist_automationsautomateAnnotation-honouring clients auto-approve read-only tools, so the common path stops
interrupting. That is the reason to do this. MCP directory review happens to require
the same split, which is how it surfaced.
Behaviour is unchanged for everything already reachable
Each pair is one handler with two or three schemas over it. The handler still
routes on which arguments arrived, so every guard, cascade and response shape is the
code that was already there. Worth knowing before you open the diff: most of it is
reindentation from extracting that handler, not new logic.
The one deliberate behaviour change:
automatenow refusesaction:"list"rather than merely not advertising it. Easet it allows, so
the split is enforced server-side. A split that exists only in a description is not a
split: a caller with a cached schema woule read through the
tool that declares itself a write. This is the line to argue with if you disagree,
since anything passing that string breaksing the read.
Two things deliberately preserved
shelvekeeps both directions on oneoriginal commentargues that splitting retire from restore "is how you end up with a surface that can
retire something and no obvious way bac response still
hands back its own undo, now naming
shelve.list_automationskeeps the owner gation branch, solisting automations always required owner. Splitting a surface is not the moment to
quietly widen a permission, even thoughmpting.
The design rule in mcp.ts needed amend
"A new capability is a parameter on an exl" still holds, and
now carries one stated carve-out: a parameter may not take a tool across the read/write
line, because annotations are per-tool anthout that, the next
reader follows a rule the code no longer fully obeys. That header also claimed TEN tools
while serving twelve, before this change.
Budget
SURFACE_BUDGET11000 to 11450 (tool deseasured 3571 + 7636= 11207. That is +532 characters for three more tools, roughly 130 tokens against a
surface already costing ~2.6k, and the paplicated
workspace/short_idstext without moving.The rationale sits at
SURFACE_BUDGET, the number the file itself says is the one thatmatters, rather than being duplicated acrd as what it is: the
first raise that buys something, fewer approval prompts and annotations that are true,
rather than paying for a new capability.
Verification
pnpm run ciclean,pnpm typecheckclesing. Both failuresare the 5000ms coverage-load flake; all 191 tests in those two files pass in isolation.
wanted("organize")now gates all three library tools andwanted("automate")bothautomation tools, so a caller naming either still gets a coherent set.
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.