MacpClient API Reference

MacpClient is the low-level gRPC transport layer. It manages the connection to the MACP runtime and provides typed wrappers for all RPCs.

Constructor

import { MacpClient } from 'macp-sdk-typescript';

const client = new MacpClient({
  address: '127.0.0.1:50051',
  secure: false,
  allowInsecure: true, // required when secure=false; dev-only escape hatch
  auth: Auth.devAgent('agent'),
  rootCertificates: undefined,
  defaultDeadlineMs: 10_000,
  clientName: 'my-app',
  clientVersion: '1.0.0',
  protoDir: '/path/to/proto',
});

Options

OptionTypeDefaultDescription
addressstringrequiredgRPC server address (host:port)
securebooleantrueUse TLS credentials (RFC-MACP-0006 (Transport Bindings) §3)
allowInsecurebooleanfalseRequired when secure: false; constructor throws otherwise
authAuthConfigundefinedDefault auth for all operations
rootCertificatesBufferundefinedTLS root CA certificates
defaultDeadlineMsnumberundefinedDefault RPC deadline (ms)
clientNamestring'macp-sdk-typescript'Client name for Initialize
clientVersionstringmatches SDK package versionClient version for Initialize
protoDirstring@multiagentcoordinationprotocol/proto's protoDirOverride the proto definitions directory

The constructor throws MacpSdkError if secure: false is passed without allowInsecure: true. This prevents shipping with TLS off by accident.

Properties

PropertyTypeDescription
authAuthConfig | undefinedDefault auth config
protoRegistryProtoRegistryProtobuf encode/decode registry

Methods

initialize(deadlineMs?)

Handshake with the runtime. Negotiates protocol version and exchanges capabilities.

const result: InitializeResult = await client.initialize();
// result.selectedProtocolVersion — '1.0'
// result.runtimeInfo — { name, title, version, description, websiteUrl }
// result.supportedModes — ['macp.mode.decision.v1', ...]
// result.capabilities — { sessions: { stream: true }, ... }
// result.instructions — optional human-readable guidance

send(envelope, options?)

Send an envelope to the runtime. Returns the Ack.

const ack: Ack = await client.send(envelope, {
  auth: Auth.devAgent('agent'),  // override default auth
  deadlineMs: 5000,              // override default deadline
  raiseOnNack: true,             // default: true; throw MacpAckError on nack
});

A duplicate ack (ack.duplicate === true) is treated as success and returned without throwing — the message was already accepted.

A gRPC ALREADY_EXISTS or FAILED_PRECONDITION on the underlying call also throws MacpAckError (failure.code 'SESSION_ALREADY_EXISTS' / 'POLICY_DENIED' respectively) rather than MacpTransportError — these are application-level rejections the runtime couldn't express as a normal Ack. Every other gRPC status is still MacpTransportError. See error-handling.md › MacpAckError.

getSession(sessionId, options?)

Query session metadata.

const { metadata } = await client.getSession('session-id', { auth, deadlineMs });
// metadata.state — one of SESSION_STATE_OPEN, _RESOLVED, _EXPIRED, _SUSPENDED, _CANCELLED
// metadata.mode — 'macp.mode.decision.v1'
// metadata.startedAtUnixMs, metadata.expiresAtUnixMs

cancelSession(sessionId, reason, options?)

Cancel an open session. Returns Ack.

As of proto 0.1.3 the resulting ack.sessionState is SESSION_STATE_CANCELLED (previously SESSION_STATE_EXPIRED) — distinct from TTL/policy expiry so consumers can tell an explicit cancellation apart from an expiry.

const ack = await client.cancelSession('session-id', 'no longer needed', { auth });
// ack.sessionState === 'SESSION_STATE_CANCELLED'

suspendSession(sessionId, reason, options?)

Pause an open session (proto 0.1.3+). Returns Ack. Suspension is non-terminal: the runtime banks the session's remaining TTL, rejects any messages sent while suspended, and surfaces ack.sessionState === 'SESSION_STATE_SUSPENDED'. Restricted to the initiator and policy-delegated roles, mirroring cancelSession.

