Spec: Ambient Platform MCP Server

May 26, 2026 · View on GitHub

Date: 2026-03-22 Status: Design Workflow: ../../workflows/integrations/mcp-server.workflow.md — implementation waves, gap table, build commands, run log


Overview

The Ambient Platform MCP Server exposes the platform's resource API as a set of structured tools conforming to the Model Context Protocol (MCP) 2024-11-05. It has two deployment modes:

  1. Sidecar — runs alongside the Claude Code CLI in every runner Job pod. Claude Code connects via stdio. The sidecar's auth token is injected from the pod environment.
  2. Public endpoint — exposed through ambient-api-server at POST /api/ambient/v1/mcp. Clients authenticate with the same bearer token used for all other API calls. The frontend session panel connects here.

The MCP server has no direct Kubernetes access. All operations proxy through ambient-api-server, inheriting the full RBAC model.


Component

Location: components/ambient-mcp/

Language: Go 1.24+

Library: mark3labs/mcp-go v0.45.0

Constraint: No direct Kubernetes API access. Reads and writes go through the platform REST API only.

Image: localhost/vteam_ambient_mcp:latest

Directory Structure

components/ambient-mcp/
├── main.go              # Entrypoint; transport selected by MCP_TRANSPORT env var
├── 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_messages

Configuration

Environment VariableRequiredDefaultDescription
AMBIENT_API_URLYesBase URL of the ambient-api-server
AMBIENT_TOKENYesBearer token. In sidecar mode, injected from the pod's service environment. In public-endpoint mode, forwarded from the HTTP request.
MCP_TRANSPORTNostdiostdio for sidecar mode, sse for public endpoint
MCP_BIND_ADDRNo:8090Bind address for SSE mode

MCP Protocol

Initialize

The server declares the following capabilities in its initialize response:

{
  "protocolVersion": "2024-11-05",
  "capabilities": {
    "tools": {}
  },
  "serverInfo": {
    "name": "ambient-platform",
    "version": "1.0.0"
  }
}

Resources and prompts capabilities are not declared. All platform operations are exposed as tools.

Transports

ModeTransportBinding
Sidecar (runner pod)stdiostdin/stdout of the sidecar process
Public endpointSSE over HTTPMCP_BIND_ADDR (proxied through ambient-api-server)

In SSE mode, the server responds to:

  • GET /sse — SSE event stream (client → server messages via query param or POST)
  • POST /message — client sends JSON-RPC messages; server replies via the SSE stream

Error Format

All tool errors follow MCP's structured error response. The content array contains a single text item with a JSON-encoded error body matching the platform's Error schema:

{
  "isError": true,
  "content": [
    {
      "type": "text",
      "text": "{\"code\": \"SESSION_NOT_FOUND\", \"reason\": \"no session with id abc123\", \"operation_id\": \"get_session\"}"
    }
  ]
}

Tool Definitions

list_sessions

Lists sessions visible to the caller, with optional filters.

RBAC required: sessions:list

Backed by: GET /api/ambient/v1/sessions

Input schema:

{
  "type": "object",
  "properties": {
    "project_id": {
      "type": "string",
      "description": "Filter to sessions belonging to this project ID. If omitted, returns all sessions visible to the caller's token."
    },
    "phase": {
      "type": "string",
      "enum": ["Pending", "Running", "Completed", "Failed"],
      "description": "Filter by session phase."
    },
    "page": {
      "type": "integer",
      "description": "Page number (1-indexed). Default: 1."
    },
    "size": {
      "type": "integer",
      "description": "Page size. Default: 20. Max: 100."
    }
  }
}

Return value: JSON-encoded SessionList. Content type text, format:

{
  "kind": "SessionList",
  "page": 1,
  "size": 20,
  "total": 3,
  "items": [
    {
      "id": "3BEaN6kqawvTNUIXoSMcgOQvUDj",
      "name": "my-session",
      "project_id": "demo-6640",
      "phase": "Running",
      "created_at": "2026-03-21T10:00:00Z",
      "llm_model": "claude-sonnet-4-6"
    }
  ]
}

Errors:

CodeCondition
UNAUTHORIZEDToken invalid or expired
FORBIDDENToken lacks sessions:list
INTERNALBackend returned 5xx

get_session

Returns full detail for a single session.

RBAC required: sessions:get

Backed by: GET /api/ambient/v1/sessions/{id}

Input schema:

{
  "type": "object",
  "required": ["session_id"],
  "properties": {
    "session_id": {
      "type": "string",
      "description": "Session ID (UUID)."
    }
  }
}

Return value: JSON-encoded Session.

Errors:

CodeCondition
SESSION_NOT_FOUNDNo session with that ID
UNAUTHORIZEDToken invalid or expired
FORBIDDENToken lacks sessions:get

create_session

Creates and starts a new agentic session. The session enters Pending phase immediately and transitions to Running when the operator schedules the runner pod.

RBAC required: sessions:create

Backed by: POST /api/ambient/v1/sessions, then POST /api/ambient/v1/sessions/{id}/start

Input schema:

{
  "type": "object",
  "required": ["project_id", "prompt"],
  "properties": {
    "project_id": {
      "type": "string",
      "description": "Project (Kubernetes namespace) in which to create the session. Must match the caller's token scope."
    },
    "prompt": {
      "type": "string",
      "description": "Task prompt for the session. Passed as Session.prompt to the runner."
    },
    "agent_id": {
      "type": "string",
      "description": "ID of the ProjectAgent to execute this session. If omitted, the project's default agent is used."
    },
    "model": {
      "type": "string",
      "description": "LLM model override (e.g. 'claude-sonnet-4-6'). If omitted, the agent's configured model is used."
    },
    "parent_session_id": {
      "type": "string",
      "description": "ID of the calling session. Used for agent-to-agent delegation. Sets Session.parent_session_id. The child session appears in the parent's lineage."
    },
    "name": {
      "type": "string",
      "description": "Human-readable name for the session. If omitted, a name is generated from the prompt (first 40 chars, slugified)."
    }
  }
}

Return value: JSON-encoded Session in Pending phase.

Behavior:

  • Creates the Session CR via POST /api/ambient/v1/sessions
  • Immediately calls POST /api/ambient/v1/sessions/{id}/start
  • Returns the created Session object
  • Does not wait for the session to reach Running — the caller must poll or watch_session_messages to observe progress

Errors:

CodeCondition
INVALID_REQUESTproject_id or prompt missing
AGENT_NOT_FOUNDagent_id specified but does not exist
FORBIDDENToken lacks sessions:create or is not scoped to project_id
INTERNALBackend returned 5xx

push_message

Appends a user message to a session's message log. Supports @mention syntax for agent-to-agent delegation (see @mention Pattern).

RBAC required: sessions:patch

Backed by: POST /api/ambient/v1/sessions/{id}/messages

Input schema:

{
  "type": "object",
  "required": ["session_id", "text"],
  "properties": {
    "session_id": {
      "type": "string",
      "description": "ID of the target session."
    },
    "text": {
      "type": "string",
      "description": "Message text. May contain @agent_id or @agent_name mentions to trigger agent delegation."
    }
  }
}

Return value: JSON object with the following fields:

{
  "message": { /* SessionMessage */ },
  "delegated_session": null
}

If the message contained a resolvable @mention, delegated_session is the newly created child Session; otherwise it is null.

Behavior:

  • Pushes the message to the session with event_type: "user"
  • If text contains one or more @mention tokens, each is resolved and a child session is created (see @mention Pattern)
  • The original message is pushed as-is (including the @mention text) before delegation

Errors:

CodeCondition
SESSION_NOT_FOUNDNo session with that ID
SESSION_NOT_RUNNINGSession is in Completed or Failed phase. Messages cannot be pushed to terminal sessions.
MENTION_NOT_RESOLVED@mention token could not be matched to any agent
FORBIDDENToken lacks sessions:patch

patch_session_labels

Merges key-value label pairs into a session's labels field. Existing labels not present in the patch are preserved.

RBAC required: sessions:patch

Backed by: PATCH /api/ambient/v1/sessions/{id}

Input schema:

{
  "type": "object",
  "required": ["session_id", "labels"],
  "properties": {
    "session_id": {
      "type": "string",
      "description": "ID of the session to update."
    },
    "labels": {
      "type": "object",
      "additionalProperties": {
        "type": "string"
      },
      "description": "Key-value label pairs to merge. Keys and values must be non-empty strings. Keys may not contain '=' or whitespace.",
      "example": {"env": "prod", "team": "platform"}
    }
  }
}

Return value: JSON-encoded updated Session.

Behavior:

  • Reads existing Session.labels (JSON-decoded from its stored string form)
  • Merges the provided labels (provided keys overwrite existing values)
  • Writes back via PATCH /api/ambient/v1/sessions/{id} with the merged label map serialized to JSON string

Errors:

CodeCondition
SESSION_NOT_FOUNDNo session with that ID
INVALID_LABEL_KEYA key contains = or whitespace
INVALID_LABEL_VALUEA value is empty
FORBIDDENToken lacks sessions:patch

watch_session_messages

Subscribes to a session's message stream. Returns a subscription_id immediately. The MCP server then pushes notifications/progress events to the client as messages arrive. The subscription terminates automatically when the session reaches a terminal phase (Completed or Failed).

RBAC required: sessions:get

Backed by: GET /api/ambient/v1/sessions/{id}/messages with Accept: text/event-stream

Input schema:

{
  "type": "object",
  "required": ["session_id"],
  "properties": {
    "session_id": {
      "type": "string",
      "description": "ID of the session to watch."
    },
    "after_seq": {
      "type": "integer",
      "description": "Deliver only messages with seq > after_seq. Default: 0 (replay all messages, then stream new ones)."
    }
  }
}

Return value:

{
  "subscription_id": "sub_abc123",
  "session_id": "3BEaN6kqawvTNUIXoSMcgOQvUDj"
}

Progress notification shape (pushed to client via notifications/progress):

{
  "method": "notifications/progress",
  "params": {
    "progressToken": "{subscription_id}",
    "progress": {
      "session_id": "3BEaN6kqawvTNUIXoSMcgOQvUDj",
      "message": {
        "id": "msg_xyz",
        "session_id": "3BEaN6kqawvTNUIXoSMcgOQvUDj",
        "seq": 42,
        "event_type": "TEXT_MESSAGE_CONTENT",
        "payload": "delta='Hello from the agent'",
        "created_at": "2026-03-21T10:01:00Z"
      }
    }
  }
}

Terminal notification (sent when session reaches Completed or Failed):

{
  "method": "notifications/progress",
  "params": {
    "progressToken": "{subscription_id}",
    "progress": {
      "session_id": "3BEaN6kqawvTNUIXoSMcgOQvUDj",
      "terminal": true,
      "phase": "Completed"
    }
  }
}

Behavior:

  • The MCP server opens an SSE connection to the backend for the given session
  • Messages received on the SSE stream are forwarded as notifications/progress events
  • The server polls session phase every 5 seconds; when Completed or Failed is observed, sends the terminal notification and closes the subscription
  • The client may call unwatch_session_messages at any time to cancel early

Errors:

CodeCondition
SESSION_NOT_FOUNDNo session with that ID
FORBIDDENToken lacks sessions:get
TRANSPORT_NOT_SUPPORTEDClient is connected via stdio transport; streaming notifications require SSE

unwatch_session_messages

Cancels an active watch_session_messages subscription.

Input schema:

{
  "type": "object",
  "required": ["subscription_id"],
  "properties": {
    "subscription_id": {
      "type": "string",
      "description": "Subscription ID returned by watch_session_messages."
    }
  }
}

Return value:

{ "cancelled": true }

Errors:

CodeCondition
SUBSCRIPTION_NOT_FOUNDNo active subscription with that ID

list_agents

Lists agents visible to the caller.

RBAC required: agents:list

Backed by: GET /api/ambient/v1/agents

Input schema:

