Quickstart: your first session
This is the shortest complete path from nothing to a working SDK program: start the host, run a turn, answer its permission request, cancel a turn, reload the session in a new process, and shut down. Follow it top to bottom. You do not need to read the SDK source. Every code step carries a TypeScript and a Python tab; pick your language once and the tabs follow you down the page.
The same journey ships as a runnable package per language — the
@muse-code/sdk-quickstart package and its Python twin — each of which runs
against a release-built host on every change to the SDKs and these docs. It
asserts what the protocol is specified to do, so every step below is one a
release-built host satisfies today. If a step ever regresses, the journey
marks it blocked and fails loudly, and this page gains a caveat saying which
step and on what open issue.
What you need
Section titled “What you need”-
Node 20 or newer.
-
The
@muse-code/sdkpackage, published on npm:Terminal window npm install @muse-code/sdkIt has zero runtime dependencies — the only thing it needs at run time is a
musebinary to talk to. TypeScript users also want@types/node, because the SDK’s types name Node’s own (ChildProcess,Readable) on its spawn and stream surfaces.
-
Python 3.10 or newer.
-
The
muse-code-sdkpackage (importmuse_code) and its generated wire-types companionmuse-code-msp(importmuse_code_msp), published on PyPI — install both:Terminal window pip install muse-code-sdk muse-code-mspThe compatibility page’s Python section lists the published wheels. The SDK declares exactly one runtime dependency (an exact-pinned
pydantic), shipspy.typed, and passesmypy --strict— your editor and type checker see the full surface.
- A
musebinary. Its MSP surface is what your program talks to; the MSP concepts guide explains the model behind every call you are about to make. - A configured provider: run
museonce and log in, or point your program’sHOMEat a profile that already has credentials. A host with no credentials fails every turn.
1. Spawn the host
Section titled “1. Spawn the host”Spawning starts your own muse serve child and returns a handshake you have
not sent anything on yet. You cannot send traffic before the handshake
completes — that is enforced by the type, not by convention.
const handshake = spawnMspConnection({ command: museBin, args: ["serve"], cwd: workspaceRoot, onStderr: (chunk) => stderr.push(chunk),});handshake = await spawn_msp_connection( muse_bin, args=("serve",), cwd=workspace_root, on_stderr=stderr.append,)Pass env if you want a specific HOME; the quickstart packages do, so they
never touch your real sessions. If the host exits immediately instead of
answering, the exit code tells you why — see
exit classification. In particular,
code 5 means that build’s experimental SDK tier is switched off and no
invocation of that binary will serve.
2. Complete the handshake
Section titled “2. Complete the handshake”const msp = await handshake.initialize({ clientInfo: { name: "my-app", version: "1.0.0" },});msp.initializeResult tells you who answered: serverInfo, museHome,
sessionDurability, and the protocol schema.fingerprint. The SDK compares
that fingerprint to the one it was built against and sets
msp.fingerprintWarning if they differ. A difference is a warning, never
an error — an older SDK keeps working against a newer host.
msp = await handshake.initialize( {"clientInfo": {"name": "my-app", "version": "1.0.0"}})msp.initialize_result tells you who answered: serverInfo, museHome,
sessionDurability, and the protocol schema.fingerprint. The Python SDK’s
fingerprint posture is deliberately strict: a host serving a schema the
SDK was not built against fails initialize with a typed
MuseHostMismatchError naming the required host version — so if you got
here, they match. The compatibility page
explains the posture and the fix (update either side to a matching pair).
From here on you talk through the connection. Subscribe to notifications right away: the connection holds a single handler — registering a new one replaces it rather than adding a listener — and anything the host pushes before you subscribe is dropped, not buffered.
msp.connection.onNotification((notification) => { /* ... */ });msp.connection.on_notification(lambda notification: ...)3. Start a session
Section titled “3. Start a session”const result = await msp.connection.command( "session/start", { workspaceRoot },);const sessionId = result.session.sessionId;result = await msp.connection.command( "session/start", {"workspaceRoot": workspace_root})session_id = result["session"]["sessionId"]Use command rather than request for anything that changes state. It mints
a commandId for you and remembers it, so a retry after a dropped reply
joins the original command instead of starting a second one — the
commands and idempotency guide
explains why that matters. result.viewCursor is your position in the
session’s event stream.
The host also pushes a session/started notification with the same session —
it can land in the same read as the command’s reply, which is why you
subscribed back in step 2.
4. Send a prompt and read the answer as it streams
Section titled “4. Send a prompt and read the answer as it streams”const ack = await msp.connection.command( "turn/start", { sessionId, input: [{ type: "text", text: "Reply with the single word: hello" }] },);// ack.status === "accepted", ack.turnIdack = await msp.connection.command( "turn/start", { "sessionId": session_id, "input": [{"type": "text", "text": "Reply with the single word: hello"}], },)# ack["status"] == "accepted"; ack["turnId"]The ack means the turn was admitted, not finished. Its disposition is
"started" for a fresh turn or "queued" when the session has not yet
settled to idle — both are admitted; turn/started still fires for that
turnId. Like terminal below, disposition is an open set —
status: "accepted" alone means the turn was admitted. The answer arrives
as notifications:
| notification | what it means |
|---|---|
turn/started |
the turn is running |
item/started |
a new item (e.g. the agent’s message) has begun |
item/delta |
more text for that item |
item/completed |
that item is final |
turn/completed |
the turn is over; terminal says how it ended |
Accumulate item/delta for live output, and treat the item/completed text
as authoritative. turn/completed.terminal says how the turn ended:
"completed", "cancelled" or "failed" today, but it is an open set —
treat any unknown value as terminal. On "failed" the notification carries
reason and error. A reason like not logged in means the host has no
credentials — see What you need.
5. Answer a permission request
Section titled “5. Answer a permission request”When the agent wants to do something that needs your consent, the host
pushes approval/requested.
Answer it:
const decided = await msp.connection.command( "approval/decide", { sessionId, approvalId, requirementId: { approvalId, sourceIndex: 0 }, choiceId: "allow_once", feedback: null, },);// decided.terminal === truedecided = await msp.connection.command( "approval/decide", { "sessionId": session_id, "approvalId": approval_id, "requirementId": {"approvalId": approval_id, "sourceIndex": 0}, "choiceId": "allow_once", "feedback": None, },)# decided["terminal"] is Trueapproval/resolved follows, naming the same approvalId. The turn then
continues. The approvals guide covers the full
requirement and choice model.
6. Cancel a running turn
Section titled “6. Cancel a running turn”await msp.connection.command("turn/cancel", { sessionId, turnId });await msp.connection.command( "turn/cancel", {"sessionId": session_id, "turnId": turn_id})Cancellation is not instant and it is not an error. Wait for
turn/completed; its terminal will be "cancelled".
7. Shut the host down cleanly
Section titled “7. Shut the host down cleanly”const exit = await msp.close(); // { code: 0, signal: null }exit_ = await msp.close() # ProcessExit(code=0, signal=None)Closing sends stdin EOF, and the host drains and exits. The session’s writer lease is released with it — which is what lets a later process load the same session.
8. Reload the session in a new process
Section titled “8. Reload the session in a new process”Spawn and handshake exactly as before, then:
const resumed = await msp.connection.command( "session/resume", { sessionId, excludeItems: false },);resumed = await msp.connection.command( "session/resume", {"sessionId": session_id, "excludeItems": False})You get back:
resumed.session— the samesessionId, itsworkspaceRoot,status,turnCount,providerIdandmodelId.resumed.viewCursor— where the session now is.resumed.history— the conversation so far.history.modesays what was actually served: for a session this small it is{ mode: "inline", items: [...] }; long or compacted sessions get a snapshot withitems: nullinstead — checkmodebefore readingitems(see the resume guide).resumed.pendingRequests— approvals and questions still waiting for you.
Pass cursor instead to resume from a specific point. The
resume guide explains the cursor and history
contract.
9. Shut down again
Section titled “9. Shut down again”Same close. Same clean exit.
Where to go next
Section titled “Where to go next”- MSP concepts — sessions, turns, approvals, resume, and exit codes as one mental model.
- MSP wire protocol — the bytes underneath, if you are building a client without the SDK.
- The generated MSP reference — one page
per method, notification, error and type used above — plus the
TypeScript SDK reference (every
@muse-code/sdkexport) and the Python SDK reference (everymuse_codefacade export; the raw connection helpers above are the MSP wire surface — the MSP reference documents the method and notification names they carry).