const ack = await client.suspendSession('session-id', 'pausing work', { auth });
// ack.sessionState === 'SESSION_STATE_SUSPENDED'

resumeSession(sessionId, reason, options?)

Resume a suspended session (proto 0.1.3+). Returns Ack. Restores SESSION_STATE_OPEN and adds the banked TTL back to the session's absolute deadline.

const ack = await client.resumeSession('session-id', 'back to work', { auth });
// ack.sessionState === 'SESSION_STATE_OPEN'

listSessions(options?)

Enumerate all sessions visible to the calling identity. Returns an array of SessionMetadata (including contextId and extensionKeys when set). Missing/empty responses normalise to [].

As of proto ≥ 0.1.6 the runtime may cap a single ListSessions response and return a nextPageToken. listSessions() transparently walks every page until the token is empty, so the returned array is always the complete listing. For manual page control use listSessionsPage.

Memory caveat: macp-runtime ≥ 0.7.0 (the current release is 0.8.3) implements real server-side pagination — pageSize is honored and capped per response. But listSessions() itself walks every page and accumulates the entire result into one in-memory array regardless of pageSize, so this method's own memory use is not bounded by that option. For a listing large enough that this matters, call listSessionsPage directly instead and process each page as it arrives.

const sessions = await client.listSessions({ auth });
for (const s of sessions) {
  console.log(s.sessionId, s.state, s.contextId, s.extensionKeys);
}
OptionTypeDefaultDescription
pageSizenumberserver defaultPer-request page cap while auto-paginating. 0 lets the server choose.
authAuthConfigclient authOverride the default identity.
deadlineMsnumberclient defaultDeadline for each unary call.

listSessionsPage(options?)

Fetch a single page of sessions. Returns { sessions, nextPageToken }. Callers MUST NOT assume the listing is complete unless nextPageToken is empty. Pass a returned non-empty nextPageToken back as pageToken to fetch the next page; a stale token yields INVALID_ARGUMENT (inspect via MacpTransportError.code).

Runtime note: macp-runtime ≥ 0.7.0 (the current release is 0.8.3) honors and caps pageSize server-side via a real keyset-scan cursor, so unlike listSessions this method genuinely bounds per-response memory. Use this directly, not listSessions(), when the full listing is too large to hold in memory at once.

let pageToken = '';
do {
  const { sessions, nextPageToken } = await client.listSessionsPage({ pageSize: 50, pageToken, auth });
  for (const s of sessions) console.log(s.sessionId);
  pageToken = nextPageToken;
} while (pageToken);
OptionTypeDefaultDescription
pageSizenumberserver defaultMax sessions to return. 0 = server-chosen; the server MAY cap it.
pageTokenstring''Continuation token from a previous page. Empty = first page.
authAuthConfigclient authOverride the default identity.
deadlineMsnumberclient defaultDeadline for the unary call.

getManifest(agentId?, deadlineMs?)

Retrieve an agent or runtime manifest.

const { manifest } = await client.getManifest();       // self manifest
const { manifest } = await client.getManifest('other'); // another agent

listModes(deadlineMs?)

List standards-track modes.

const { modes } = await client.listModes();
// modes: ModeDescriptor[]

listExtModes(deadlineMs?)

List dynamically registered extension modes.

const { modes } = await client.listExtModes();

listRoots(deadlineMs?)

List coordination roots/boundaries.

const { roots } = await client.listRoots();
// roots: Root[] — [{ uri, name }]

Runtime caveat: the runtime always returns an empty root list — it does not populate roots yet.

registerExtMode(descriptor, options?)

Register a dynamic extension mode.

const result = await client.registerExtMode({
  mode: 'ext.custom.v1',
  modeVersion: '1.0.0',
  title: 'Custom Mode',
  description: 'My custom coordination mode',
  messageTypes: ['CustomMsg', 'Commitment'],
  terminalMessageTypes: ['Commitment'],
}, { auth });

