pub enum Request {
Show 48 variants
RegisterService(RegisterServiceParams),
Status,
Invite(InviteParams),
Pair(PairParams),
PeerRemove(PeerRemoveParams),
PeerRename(PeerRenameParams),
AttestOffer,
AttestTo(AttestToParams),
PeerRevoke(PeerRevokeParams),
PeerUnrevoke(PeerUnrevokeParams),
DeviceRevoke(DeviceRevokeParams),
DeviceRevocationImport(DeviceRevocationImportParams),
PeerAdd(PeerAddParams),
PeerIntroduce(PeerIntroduceParams),
PeerEndorse(PeerEndorseParams),
OpenSession(OpenSessionParams),
RosterInstall(RosterInstallParams),
RosterMembers,
OrgCreate(OrgCreateParams),
OrgApprove(OrgApproveParams),
OrgRotate(OrgRotateParams),
OrgJoinCode(OrgJoinCodeParams),
OrgRevoke(OrgRevokeParams),
UserKeyExport,
UserKeyImport(UserKeyImportParams),
OrgJoin(OrgJoinParams),
SetRosterUrl(SetRosterUrlParams),
SetNickname(SetNicknameParams),
SetAppMetadata(SetAppMetadataParams),
SetRelays(SetRelaysParams),
UnregisterService(UnregisterServiceParams),
PeerServices(PeerServicesParams),
PeerDiagnostics(PeerDiagnosticsParams),
PeerHintClear(PeerHintClearParams),
ServiceAllowGrant(ServiceAllowParams),
ServiceAllowRevoke(ServiceAllowParams),
BlobPublish(BlobPublishParams),
BlobGrant(BlobGrantParams),
BlobRevoke(BlobRevokeParams),
BlobUnpublish(BlobUnpublishParams),
BlobRepublish(BlobRepublishParams),
BlobList(BlobListParams),
BlobFetch(BlobFetchParams),
BlobFetchCancel(BlobFetchCancelParams),
AuditSummary,
AuditPrune(AuditPruneParams),
AuditList(AuditListParams),
Subscribe,
}Expand description
Control-API requests. Serialized as { "method": "...", "params": {...} }
(JSON-RPC-shaped; the id/jsonrpc envelope is added by the transport layer).
Each param-carrying variant wraps its named *Params struct — the ONE wire truth for that
method’s params, shared by clients (which serialize whole Requests) and the daemon (which
deserializes params into the same struct after its method-string dispatch). Adjacent
tagging serializes a newtype variant’s content as the struct’s fields, so the wire shape is
identical to inline variant bodies.
Servers dispatch on the method string and deserialize params per-method — tolerating
omitted / null / empty-object params for parameterless methods — rather than deserializing a
whole message into Request (adjacent tagging rejects params:{} for unit variants).
This keeps the wire tolerant for third-party clients (the versioned, additive-only surface).
Use method_of to extract the tag, then match + deserialize params per-method.
Variants§
RegisterService(RegisterServiceParams)
Register/update a [services.*] entry idempotently.
Status
Invite(InviteParams)
Mint a pairing invite granting services — single-use unless max_uses says otherwise
(#87). The daemon
answers an InviteResult carrying the copyable mcpmesh-invite: line. Tag
"invite" (snake_case). method_of needs no per-variant arm — it reads the
method string generically; the tag comes from rename_all.
Pair(PairParams)
Redeem a pairing invite. The daemon dials the inviter named by
invite_line on mcpmesh/pair/1, proves the secret, writes the mutual
(dial-back) PeerEntry, and answers a PairResult. Tag "pair"
(snake_case); method_of reads the method string generically.
PeerEntry — the durable allowlist row — lives in the daemon crate.
PeerRemove(PeerRemoveParams)
Remove a paired peer by nickname (mcpmesh pair --remove). The daemon drops the
peer’s PeerEntry (identity) AND revokes its access by stripping its stable principals from every
[services.*].allow (authorization) — the inverse of the pairing grant. Idempotent: a
nickname with no entry / no allow membership is a clean no-op. Live in-flight sessions are
NOT severed here: existing sessions run to completion; the peer only loses the
ability to establish NEW authorized sessions. Tag "peer_remove" (snake_case);
method_of reads the method string generically (no per-variant arm).
PeerEntry — the durable allowlist row — lives in the daemon crate.
PeerRename(PeerRenameParams)
Rename a contact’s nickname (nickname) authoritatively. Renames the
PERSON — every PeerEntry sharing user_id when given (one op for all their devices), else the
single nickname entry (a provisional, no-user_id contact) — to to, AND rewrites the old
nickname → to in every [services.*].allow so grants follow the rename. Refuses (error frame)
when to is empty or already names/grants a DIFFERENT identity — the same collision guard the
pairing rendezvous uses, so a rename can’t inherit another peer’s access. Tag "peer_rename";
host-privileged like the other pair ops.
AttestOffer
Mint an attestation OFFER (#85 ask 3): a mcpmesh-attest: line telling another device of a
person you already pair with where to dial. Tag "attest_offer".
Carries nothing secret — the offering node’s id and address, both of which an invite line already carries in the clear. It exists because a device freshly restored from a recovery phrase holds no rows and so has no way to find anyone.
Refused unless this node has [identity].admit_attested_devices on: an offer it would not
honour is worse than no offer.
AttestTo(AttestToParams)
Present THIS device’s identity to a peer, using their mcpmesh-attest: line (#85 ask 3).
Tag "attest_to".
The recovery path’s second half: user_key_import restores the b64u: your peers pinned,
and this is what gets the machine holding it ADMITTED. Requires a user key — with none there
is nothing to attest.
The peer admits this device only if it already holds a row for this person, has opted in, and has revoked neither this endpoint nor the IDENTITY. It cannot admit a stranger.
PeerRevoke(PeerRevokeParams)
REVOKE an endpoint locally: “I no longer trust this device” (#85 ask 4). Tag
"peer_revoke".
Not the same act as PeerRemove, deliberately. Removal is routine —
we are not working together any more — and re-pairing afterwards is normal. Revocation is a
claim about COMPROMISE, and it outlives the pair row: a revoked endpoint is refused whether
or not it is in the allowlist, so it cannot be undone by a fresh pairing. Making them one
verb would either make removal irreversible or make revocation weak.
Takes effect IMMEDIATELY, like service_allow_revoke (#54): live sessions are severed
rather than left to end on their own, which for long-lived MCP sessions is unbounded.
A b64u: principal revokes EVERY endpoint this node associates with that person — the “not
on any of their devices” case, which an operator will otherwise get wrong doing it device by
device.
Reversible with PeerUnrevoke: this list is LOCAL and not
authoritative for anyone else, so an operator mistake has to be fixable. Both directions are
audited.
PeerUnrevoke(PeerUnrevokeParams)
Lift a local revocation (#85 ask 4). Tag "peer_unrevoke". Idempotent.
Restores the peer only if its pair row still exists — revocation never deleted it. A revocation applied from a SIGNED statement can be lifted too: the signature proved who asked, not that this node must obey forever.
DeviceRevoke(DeviceRevokeParams)
Sign a revocation of one of THIS node’s own endpoints with THIS node’s user key (#85 ask 4),
producing a portable token. Tag "device_revoke".
The direction that matters, and the one local revocation cannot express: my laptop was stolen, and my peers cannot discover that by themselves. I have to tell them, and they have to be able to verify it was me.
Requires a user key — a node with none has no authority to speak for its person. Distribution is out of band: pairing mode has no gossip, so getting the token to your peers is the same problem as getting them an invite line, and has the same answer.
DeviceRevocationImport(DeviceRevocationImportParams)
Apply a signed device revocation from a peer (#85 ask 4). Tag "device_revocation_import".
Refused unless this node ALREADY trusts the signing user_id (holds a paired entry carrying
it) and the revoked endpoint is one this node associates with that same person, or is
entirely unknown to it. That bound is what keeps this from being an “mark any endpoint dead”
primitive: a peer may kill their OWN devices in your node, and nothing else.
PeerAdd(PeerAddParams)
RESERVED / INTERNAL (docs/local-protocol.md “Reserved / internal methods”): install a
peer directly from a raw endpoint_id — the trust-population stand-in for pairing behind
mcpmesh internal peer add. A deliberate, documented exception to the surface discipline
(raw endpoint identifiers otherwise never cross this socket); NOT part of the stable
vocabulary — do not build on it. Tag "peer_add".
PeerIntroduce(PeerIntroduceParams)
Install a peer from a SIGNED endorsement by someone you are already paired with (#65) — O(N) onboarding for a small group, without a fresh two-human SAS ceremony per pair.
It installs IDENTITY, never AUTHORIZATION. The subject becomes resolvable; it is granted nothing. Service access stays principal-keyed in config (#38) and an explicit, separate act. That is what bounds the feature: a compromised endorser can make you KNOW about an attacker, it cannot make you SERVE one.
Unlike PeerAdd — which is reserved precisely because the caller merely
ASSERTS an id — this is verifiable: the endorsement is checked against a user key you
already hold from pairing with the endorser. Tag "peer_introduce".
PeerEndorse(PeerEndorseParams)
PRODUCE an endorsement of a peer, for someone else to redeem with
PeerIntroduce (#65). The other half of an introduction: without it
nothing can generate evidence, and the install half is unusable.
Signs with THIS node’s user key, so the result is only meaningful to someone who has paired
with you. Endorsing does not change your own trust in the subject. Tag "peer_endorse".
OpenSession(OpenSessionParams)
Open a mesh session to peer/service; the daemon dials and pipes.
Distinct from the proxy’s job: this returns a session the client streams.
Named open_session rather than connect to avoid colliding
with the connect porcelain.
RosterInstall(RosterInstallParams)
Install a signed roster from a local file (the manual internal roster install path).
path is a LOCAL file the same-uid daemon reads (the daemon runs as the caller’s own
uid, so passing a path rather than the bytes crosses no trust boundary). org_root_pk
pins the org root on FIRST install (b64u:); omit it
once pinned (config carries it). Tag "roster_install".
RosterMembers
Read the installed roster’s MEMBERSHIP: the declared groups, and every person with their
display name, groups, and devices (#93). Parameterless. Tag "roster_members".
The read half of roster mode. status reports that a roster exists (RosterStatus) and who
is currently online (PresencePeer); neither answered “who is in this org” — a person with
no live device appeared nowhere at all, so an embedder could not draw a member list, and
the only route to one was hand-parsing the daemon-owned roster.json.
ADVISORY and display-oriented, like status: the gate reads the roster document, never
this. Empty in a pure-pairing daemon or before the first roster is installed.
OrgCreate(OrgCreateParams)
AUTHOR an org: mint this node’s org root key, sign an empty roster (serial 1), install it
(which pins the root), and return the copyable org invite (#66). Tag "org_create".
One-time per node — a second call is refused rather than replacing the key, because replacing it would orphan every roster it has signed.
OrgApprove(OrgApproveParams)
APPROVE a join code into the roster: verify its device→user-key binding, upsert the member
with the given groups, bump the serial, re-sign, install (#66). Tag "org_approve".
The cryptographic half of the ceremony. Verifying that the code came from the PERSON you
think it did stays an out-of-band human step, and join_code_fingerprint in the result is
what the two humans compare.
OrgRotate(OrgRotateParams)
ROTATE the org root (#93 ask c). Tag "org_rotate".
The org’s trust anchor is one pinned key, and nothing could move it: an operator laptop that died took the org with it 90 days later, when the roster expired — and the delay is what made that hard to diagnose. Recovery was O(N) fresh ceremonies with every member.
Publishes a roster signed by the SUCCESSOR carrying a cross-signature by the CURRENT root, so
a member still pinned to the current key adopts the successor with the key it already has.
The bridge rides every subsequent roster, so a member offline for one publication catches up
— but one two rotations behind needs a fresh org_join.
This is not escrow. If the current root key is LOST there is nothing to sign the bridge
with. Copying org-root.key to a second operator machine already works and remains the
answer for that.
OrgJoinCode(OrgJoinCodeParams)
INSPECT a join code without approving it (#66): who it claims to be, and the fingerprint the
two humans compare. Read-only — nothing is signed, installed, or persisted.
Tag "org_join_code".
This is what makes an “approve this person” button correct rather than merely possible. The
fingerprint has to be shown and confirmed out-of-band BEFORE the approval, because a
substituted code is caught there or not at all — and reading it off OrgApprove’s result
is too late, the member is already in the signed roster. The CLI always had this (it
decoded the code locally); an embedder could not, since the join-code format lives in
mcpmesh-node and not on this seam.
OrgRevoke(OrgRevokeParams)
REVOKE from the roster: remove a person, one device, or a person’s user key, then bump,
re-sign, and install — which severs the cut devices’ live sessions (#66).
Tag "org_revoke".
UserKeyExport
EXPORT this node’s user key as a recovery phrase (#85 ask 2). Parameterless.
Tag "user_key_export".
The phrase IS the private key, in a form a human can write down. Anyone who reads it can present this identity. It is deliberately not logged, not audited, and not echoed anywhere but this response.
UserKeyImport(UserKeyImportParams)
IMPORT a user key from a recovery phrase (#85 ask 2), so a person’s b64u: survives the
hardware. Tag "user_key_import".
OrgJoin(OrgJoinParams)
Pin the org root on a JOINER — WITHOUT a roster (the joiner has none yet; its poll loop
fetches the first one). Records [identity] org_id / org_root_pk / user_id / user_key.
user_key is a LOCAL path
(the key never crosses the API). Tag "org_join".
SetRosterUrl(SetRosterUrlParams)
Pin the HTTPS roster URL ([roster].url) in config. Written by org create --roster-url (the operator keeps it current) AND by join when the org invite carries one —
so the joiner’s poll loop bootstraps its FIRST roster. The daemon writes it under
reload_lock (single-writer), then the poll loop picks it up on the next daemon start. Tag
"set_roster_url".
SetNickname(SetNicknameParams)
Rename this node LIVE (#37): validate + upsert [identity].nickname through the
daemon’s own serialized config-write path (no lost-update window against a
concurrent grant/registration) and update the in-memory name future invites
present — no restart. Ack result. Tag "set_nickname" (snake_case).
SetAppMetadata(SetAppMetadataParams)
Set this node’s opaque app-metadata blob (#39): validated (≤256B) and folded, signed,
into each outgoing presence heartbeat, so paired roster peers see it in their status
presence — no per-peer session. Ack result. Tag "set_app_metadata". In-memory (lost
on restart; the embedder re-sets on startup).
SetRelays(SetRelaysParams)
Set this node’s CUSTOM relay set LIVE (#53): validate each URL as an iroh RelayUrl, diff
against the running endpoint’s current custom relays and apply the delta via iroh 1.0.3
Endpoint::insert_relay/remove_relay (no endpoint rebuild, no dropped sessions), then
persist [network] relay_mode="custom" relay_urls=[…] under reload_lock. When the node
is currently default/disabled, the config is persisted but the live mode transition
isn’t possible — SetRelaysResult::restart_required is true. Answers a
SetRelaysResult. Tag "set_relays".
UnregisterService(UnregisterServiceParams)
Grant a single stable principal access to a single service’s allow (#44) — the per-peer
“sharing on” toggle, idempotent + serialized under the config lock. Ack result.
Remove a service registration (#50) — the deregistration mirror of register_service.
Removes the whole [services.<name>] entry (allow included) + any ephemeral one, then
hot-reloads. Idempotent. Ack result.
PeerServices(PeerServicesParams)
Discover which services a paired peer CURRENTLY grants the caller (#52) — dials the peer
and returns the service names whose allow admits the caller’s principal. Answers
PeerServicesResult.
PeerDiagnostics(PeerDiagnosticsParams)
Dump the DURABLE per-peer state this node carries for one peer (#140) — the persisted dial
hint, the pairing stamp, and the live reachability row, in one capture. A DIAGNOSTIC verb:
unlike every other surface it carries transport vocabulary on purpose. Answers with
PeerDiagnosticsResult. api_minor >= 33.
PeerHintClear(PeerHintClearParams)
FORGET the persisted dial hint for one peer (#140) — api_minor >= 59.
An experiment tool, and a workaround. PeerEntry.last_addr is the only durable per-peer
state on this node’s disk that the dial path reads, and it is the ONLY thing a long-lived
pairing carries that a freshly paired identity does not. Nothing invalidates it: a
relay-only connection deliberately declines to overwrite it (persisting a relay URL over a
direct candidate was #124’s own bug), and “learned nothing” means leave alone. So a hint
written before a network change can persist indefinitely on a pair whose every connection
since has been relayed.
Clearing it makes an existing pairing addressing-equivalent to a fresh identity, which is exactly the difference #140 is about — a pair that cannot punch as a long-lived pairing while punching in 23ms with fresh identities on the same hardware.
Advisory, never authorization. A hint is addressing; its absence is a supported state
(the dial degrades to id-only, which is what every peer does before its first refresh). The
worst case is one slower dial while discovery runs. The peer row, its user_id, its
services and its pairing stamp are untouched.
Nothing clears a hint automatically — choosing an invalidation policy before the data exists
is what produced a fix that helped one peer and not this one. Answers
PeerHintClearResult.
ServiceAllowGrant(ServiceAllowParams)
ServiceAllowRevoke(ServiceAllowParams)
Revoke a single stable principal from a single service’s allow (#44) — “sharing off” WITHOUT unpairing (the peer’s identity row is untouched; only NEW sessions are refused). Idempotent. Ack result.
BlobPublish(BlobPublishParams)
Publish a LOCAL file INTO a scope: the daemon adds the bytes to its gated
app-blob store and records the hash in scope. path is a local file the same-uid daemon
reads. Answers a BlobPublishResult carrying the mcpmesh/blob/1 ticket + hash.
Tag "blob_publish".
BlobGrant(BlobGrantParams)
Grant a scope to a principal — any flat-namespace entry: a group name, a user_id, or a
nickname (the shared principal_set expansion). Tag
"blob_grant".
BlobRevoke(BlobRevokeParams)
Tag "blob_revoke": withdraw principals from ONE scope’s grants (#62).
BlobUnpublish(BlobUnpublishParams)
Tag "blob_unpublish": remove a hash from ONE scope (#62). Withdraws reachability, not
bytes.
BlobRepublish(BlobRepublishParams)
#83: make a blob this daemon already holds servable from HERE, in a scope it controls.
Answers a BlobPublishResult — same shape as blob_publish, so a client can treat the
two interchangeably after a fetch.
BlobList(BlobListParams)
List the daemon’s blob scopes (name → hashes + grants). Tag "blob_list".
BlobFetch(BlobFetchParams)
Fetch a mcpmesh/blob/1 ticket THROUGH the daemon (BLAKE3-verified streaming) and export the
verified blob to dest_path (a local file the same-uid daemon writes). Answers a
BlobFetchResult with the verified hash + byte length. Tag "blob_fetch".
BlobFetchCancel(BlobFetchCancelParams)
Cancel every in-flight BlobFetch of one hash (#172). Answers a
BlobFetchCancelResult; the cancelled fetches themselves answer ERR_CANCELLED.
Tag "blob_fetch_cancel".
AuditSummary
Summarize this node’s LOCAL audit log into per-peer / per-service SESSION counts
(local-only — the daemon reads its OWN audit dir, nothing is transmitted). The host Mesh surface
renders these as “who serves me / whom I serve / session counts”. Parameterless (like Status);
the server dispatches on the method string. Tag "audit_summary" (snake_case);
method_of reads the method string generically (no per-variant arm).
AuditPrune(AuditPruneParams)
Delete audit months strictly older than before (#88) — the retention lever the log
never had. Local-only and owner-only (the control socket is the daemon owner’s). Answers
AuditPruneResult. Tag "audit_prune".
AuditList(AuditListParams)
Read this node’s LOCAL audit records, filtered and paged (#88) — the “show me everything
you hold about me” verb. Local-only; nothing is transmitted. Answers
AuditListResult. Tag "audit_list".
Subscribe
Open a live event stream (pairing liveness & health telemetry). Like open_session, the
connection STOPS being request/response after this call and becomes a one-way push stream
of StreamFrames. Parameterless. Tag "subscribe".