Surface // exactly the grants

mcp-beaver

a MCP server generator with a natural flow

You handed an agent a write-capable API. Now name every operation it may call.

wrap ward mcp forgejo {
  restrict owner matches "coilyco-*"
  can get issue {
    path "/repos/{owner}/{repo}/issues/{index}"
  }
}
withhold "delete_issue" { reason "No undo." }

One grant, one MCP tool, one HTTP endpoint. No handler, no input schema, and no per-server image, because all three are derived from the line above.

  • Preview
  • MIT
  • image // helm chart
  • MCP 2026-07-28

Deny by absence

5 grants mint 5 tools and 5 endpoints delete_issue withheld, and says so 30 others never named, so never served

The problem, and what it costs to leave alone

A write-capable MCP is usually the whole API with a friendlier name.#

The normal way to expose a service to an agent is to write a server that holds a credential and forwards calls. Its blast radius is whatever that credential can reach, which is the entire upstream, and the only thing standing between the agent and a destructive verb is that nobody wrote a tool for it yet. Somebody will, because adding one is a small pull request.

Reviewing that is reading a Go package, a handler per tool, an input schema per tool, and a Dockerfile, then deciding whether you believe the combination. By the fourth service it is four codebases, and the honest answer to what an agent can do through them is that nobody has held all of it in their head at once.

What it does about it

One runtime, many guardfiles.#

mcp-beaver renders an umbra guardfile into a guarded MCP server and a matching HTTP tool API, baked into one generic image. Each can grant is one MCP tool named verb_resource, and its input schema is derived from the grant's own path, query, and body. There is no per-server Go, no per-server Dockerfile, no per-server handler, and no per-tool schema to keep in sync with anything.

An unwritten delete issue grant means no delete_issue tool and no /api/delete_issue endpoint is ever served. Deployment is a values file and a helm upgrade, and the chart stays spec-opaque: in spec mode it never parses the guardfile at all.

Four ways to serve

serve
Reads a .mcp.kdl and guards an HTTP upstream. The general case.
serve-upstream
Wraps a private MCP behind an exact tool allowlist.
serve-ssm
An exact-parameter AWS reader. The policy names one parameter, and the general getter rejects every other name before AWS sees a request. IAM bounds the principal independently, so there are two bounds rather than one.
serve-s3
The asset publisher, and the first write-capable mode. Its policy fixes one bucket, the media types it will serve, the public base URL, and an optional key prefix.

lint and lint-upstream are the same paths minus the listener, so a guardfile is validated in CI before it is ever mounted.

What it looks like in use

The reviewable surface is one small file, read end to end.#

This is the whole contract for a server that can read, file, comment on, and close issues, and can do nothing else to the host it points at.

wrap ward mcp forgejo {
  base-url "forgejo.coilysiren.me/api/v1"              1
  auth header-token { value env "FORGEJO_TOKEN" }     2
  restrict owner matches "coilyco-*"                  3
  can get issue { path "/repos/{owner}/{repo}/issues/{index}" }  4
}
withhold "delete_issue" { reason "The upstream has no undo." }  5
1 base-url
The one host this server may reach.
2 auth
Resolved from the environment at run time, never baked into the image. This is the credential mcp-beaver presents upstream, not a caller's credential to mcp-beaver.
3 restrict
Every {owner} path leaf must match, so a granted verb still cannot travel outside the accounts you named.
4 can get
One grant, one tool, one endpoint, and an input schema derived from the path.
5 withhold
A verb left out on purpose, said out loud. The stub appears in discovery, states why, refuses every call, and reaches no upstream.

That last node exists because silence means four things at once: withheld by policy, unimplemented, not offered upstream, or simply not found by the agent's search. An agent guesses between them, and it guesses wrong in both directions.

A design call worth stating

Every result leads with what it does not contain.#

Grant-backed results are {"coverage": {...}, "result": ...}, in that order, in both the text and the structured content. Coverage names every array in the payload and its length, because a count in meaning is what changes an answer.

Note

Coverage leads because a consuming harness bounds a tool result by keeping the front and discarding the tail. A caveat serialized last is the first thing destroyed, and the model then reads rows carrying no caveat and answers as though the view were complete.

What it does not do

mcp-beaver performs no inbound authentication.#

It is not an API gateway and it is not an identity layer. Caller identity, TLS, ingress, and network reachability belong to the deployment that runs it. Guardfile auth configures mcp-beaver's credential to the upstream service, and never a caller's credential to mcp-beaver.

Note

The deny-by-absence claim is about the running server, not the image. The image is deliberately generic and carries no guardfile, so a consumer mounts the spec at deploy time.

What absence means, per serve mode
Mode An operation you did not declare
Spec mode Has no handler at all. Absent, in the strongest sense the word has.
Upstream-proxy mode Still exists behind an endpoint the container holds credentials for. The runtime re-checks allowlist membership on every call, which is unreachable rather than absent.

The distinction is worth carrying, because the two modes earn different sentences and only one of them earns the stronger one.

One boundary, four proofs

Constrain what an agent can do, and prove what it did.#

  1. umbra v0.170.0 Writes the policy.
  2. mcp-beaver Preview Renders that same guardfile into a guarded MCP server. You are here.
  3. agent-compose v2.61.0 Composes the context around it, carrying no authority.
  4. sirens-echo Live The whole thing deployed, answering real people.

Repository and docs

Reference#