Project the granted surface into served tool definitions #346

Merged
coilysiren merged 2 commits from served-surface into main 2026-08-30 05:47:21 +00:00
Owner

Refs #339. Phase 2 of the mcp dialect, scoped to what belongs in umbra.

Most of it already existed

Descriptor.InputSchema().JSONSchema() was built for ward-mcp and does the schema projection. #339 assumed this was mostly wiring, and that was right. Three things were genuinely missing.

1. Descriptor.Meta

Carries upstream _meta. This is the model change #336 deferred with "only on the day the generated runtime acts on _meta.ui.resourceUri" - and a served surface is that day. Forwarded verbatim, never parsed.

2. Enum on Field and Property

The dialect folded an enum into the flag's help text, which is right for a human reading --help and useless to an agent calling a served tool. A served schema that lost it would be callable but not correctly callable, so the enum now survives as enum.

Found by writing the round-trip test rather than by reading the code.

3. ServedTools

Returns the tool definitions plus the runtime that fires them. It advertises the upstream tool name, not the kebab-cased CLI leaf, because a served surface speaks the protocol's namespace rather than a shell one.

What this deliberately does not do

umbra builds the surface and does not serve it. No transport, no session registry, no serve command, no process lifetime.

#339 asked this outright: "Does the served surface belong in umbra at all, or in ward-mcp with umbra supplying descriptors?" Answer: the latter. Gating a request and hosting a server are different jobs, and the consumer already has the second one. #336 kept an aggregating proxy out of umbra on exactly this reasoning, and shipping a server here would reopen a boundary you already merged.

So #339 stays open for the consumer-side half. This PR is the part umbra owes it.

Two properties that carry over rather than being re-derived

  • A served call runs through opcore.Operation.Execute, the same entry a CLI leaf uses, so the guards cannot drift into two implementations.
  • Denied and merely-unnamed tools are both absent. A served surface listing a tool it will refuse spends an agent's context teaching it a call it cannot make.

Verification

The round-trip test is load-bearing: locked schema → flags → served schema, asserting type, description, enum, numeric bounds, array items, and requiredness all survive. Plus _meta forwarded byte-for-byte and the descriptor identity.

go test ./..., golangci-lint run ./..., pre-commit run --all-files pass.

Refs #339. Phase 2 of the mcp dialect, **scoped to what belongs in umbra**. ## Most of it already existed `Descriptor.InputSchema().JSONSchema()` was built for ward-mcp and does the schema projection. #339 assumed this was mostly wiring, and that was right. Three things were genuinely missing. ## 1. `Descriptor.Meta` Carries upstream `_meta`. This is the model change #336 deferred with *"only on the day the generated runtime acts on `_meta.ui.resourceUri`"* - and a served surface is that day. Forwarded verbatim, never parsed. ## 2. `Enum` on `Field` and `Property` The dialect folded an enum into the flag's **help text**, which is right for a human reading `--help` and useless to an agent calling a served tool. A served schema that lost it would be callable but not *correctly* callable, so the enum now survives as `enum`. Found by writing the round-trip test rather than by reading the code. ## 3. `ServedTools` Returns the tool definitions plus the runtime that fires them. It advertises the **upstream tool name**, not the kebab-cased CLI leaf, because a served surface speaks the protocol's namespace rather than a shell one. ## What this deliberately does not do **umbra builds the surface and does not serve it.** No transport, no session registry, no `serve` command, no process lifetime. #339 asked this outright: *"Does the served surface belong in umbra at all, or in ward-mcp with umbra supplying descriptors?"* Answer: the latter. Gating a request and hosting a server are different jobs, and the consumer already has the second one. #336 kept an aggregating proxy out of umbra on exactly this reasoning, and shipping a server here would reopen a boundary you already merged. So #339 stays open for the consumer-side half. This PR is the part umbra owes it. ## Two properties that carry over rather than being re-derived - A served call runs through `opcore.Operation.Execute`, the same entry a CLI leaf uses, so the guards cannot drift into two implementations. - Denied and merely-unnamed tools are both absent. A served surface listing a tool it will refuse spends an agent's context teaching it a call it cannot make. ## Verification The round-trip test is load-bearing: locked schema → flags → served schema, asserting type, description, **enum**, numeric bounds, array items, and requiredness all survive. Plus `_meta` forwarded byte-for-byte and the descriptor identity. `go test ./...`, `golangci-lint run ./...`, `pre-commit run --all-files` pass.
feat(mcpverb): project the granted surface into served tool definitions
All checks were successful
ci / secrets (pull_request) Successful in 11s
ci / lint (pull_request) Successful in 39s
ci / test (pull_request) Successful in 51s
66e3fb7e72
refs #339

Phase 2 of the mcp dialect, scoped to what belongs in umbra. Most of it
already existed: Descriptor.InputSchema().JSONSchema() was built for
ward-mcp and does the schema projection. Three things were missing.

Descriptor.Meta carries upstream `_meta`. This is the model change #336
deferred with "only on the day the runtime acts on _meta.ui.resourceUri",
and a served surface is that day. It is forwarded verbatim, never parsed.

Field and Property carry Enum, and the draft-07 fragment emits it. The mcp
dialect folded an enum into the flag's help text, which is right for a
human reading --help and useless to an agent calling a served tool. A
served schema that lost it would be callable but not correctly callable.

ServedTools returns the tool definitions plus the runtime that fires them.
It advertises the upstream tool name rather than the kebab-cased CLI leaf,
because a served surface speaks the protocol's namespace.

umbra builds this and does not serve it. Owning a transport, a session
registry, and a process lifetime is a different job from gating a request,
and it is the job the consumer already has. Same boundary that kept an
aggregating proxy out of umbra in #336, so this does not reopen it.

A served call runs through opcore.Operation.Execute, the same entry a CLI
leaf uses, so the guards cannot drift into two implementations. Denied and
merely-unnamed tools are both absent, as they are from the CLI tree.

The round-trip test is the load-bearing one: locked schema to flags and
back, asserting type, description, enum, numeric bounds, array items, and
requiredness all survive.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Agent-Role: platform
fix(mcpverb): hold served.go to the comment cap, and split the serving doc
All checks were successful
ci / secrets (pull_request) Successful in 9s
ci / lint (pull_request) Successful in 38s
ci / test (pull_request) Successful in 50s
9d2d9efb2c
Two defects my own gate missed on this branch.

served.go and served_test.go carried comment blocks over the 2-line cap.
They passed `pre-commit run --all-files` because the files were untracked
at the time, and that command only sees files git knows about. This clone
has no commit-time hook to catch it after `git add`. Staging before gating
is the fix, and it applies to every branch in this series.

docs/mcpverb.md would have breached the 120-line and 8000-char caps once
this branch, stdio-close, and mcp-auth all landed, though each is under on
its own. Found by merging all five into a throwaway branch and running the
hooks on the result rather than on any one of them.

The serving material moves to docs/mcpverb-serving.md, which is where the
rationale the comment cap pushed out of served.go now lives.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Kai Siren <coilysiren@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
Agent-Role: platform
coilysiren deleted branch served-surface 2026-08-30 05:47:22 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
coilyco-flight-deck/umbra!346
No description provided.