{
  "type": "object",
  "properties": {
    "search": {
      "type": "string",
      "description": "Search filter in SQL-like syntax (e.g. \"name like 'code-%'\"). Forwarded as the 'search' query parameter."
    },
    "page": {
      "type": "integer",
      "description": "Page number (1-indexed). Default: 1."
    },
    "size": {
      "type": "integer",
      "description": "Page size. Default: 20. Max: 100."
    }
  }
}

Return value: JSON-encoded AgentList.

{
  "kind": "AgentList",
  "page": 1,
  "size": 20,
  "total": 2,
  "items": [
    {
      "id": "agent-uuid-1",
      "name": "code-review",
      "owner_user_id": "user-uuid",
      "version": 3
    }
  ]
}

Note: Agent.prompt is write-only in the API. list_agents and get_agent do not return prompt text.

Errors:

CodeCondition
UNAUTHORIZEDToken invalid
FORBIDDENToken lacks agents:list

get_agent

Returns detail for a single agent by ID or name.

RBAC required: agents:get

Backed by: GET /api/ambient/v1/agents/{id} (by ID), or GET /api/ambient/v1/agents?search=name='{name}' (by name)

Input schema:

{
  "type": "object",
  "required": ["agent_id"],
  "properties": {
    "agent_id": {
      "type": "string",
      "description": "Agent ID (UUID) or agent name. If the value does not parse as a UUID, it is treated as a name and resolved via search."
    }
  }
}

Return value: JSON-encoded Agent.

Errors:

CodeCondition
AGENT_NOT_FOUNDNo agent matches the ID or name
AMBIGUOUS_AGENT_NAMEName search returns more than one agent
FORBIDDENToken lacks agents:get

create_agent

Creates a new agent.

RBAC required: agents:create

Backed by: POST /api/ambient/v1/agents

Input schema:

{
  "type": "object",
  "required": ["name", "prompt"],
  "properties": {
    "name": {
      "type": "string",
      "description": "Agent name. Must be unique for the owning user. Alphanumeric, hyphens, underscores only."
    },
    "prompt": {
      "type": "string",
      "description": "System prompt defining the agent's persona and behavior."
    }
  }
}

Return value: JSON-encoded Agent at version: 1.

Errors:

CodeCondition
AGENT_NAME_CONFLICTAn agent with this name already exists for the caller
INVALID_REQUESTname contains disallowed characters or prompt is empty
FORBIDDENToken lacks agents:create

update_agent

Updates an agent's prompt. Creates a new immutable version (increments Agent.version). Prior versions are preserved.

RBAC required: agents:patch

Backed by: PATCH /api/ambient/v1/agents/{id}

Input schema:

{
  "type": "object",
  "required": ["agent_id", "prompt"],
  "properties": {
    "agent_id": {
      "type": "string",
      "description": "Agent ID (UUID)."
    },
    "prompt": {
      "type": "string",
      "description": "New system prompt. Creates a new agent version."
    }
  }
}

Return value: JSON-encoded Agent at the new version number.

Errors:

CodeCondition
AGENT_NOT_FOUNDNo agent with that ID
FORBIDDENToken lacks agents:patch or caller does not own the agent

patch_session_annotations

Merges key-value annotation pairs into a session's annotations field. Annotations are unrestricted user-defined string metadata — unlike labels they are not used for filtering, but they are readable by any agent or external system with sessions:get. This makes them a scoped, programmable state store: any application can write and read agent/session state without a custom database.

RBAC required: sessions:patch

Backed by: PATCH /api/ambient/v1/sessions/{id}

Input schema:

{
  "type": "object",
  "required": ["session_id", "annotations"],
  "properties": {
    "session_id": {
      "type": "string",
      "description": "ID of the session to update."
    },
    "annotations": {
      "type": "object",
      "additionalProperties": { "type": "string" },
      "description": "Key-value annotation pairs to merge. Keys use reverse-DNS prefix convention (e.g. 'myapp.io/status'). Values are arbitrary strings up to 4096 bytes. Existing annotations not present in the patch are preserved. To delete an annotation, set its value to the empty string.",
      "example": {"myapp.io/status": "blocked", "myapp.io/blocker-id": "PROJ-1234"}
    }
  }
}

Return value: JSON-encoded updated Session.