Guardrails (runtime ≥ 0.5.0):

  • The descriptor must list 'Commitment' in terminalMessageTypes — a mode without a terminal Commitment can never resolve. The SDK throws MacpSessionError before the wire call if it's missing (mirrors the runtime).
  • promoteMode into the reserved macp.mode.* namespace is rejected.
  • A SessionStart with an empty mode_version binds the registered descriptor's modeVersion; a later Commitment must echo that bound version. BaseSession defaults modeVersion to '1.0.0', so set the descriptor's version to match (or override the session's modeVersion) or the commitment will mismatch.

registerExtMode, unregisterExtMode, promoteMode, registerPolicy, and unregisterPolicy all throw MacpAckError with failure.code 'FAILED_PRECONDITION' — not MacpTransportError — when the underlying gRPC call fails with FAILED_PRECONDITION (e.g. a read-only registry started with MACP_POLICIES_DIR; see policy.md). Every other gRPC status on these calls is still MacpTransportError.

unregisterExtMode(mode, options?)

Remove a dynamically registered extension mode.

const result = await client.unregisterExtMode('ext.custom.v1', { auth });

promoteMode(mode, promotedModeName?, options?)

Promote an extension mode to standards-track.

const result = await client.promoteMode('ext.custom.v1', 'macp.mode.custom.v1', { auth });

registerPolicy(descriptor, options?)

Register a governance policy. Returns { ok, error? }. Build the descriptor with the typed builders in policy.md rather than by hand.

const result = await client.registerPolicy(descriptor, { auth });

unregisterPolicy(policyId, options?)

Remove a registered policy. Returns { ok, error? }.

const result = await client.unregisterPolicy('policy-id', { auth });

getPolicy(policyId, options?)

Fetch a single policy descriptor. Returns the PolicyDescriptor directly (not wrapped in a response object).

const descriptor: PolicyDescriptor = await client.getPolicy('policy-id', { auth });

listPolicies(mode?, options?)

List registered policies, optionally filtered by mode. Returns PolicyDescriptor[] (missing/empty responses normalise to []).

const all = await client.listPolicies(undefined, { auth });
const decisionOnly = await client.listPolicies('macp.mode.decision.v1', { auth });

openStream(options?)

Open a bidirectional session stream. Requires auth (throws MacpSdkError if neither options.auth nor the client default is set).

const stream: MacpStream = client.openStream({ auth });

sendSignal(options)

Convenience wrapper for the ambient signal plane: builds, encodes and sends a Signal envelope in one call. Returns the Ack.

const ack = await client.sendSignal({
  signalType: 'ext.signal.heartbeat',   // required; validated client-side
  data: Buffer.from('{}'),              // optional payload bytes
  confidence: 0.9,                      // optional
  correlationSessionId: 'session-id',   // optional
  sender: 'alice',                      // optional; defaults to senderHint()
  auth,                                 // optional; defaults to client auth
  deadlineMs: 5000,                     // optional
});

Throws MacpIdentityMismatchError if sender conflicts with the auth config's expectedSender.

sendProgress(options)

Convenience wrapper for Progress messages. Returns the Ack.

const ack = await client.sendProgress({
  sessionId: 'session-id',
  mode: 'macp.mode.task.v1',
  progressToken: 'token-1',   // required
  progress: 3,                // required
  total: 10,                  // required
  message: '3 of 10 done',    // optional
  targetMessageId: 'msg-id',  // optional
  auth,
});

Per RFC-MACP-0001 §6, Progress is legal in exactly two shapes:

ShapesessionIdmode
Ambientomitted/emptyomitted/empty
Session-scopednon-emptynon-empty

Supplying exactly one of the two throws MacpSessionError before the RPC is issued, naming the mismatched field. The runtime rejects that mixed envelope with INVALID_ENVELOPE, so this only surfaces the failure earlier and more clearly.

// throws MacpSessionError — sessionId is set but mode is empty
await client.sendProgress({ sessionId: session.sessionId, progressToken: 't1', progress: 1, total: 4 });

Watch methods

Server-streaming RPCs that return a raw grpc.ClientReadableStream. Prefer the watcher classes in src/watchers.ts (ModeRegistryWatcher, RootsWatcher, SignalWatcher, PolicyWatcher, SessionLifecycleWatcher), which wrap these in async generators. The deprecated _-prefixed aliases (_watchModeRegistry, etc.) were removed in 0.5.0 — use the names below.

MethodRPCAuth
watchModeRegistry(auth?)WatchModeRegistryoptional
watchRoots(auth?)WatchRootsoptional
watchSignals(auth?)WatchSignalsrequired (runtime ≥ 0.5.0)
watchSessions(auth?)WatchSessionsoptional
watchPolicies(auth?)WatchPoliciesoptional

watchSignals throws MacpSdkError client-side when no auth is available (neither the argument nor client.auth), rather than failing later with a stream UNAUTHENTICATED.

const call = client.watchSignals(auth);
call.on('data', (event) => console.log(event));

senderHint(auth?)

Get the sender identity hint from the given or default auth config.

const sender: string | undefined = client.senderHint();

close()

Close the gRPC connection.

client.close();

MacpStream

Wraps a gRPC duplex stream for session streaming.

send(envelope)

Write an envelope to the stream.

await stream.send(envelope);

sendSubscribe(sessionId, afterSequence?)

Send a subscribe-only frame so the runtime replays accepted envelopes for the session from afterSequence onwards, then continues with live broadcast (RFC-MACP-0006-A1). Use this to attach a late observer to a session that is already in flight, or to resume after a reconnect with a replay cursor.

// Fresh subscriber — replay everything, then keep streaming live.
await stream.sendSubscribe('session-abc');

// Resume after a reconnect with a known cursor.
await stream.sendSubscribe('session-abc', lastSeenSequence);
ParamTypeDefaultDescription
sessionIdstringrequiredSession to subscribe to
afterSequencenumber0Exclusive cursor: replay envelopes whose ordinal is > afterSequence. Pass 0 for a full replay.

Ordinal contract (runtime ≥ 0.5.0, RFC-MACP-0006 §3.2): afterSequence is the 1-based accepted-envelope ordinal — the Nth accepted envelope has ordinal N. Clients derive it by counting distinct delivered envelopes, keyed on message_id — a redelivery MUST NOT advance the count (§3.2 Redelivery); the agent framework's GrpcTransportAdapter exposes this as its lastSequence getter, computed exactly this way. Ordinals are stable across log compaction and runtime restart, and envelopes accepted during the subscribe window are never delivered twice. Resuming below a compacted base fails with FAILED_PRECONDITION (inspect via MacpTransportError.code) rather than silently skipping history.

Rejects with MacpSdkError if the stream has already been closed. The agent framework's GrpcTransportAdapter calls this automatically after opening the stream — including passing its own lastSequence as the resume cursor on a second start() — so you only need it when driving a raw MacpStream.

responses()

Async generator yielding received envelopes. Throws MacpTransportError if the underlying stream errors; returns when the stream ends.

for await (const envelope of stream.responses()) {
  console.log(envelope.messageType);
}

read(timeoutMs?)

Read a single envelope from the stream (parity with the Python SDK's MacpStream.read(timeout)). Returns the next envelope, or null if the stream has ended. If timeoutMs is omitted, blocks until an envelope arrives, the stream ends, or an error occurs.

const envelope = await stream.read(5000); // Envelope | null

Throws MacpTimeoutError if timeoutMs elapses first, and MacpTransportError if the underlying stream errored.

onInlineError(callback)

Register a callback for inline application-level errors delivered on the stream (a StreamSessionResponse frame whose oneof arm is error, not envelope). The stream stays open when these arrive — they are not transport failures. The SDK also logs each one at warn (via src/logging.ts's logger/configureLogging) before invoking registered callbacks.

stream.onInlineError(({ code, message }) => {
  console.warn('inline error', code, message);
});

close()

End the stream. Idempotent; subsequent send()/sendSubscribe() calls reject with MacpSdkError.

stream.close();

grpcStatusName(code)

Top-level export mapping a numeric gRPC status code to its name (e.g. 8 → 'RESOURCE_EXHAUSTED'). Returns undefined for non-numeric or unknown codes. The SDK uses it to populate MacpTransportError.code.

import { grpcStatusName } from 'macp-sdk-typescript';

grpcStatusName(9); // 'FAILED_PRECONDITION'