API Reference
This is the reference for all 24 gRPC RPCs exposed by the MACP Runtime on macp.v1.MACPRuntimeService. The default endpoint is 127.0.0.1:50051, configurable via MACP_BIND_ADDR.
Not every state transition a client observes comes from an RPC: Background maintenance covers the ones the runtime makes on its own timer.
For protocol-level transport semantics, see the protocol transports documentation.
Protocol Handshake
Initialize
Every client session should begin with an Initialize call to negotiate the protocol version and discover runtime capabilities.
rpc Initialize(InitializeRequest) returns (InitializeResponse)The client sends its supported protocol versions in descending preference order. The runtime selects the highest mutually supported version and returns it along with its identity, capabilities, and supported modes.
Request fields:
| Field | Type | Required | Description |
|---|---|---|---|
supported_protocol_versions | repeated string | Yes | Versions in descending preference |
client_info | ClientInfo | No | Client name, version, description |
capabilities | Capabilities | No | Client capabilities |
Response fields:
| Field | Type | Description |
|---|---|---|
selected_protocol_version | string | Selected mutual version |
runtime_info | RuntimeInfo | name: "macp-runtime", version: the running binary's crate version (env!("CARGO_PKG_VERSION"), src/server.rs:820) |
capabilities | Capabilities | Runtime capabilities (streaming, cancellation, policy, etc.) |
supported_modes | repeated string | All supported mode identifiers |
instructions | string | Optional human-readable guidance |
Returns UNSUPPORTED_PROTOCOL_VERSION if no mutual version exists.
Capabilities advertised: sessions.stream, sessions.list_sessions, sessions.watch_sessions, cancellation.cancel_session, progress.progress, manifest.get_manifest, mode_registry.list_modes, mode_registry.list_changed, roots.list_roots, roots.list_changed, policy_registry.register_policy, policy_registry.list_policies, policy_registry.list_changed.
Message Transport
Send
The primary RPC for submitting messages. Accepts a single envelope and returns an acknowledgement indicating whether the message was accepted.
rpc Send(SendRequest) returns (SendResponse)Envelope fields:
| Field | Type | Description |
|---|---|---|
macp_version | string | Must be "1.0" |
mode | string | Mode identifier (empty for signals) |
message_type | string | "SessionStart", "Proposal", "Commitment", "Signal", etc. |
message_id | string | Unique ID for deduplication |
session_id | string | Target session (empty for signals) |
sender | string | Overridden by runtime with authenticated identity |
timestamp_unix_ms | int64 | Client timestamp (informational) |
payload | bytes | Protobuf-encoded mode-specific payload |
Ack fields:
| Field | Type | Description |
|---|---|---|
ok | bool | Whether the message was accepted |
duplicate | bool | True if message_id was already processed |
message_id | string | Echo of the submitted ID |
session_id | string | Session the message was applied to |
accepted_at_unix_ms | int64 | Server acceptance timestamp |
session_state | SessionState | Session state after processing |
error | MACPError | Present when ok is false |
The runtime overrides envelope.sender with the authenticated identity. If the envelope contains a non-empty sender that does not match the authenticated identity, the request is rejected with UNAUTHENTICATED.
Reserved message_id namespace. In a macp.mode.handoff.v1 session started at semantics_rev >= 2, a client envelope whose message_id begins with the literal implicit-accept: is rejected with INVALID_ENVELOPE, whatever its message_type -- SessionStart, Commitment and HandoffContext included. The runtime mints its own implicit HandoffAccept under a deterministic id in that namespace (RFC-MACP-0010 §5.1(3), see Handoff implicit accept), and a client that squats the id a future offer would use would consume the runtime's dedup slot and strand the session short of commitment. The match is case-sensitive: Implicit-Accept:h1 is an ordinary client id and is accepted. Sessions at semantics_rev <= 1 are unaffected -- their wire behavior is byte-identical to earlier releases. In the same sessions, a HandoffAccept whose payload sets implicit = true is rejected with the same code; only the runtime may originate one.
SessionStart requirements. RFC-MACP-0001 §7.1 requires a SessionStartPayload to bind intent (MAY be empty, descriptive only), mode_version, configuration_version, a ttl_ms greater than zero, participants (when required by the Mode), and policy_version (MUST be present in the payload, MAY be empty -- an empty value resolves to policy.default). For every standards-track mode (and ext.multi_round.v1) a payload missing mode_version, configuration_version, or a positive ttl_ms is rejected with INVALID_ENVELOPE and no session is created. (There is no distinct INVALID_PAYLOAD or INVALID_TTL code in the RFC vocabulary: MacpError::InvalidPayload, InvalidTtl and InvalidModeState all map to INVALID_ENVELOPE -- see MacpError::error_code.)
Runtime limits, with no basis in the spec. This runtime additionally caps ttl_ms at 86400000 and participants at 1000 distinct non-empty entries, and requires max_suspend_ms to be non-negative when present -- none of these ceilings are required by §7.1, which leaves ttl_ms's only constraint as "greater than zero" and imposes no size bound on participants at all. A payload exceeding either cap is also rejected with INVALID_ENVELOPE.
participants must be non-empty for every mode except macp.mode.decision.v1, which accepts an empty list. RFC-MACP-0001 §7.1 requires the field only "when required by the Mode", and RFC-MACP-0007 makes the Decision initiator's authority role-based rather than membership-based. A zero-participant Decision session is accepted and inert: Proposal, Evaluation, Objection and Vote are authorized only for declared participants, so with none declared every one of them is refused with FORBIDDEN -- including from the initiator -- no proposal can ever be accepted, and therefore no Commitment can be sealed. Such a session can only expire or be cancelled. Do not start one expecting to add participants later; the roster is bound at SessionStart and never changes.
StreamSession
Provides bidirectional streaming scoped to a single session. Clients send envelopes and receive the ordinal-consuming accepted envelopes for that session in real time -- runtime-internal bookkeeping entries (SessionSuspend, SessionResume, SessionCancel, TTL expiry, checkpoints) consume no accepted ordinal and are never delivered on this stream (RFC-MACP-0006 §3.2:122).
rpc StreamSession(stream StreamSessionRequest) returns (stream StreamSessionResponse)The first envelope on the stream binds it to a session_id. All subsequent envelopes must target the same session. Responses contain either an accepted envelope or an application-level error (the stream stays open for application errors). If the client falls behind the broadcast buffer, the stream terminates with ResourceExhausted.
Passive subscribe (RFC-MACP-0006-A1). A client may observe a session without sending envelopes by sending a request frame where envelope is absent and subscribe_session_id is set. The runtime replays the session's accepted history and then delivers live envelopes on the same stream. after_sequence is the 1-based ordinal of accepted session-scoped envelopes and is exclusive: replay resumes at after_sequence + 1, and 0 replays from the session's first accepted envelope (RFC-MACP-0006 §3.2 "Sequence semantics"). It is not an offset into the durable log -- the runtime-internal entries noted above and under CancelSession, SuspendSession and ResumeSession consume no ordinal. A single frame must not contain both an envelope and subscribe_session_id -- the stream terminates with InvalidArgument if both are set. Subscribes bind the stream to the given session just like a first envelope; mixing session IDs on the same stream is rejected. Authorization: the caller must be the session initiator, a declared participant, or hold the is_observer identity capability. Non-participants receive an inline FORBIDDEN error frame and the stream stays open.
Session Lifecycle
GetSession
Retrieves metadata and current state for a session.
rpc GetSession(GetSessionRequest) returns (GetSessionResponse)Returns SessionMetadata with the session's mode, state, TTL deadline, bound versions, participants, per-participant activity summaries, and initiator identity. Only the session initiator and declared participants can query a session.
participant_activity counts messages a participant sent, not entries attributed to them. The one entry the runtime originates on a participant's behalf -- the handoff implicit accept (see Background maintenance) -- deliberately does not advance the target's message_count or last_seen, so a session can hold an accepted HandoffAccept from a participant whose activity summary is still empty. Replay records activity for no entry kind, so crediting it live would make a rebuilt session disagree with the live one.
ListSessions
Enumerates metadata for the sessions currently held in the registry (including terminal sessions still within the retention window), one bounded page per call.
rpc ListSessions(ListSessionsRequest) returns (ListSessionsResponse)Request fields.
| Field | Type | Meaning |
|---|---|---|
page_size | int32 | Maximum entries in this page. 0 means "server default" -- see below. |
page_token | string | Opaque continuation token from a previous response's next_page_token. Empty starts a new traversal. |
Page-size default and clamp.
page_size = 0yields the server default,MACP_LIST_SESSIONS_DEFAULT_PAGE_SIZE(default100).page_sizeaboveMACP_LIST_SESSIONS_MAX_PAGE_SIZE(default1000) is clamped down to that maximum. The clamp is silent -- the response simply carries at most the maximum number of entries, withnext_page_tokenset if more remain.- Otherwise
page_sizeis honored as requested.
See Resource limits for both variables.
INVALID_ARGUMENT is returned when:
page_sizeis negative (INVALID_ARGUMENT: page_size must not be negative), orpage_tokenis non-empty and cannot be decoded as a continuation token -- oversized, not valid base64url, not valid UTF-8, missing this runtime'sv1:token-version prefix, or carrying an empty cursor after that prefix (INVALID_ARGUMENT: page_token is not a valid continuation token). The message is deliberately identical for every rejection cause, so the token is not an oracle for which check failed.
The decode is a format check, not a provenance check. The token is unsigned and carries no proof it was minted by this runtime, so any correctly-formed token a caller synthesizes is accepted and used as a cursor. Do not treat a page token as an authenticated capability -- see Observation-surface authorization for why that is safe for this RPC today, and the condition that would void it.
Response. A sessions array of SessionMetadata entries plus a next_page_token. Pass next_page_token back verbatim in the next request's page_token and stop when it comes back empty:
token = ""
loop:
resp = ListSessions(page_size = 100, page_token = token)
process(resp.sessions)
token = resp.next_page_token
if token == "":
breakDo not parse, truncate, or synthesize a token -- it is opaque and its format may change between runtime versions.
Traversal semantics.
Traversal is a keyset scan over session IDs in ascending byte order. Every session that exists for the whole traversal is returned exactly once. A session created during a traversal is returned if and only if its ID sorts after the current cursor. A session deleted during a traversal may or may not appear, depending on whether the cursor had already passed it. A page is not a point-in-time snapshot;
next_page_tokenis a position, not a snapshot handle. A page may contain fewer thanpage_sizeentries whilenext_page_tokenis still non-empty — per the proto, only an emptynext_page_tokenmeans the result set is complete.
Authentication is required; the RPC is not filtered by caller identity, so callers should apply their own participation or tenancy checks before exposing results to end users. (This unfiltered property is also why the continuation token carries no signature -- see Observation-surface authorization.)
Normative source. RFC-MACP-0006 §3.8 and RFC-MACP-0001 §9, which describe the paged
ListSessionscontract, together with themacp-protodefinitions ofpage_size,page_token, andnext_page_tokeninproto/macp/v1/core.proto:411-426. The RFC prose originally predated the proto fields and described an unpaginated listing; that gap was closed upstream in multiagentcoordinationprotocol/multiagentcoordinationprotocol#77, so the prose and the proto now agree and this runtime follows both.The RFC leaves traversal strategy open — it requires only a stable order in which a session present for the whole traversal is returned exactly once. The keyset scan documented above satisfies that requirement without being mandated by it. The RFC also states explicitly that the
INVALID_ARGUMENTreturned for an undecodablepage_tokenis a transport status rather than a MACP error code, since the codes in the error-codes registry are envelope-scoped andListSessionscarries no Envelope.
WatchSessions
Server-streaming RPC for observing session lifecycle transitions across the runtime.
rpc WatchSessions(WatchSessionsRequest) returns (stream WatchSessionsResponse)On connect, the runtime emits one Created event per session currently in the registry (initial sync), then streams live SessionLifecycleEvent entries as sessions are Created, Resolved, Expired, Suspended, Resumed, or Cancelled. Each event carries event_type, the current SessionMetadata snapshot, and observed_at_unix_ms.
The initial sync is materialized incrementally. The runtime snapshots the session set once, then loads and emits one session at a time, so a subscriber that reads slowly costs one session's worth of memory rather than a copy of the whole registry. The sync carries every session that was registered when the snapshot was taken, each exactly once, whatever happens to those sessions while it is still emitting -- it is never truncated for length, and a session that reaches a terminal state or is evicted mid-sync is still delivered.
Initial-sync events always carry session. A live event's session is unset if the session had already been evicted from memory when the event reached the subscriber -- terminal sessions are evicted after MACP_SESSION_RETENTION_SECS (one hour by default, measured from session start, not from resolution), and their history remains on disk. Reconcile with ListSessions/GetSession rather than treating the event stream as an authoritative session inventory.
Two bounds apply to a subscriber that cannot keep up. The live lifecycle broadcast channel holds 64 events; a subscriber that falls further behind is terminated with RESOURCE_EXHAUSTED rather than silently skipping events. Events that arrive while the initial sync is still emitting are buffered (up to 1024) and delivered immediately after it, which is why an ordinary burst during a slow sync does not end the stream; exceeding that buffer is reported as the same RESOURCE_EXHAUSTED. In both cases, reconnect and reconcile with ListSessions.
Per-stream state is bounded by the size of the initial sync: the runtime remembers the session IDs that sync emitted, to suppress the duplicate Created the live bus would otherwise report for a session that was registered just before the stream subscribed (a session enters the registry before its Created event is published, so that event can arrive after the sync has already emitted it). The set does not grow with sessions created later -- their Created events cannot repeat -- and it is released when the client disconnects.
CancelSession
Allows the session initiator to terminate a session. This is a core control-plane operation -- mode authorization does not apply.
rpc CancelSession(CancelSessionRequest) returns (CancelSessionResponse)Request fields: session_id (string), reason (string, optional).
Only the session initiator can cancel. The runtime writes a SessionCancelPayload to the log with cancelled_by set to the authenticated sender. If the session is already terminal, the current state is returned without error.
The runtime is the sole emitter of this entry -- a client cannot submit a SessionCancel envelope via Send. It enters the durable, replayed history but consumes no accepted ordinal and is not delivered on a subscribe stream (RFC-MACP-0006 §3.2:117/:122).
SuspendSession
Suspends an OPEN session (RFC-MACP-0001 §7.5). Like CancelSession, this is a core control-plane operation restricted to the session initiator or policy-delegated roles -- mode authorization does not apply.
rpc SuspendSession(SuspendSessionRequest) returns (SuspendSessionResponse)Request fields: session_id (string), reason (string, optional).
While suspended, mode traffic into the session is rejected. Time spent suspended is banked against the session's max_suspend_ms bound (from SessionStartPayload, defaulting to the runtime cap when 0); exceeding it expires the session.
The runtime records a SessionSuspendPayload in the durable log with suspended_by set to the authenticated sender. The runtime is the sole emitter of this entry -- it is not submittable via Send -- and it enters the durable, replayed history but consumes no accepted ordinal and is not delivered on a subscribe stream (RFC-MACP-0006 §3.2:117/:122).
ResumeSession
Resumes a SUSPENDED session back to OPEN, banking the suspended duration into the TTL deadline. Same authority model as SuspendSession.
resume can force-expire the session instead of succeeding, via two independent triggers: (1) the cumulative suspended duration exceeding max_suspend_ms (RFC-MACP-0001 §7.5, the trigger documented under SuspendSession above), or (2) with no basis in the spec -- at semantics_rev >= 2, the session's count of completed suspend/resume cycles exceeding MAX_SUSPENSION_CYCLES (1024, crates/macp-core/src/session.rs:65). This cap applies to every mode, not just Handoff; it bounds the O(N²) total snapshot growth an unbounded, un-rate-limited suspend/resume loop would otherwise cause, since each cycle persists a full session snapshot including the growing suspension_intervals vec. A semantics_rev <= 1 session instead silently stops recording past the cap and is never force-expired by it, keeping legacy replay bit-identical.
rpc ResumeSession(ResumeSessionRequest) returns (ResumeSessionResponse)Request fields: session_id (string), reason (string, optional).
The runtime is the sole emitter of this entry -- it is not submittable via Send. The runtime records a SessionResumePayload in the durable log (RFC-MACP-0001 §7.5, RFC-MACP-0003 §2), whose banked_ms field is the remaining TTL banked at suspend (deadline - suspend_time), not the pause's duration. This entry enters the durable, replayed history but consumes no accepted ordinal, is not delivered on a subscribe stream (RFC-MACP-0006 §3.2:117/:122), and replay re-derives the banked duration from the suspend/resume entries' own recorded timestamps rather than trusting this field.
Background maintenance
Some state transitions are not driven by a client message at all. The runtime runs a background maintenance pass whose period is set by one variable:
| Variable | Meaning | Default |
|---|---|---|
MACP_CLEANUP_INTERVAL_SECS | interval between background maintenance passes: TTL expiry, terminal-session eviction, and eager observation of mode-computed deadlines | 60 |
Two of those are protocol-visible to a client that is only watching:
- TTL expiry. A session past its
ttl_mstransitions toEXPIREDon the next pass even if nobody sends anything, soWatchSessionsreports it without a triggering message. - Mode deadlines. Where a mode computes a deadline, the runtime observes it
on this timer. The case in the standards-track modes today is the handoff
implicit accept (RFC-MACP-0010 §5.1(2)): once
acceptance.implicit_accept_timeout_mshas elapsed on an outstandingHandoffOffer, the runtime appends theHandoffAcceptitself, as an ordinary accepted history entry attributed to the offer's target. It consumes an accepted ordinal and is published toStreamSessionsubscribers, so a client can receive a message nobody sent.
This interval bounds only when the runtime looks, never what it records: the
synthetic envelope's timestamp_unix_ms is the computed deadline, not the time
the pass noticed it, and the deadline is also observed on demand ahead of the
next session-scoped message. Lowering the interval reduces observation latency;
raising it does not change any recorded timestamp.
Discovery
GetManifest
Returns the runtime's full capability manifest, including all supported modes (standards-track and extensions), content types, and identity information.
rpc GetManifest(GetManifestRequest) returns (GetManifestResponse)ListModes
Returns descriptors for standards-track modes only. Extension modes are excluded.
rpc ListModes(ListModesRequest) returns (ListModesResponse)Each ModeDescriptor includes the mode identifier, version, title, description, determinism class, participant model, accepted message types, terminal message types, and schema URIs.
ListRoots
Discovers available resource roots.
rpc ListRoots(ListRootsRequest) returns (ListRootsResponse)Returns a list of Root entries, each with a uri and name.
Extension Mode Lifecycle
ListExtModes
Returns descriptors for extension modes, including both built-in extensions (like ext.multi_round.v1) and dynamically registered ones.
rpc ListExtModes(ListExtModesRequest) returns (ListExtModesResponse)RegisterExtMode
Dynamically registers a new extension mode. The mode identifier must not be empty, must not already exist, and must not use the reserved macp.mode.* namespace. Requires can_manage_mode_registry on the auth identity.
rpc RegisterExtMode(RegisterExtModeRequest) returns (RegisterExtModeResponse)UnregisterExtMode
Removes a dynamically registered extension mode. Built-in modes cannot be unregistered.
rpc UnregisterExtMode(UnregisterExtModeRequest) returns (UnregisterExtModeResponse)PromoteMode
Promotes an extension mode to standards-track status, optionally assigning a new identifier. The new identifier is refused if it falls in the reserved macp.mode.* namespace: RFC-MACP-0002 §12 permits such a rename only when the mode has been published in the main MACP RFC repository or carries explicit community-governance approval, and this runtime has no way to verify either, so it refuses the rename unconditionally (crates/macp-modes/src/mode_registry.rs) rather than taking the RFC's word for it.
rpc PromoteMode(PromoteModeRequest) returns (PromoteModeResponse)Governance Policy
RegisterPolicy
Registers a governance policy definition. The built-in policy.default cannot be overwritten, and the reserved policy.std. namespace only accepts the canonical RFC-MACP-0012 §5.2 definitions.
Every rejection of the definition itself is reported with INVALID_POLICY_DEFINITION at the head of the message, because RegisterPolicyResponse carries no structured error code; a duplicate policy_id is a conflict rather than an invalid definition and is reported without that prefix.
See Policy > What registration checks for the full three-layer registration contract, including the known deviation where this runtime's deserialization step ignores unknown keys rather than rejecting them as RFC-MACP-0012 §4 requires.
rpc RegisterPolicy(RegisterPolicyRequest) returns (RegisterPolicyResponse)See the Policy page for JSON rule examples and validation details.
UnregisterPolicy, GetPolicy, ListPolicies
Standard CRUD operations for the policy registry. UnregisterPolicy cannot remove policy.default. ListPolicies accepts an optional mode filter.
rpc UnregisterPolicy(UnregisterPolicyRequest) returns (UnregisterPolicyResponse)
rpc GetPolicy(GetPolicyRequest) returns (GetPolicyResponse)
rpc ListPolicies(ListPoliciesRequest) returns (ListPoliciesResponse)Streaming Watches
WatchModeRegistry
Server-streaming RPC that sends a notification on connection and then fires whenever the mode registry changes (register, unregister, or promote).
rpc WatchModeRegistry(WatchModeRegistryRequest) returns (stream WatchModeRegistryResponse)WatchRoots
Server-streaming RPC for root change notifications.
rpc WatchRoots(WatchRootsRequest) returns (stream WatchRootsResponse)WatchSignals
Server-streaming RPC that delivers ambient signal broadcasts. Signals have empty session_id and empty mode, carry a SignalPayload with signal_type, data, optional confidence, and optional correlation_session_id. Signals never enter session history.
rpc WatchSignals(WatchSignalsRequest) returns (stream WatchSignalsResponse)WatchPolicies
Server-streaming RPC that fires when policies are registered or unregistered.
rpc WatchPolicies(WatchPoliciesRequest) returns (stream WatchPoliciesResponse)Authentication
The runtime applies a resolver chain in this order:
- JWT bearer (when
MACP_AUTH_ISSUERis set):Authorization: Bearer <jwt>. The JWT'ssubclaim becomes the sender;macp_scopescarries capability flags (allowed_modes,can_start_sessions,max_open_sessions,can_manage_mode_registry,is_observer). - Static bearer (when
MACP_AUTH_TOKENS_*is set):Authorization: Bearer <token>orx-macp-token: <token>header. The opaque token is mapped to anAuthIdentityvia the configured token file. - Dev-mode fallback (when neither JWT nor static bearer is configured): any
Authorization: Bearer <value>header authenticates the caller as sender<value>with all capabilities. Intended only for local development. - Reject: Returns
UNAUTHENTICATED.
See the Getting Started guide for token configuration examples.
Resource limits
Five bounds on request size, request frequency, and response size:
| Variable | Meaning | Default |
|---|---|---|
MACP_MAX_PAYLOAD_BYTES | max envelope payload size, in bytes | 1048576 |
MACP_SESSION_START_LIMIT_PER_MINUTE | per-sender session start limit | 60 |
MACP_MESSAGE_LIMIT_PER_MINUTE | per-sender message limit | 600 |
MACP_LIST_SESSIONS_DEFAULT_PAGE_SIZE | ListSessions page size used when the request sends page_size = 0 | 100 |
MACP_LIST_SESSIONS_MAX_PAGE_SIZE | hard cap a requested ListSessions page_size is clamped to | 1000 |
MACP_MAX_PAYLOAD_BYTES bounds the envelope payload; the gRPC request ceiling is MACP_MAX_PAYLOAD_BYTES plus a fixed 64 KiB envelope-overhead allowance (~1.06 MiB at the default), which is what max_decoding_message_size is set to.
The same five variables appear in README.md and docs/deployment.md.
Rate limiting
MACP_SESSION_START_LIMIT_PER_MINUTE and MACP_MESSAGE_LIMIT_PER_MINUTE are per-sender sliding-window limits on session creation and message throughput. When either is exceeded, the runtime returns RATE_LIMITED.
Page caps
MACP_LIST_SESSIONS_DEFAULT_PAGE_SIZE and MACP_LIST_SESSIONS_MAX_PAGE_SIZE bound the size of a single ListSessions response. They are not rate limits: exceeding the cap is not an error and never returns RATE_LIMITED -- an over-large page_size is silently clamped, and the caller continues the traversal with next_page_token. A caller may page as fast as the message rate limit allows.