Behavior:

  • Reads existing Session.annotations
  • Merges the provided annotations (provided keys overwrite existing values; empty-string values remove the key)
  • Writes back via PATCH /api/ambient/v1/sessions/{id}

Errors:

CodeCondition
SESSION_NOT_FOUNDNo session with that ID
ANNOTATION_VALUE_TOO_LARGEA value exceeds 4096 bytes
FORBIDDENToken lacks sessions:patch

patch_agent_annotations

Merges key-value annotation pairs into a ProjectAgent's annotations field. Agent annotations are persistent across sessions — they survive session termination and are visible to all future sessions for that agent. Use them to store durable agent state: last-known task, accumulated context index, external system IDs, etc.

RBAC required: agents:patch

Backed by: PATCH /api/ambient/v1/projects/{project_id}/agents/{agent_id}

Input schema:

{
  "type": "object",
  "required": ["project_id", "agent_id", "annotations"],
  "properties": {
    "project_id": {
      "type": "string",
      "description": "Project ID the agent belongs to."
    },
    "agent_id": {
      "type": "string",
      "description": "Agent ID (UUID) or agent name."
    },
    "annotations": {
      "type": "object",
      "additionalProperties": { "type": "string" },
      "description": "Key-value annotation pairs to merge. Empty-string values remove the key.",
      "example": {"myapp.io/last-task": "PROJ-1234", "myapp.io/index-sha": "abc123"}
    }
  }
}

Return value: JSON-encoded updated ProjectAgent.

Errors:

CodeCondition
AGENT_NOT_FOUNDNo agent with that ID or name
ANNOTATION_VALUE_TOO_LARGEA value exceeds 4096 bytes
FORBIDDENToken lacks agents:patch

patch_project_annotations

Merges key-value annotation pairs into a Project's annotations field. Project annotations are the widest-scope state store — visible to every agent and session in the project. Use them for project-level configuration, feature flags, shared context, and cross-agent coordination state that outlives any single session.

RBAC required: projects:patch

Backed by: PATCH /api/ambient/v1/projects/{id}

Input schema:

{
  "type": "object",
  "required": ["project_id", "annotations"],
  "properties": {
    "project_id": {
      "type": "string",
      "description": "Project ID (UUID) or project name."
    },
    "annotations": {
      "type": "object",
      "additionalProperties": { "type": "string" },
      "description": "Key-value annotation pairs to merge. Empty-string values remove the key.",
      "example": {"myapp.io/feature-flags": "{\"dark-mode\":true}", "myapp.io/release": "v2.3.0"}
    }
  }
}

Return value: JSON-encoded updated Project.

Errors:

CodeCondition
PROJECT_NOT_FOUNDNo project with that ID or name
ANNOTATION_VALUE_TOO_LARGEA value exceeds 4096 bytes
FORBIDDENToken lacks projects:patch

list_projects

Lists projects visible to the caller.

RBAC required: projects:list

Backed by: GET /api/ambient/v1/projects

Input schema:

{
  "type": "object",
  "properties": {
    "page": {
      "type": "integer",
      "description": "Page number (1-indexed). Default: 1."
    },
    "size": {
      "type": "integer",
      "description": "Page size. Default: 20. Max: 100."
    }
  }
}

Return value: JSON-encoded ProjectList.


get_project

Returns detail for a single project by ID or name.

RBAC required: projects:get

Backed by: GET /api/ambient/v1/projects/{id} (by ID), or GET /api/ambient/v1/projects?search=name='{name}' (by name)

Input schema:

{
  "type": "object",
  "required": ["project_id"],
  "properties": {
    "project_id": {
      "type": "string",
      "description": "Project ID (UUID) or project name."
    }
  }
}

Return value: JSON-encoded Project.

Errors:

CodeCondition
PROJECT_NOT_FOUNDNo project matches the ID or name
FORBIDDENToken lacks projects:get

@mention Pattern

Syntax

A mention is any token in message text matching the pattern @{identifier}, where {identifier} matches [a-zA-Z0-9_-]+.

Multiple mentions in a single message are supported. Each resolves independently and spawns a separate child session.

Resolution Algorithm

