Node API

The local API that sam-node run serves to agents and scripts on the same machine. It is available on TCP (--bind-addr, default 127.0.0.1:8080) and on a Unix socket (--socket-path, default <data-dir>/sam.sock).

Authentication

ListenerRequirement
TCPX-Sam-Authentication: Bearer <api-token> on every request, or a client certificate when --tls-ca is set.
Unix socketNone. The socket has mode 0600. Being able to open it is the credential.

Authorization is reserved for the credential of the service that is being called through the node, and the proxy path forwards it. The endpoints that never forward anything (/mcp, /v1/*, /sam/service/*, /metrics, /sam/identity, /sam/peer/*, /debug/*) also accept the API token in Authorization: Bearer, for clients that cannot set custom headers. The proxy path does not accept it there.

Routes

RouteAuthPurpose
GET /healthz, GET /readyznone200 while the process is up. Every other route except /debug/* answers 503 until the node is connected to the mesh, so a 503 on /mcp is the practical readiness signal.
GET /metricstokenPrometheus metrics (sam_node_*).
POST /mcptokenThe MCP server (Streamable HTTP). / is an alias.
GET /v1/modelstokenModels served by every reachable inference provider.
POST /v1/chat/completions, POST /v1/completionstokenOpenAI-compatible inference, routed to a provider of the requested model.
GET /sam/service/discovertokenDiscover services on the mesh.
ANY /sam/{peer-id}/{type}/{name}[/{path}]tokenReverse proxy to one service on one peer.
GET /sam/identitytoken, socket or mTLS onlyThis node’s credential and the key it verifies under.
GET /sam/peer/{peer-id}/evidencetoken, socket or mTLS onlyA peer’s credential as this node last verified it.
GET /debug/*tokenOperator diagnostics. These answer even when the mesh is unreachable.

--metrics-addr serves /metrics, /healthz and /readyz on a second listener without authentication, for scrapers that hold no token.

MCP tools

The /mcp endpoint speaks Streamable HTTP. The older SSE transport is refused with 400. The server’s instructions field tells a client how the tools fit together. The tools are:

get_mesh_info

No parameters. Returns router_peer_id, connected_peers, dht_size and local_api_socket. connected_peers and dht_size count different things (transport connections and routing-table entries), so a small dht_size next to a long peer list is normal.

list_local_services

ParameterMeaning
typeOptional filter: mcp, inference, a2a.

Services this node publishes.

discover_remote_services

ParameterMeaning
typeRequired: mcp, inference or a2a.
nameOptional service name. Omit to list every reachable service of the type.
limit, offsetPagination. Defaults are 20 and 0.

Each result has peer_id, srv_name, srv_description and, when the provider declared them, labels. Service names are not unique across the mesh. The peer ID identifies the provider. For type: inference the response adds a local_proxy_url per provider and a note that inference is called over HTTP and not with call_remote_tool.

find_remote_tools

ParameterMeaning
peer_idRestrict the search to one peer.
service_nameRestrict the search to one service, bare (code-reviewer) or namespaced (mcp://code-reviewer).
tool_nameExact tool name to locate across the mesh (review_pr). Answered from gossip announcements when they are fresh.
intentAccepted and ignored. Reserved for later use.

Returns one row per tool with peer_id, tool_name (namespaced as mcp://service/tool), description and labels. A peer that could not be queried appears as a row with an error field. The call itself does not fail.

describe_remote_tool

ParameterMeaning
peer_idRequired.
tool_nameRequired, namespaced as returned by find_remote_tools.

Returns description, input_schema and output_schema.

call_remote_tool

ParameterMeaning
peer_idRequired.
tool_nameRequired, namespaced.
argumentsObject matching the tool’s input_schema.
required_labelskey=value[,key=value]. The call is refused unless the peer’s credential attests at least one of them.

Returns the tool’s result content. A policy denial comes back as a tool error that the caller can read, not as a transport failure.

When the node has no credential

A node that starts without a credential and without a way to enroll serves a reduced MCP server with one tool, get_login_instructions, and one prompt, help_user_login. Both tell the client to run sam-node join.

Service discovery over HTTP

GET /sam/service/discover?type=mcp[&name=calculator][&limit=20&offset=0][&timeout=10s] returns the same JSON array as discover_remote_services. With &stream=true or Accept: text/event-stream, results arrive as server-sent events as they are found, and the stream ends with event: done.

The proxy path

/sam/<peer-id>/<type>/<name>/<path...>

The node verifies the peer’s credential and, if set, the operator’s egress.require_labels. It then opens an authenticated stream and forwards the request to /<type>/<name>/<path> on the peer, which proxies it to the backend. Headers pass through, including Authorization. Paths that contain .. are refused. For an mcp service, <path> is empty and the request body is the JSON-RPC message. For inference, <path> is v1/models or v1/chat/completions. For a2a, <path> is whatever the agent serves, and .well-known/agent-card.json is rewritten for the mesh.

Two headers modify a proxied request:

HeaderEffect
X-Sam-Required-Labels: k=v[,k=v]Verify that the peer attests at least one of the pairs before forwarding. Otherwise 403. Removed before forwarding. Also honoured on /v1/*.
X-Sam-Agent: <agent-id>Name the agent for which this request is made. Only meaningful when sent by a sam-box. See the preview.

Talking MCP through the proxy

An MCP session with a remote service, without using the node’s own tools:

SOCK=~/.config/sam-mesh/sam.sock
URL=http://localhost/sam/<peer-id>/mcp/everything

# initialize; the Mcp-Session-Id response header identifies the session
curl -si --unix-socket $SOCK $URL \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"curl","version":"0"}}}'

# subsequent calls carry the session id
curl -s --unix-socket $SOCK $URL \
  -H 'Content-Type: application/json' -H 'Accept: application/json, text/event-stream' \
  -H 'Mcp-Session-Id: <id from above>' \
  -d '{"jsonrpc":"2.0","id":2,"method":"tools/list"}'

Over TCP, add -H "X-Sam-Authentication: Bearer $TOKEN" and use http://127.0.0.1:8080 as the base URL.

Talking A2A through the proxy

An A2A client starts from the agent card. Fetched through the proxy path, the card comes back regenerated: its interface URL is the proxy path itself, so a stock client sends message/send there without changes:

curl -s --unix-socket $SOCK \
  http://localhost/sam/<peer-id>/a2a/triage/.well-known/agent-card.json

Inference

/v1/models collects the model list of every reachable inference provider (cached for 30 seconds) and returns the union. owned_by on each entry is the peer that serves the model. A completion request names a model, and the node picks a provider that serves it. It prefers a local provider, and ranks remote providers by the labels the caller required, the operator’s floor, and load. A model that no provider lists answers 404.

curl -s --unix-socket $SOCK http://localhost/v1/models

curl -s --unix-socket $SOCK http://localhost/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{"model":"<model-id>","messages":[{"role":"user","content":"hi"}]}'

Any OpenAI SDK works with base_url set to http://127.0.0.1:8080/v1 and the API token as api_key. The node accepts the token in Authorization here because nothing on this path forwards that header. Streaming responses are passed through. One provider can also be addressed directly, at /sam/<peer-id>/inference/<name>/v1/chat/completions.

Identity evidence

GET /sam/identity returns the node’s credential (base64), the control plane public key it verifies under (SPKI DER), the peer ID, roles, labels and expiry. GET /sam/peer/{peer-id}/evidence returns the same for a peer that the node has authenticated. Both refuse plain TCP. They are reachable only over the Unix socket or over an mTLS-verified connection, because this material is meant for the node’s owner and for auditors, not for agents.

Debug

GET /debug/mesh-info, GET /debug/connectivity, GET /debug/network-info, GET /debug/token-info, GET /debug/logs and POST /debug/connect-peer report and probe the node’s view of the mesh for troubleshooting, and answer even while the mesh is unreachable. sam-node debug on the command line wraps them. Their output is not stable across releases.