๐ŸŒฌ๏ธ Breeze docs

MCP for AI agents

breeze-mcp exposes the Breeze toolchain โ€” 40 tools across generation, introspection, verification, runtime debugging, Fleet tracing, and Docker provisioning โ€” to an AI agent over the Model Context Protocol. It runs standalone, or in-process inside a generated application, serving its own control plane beside its own traffic.

app := breeze.New(router, pool)

server, token, err := mcp.StartInProcess(app, mcp.InProcessConfig{
	Mode:  mcp.ModeAppRuntime, // required โ€” see "Two kinds of server"
	Port:  2000,               // control address
	Token: os.Getenv("BREEZE_MCP_TOKEN"),
})
if err != nil {
	log.Fatal(err) // a port conflict is fatal at startup
}
go server.Serve()

app.Run(3000, true) // app address

Four addresses, never conflated

KindWhat listens thereWho dials itNamed by
Control addressa breeze-mcp process in --mode generatorthe MCP client, as a configured servercontrol_port / control_token
App addressthe generated application itselfruntime/fleet tools, as an argumentapp_port
App-MCP addressthe app's own embedded app-runtime endpointthe MCP client, as a second serverapp_mcp_port
Aggregator addressthe Fleet Aggregator's API/WS endpointfleet tools, as an argumenta fleet registry entry

The first and third are both MCP, which is exactly why they must not be conflated: the control address serves the generator-level toolchain over that container's source tree, so whoever holds its token can rewrite the project. The app-MCP address serves read-only introspection of the running process โ€” it has no mutating tool registered at all.

A tool call travels through a control-plane connection (configured once, as a server URL). A tool argument such as service_url points at something else entirely โ€” the application's own address. So one question โ€” "what routes is the users service serving?" โ€” involves two addresses at once: the control connection carries the call, and the app address is what the call is about. Conflating them produces the most misleading failure available: a control port answers an app-port request with a well-formed MCP rejection rather than a connection error.

Two kinds of server: --mode

There is no default, and construction fails without one:

--mode generator--mode app-runtime
Answers"help me build and change this project""help me understand what this instance is doing right now"
Toolsthe full toolchain โ€” generate, plan, verify, provision, plus every read-only toollive introspection only
Mutating toolsregisterednot registered at all
Belongsa developer's machine, a build agentinside a deployed process

The distinction is structural, not configuration: an app-runtime server doesn't check a permission before running breeze_generate โ€” it has no breeze_generate to run. No token scope and no argument trick reaches one, because there is nothing to reach. Choosing generator as a default would mean a deployed app that forgot the flag silently exposes project generation and Docker provisioning to whoever holds its token โ€” so neither mode is assumed.

The handshake reports which one you reached:

{
  "protocolVersion": "2024-11-05",
  "serverInfo": {"name": "breeze", "version": "v0.1.0"},
  "breezeServerKind": "generator"
}

What a token may reach: --scope

--mode decides what a server has. --scope decides what a credential reaches โ€” independent layers, both worth using. Every tool belongs to exactly one of eight categories:

CategoryWhat it does
generationwrites project files โ€” breeze_new, breeze_generate, breeze_add
introspectionreads what exists in a project
planningpreviews changes, holds change sets open
knowledgemaintains and searches llms.txt, suggests next steps
verificationruns the Go toolchain โ€” and therefore project code
runtimereads live state from a running service โ€” routes, errors, logs, performance, breeze_diagnose_service
fleetreads a Fleet Aggregator: traces, topology, contract violations
provisioningdrives Docker: builds images, starts and removes containers
breeze-mcp --mode generator --port 2000 --scope fleet,runtime

Omitting --scope grants every category โ€” every existing deployment's current behaviour. What is not silent is the risky combination: a generator-mode server on a non-loopback bind with an unscoped token says so at startup.

Granting is by category, never by tool name โ€” a token minted with 40 tool names would be stale the moment a tool was added. The handshake reports both the granted and the full known set, so an agent can tell "never built" from "withheld from my token" โ€” a difference that decides whether to give up or ask for a wider credential:

"breezeCapabilities": {
  "granted": ["fleet", "runtime"],
  "known": ["fleet", "generation", "introspection", "knowledge",
            "planning", "provisioning", "runtime", "verification"],
  "scoped": true
}

A human, or tooling that would rather not implement a handshake, can check a token directly:

curl -H "Authorization: Bearer $BREEZE_MCP_TOKEN" http://127.0.0.1:2000/mcp/features

What a refused call looks like

An out-of-scope tool is absent from tools/list, but a client working from a stale cache may call one anyway. The refusal is a tool result, not a JSON-RPC error โ€” a -32602 would tell a model its request was malformed and invite it to reformat and retry forever:

{
  "isError": true,
  "structuredContent": {
    "tool": "provision_service",
    "refused": true,
    "reason": "outside this token's granted capabilities",
    "requires": "provisioning",
    "granted": ["fleet", "runtime"],
    "retry_will_succeed": false
  }
}

Where a tool may reach on disk: --workspace

--mode decides what tools exist; --scope decides which of them a credential reaches; neither says where they may operate โ€” and most of these tools take a path.

breeze-mcp --mode generator                          # confined to the CWD (default)
breeze-mcp --mode generator --workspace /srv/projects # one tree
breeze-mcp --mode generator --allow-any-path          # no confinement

This is not tidiness. breeze_verify_project runs go test in the directory it's given, and go test compiles and executes whatever it finds there โ€” an unconfined server handed {"path": "/etc"} runs that directory's code under the server's own identity. resolvePath is the only way any tool turns a caller's path into one it will touch, so a new tool cannot forget the check.

Refused: a path outside every root (absolute or by ../ traversal); a path that resolves outside through a symlink (both the roots and the candidate are symlink-resolved before comparison โ€” a prefix test alone is not a containment test); a path through a Windows junction (its target can't be established, so containment can't be, so it's refused rather than assumed). Allowed: a path that doesn't exist yet โ€” breeze_new's target โ€” checked against its nearest existing ancestor instead.

--allow-any-path exists for the deployment where the process boundary is already the security boundary โ€” a disposable container. It's mutually exclusive with --workspace.

Transports

stdio โ€” with --mode and no --port, breeze-mcp speaks stdio and the client launches it as a subprocess. No port, no token: the process boundary is the trust boundary.

network โ€” --port starts an HTTP+SSE server: bearer token required on every request including the handshake, Origin validated, loopback unless Host says otherwise.

in-process โ€” mcp.StartInProcess (shown above) serves the same security posture from inside a running application, with no separate binary.

The in-process endpoint serves a deliberate subset

16 of the 40 tools โ€” the ones that generate, plan, verify, or provision are excluded, for two independent reasons: concurrency (they chdir and replace os.Stdout under a process-wide lock, which inside a live app resolving relative paths for real traffic would be actively dangerous), and no source tree (a deployed binary was built from a module cache, not a clone โ€” there's nothing on disk for those tools to operate on). InProcessConfig.AllowWorkspaceTools restores the excluded set, for a development container where the app runs from its own clone and serves no real traffic โ€” a deployed app should not set it. mcp.Tools() and mcp.ExcludedTools() report both lists at runtime.

scope, _ := mcp.NewScope(mcp.CapFleet) // traces and topology, nothing else
server, token, _ := mcp.StartInProcess(app, mcp.InProcessConfig{
	Mode:  mcp.ModeAppRuntime,
	Port:  2000,
	Token: os.Getenv("BREEZE_MCP_TOKEN"),
	Scope: scope,
})

Provisioning containers is boxed in too

provision_service starts a second breeze-mcp inside each container it creates, passed --workspace /workspace and nothing wider. Nothing in a provisioning request can escape that: the docker object decodes strictly, so privileged, volumes, network_mode, cap_add, devices and the rest are refused rather than ignored โ€” a silently dropped option would look to the caller like an honoured one. Every docker invocation passes through one function that checks the complete argv for mount, privilege, namespace and entrypoint flags. There is no opt-in for a host mount: an operator who genuinely needs one has docker run, which is a deliberate act by someone who already holds Docker access, rather than a JSON field an agent can populate.

No tool anywhere in internal/mcp or internal/generator builds a shell command โ€” every exec.Command passes an argument array, so ; rm -rf / as a service name arrives at Docker as one literal argument, which Docker rejects.

The report the framework gives about itself

GET /dashboard/api/diagnostics?subsystem=mcp is the diagnostic an agent needs most โ€” because breeze_diagnose_service reads this same registry, so the endpoint serving the call was previously the one subsystem missing from its own report. It surfaces three states no error anywhere else reveals: a scope withholding a tool, generator mode running with no source tree, and AllowWorkspaceTools left on in production. See Diagnostics.

The five layers, together

--mode (what exists) โ†’ --scope (what a token reaches) โ†’ --workspace (where it may act on disk) โ†’ transport (stdio / network / in-process) โ†’ provisioning boundaries (what a spawned container itself can reach). Each layer is independent and each is reported back to the client at handshake time, so a missing capability can always be attributed to the right one rather than debugged by elimination.

Generated documentation for the Breeze framework ยท built with SvelteKit, fully static.