Given @{identifier} in a push_message call:

  1. If {identifier} matches UUID format ([0-9a-f-]{36}): call GET /api/ambient/v1/agents/{identifier}. If found, resolution succeeds.
  2. Otherwise: call GET /api/ambient/v1/agents?search=name='{identifier}'. If exactly one result, resolution succeeds. If zero results, return MENTION_NOT_RESOLVED. If more than one result, return AMBIGUOUS_AGENT_NAME.

Delegation Behavior

For each successfully resolved mention:

  1. The mention token is stripped from the prompt text. Example: @code-review please check this becomes please check this.
  2. create_session is called with:
    • project_id = same project as the calling session
    • prompt = mention-stripped text
    • agent_id = resolved agent ID
    • parent_session_id = calling session ID
  3. The child session is started immediately (same behavior as create_session).
  4. The push_message response includes the child session in delegated_session.

Example

Calling session ID: sess-parent
Message text: "@code-review check the auth module for security issues"

Resolution:
  @code-review → GET /api/ambient/v1/agents?search=name='code-review'
               → agent ID: agent-abc123

Delegation:
  POST /api/ambient/v1/sessions
    { name: "check-auth-...", project_id: "demo-6640",
      prompt: "check the auth module for security issues",
      project_agent_id: "agent-abc123",
      parent_session_id: "sess-parent" }
  POST /api/ambient/v1/sessions/{new-id}/start

Response:
  {
    "message": { "id": "msg-xyz", "seq": 5, "event_type": "user", "payload": "@code-review check the auth module for security issues" },
    "delegated_session": { "id": "sess-child", "name": "check-auth-...", "phase": "Pending", ... }
  }

HTTP Endpoint (ambient-api-server Integration)

The ambient-api-server exposes the MCP server's SSE transport at:

GET  /api/ambient/v1/mcp/sse
POST /api/ambient/v1/mcp/message

Authentication: Authorization: Bearer {token} header. Required on all requests. The token is forwarded to the MCP server process as AMBIENT_TOKEN, which it uses for all backend API calls during the session.

Request flow:

Browser / MCP 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

Error codes:

HTTP StatusCondition
401Missing or invalid bearer token
403Token valid but lacks minimum required permissions
503MCP server process could not be started

Sidecar Deployment

Platform MCP Sidecar (ambient-mcp)

Sessions opt into the platform MCP sidecar by setting the annotation:

ambient-code.io/mcp-sidecar: "true"

This annotation is set on the Session resource at creation time. The CP reads it and injects the ambient-mcp container into the runner Job pod.

Integration Credential Sidecars

For each credential bound to the session's Project (via CREDENTIAL_IDS), the CP injects an additional sidecar container running the corresponding MCP server. Each sidecar has its own isolated environment containing only its credential. The runner container has no integration credential tokens in its environment or filesystem.

Credential ProviderSidecar NameImagePortEnv Vars Injected
githubcredential-githubghcr.io/github/github-mcp-server (via mcp-proxy):8091GITHUB_PERSONAL_ACCESS_TOKEN, AMBIENT_API_URL, AMBIENT_CP_TOKEN_URL, SESSION_ID
jiracredential-jiramcp-atlassian (native SSE):8092JIRA_URL, JIRA_API_TOKEN, JIRA_EMAIL, AMBIENT_API_URL, AMBIENT_CP_TOKEN_URL, SESSION_ID
kubeconfigcredential-k8skubernetes-mcp-server (Go binary):8093KUBECONFIG (file mount), AMBIENT_API_URL, AMBIENT_CP_TOKEN_URL, SESSION_ID
googlecredential-googleworkspace-mcp (init + run):8094GOOGLE_OAUTH_*, USER_GOOGLE_EMAIL, AMBIENT_API_URL, AMBIENT_CP_TOKEN_URL, SESSION_ID

The runner connects to each sidecar as an SSE MCP client on http://localhost:{port}/sse.

Each credential sidecar receives AMBIENT_API_URL, AMBIENT_CP_TOKEN_URL, and SESSION_ID so it can re-fetch tokens from the backend API when credentials approach expiry. The sidecar authenticates to the backend using the same RSA-OAEP token exchange mechanism as the ambient-mcp sidecar.

