On October 27, 2026, at 8:29 p.m. ET, a program on my homelab server sent a message into a Muse side chat it had never opened, and the message landed as an ordinary inbound message in that thread. The receipt said ok: true, HTTP 200, is_thread: true. The test marker was LKPT-20261027-7F3A. That was test T6 in a series of six, and it settled the only question the rest of the night depended on: a Muse chat ID is not just an identifier the app uses internally. It is a routable address.
What changes is what a chat is. A conversation with its own memory, its own standing brief, and an agent that reads in context stops being only a place I visit and becomes an endpoint: an inbox that triages, a sensor surface that accumulates, a newsroom that edits. Any process that can reach an MCP server can speak into a specific conversation and have that conversation's context do the reading.
I built that server. It is called muse-mcp, it is open source under Apache-2.0, and it is here: https://github.com/niclydon/muse-mcp
The first registry sync made the premise visible in one line of output: 125 enumerated chats, 5 registered, 120 unregistered drafts, 56 archived. I had 125 conversations and a 5-entry address book. muse-mcp is the address book, the directory service, and the delivery mechanism, in one server.
A few honesty markers before anything else, because they apply to every paragraph that follows. muse-mcp is not a scrape. It is not a private API. It is not an official Meta project. It is built on the exposed primitives of the official SDK, facebookincubator/muse-gadget-sdk (Apache-2.0), used as exposed. It is an unofficial community project, not affiliated with, endorsed by, or sponsored by Meta.
What this takes
This is not a hosted API and it is not zero-setup. If you want to run it, this is the shape of the work:
- A host paired as a Muse Link device. The same pairing model a physical gadget uses. A software client pairs via the SDK on a Linux box. This pairing is the one real piece of work.
- A small always-on Linux host for the MCP server itself. Mine runs as a systemd service on a homelab box. A Pi-class machine is plenty. An ESP32 gadget can be a voice endpoint in this story — two of mine are — but the MCP server wants a general-purpose host.
- The bridge half. Operations the Link does not expose (create, rename, archive, delete, read) run through a Muse-side worker chat executing queued requests. Two halves, stated as architecture, not apology.
- Optional: a second host for a redundant twin. Mine runs on two machines; nothing about the design requires it.
There is no cloud version and no hosted API. You bring the paired device and the box.
The finding came first; the server was an argument about scope
The design work assumed a narrow server: two tools, list and send. It was wrong within eleven minutes of me reading it. Between 8:41 and 8:56 p.m. that same evening I corrected the scope five times, and each correction widened it: CRUD and conversational linkage, the full toolset, a registry of every chat, a second host wired in as a backup, public repo from the start. The one-line summary I gave the agents, and the one that survived, was "nova-mcp in reverse." nova-mcp is my estate's control plane outward — it lets Muse operate the homelab. muse-mcp is the same posture pointed inward, at Muse conversations as the resource.
Chats are the records. Callers create, list, read, rename, archive, and — behind a preview-then-confirm gate — delete them. Sending is the write. Asking a chat a question and receiving its agent's reply is the conversational half. A send-only pipe is a pager, not a conversation, and I refused to ship the pager.
The receipts from that night, in order:
October 27, 9:16 p.m. ET. Version 0.2.0 shipped with 19 tools: list, send, ask, history, events, voice send, link status, the registry sync, and the rest. The ask round trip worked through the SDK's MuseTurn follower. The first run burned its full timeout, because MuseTurn.complete() waits for the target's entire agentic task to go terminal and the target chat kept working after it had answered. The shipped fix is a labelled early completion — reply done, plus 8 seconds of turn silence — and the re-check returned in about 18 seconds. Completion semantics belong to the conversation, not to the caller's patience.
October 27, later. Build Pass 2 took the count from 19 tools to 38 and gave the server hands: the Bridge. The SDK's Link surface delivers synchronously into a known thread, carries voice, and streams live events. It cannot enumerate chats, and it cannot create, rename, archive, or delete them. Those operations exist only where an agent's turn can run, so muse-mcp queues them, keyed by correlation ID, and a dedicated Muse-side chat — the Bridge — executes exactly the named operation with the runtime's native chat tools and writes the result back. Acceptance ran the full lifecycle on a throwaway chat: create, register, ask, relay, rename, archive, unarchive, archive, and a delete that first had to survive its own safety checks. A delete with no confirmation token is refused. A delete of the main chat is refused at preview and at delete. That is the correct shape for a tool that can erase a conversation.
October 27, later still. Build Pass 3 brought the count to 39 tools and rebuilt the status tool honestly, which is the subject of the limitations below. Version 0.4.6, the release in the public repo, is that line: 39 tools, file-backed directory, both transports.
I directed, pictured, approved, and verified. The agents wrote, planned, pushed, and built. Both halves of that sentence are true everywhere in this article, and the receipts are the receipts either way.
Two transports, and a registry that cannot refresh itself
The core function is the registry loop, and it exists because of a proven limitation: the Link client cannot enumerate chats. The server can therefore never refresh its own directory. Only the Muse side can, because only there does a chat listing exist. Every addressing tool reads the registry, so a stale registry silently degrades the whole server, and chat churn is constant — renames land on essentially every tidy sweep of my sidebar.
So the loop is asymmetric by design. The server sends a structured sync request into a Muse chat by Link delivery. The Muse side enumerates every chat including archived ones, diffs against the current revision, preserves human-approved classifications (enumeration supplies identity and liveness, never classification), and returns the result as data — a registry file — not by conversation. The ask travels by conversation; the answer travels as data. New chats enter as unregistered drafts and are not addressable by producers until a human classifies them. A chat that comes back archived is suspended from targeting immediately, fail closed. That is the whole addressing discipline: nothing is reachable by accident, and nothing stale stays reachable by neglect.
What the surface does not expose
I am stating these flatly, the way I would want them stated to me, because a map of what a surface does not do is documentation, not complaint.
There is no unread state. I probed the SDK across its Linux, ESP32, Android, iOS, and web-flasher sources for the blue dot. There is no unread field, no unread count, no read or seen receipt, no mark-as-read endpoint. The dot is Muse chat-backend state that the app reads directly; the Link does not carry it. So no muse-mcp tool outputs anything unread-shaped under any name. The status tool returns unread: null, with the explanation attached, rather than guessing.
Pinning is real and unreachable. The backend's canonical chat record carries pinned and pinned_order, and a live query that night returned 19 pinned chats in a clean contiguous order, 1 through 19. Pinning and ordering are actively maintained state with exactly one write path: the app itself. Chat listing exposes the flag read-only; no agent surface writes it. New chats are created unpinned. That is a fact about the surface, recorded, not worked around.
The filesystem routes are entitlement-blocked. The FS tools are implemented to the documented shapes and they reach the VM. The VM answers 403: "route not allowed for this credential's scopes." That is a backend entitlement on the credential, not a client bug, and there is no documented toggle or re-pair path that changes it. The tools stay implemented and documented as blocked.
History is a window, not an archive. History returns the newest 50 events at most, and subscription is live-only with no backlog. The watch and status tools are built inside that constraint: a live snapshot and canonical history are returned as two visibly separate halves, never blended into a confidence neither one earned. The status tool's pending-approvals count is null unless an approvals snapshot was actually received. Never a guessed 0. On its first live run the two halves disagreed — history said the chat was waiting on me while the live snapshot said it was working with 2 active subagents — and that disagreement is the designed case, which is why both halves ship in the output.
The pattern across all four is the same, and it is the pattern the whole project ran on: the backend models more than any agent surface exposes. Record the state. Refuse to fake it. Build only on what is reachable.
Cant: the machines needed a wire format, not a secret
Once the Bridge existed, a second population appeared that nobody had specced: agents talking to agents, in English prose, at conversational prices. The Bridge's wakes were paragraphs carrying machine content, read by a language model, several dozen times a night. So the machine channels got a dialect. It is called Cant — the plain dictionary word, the jargon of a closed group — and it is a rigid wire frame: an opening line, key=value body lines drawn only from a fixed dictionary, and a closing line that repeats the correlation prefix, which buys threading and truncation detection for free. Payloads travel by reference, never inline.
Here is a real frame, the server asking the Bridge to create a chat, beside its full English expansion:
§CANT/1 REQ 9f3ac21e
op=CRT
name=🧪 muse-mcp throwaway P2
arg=@file:/services/muse-mcp/bridge-queue/9f3ac21e.json
§/CANT 9f3ac21e
Request, correlation 9f3ac21e. Operation: create a chat. Name it "🧪 muse-mcp throwaway P2". The arguments — the standing brief for the new chat — are not in this message; they are in the file
/services/muse-mcp/bridge-queue/9f3ac21e.json, which the Bridge reads, executes against, and answers with one result frame and nothing else.
Now the doctrine, plainly, because the wrong impression is easy to take away: Cant is not encryption. It is not a code, and it is not a secret language. It exists for machine-to-machine talk — rigidity, auditability, envelope discipline — and the fact that anyone else could work out what a frame means is not a concern and was never a design consideration. Its privacy, such as it is, is a publication choice about a repo, not a security property of the dialect. Anything said in Cant must be expandable to plain English, the way the frame above just was. Channels I read stay in English permanently. Errors addressed to humans are in English — a malformed frame gets an English parse-failure reply, deliberately, because a parse failure is an operator-visible event, not machine traffic.
The first real traffic was a create round trip through the Bridge, correlation eb152f34, result frame back with st=ok and a duration of 14,625 milliseconds, both frames validated clean by the reference decoder. Cant entered the record as a protocol with a verified round trip, not as a proposal.
Side note: the address works for voice, too
One discovery from building muse-mcp deserves a mention beyond the repo itself. muse-mcp is built on a simple premise: a Muse chat ID is a routable address, and software can deliver a message into any chat it can name. While proving that out for text, we found the same address works somewhere we did not expect: it is also the thread target for a hardware voice call.
The Muse gadget firmware an ESP32 device ships with has no notion of a call destination; its call admission carries no thread field, so an unmodified gadget can only ever wake up in the account's main chat. We modified the firmware to put a chosen side chat's ID in the admission record, and the platform honored it: the call was admitted into that side chat, and a full two-way voice conversation took place in that thread, verified from the thread itself. One chat ID, three surfaces: the app thread, a Link message destination, and a hardware voice-call target.
A corollary worth knowing before anyone builds characters on top of this: the persona does not follow the thread. A call admitted into a chat briefed as a different character still spoke as the account's main assistant. As far as we can tell, the thread is the room and the persona is the seat's: you can give every device its own room, with its own memory and continuity, but they all walk in as the same assistant.
Why it matters here: muse-mcp did not invent chat addressing, it exposed a property the platform already had. The same is true in reverse for gadgets: per-device chats, and fleets of devices each backed by their own thread, are a composition of things that already work, waiting on the one field the stock firmware does not send.
What is in the repo, and what is not
The public release, v0.4.6, is the single-host line: 39 tools, the file-backed directory as its store, both transports, the Bridge brief and the registry-sync procedure documented for a stranger to stand up. It was installed cold, from its own README, on a fresh machine before it was published, and the places where the README failed that test were fixed in the README. Where my own deployment has since grown past the public line — a shared store across both hosts, verified provenance for callers — the repo's docs say so as roadmap, plainly, rather than letting the snapshot imply it. The snapshot is what it says it is.
What does not work yet is listed above and stays listed: no unread state exists to expose, pinning has no write path, the filesystem routes wait on an entitlement nobody has documented a route to, and history is a 50-event window. None of that is hidden in the repo either. The tools that are blocked say they are blocked, in their own documentation, in the same words I have used here.
A chat ID is an address. That was the finding. Everything after it — the registry, the Bridge, the dialect, the rooms — was logistics.


Top comments (0)