TypeScript SDK
@wakeshift/sdk for Node 22 or later. Its peer dependencies are @modelcontextprotocol/sdk (1.23 or later) and zod (3.25 or later, or 4). The Go and Python SDKs have the same calls. Import z from "zod", not "zod/v4": wake.args is built from "zod", and the MCP SDK refuses a schema that mixes the two. During the beta it comes with your invite as a tarball; it isn't on npm. Install the tarball, not the source directory:
npm install ./wakeshift-sdk-0.1.0.tgz @modelcontextprotocol/sdk zodnew Wakeshift
new Wakeshift({ key, core?, mcpUrl?, fetch?, timeoutMs?, onWarning? })| Option | What it is |
|---|---|
key |
Your app's key, wst_test_<app>_<seed>. It's a private key and never goes over the wire. Each request carries a fresh assertion signed with it, valid for two minutes. |
core |
The address of the console the key came from, such as https://app.wakeshift.ai. Default: WAKESHIFT_CORE from the environment. A test key needs one of them, and the constructor throws without; a live key falls back to https://app.wakeshift.ai. |
mcpUrl |
Your MCP server's URL, for apps whose users sign in with Wakeshift: the tokens they bring are for it. |
fetch, timeoutMs |
The fetch to use, and a timeout for each request (default 20 seconds). |
onWarning |
Called once for each kind of problem that doesn't fail a call, such as a tool call Wakeshift recognised no agent in. Default: console.warn. |
wake.appId, wake.mode ("test" or "live") and wake.core tell you which app and Wakeshift the key is for.
Events
| Call | What it does |
|---|---|
wake.defineEvent(name, { description, args, payload, text, authorize?, personal?, title? }) |
Defines one of your app's events and returns it. args and payload are zod objects; text(payload, args) is the line the agent reads first; authorize(subscriber, args) decides who may subscribe, where subscriber is { user?, client, native }. See events. |
wake.serveEvents(server, { user?, agent? }) |
Serves your events on an McpServer: MCP Events requests for clients that speak it, and the tools subscribe_events, unsubscribe_events and list_subscriptions. user(extra) is your ID for the person; by default, the guest GuestAuth signed in. agent(extra) is the signed-in agent, for apps on Wakeshift accounts. Call it once per server, after the last defineEvent, before connecting. |
event.emit(args, payload, { id?, to? }) |
Sends an occurrence to everyone subscribed with those routing arguments, or, for a personal event, to the subscriber to names. Returns { matched, duplicate }. |
event.subscribe(extra, args, { server or client, user? or agent?, wakeArgs? }) |
Subscribes the conversation calling one of your own tools. Returns { id, created, session, agent, result(text) }. |
event.unsubscribe(extra, args, { server or client, user? or agent?, wakeArgs? }) |
Ends the calling conversation's subscription with those arguments. true if there was one. |
event.publishUrl(args, { to? }) |
A URL that emits the event with those arguments: each POST's body is the payload, checked against the schema. Returns { id, event, url }; wake.revokePublishUrl(id) turns it off. See emit from a webhook. |
event.end(user, id) |
Ends one of a user's subscriptions by its ID. true if there was one. |
serveEvents publishes your events in your app's manifest the first time it's needed; publishManifest includes them too.
Your tools stay yours
The SDK changes only what you hand it. serveEvents adds its three tools and nothing else. A tool of yours gets Wakeshift's arguments only if you spread ...wake.args into its input, and a result gets Wakeshift's text and _meta only from sub.result(…) or wake.annotate(call, result). Every other tool, and every other result, is exactly as you wrote it.
The caller
For your own tools that subscribe (event.subscribe) and for topics.
| Call | What it does |
|---|---|
wake.call(extra, { server, user?, args?, session?, harness?, conversation?, client? }) |
Packs a tool call from your tool callback's extra: its _meta (Codex sends its thread there), the request's User-Agent, the client's name (from server, your McpServer, or pass client), user, and session_key, harness and conversation_id from args. |
wake.callOf({ meta, clientName, user?, session?, userAgent?, harness?, conversation? }) |
The same, without an MCP framework. |
wake.args |
A zod shape for the inputSchema of a tool of yours that subscribes (and any other tool whose results you annotate), with three optional strings the agent fills in. |
The arguments wake.args adds (SESSION_ARG, HARNESS_ARG and CONVERSATION_ARG name them):
| Argument | What the agent is told | Why |
|---|---|---|
session_key |
"The session key an earlier result gave you, if it did. It's a secret: keep it to this conversation; don't share or log it" | Claude Code and some others send no conversation ID, so the result gives the agent a key to pass back. Without it, calling again makes a second session. |
harness |
"The program you're running in: codex, claude-code, cursor (local CLI or IDE), cursor-cloud (Cursor cloud agent), grok-bot, copilot-cli, opencode, antigravity, openclaw (even when it runs your turns in Codex), hermes, chatgpt, or your program's name if it isn't listed" | Some programs' calls look the same (Grok Bot's and Cursor's cloud agents' are Cursor's), so the agent says which it is, and gets the instructions that work there. |
conversation_id |
"Your own ID for this conversation, if your program gives you one. Cursor and Grok Bot: the output of printenv CURSOR_CONVERSATION_ID. Codex and Antigravity send it automatically; others leave it empty" |
Cursor tells MCP servers nothing about the conversation. |
Pass all three on unchanged. They're hints, not credentials: harness only picks between programs whose calls look alike, or names one Wakeshift didn't recognise. One that contradicts the client name (a Cursor client saying claude-code) is ignored.
user is your own ID for the person. Their conversations are grouped, and u/<user>/… topics are theirs. See user IDs for what it can contain.
The client's name comes from the server's record of the client's initialize. On a server created per request (stateless), there's none. Stateless servers work for Claude Code, Codex and Antigravity, which Wakeshift recognises from _meta, and for opencode, Cursor and GitHub Copilot CLI, from the User-Agent. For any other client, keep one server per MCP session, or pass client yourself. sub.agent says what Wakeshift recognised: { harness, via, claimed }, where via is "client", "meta", "user_agent" or "claim" (only the agent's harness named it), and claimed is what the agent said (normalized, or null). A claimed that differs from harness was ignored. The SDK warns once, through onWarning, when Wakeshift recognised nothing and the agent didn't say.
Topic calls
Topics are what events are built on.
| Call | Returns |
|---|---|
subscribe(call, topic, { filter?, once?, replay?, ttl? }) |
{ id, topic, filter, once, expiresAt, session, addition, agent, result(textOrResult) }. result returns your text (or CallToolResult) followed by Wakeshift's, with its _meta. See subscribe options. |
unsubscribe(call, topic?) |
How many subscriptions ended. Without topic, all of your app's topics. |
publish(topics, { text, data?, id? }) |
{ matched, duplicate }. One topic or up to 20. text at most 1000 bytes, data a JSON object within the 64 KB request limit. The same id within a day publishes once (duplicate: true); after a day, it publishes again. |
send(subscriptionId, event) |
"delivered", "queued" or "closed". Ignores the filter. |
topicStatus(topic) |
{ topic, subscriptions, connected, via: { stream, poll, hooks, webhook } }. Only reads. |
subscriptionStatus(subscriptionId) |
{ id, topic, status: "active" | "closed", connected, via?, expiresAt? }. Only reads. |
publishUrl(topic) |
{ id, topic, url }. See publish URLs. |
revokePublishUrl(id) |
The URL, a topic's or an event's, answers 410 from then on. |
annotate(call, result) |
A copy of result, with what Wakeshift adds for this conversation: what's waiting for an agent that can't be woken, or what an agent needs to reconnect. Usually nothing. Only results you pass here change. addition(call) returns the raw { text?, meta?, agent }. |
closeTopic(topic) |
How many subscriptions ended. Each conversation is told. |
endSubscription(subscriptionId) |
true if it ended the subscription, and its conversation is told; false if it had already ended or isn't your app's. |
unsubscribeUser(user, topic?) |
Ends a user's subscriptions to topic, or to all your app's topics. Each conversation is told. |
forgetUser(user) |
Ends the user's subscriptions and deletes what Wakeshift keeps for them. true if there was something to delete, false after that. See forgetting a user. |
Durations (replay, ttl) are seconds, or strings like "90s", "5m", "2h" and "30d" (days are whole numbers).
Sign-in
new GuestAuth({ wake, mcpUrl, … }) |
Signs MCP clients in as guests, with no user step. handle(req, res) serves the OAuth endpoints, authenticate(req, res) checks a request's token, and guestOf(extra) is the guest in a tool call. See guest sign-in. |
new WakeshiftSignIn({ wake, redirectUri }) |
Sends a person to sign in with Wakeshift (start) and returns their Wakeshift user ID (finish). Needs a live key. See linking a person's Wakeshift account. |
wake.verifyAccessToken(token), wake.exchange(token) |
For apps on Wakeshift accounts: checks a bearer token is Wakeshift's, for your mcpUrl, and returns its claims; exchange returns the Agent it signs in, to act for within the person's consent. wake.protectedResource() and wake.challenge() are the metadata and WWW-Authenticate header MCP clients need to find Wakeshift. |
publishManifest(manifest) |
Tells Wakeshift what your app may do: its agent scope, and the redirect URIs Wakeshift sign-in returns to. |
Errors
API errors throw WakeshiftError, with status and code. They're the HTTP API's, plus unreachable:
| Status | Code | When |
|---|---|---|
400 |
bad_request |
Something in the request is invalid, including a field the API doesn't take. The message says what. |
401 |
unauthenticated |
The assertion was refused: expired, reused, signed with another key, or for another aud (a key for another Wakeshift). The message says which. |
403 |
private_topic |
Subscribing a caller to another user's u/<user>/… topic. |
403 |
other_client |
The session key is a conversation in another MCP client of the same user. Call again without it. |
403 |
not_headless |
Topics need an app whose users are its own. |
404 |
unknown_subscription |
The subscription ID isn't one of your app's. |
404 |
unknown_publish_url |
The publish URL ID isn't one of your app's. |
413 |
too_large |
The request body is over 64 KB. The limit is on the whole body, so data must leave room for the rest of the request. |
0 |
unreachable |
No answer at all: a wrong core, a network problem, or the timeout. The message names the core it tried. |
isCode(err, code) checks one.
try {
await wake.subscribe(call, topic)
} catch (err) {
if (isCode(err, "private_topic")) return { content: [{ type: "text", text: "That topic isn't yours." }] }
throw err
}