feat(runtime): generic ward-mcp serve runtime - .mcp.kdl -> guarded MCP tools over SSE (thin shell over cli-guard#196) #7
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Build the generic
ward-mcp serveruntime - the one image that renders any inline.mcp.kdlinto a guarded MCP server over HTTP/SSE. No per-guardfile Go, no per-server handler. Entrypointward-mcp serve /spec/<name>.mcp.kdl --http :8080: parse → one MCP tool per grant → bind an SSE / streamable-HTTP listener. Never stdio. cli-mcp is a code reference only, not a dependency.The cli-guard engine is READY (opcore landed)
cli-guard#196 shipped as PR1-3 (cli-guard#197 / #198 / #199, all merged to main). The runtime is a thin shell over
http/opcore. Pin a cli-guard version that includeshttp/opcorein go.mod, and consume this concrete API:opcore.ParseInline(src []byte) ([]opcore.Descriptor, opcore.RuntimeConfig, error)- parses the whole.mcp.kdl(wrap header,base-url,auth <scheme> { value env <VAR> },restrict, and eachcan <verb> <resource> { path/query/body/set }grant). Method inferred from the verb, path params from the{template}. This is the frozen ward-mcp#6 grammar.descriptor.InputSchema() opcore.Schemaandschema.JSONSchema() []byte- the tool inputSchema (draft-07). cli-guard is MCP-agnostic, so you wrap that schema in the MCP tool envelope.opcore.Operation{Desc: descriptor, RT: runtime}.Execute(ctx, opcore.Args{Path,Query,Body}) (opcore.Response, error)- self-guarding (metachar + restrict gates → assemble → sign with the env token → fire)..Preview(args)gives a resolved-but-unfired request if you want a dry-run.Build the
opcore.Runtimefrom theRuntimeConfigthatParseInlinereturns (auth env-value provider, base-url, restrictions).What ward-mcp owns (the thin shell - everything else is opcore)
ward-mcp serve <spec> --http :addrcommand.Descriptor→ one MCP tool: name =verb_resource, inputSchema =descriptor.InputSchema().JSONSchema(), description = the grant'sDescribe.opcore.Args→Operation.Execute→ render theResponseback over the MCP channel.actioncomposition (one tool chaining multiple ops, e.g.view issue= issue + comments) if opcore exposes the chain; otherwise defer per DESIGN.md'scollectfollow-up and keep the floor grants reachable.Deny-by-absence,
never,restrict, and fixed-body all come from ParseInline / opcore - do not re-implement the guard.First end-to-end target
skillsmp's two read tools (
search_skills,ai_search_skills) served over SSE against skillsmp.com with aSKILLSMP_API_KEYenv token - the narrowest slice that proves parse → project → serve → guarded-execute. forgejo (writes, path params,action) is additive once the spine works, no new runtime code.Done
ward-mcp serveruns a.mcp.kdl, serves the derived tools over SSE, and guarded-executes each call through opcore.http/opcore..mcp.kdl, and a tool call round-trips throughExecuteagainst a mock upstream.ward vet/ward testor equivalent) is green.Related
cli-guard#196 + #197 / #198 / #199 (the opcore engine, landed), ward-mcp#6 (grammar + distribution), ward-mcp#8 (the chart that runs this image), coilysiren/inbox#164 (status tracker).
🔒 Reserved by
ward agent --driver claude— containerengineer-claude-ward-mcp-7on hostKAI-DESKTOP-TOWERis carrying this issue (reserved 2026-07-07T09:33:21Z). Concurrentward agentruns are blocked until it finishes or the reservation goes stale (2h0m0s TTL);--forceoverrides.Do not comment on or edit this issue to steer the run while it is reserved. The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a new issue, dispatched fresh — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494).
run seed context — what this run is carrying (ward#609)
coilyco-flight-deck/ward-mcp#7· branchissue-7· driverclaude· workflowdirect-mainengineer-claude-ward-mcp-7· wardv0.419.0· dispatched2026-07-07T09:33:21ZIssue body as seeded:
… (truncated to 2000 chars; full body is on this issue)
Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.419.0).
— Claude (she/her), via
ward agentWARD-OUTCOME: done -
ward-mcp serveruntime landed on main (opcore v0.80.0 thin shell, MCP over streamable-HTTP + legacy SSE, guarded-execute), closes #7.This one went smoothly, mostly because opcore did the heavy lifting exactly as the issue promised.
ParseInline->Descriptor->InputSchema().JSONSchema()->Operation.Executeis a genuinely clean seam: the whole runtime is projection + JSON-RPC + transport, with zero guard logic re-implemented. Reading the opcore source and its tests first (before writing a line) paid off - the API was frozen and honest.Two things fought back, both worth flagging as follow-ups rather than fixes:
examples/forgejo-issues.mcp.kdlusedspec/never/no-pathfrom an earlier draft; the frozenParseInlinegrammar has none of those - it fails closed on unknown nodes, and denial is deny-by-absence, not anevernode. I rewrote the example to the real grammar and updated DESIGN/README to match. Worth a glance to confirm the deny-by-absence framing reads right to you.verb_resource, so skillsmp'sai_search_skillscan't come out clean (verbs are single tokens; I gotsearch_ai-skills). Filed as a naming follow-up in DESIGN's "Still open" - it's cosmetic, the guarded surface is correct either way. Also deferredactioncomposition since opcore v0.80.0 exposes single-opExecuteonly, no chain.Confidence is high on the spine: build/vet/test green, and I smoke-tested both example specs end-to-end over real HTTP (initialize, tools/list, the SSE frame) plus a mock-upstream round-trip through Execute with auth-signing and a restrict denial. The one thing I could NOT exercise is a live call against real skillsmp/forgejo (no creds, and the skillsmp endpoints/params in the example are my best guess at the vendor's real OpenAPI) - so the skillsmp example is illustrative-pending-real-API, and deploy#40 will be the true first live proof. Merge with #8's chart was a 2-file doc conflict, resolved by keeping both the runtime and chart sections.