When no credentials are bound to the Project, no credential sidecars are injected. The runner operates without integration credentials — this is the credential-free fallback.

Git Operations Without Token Exposure

The runner container has no git credential helper and no GitHub/GitLab tokens. The agent performs git operations exclusively through MCP tools:

  • Push commits: github-mcpPushFiles tool (commits and pushes in one call)
  • Create PRs: github-mcpCreatePullRequest tool
  • Clone repos: Init container (runs before the agent, has its own isolated credentials)

The agent SHOULD NOT use git push or gh pr create directly — these require tokens in the runner environment, which violates the isolation model. System prompts instruct the agent to use MCP tools for all git write operations.

Pod Layout

Job Pod (session-{id}-runner)
├── container: runner
│     Environment:
│       SESSION_ID, PROJECT_NAME, WORKSPACE_PATH, LLM_MODEL, ...
│       USE_VERTEX, ANTHROPIC_API_KEY or GOOGLE_APPLICATION_CREDENTIALS
│       AMBIENT_MCP_URL=http://localhost:8090
│       CREDENTIAL_MCP_URLS={"github":"http://localhost:8091", ...}
│     NO integration tokens: no GITHUB_TOKEN, JIRA_API_TOKEN, etc.
│     NO token files: no /tmp/.ambient_github_token, etc.
│     Connects to sidecars via SSE MCP on localhost ports

├── container: ambient-mcp
│     image: localhost/vteam_ambient_mcp:latest
│     MCP_TRANSPORT=sse, MCP_BIND_ADDR=:8090
│     AMBIENT_API_URL, AMBIENT_CP_TOKEN_URL, AMBIENT_CP_TOKEN_PUBLIC_KEY
│     SESSION_ID (for RSA-OAEP token exchange)

├── container: github-mcp          (only if github credential bound)
│     image: ghcr.io/github/github-mcp-server
│     GITHUB_PERSONAL_ACCESS_TOKEN={from backend API}
│     GITHUB_TOOLSETS=repos,issues,pull_requests,code_security
│     AMBIENT_API_URL, AMBIENT_CP_TOKEN_URL, SESSION_ID
│     Listens :8091 (SSE)

├── container: jira-mcp            (only if jira credential bound)
│     JIRA_URL, JIRA_API_TOKEN, JIRA_EMAIL
│     AMBIENT_API_URL, AMBIENT_CP_TOKEN_URL, SESSION_ID
│     Listens :8092

└── ... (additional credential sidecars as needed)

CLI Commands

The acpctl CLI gains a new subcommand group for interacting with the MCP server in development and testing contexts.

acpctl mcp tools

Lists all tools registered on the MCP server.

Flags: none

Behavior: Connects to the MCP server in stdio mode, sends tools/list, prints results, exits.

Example:

$ acpctl mcp tools
TOOL                      DESCRIPTION
list_sessions             List sessions with optional filters
get_session               Get full detail for a session by ID
create_session            Create and start a new agentic session
push_message              Send a user message to a running session
patch_session_labels      Merge labels into a session
watch_session_messages    Subscribe to a session's message stream
unwatch_session_messages  Cancel a message stream subscription
list_agents               List agents visible to the caller
get_agent                 Get agent detail by ID or name
create_agent              Create a new agent
update_agent              Update an agent's prompt (creates new version)
patch_session_annotations Merge annotations into a session (programmable state)
patch_agent_annotations   Merge annotations into an agent (durable state)
patch_project_annotations Merge annotations into a project (shared state)
list_projects             List projects visible to the caller
get_project               Get project detail by ID or name

Exit codes: 0 success, 1 connection failed, 2 auth error.


acpctl mcp call <tool> [flags]

Calls a single MCP tool and prints the result as JSON.

Flags:

FlagTypeDescription
--inputstringJSON-encoded tool input. Required.
--urlstringMCP server URL (SSE mode). If omitted, uses stdio mode against a locally started mcp-server binary.

Example:

$ acpctl mcp call list_sessions --input '{"phase":"Running"}'
{
  "kind": "SessionList",
  "total": 2,
  "items": [...]
}

$ acpctl mcp call push_message --input '{"session_id":"abc123","text":"@code-review check auth.go"}'
{
  "message": { "seq": 7, "event_type": "user", ... },
  "delegated_session": { "id": "sess-child", "phase": "Pending", ... }
}

Exit codes: 0 success, 1 tool returned error, 2 auth error, 3 tool not found.


Go SDK Example

Location: components/ambient-sdk/go-sdk/examples/mcp/main.go

This example demonstrates connecting to the MCP server via SSE and calling list_sessions and push_message. It is runnable against a live cluster.

package main

import (
    "context"
    "encoding/json"
    "fmt"
    "log"
    "os"

    mcp "github.com/mark3labs/mcp-go/client"
)

func main() {
    serverURL := os.Getenv("AMBIENT_MCP_URL") // e.g. http://localhost:8090
    token := os.Getenv("AMBIENT_TOKEN")

    client, err := mcp.NewSSEMCPClient(serverURL+"/sse",
        mcp.WithHeader("Authorization", "Bearer "+token),
    )
    if err != nil {
        log.Fatal(err)
    }
    ctx := context.Background()

    if err := client.Start(ctx); err != nil {
        log.Fatal(err)
    }

    _, err = client.Initialize(ctx, mcp.InitializeRequest{})
    if err != nil {
        log.Fatal(err)
    }

    result, err := client.CallTool(ctx, mcp.CallToolRequest{
        Params: mcp.CallToolParams{
            Name:      "list_sessions",
            Arguments: map[string]any{"phase": "Running"},
        },
    })
    if err != nil {
        log.Fatal(err)
    }

    var out any
    _ = json.Unmarshal([]byte(result.Content[0].Text), &out)
    b, _ := json.MarshalIndent(out, "", "  ")
    fmt.Println(string(b))
}

Error Catalog

CodeHTTP equivalentDescription
UNAUTHORIZED401Token missing, invalid, or expired
FORBIDDEN403Token valid but lacks required RBAC permission
SESSION_NOT_FOUND404No session with the given ID
SESSION_NOT_RUNNING409Operation requires session in Running phase
AGENT_NOT_FOUND404No agent matches the given ID or name
AMBIGUOUS_AGENT_NAME409Name search matched more than one agent
PROJECT_NOT_FOUND404No project matches the given ID or name
MENTION_NOT_RESOLVED422@mention token could not be matched to any agent
INVALID_REQUEST400Missing required field or malformed input
INVALID_LABEL_KEY400Label key contains = or whitespace
INVALID_LABEL_VALUE400Label value is empty
AGENT_NAME_CONFLICT409Agent name already exists for this owner
SUBSCRIPTION_NOT_FOUND404No active subscription with the given ID
TRANSPORT_NOT_SUPPORTED400Operation requires SSE transport; caller is on stdio
ANNOTATION_VALUE_TOO_LARGE400Annotation value exceeds 4096 bytes
INTERNAL500Backend returned an unexpected error

Spec Completeness Checklist

Per ambient-spec-development.md, this spec is complete when:

  • Tool input schemas defined — done above
  • Tool return shapes defined — done above
  • Error codes per tool — done above
  • MCP protocol behavior (initialize, capabilities, transport) — done above
  • @mention parsing rules and resolution algorithm — done above
  • watch_session_messages notification shape — done above
  • HTTP endpoint spec for ambient-api-server — done above
  • Auth/RBAC per tool — done above
  • CLI commands (acpctl mcp tools, acpctl mcp call) — done above
  • Go SDK example — done above (stub; must be runnable against kind before implementation merge)
  • Sidecar opt-in annotation specified — done above
  • Operator changes to inject sidecar — not in this spec (requires separate operator spec update)
  • openapi.mcp.yaml fragment — not yet written (required before implementation)
  • Frontend session panel integration — not in this spec (requires frontend spec)
  • Annotation tools (patch_session_annotations, patch_agent_annotations, patch_project_annotations) — done above
  • Annotations-as-state-store design rationale — done above (per-tool descriptions)