Date: 2026-03-22
Status: Living Document — updated continuously as the workflow is executed and improved
Spec: mcp-server.spec.md — tool definitions, input schemas, return shapes, error codes, transport, sidecar
This document is updated as implementation runs. Each time the workflow is invoked, start from the top, follow the steps, and update this document with what was learned — what worked, what broke, what the step actually requires in practice.
We start from the top each time. We update as we go. We run it until it Just Works™.
The MCP server exposes the Ambient platform API as structured tools conforming to the Model Context Protocol (MCP) 2024-11-05. It is the primary interaction surface for agents running inside the platform — every SEND, WAIT, and state read/write in an agent script is an MCP tool call.
Two deployment modes:
- Sidecar — runs alongside Claude Code CLI in every runner Job pod. Claude Code connects via stdio. Auth token injected from the pod environment.
- Public endpoint — exposed through
ambient-api-serveratPOST /api/ambient/v1/mcp. Clients authenticate with the same bearer token as all other API calls.
The MCP server has no direct Kubernetes access. All operations proxy through ambient-api-server, inheriting the full RBAC model.
REST API (openapi.yaml)
└─► MCP Tool Registry (server.go + tools/)
├─► Session tools (tools/sessions.go)
├─► Agent tools (tools/agents.go)
├─► Project tools (tools/projects.go)
└─► Annotation tools (tools/annotations.go)
└─► Annotation State Protocol (agent-fleet-state-schema.md)The MCP server depends on:
- A stable REST API — do not implement tools against unreleased endpoints
- The annotation schema defined in
docs/internal/proposals/agent-fleet-state-schema.md— allpatch_*_annotationstools must use the key conventions from that doc - The sidecar annotation
ambient-code.io/mcp-sidecar: "true"— required on Session for operator injection
components/ambient-mcp/
├── main.go # Entrypoint; MCP_TRANSPORT env var selects stdio or SSE
├── server.go # MCP server init, capability declaration, tool registration
├── Dockerfile # ubi9/go-toolset builder → ubi9/ubi-minimal runtime, UID 1001
├── go.mod # module: github.com/ambient-code/platform/components/ambient-mcp
├── client/
│ └── client.go # Thin HTTP client wrapping ambient-api-server
├── mention/
│ └── resolve.go # @mention extraction and agent resolution
└── tools/
├── helpers.go # jsonResult / errResult utilities
├── sessions.go # Session tool handlers + annotation merge
├── agents.go # Agent tool handlers + annotation merge
├── projects.go # Project tool handlers + annotation merge
└── watch.go # watch_session_messages, unwatch_session_messagesImage: localhost/vteam_ambient_mcp:latest
Naming rationale: follows the ambient-{role} convention (ambient-runner, ambient-cli, ambient-sdk). Separate component with its own image — required because the operator injects it as a sidecar subprocess that Claude Code spawns via stdio. Cannot be embedded in ambient-api-server.
| Tool | RBAC | Backed by | Description |
|---|---|---|---|
list_sessions |
sessions:list |
GET /sessions |
List sessions with optional phase/project filter |
get_session |
sessions:get |
GET /sessions/{id} |
Full session detail |
create_session |
sessions:create |
POST /sessions + /start |
Create and start a session; returns Pending |
push_message |
sessions:patch |
POST /sessions/{id}/messages |
Append user message; @mention spawns child session |
patch_session_labels |
sessions:patch |
PATCH /sessions/{id} |
Merge filterable label pairs |
patch_session_annotations |
sessions:patch |
PATCH /sessions/{id} |
Merge arbitrary state KV (scoped to session lifetime) |
watch_session_messages |
sessions:get |
GET /sessions/{id}/messages SSE |
Subscribe to message stream; pushes notifications/progress |
unwatch_session_messages |
— | internal | Cancel active subscription |
| Tool | RBAC | Backed by | Description |
|---|---|---|---|
list_agents |
agents:list |
GET /projects/{p}/agents |
List agents with search filter |
get_agent |
agents:get |
GET /projects/{p}/agents/{id} |
Agent detail by ID or name |
create_agent |
agents:create |
POST /projects/{p}/agents |
Create agent with name + prompt |
update_agent |
agents:patch |
PATCH /projects/{p}/agents/{id} |
Update prompt (creates new version) |
patch_agent_annotations |
agents:patch |
PATCH /projects/{p}/agents/{id} |
Merge durable state KV (persists across sessions) |
| Tool | RBAC | Backed by | Description |
|---|---|---|---|
list_projects |
projects:list |
GET /projects |
List projects |
get_project |
projects:get |
GET /projects/{id} |
Project detail |
patch_project_annotations |
projects:patch |
PATCH /projects/{id} |
Merge fleet-wide shared state KV |
Annotations form a three-level scoped state store. All annotation tools follow the merge-not-replace semantics: existing keys not in the patch are preserved; empty-string values delete a key.
| Scope | Tool | Lifetime | Primary Use |
|---|---|---|---|
| Session | patch_session_annotations |
Session lifetime | Retry count, current step, in-flight task status |
| Agent | patch_agent_annotations |
Persistent | Last task, index SHA, external IDs, PR status |
| Project | patch_project_annotations |
Project lifetime | Fleet protocol, contracts, agent roster, shared flags |
Annotation keys follow reverse-DNS prefix conventions. All agent self-reporting uses these namespaces:
| Namespace | Used for |
|---|---|
ambient.io/ |
Platform coordination state (blocked, ready, blocker, roster, protocol, contracts, summary) |
work.ambient.io/ |
Task tracking (epic, issue, current-task, next-tasks, completed-tasks) |
git.ambient.io/ |
Git state (branch, worktree, pr-url, pr-status, last-commit-sha) |
myapp.io/ |
User application state (any key; 4096 byte value limit) |
The project carries four top-level annotation keys that define the self-describing coordination layer:
ambient.io/protocol— how agents communicate (check-in triggers, blocker escalation, handoff rules, roster entry field list)ambient.io/contracts— shared agreements (git conventions, API source of truth, SDK regeneration requirements, blocking thresholds)ambient.io/agent-roster— live fleet state array; each agent owns and writes only its own entryambient.io/summary— human-readable current project state
Agents read these on every session start via get_project, reconcile their own state against the protocol and contracts, update their roster entry via patch_agent_annotations + patch_project_annotations, and then proceed with work.
push_message supports @{identifier} syntax for agent-to-agent delegation.
Resolution: UUID → direct lookup. Name → search. Ambiguous name → AMBIGUOUS_AGENT_NAME error.
Delegation: each resolved mention strips the token from the prompt, calls create_session with the remaining text as prompt and parent_session_id set to the calling session. The child session is started immediately.
Response shape:
{
"message": { "seq": 5, "event_type": "user", "payload": "..." },
"delegated_session": { "id": "...", "phase": "Pending" }
}| Mode | Transport | Binding |
|---|---|---|
| Sidecar (runner pod) | stdio | stdin/stdout of sidecar process |
| Public endpoint | SSE over HTTP | MCP_BIND_ADDR (proxied through ambient-api-server) |
Session must have annotation ambient-code.io/mcp-sidecar: "true" at creation time. Operator reads this and injects the mcp-server container into the runner Job pod.
Pod layout:
Job Pod (session-{id}-runner)
├── container: claude-code-runner
│ CLAUDE_CODE_MCP_CONFIG=/etc/mcp/config.json
│ connects to mcp-server via stdio
└── container: mcp-server
MCP_TRANSPORT=stdio
AMBIENT_API_URL=http://ambient-api-server.ambient-code.svc:8000
AMBIENT_TOKEN={session bearer token from projected volume}The ambient-api-server exposes the MCP server's SSE transport at:
GET /api/ambient/v1/mcp/sse
POST /api/ambient/v1/mcp/messageAuth: Authorization: Bearer {token} forwarded to MCP server as AMBIENT_TOKEN.
Request flow:
Client
│ GET /api/ambient/v1/mcp/sse
│ Authorization: Bearer {token}
▼
ambient-api-server
│ spawns or connects to mcp-server process
│ injects AMBIENT_TOKEN={token}
▼
mcp-server (SSE mode)
│ MCP JSON-RPC over SSE
▼
ambient-api-server REST API
│ Authorization: Bearer {token} ← same token, forwarded
▼
platform resources| Code | HTTP | Description |
|---|---|---|
UNAUTHORIZED |
401 | Token missing, invalid, or expired |
FORBIDDEN |
403 | Token valid but lacks required RBAC permission |
SESSION_NOT_FOUND |
404 | No session with the given ID |
SESSION_NOT_RUNNING |
409 | Operation requires session in Running phase |
AGENT_NOT_FOUND |
404 | No agent matches ID or name |
AMBIGUOUS_AGENT_NAME |
409 | Name search matched more than one agent |
PROJECT_NOT_FOUND |
404 | No project matches ID or name |
MENTION_NOT_RESOLVED |
422 | @mention token could not be matched to any agent |
INVALID_REQUEST |
400 | Missing required field or malformed input |
INVALID_LABEL_KEY |
400 | Label key contains = or whitespace |
ANNOTATION_VALUE_TOO_LARGE |
400 | Annotation value exceeds 4096 bytes |
AGENT_NAME_CONFLICT |
409 | Agent name already exists for this owner |
SUBSCRIPTION_NOT_FOUND |
404 | No active subscription with the given ID |
TRANSPORT_NOT_SUPPORTED |
400 | Streaming requires SSE transport; caller is on stdio |
INTERNAL |
500 | Backend returned an unexpected error |
Each invocation: start from Step 1. Update this document before moving to the next step if anything is discovered.
Before doing anything else, internalize that this run may not succeed. The workflow is the product. If a step fails, edit this document to capture the failure and what the step actually requires.
Checklist:
- Read this document top to bottom
- Note the last run's lessons (see Run Log below)
- Confirm the REST API endpoints the tools depend on are present and stable
- Confirm
components/mcp-server/directory exists or create the scaffold
Read specs/integrations/mcp-server.spec.md in full for the complete per-tool input schemas, return shapes, and error tables.
Read docs/internal/proposals/agent-fleet-state-schema.md for the annotation key conventions that all patch_*_annotations tools must honor.
Extract and hold in working memory:
- Every tool name, required inputs, optional inputs, return shape
- Every error code per tool
- The three annotation scopes and their lifetime semantics
- The
@mentionresolution algorithm - The fleet protocol keys (
ambient.io/protocol,ambient.io/contracts,ambient.io/agent-roster,ambient.io/summary)
For each tool, determine its current status:
| Tool | Status | Gap |
|---|---|---|
list_sessions |
✅ implemented | — |
get_session |
✅ implemented | — |
create_session |
✅ implemented | — |
push_message |
✅ implemented | — |
patch_session_labels |
✅ implemented | — |
patch_session_annotations |
✅ implemented | — |
watch_session_messages |
✅ implemented | SSE transport guard in place; full streaming (Wave 5) pending |
unwatch_session_messages |
✅ implemented | — |
list_agents |
🔲 planned | — |
get_agent |
🔲 planned | — |
create_agent |
✅ implemented | — |
update_agent |
✅ implemented | — |
patch_agent_annotations |
✅ implemented | — |
list_projects |
✅ implemented | — |
get_project |
✅ implemented | — |
patch_project_annotations |
✅ implemented | — |
@mention resolution |
✅ implemented | — |
| stdio transport | ✅ implemented | — |
| SSE transport | ✅ implemented | — |
| sidecar injection (operator) | 🔲 planned | operator spec update required |
Update each row as implementation progresses. Mark ✅ when the tool has unit test coverage and the acpctl mcp call smoke test passes.
Wave 1 — Scaffold
- Create
components/ambient-mcp/withgo.mod,main.go,server.go - Wire
mark3labs/mcp-golibrary - Implement
MCP_TRANSPORTenv var dispatch (stdio vs SSE) - Register all tools with real handlers — get
tools/listto return all 16 tools - Acceptance:
go build ./...clean;tools/listvia stdio shows complete tool list ✅ DONE
Wave 2 — Read-only tools
Implement tools that only GET from the REST API (no side effects):
list_sessions,get_sessionlist_agents,get_agentlist_projects,get_project
No @mention. No SSE. No annotations. Get reads working first.
- Acceptance:
acpctl mcp call list_sessions --input '{}'returns valid JSON;get_sessionreturns 404 for unknown ID
Wave 3 — Write tools (non-streaming)
create_session(POST + start)push_message(without @mention)patch_session_labelspatch_session_annotationspatch_agent_annotationspatch_project_annotationscreate_agent,update_agent
Annotation merge semantics: read existing → merge patch → write back. Empty-string values delete the key.
- Acceptance:
acpctl mcp call push_message --input '{"session_id":"...","text":"hello"}'returns message with seq; annotations round-trip correctly
Wave 4 — @mention
- Implement
mention/resolve.go: UUID direct lookup, name search, ambiguity detection - Wire into
push_message: resolve mentions → spawn child sessions → returndelegated_session - Acceptance:
@agent-namein message text spawns a child session with correctparent_session_id
Wave 5 — Streaming
- Implement
watch_session_messages: open SSE to backend, forward asnotifications/progress - Implement
unwatch_session_messages - Phase polling loop (every 5s) for terminal notification
- Stdio guard: return
TRANSPORT_NOT_SUPPORTEDwhen called in stdio mode - Acceptance:
watch_session_messagesdelivers messages as they arrive; terminal notification fires on session completion
Wave 6 — Sidecar
- Update operator to read
ambient-code.io/mcp-sidecar: "true"annotation - Inject
mcp-servercontainer into Job pod spec - Generate and mount
CLAUDE_CODE_MCP_CONFIGvolume - Acceptance: session with
mcp-sidecar: trueannotation launches pod with two containers; Claude Code connects via stdio andlist_sessionsreturns data
Wave 7 — Integration
- End-to-end smoke: ignite agent → agent calls
push_messagewith @mention → child session starts → parent callswatch_session_messages→ child completes → terminal notification received - Annotation state round-trip: agent writes
patch_agent_annotations→ external call reads back viaget_agent make testandmake lintincomponents/ambient-mcp/
acpctl send mcp --body "Wave 1: Scaffold components/mcp-server/. Wire mark3labs/mcp-go. Implement MCP_TRANSPORT dispatch. Register all 16 tools as stubs. Done = acpctl mcp tools lists all tools."
acpctl start mcp
acpctl send mcp --body "Wave 2: Implement read-only tools (list_sessions, get_session, list_agents, get_agent, list_projects, get_project). All tools proxy to REST API. Done = acpctl mcp call get_session returns correct data."
acpctl start mcpDo not ignite Wave 3+ until Wave 2 is ✅. Do not ignite Wave 5 (streaming) until Wave 3 write tools are ✅.
Monitor via acpctl get sessions -w and the board at http://localhost:8899.
For each wave, the MCP agent reports done when:
- All tools in the wave have passing unit tests
go build ./...andgo vet ./...are cleangolangci-lint runis clean- The
acpctl mcp toolsoutput matches the full tool list above acpctl mcp call {tool}smoke passes for each implemented tool
The workflow is complete when:
- All 16 tools are ✅ in the gap table (Step 3)
- Wave 6 (sidecar) is ✅
- An agent session successfully uses MCP tools to write and read its own annotations
- The fleet protocol keys (
ambient.io/protocol,ambient.io/agent-roster) round-trip throughpatch_project_annotations+get_project
# Build binary
cd components/ambient-mcp && go build ./...
# Vet + lint
cd components/ambient-mcp && go vet ./... && golangci-lint run
# Unit tests
cd components/ambient-mcp && go test ./...
# Build image
podman build --platform linux/amd64 -t vteam_ambient_mcp:latest components/ambient-mcp/
# Load into kind cluster
podman save localhost/vteam_ambient_mcp:latest | \
podman exec -i ambient-main-control-plane \
ctr --namespace=k8s.io images import -
# Verify image in cluster
podman exec ambient-main-control-plane \
ctr --namespace=k8s.io images ls | grep ambient_mcp
# Smoke test via stdio (no cluster needed)
echo '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}' | \
AMBIENT_TOKEN=your-token go run ./components/ambient-mcp/Update this section after each implementation run.
Outcome: Waves 1–4 complete. Image built and loaded into kind cluster ambient-main.
Gap table state at end of Run 1:
- All 16 tools: ✅ implemented
- stdio transport: ✅
- SSE transport: ✅ (server starts; full streaming/progress notifications pending Wave 5)
- @mention resolution: ✅
- Sidecar injection (operator): 🔲 planned
Component renamed: components/mcp-server/ → components/ambient-mcp/ (follows ambient-{role} naming convention).
Image: localhost/vteam_ambient_mcp:latest — built with podman build, loaded into ambient-main-control-plane via ctr import.
Lessons learned:
mark3labs/mcp-go v0.45.0—Required()is aPropertyOption(notWithRequired); tool registration iss.AddTool(mcp.NewTool(...), handler)- Annotation merge semantics must be implemented in tool layer: GET existing → unmarshal JSON string → merge map → marshal → PATCH back
watch_session_messagesmust guard against stdio transport (TRANSPORT_NOT_SUPPORTED) before attempting SSE- The binary is
./ambient-mcpin the container (not/usr/local/bin/mcp-server); MCP config command must match
- Full per-tool schemas, return shapes, and error tables:
specs/integrations/mcp-server.spec.md - Annotation key conventions and fleet state protocol:
docs/internal/proposals/agent-fleet-state-schema.md - Agent visual language (how purple SEND/WAIT blocks map to MCP tools):
docs/internal/proposals/agent-script-visual-language.md - Platform data model:
specs/api/ambient-model.spec.md - Component pipeline and wave pattern:
workflows/sessions/ambient-model.workflow.md