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
| Option | Type | Default | Description |
|---|---|---|---|
address | string | required | gRPC server address (host:port) |
secure | boolean | true | Use TLS credentials (RFC-MACP-0006 (Transport Bindings) §3) |
allowInsecure | boolean | false | Required when secure: false; constructor throws otherwise |
auth | AuthConfig | undefined | Default auth for all operations |
rootCertificates | Buffer | undefined | TLS root CA certificates |
defaultDeadlineMs | number | undefined | Default RPC deadline (ms) |
clientName | string | 'macp-sdk-typescript' | Client name for Initialize |
clientVersion | string | matches SDK package version | Client version for Initialize |
protoDir | string | @multiagentcoordinationprotocol/proto's protoDir | Override the proto definitions directory |
The constructor throws
MacpSdkErrorifsecure: falseis passed withoutallowInsecure: true. This prevents shipping with TLS off by accident.
Properties
| Property | Type | Description |
|---|---|---|
auth | AuthConfig | undefined | Default auth config |
protoRegistry | ProtoRegistry | Protobuf 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 guidancesend(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.expiresAtUnixMscancelSession(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 —pageSizeis honored and capped per response. ButlistSessions()itself walks every page and accumulates the entire result into one in-memory array regardless ofpageSize, so this method's own memory use is not bounded by that option. For a listing large enough that this matters, calllistSessionsPagedirectly 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);
}| Option | Type | Default | Description |
|---|---|---|---|
pageSize | number | server default | Per-request page cap while auto-paginating. 0 lets the server choose. |
auth | AuthConfig | client auth | Override the default identity. |
deadlineMs | number | client default | Deadline 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 capspageSizeserver-side via a real keyset-scan cursor, so unlikelistSessionsthis method genuinely bounds per-response memory. Use this directly, notlistSessions(), 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);| Option | Type | Default | Description |
|---|---|---|---|
pageSize | number | server default | Max sessions to return. 0 = server-chosen; the server MAY cap it. |
pageToken | string | '' | Continuation token from a previous page. Empty = first page. |
auth | AuthConfig | client auth | Override the default identity. |
deadlineMs | number | client default | Deadline 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 agentlistModes(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'interminalMessageTypes— a mode without a terminalCommitmentcan never resolve. The SDK throwsMacpSessionErrorbefore the wire call if it's missing (mirrors the runtime). promoteModeinto the reservedmacp.mode.*namespace is rejected.- A
SessionStartwith an emptymode_versionbinds the registered descriptor'smodeVersion; a laterCommitmentmust echo that bound version.BaseSessiondefaultsmodeVersionto'1.0.0', so set the descriptor's version to match (or override the session'smodeVersion) 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:
| Shape | sessionId | mode |
|---|---|---|
| Ambient | omitted/empty | omitted/empty |
| Session-scoped | non-empty | non-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.
| Method | RPC | Auth |
|---|---|---|
watchModeRegistry(auth?) | WatchModeRegistry | optional |
watchRoots(auth?) | WatchRoots | optional |
watchSignals(auth?) | WatchSignals | required (runtime ≥ 0.5.0) |
watchSessions(auth?) | WatchSessions | optional |
watchPolicies(auth?) | WatchPolicies | optional |
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);| Param | Type | Default | Description |
|---|---|---|---|
sessionId | string | required | Session to subscribe to |
afterSequence | number | 0 | Exclusive 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 | nullThrows 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'