@wakeshift/sdk 0.6.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,29 @@
2
2
 
3
3
  What changed in each release of `@wakeshift/sdk`. 0.4.0 is the first published release: to start with it, read the [README](README.md). Its notes below, and the earlier releases', are for apps that ran the preview builds.
4
4
 
5
+ ## 0.7.1 (2026-11-01)
6
+
7
+ ### Fixed
8
+
9
+ - **Subscribing again after unsubscribing keeps the conversation's session.** The reply from the built-in `unsubscribe_events` tool now tells the agent its session key stays the same and to pass it when it subscribes again. Without it, Claude Code often subscribed again without the key, which started a second session and, on the MCP task path, a second `wakeshift_wait`.
10
+
11
+ ### Changed
12
+
13
+ - The README and docs lead with `@modelcontextprotocol/server` 2.x; 1.x is the alternative. Nothing changes in the code.
14
+
15
+ ## 0.7.0 (2026-10-31)
16
+
17
+ ### Added
18
+
19
+ - **Mastra.** `@wakeshift/sdk/mastra` serves Wakeshift from a Mastra `MCPServer` (`@mastra/mcp` 2.7 or later): `attachMastra(wake, opts)`, then `attached.inputs` and `attached.events` for the Mastra tools that subscribe, and `attached.serve(new MCPServer({ … }))`. Everything `attach` gives an `McpServer` works there: Wakeshift's block (text and `_meta`) in your wrapped tools' results, the built-in tools, MCP Events, Claude Code's `wakeshift_wait`, and who's calling (`identify` gets the request's `ctx`). Mastra's own MCP Events keep working beside Wakeshift's: `events/list` lists both. See [Mastra](README.md#mastra).
20
+ - **xmcp.** `@wakeshift/sdk/xmcp` brings Wakeshift into an xmcp app through its tool files: `attachXmcp(wake, opts)`, then `attached.inputs` and `attached.events` in the tools that subscribe, and `attached.tool("subscribe_events")` (and the other two) for a tool file each. xmcp offers no way to add MCP methods, so clients that subscribe natively (ChatGPT) and Claude Code's `wakeshift_wait` aren't served there; Claude Code and every other agent are woken their usual ways. See [xmcp](README.md#xmcp).
21
+ - Both are subpath exports, so an app that uses neither loads neither, and `@mastra/mcp` and `xmcp` are optional peer dependencies.
22
+
23
+ ### Changed
24
+
25
+ - **A smaller package.** It no longer ships source maps (`dist/*.js.map`), which carried the SDK's TypeScript source inline: about half the unpacked size. Stack traces point at the compiled JavaScript in `dist/`, which is readable as it is.
26
+ - **A clearer error from `attach`.** Given something other than an `McpServer`, `attach` says what it got and what it takes, and, for a framework it recognizes (Mastra's `MCPServer`, punkpeye's `fastmcp`, a low-level `Server`, a Cloudflare `McpAgent`), what to do instead (for Mastra, `attachMastra`), with a link to [the frameworks Wakeshift works with](https://wakeshift.ai/docs/frameworks). It says to call `attach` before connecting only when the server is already connected.
27
+
5
28
  ## 0.6.0 (2026-10-31)
6
29
 
7
30
  ### Breaking
@@ -10,7 +33,7 @@ What changed in each release of `@wakeshift/sdk`. 0.4.0 is the first published r
10
33
 
11
34
  ### Added
12
35
 
13
- - **`@modelcontextprotocol/server` 2.x.** `attach` now takes an `McpServer` from the MCP TypeScript SDK 2.x (2.0 or later) as well as from `@modelcontextprotocol/sdk` 1.x, and tells them apart by the server you pass. Everything works the same on both: wrapped tools (Wakeshift's block in text and in `structuredContent`, rollback, `call.subscribe`), the built-in tools, MCP Events (`events/list`, `events/subscribe`, `events/unsubscribe`, `events/poll`), Claude Code's `wakeshift_wait` with `tasks/get` and `tasks/cancel`, and who's calling. 2.x speaks protocol 2026-07-28 itself, so with `createMcpHandler` clients on either revision connect with nothing more to set up. See [Using @modelcontextprotocol/server 2.x](README.md#using-modelcontextprotocolserver-2x).
36
+ - **`@modelcontextprotocol/server` 2.x.** `attach` now takes an `McpServer` from the MCP TypeScript SDK 2.x (2.0 or later) as well as from `@modelcontextprotocol/sdk` 1.x, and tells them apart by the server you pass. Everything works the same on both: wrapped tools (Wakeshift's block in text and in `structuredContent`, rollback, `call.subscribe`), the built-in tools, MCP Events (`events/list`, `events/subscribe`, `events/unsubscribe`, `events/poll`), Claude Code's `wakeshift_wait` with `tasks/get` and `tasks/cancel`, and who's calling. 2.x speaks protocol 2026-07-28 itself, so with `createMcpHandler` clients on either revision connect with nothing more to set up. See the [README](README.md).
14
37
  - On 2.x, `identify`, `events.native` and a wrapped tool's `call.extra` get 2.x's request context (`ctx`: the token in `ctx.http?.authInfo`), and are typed as it. On 1.x they get `extra`, as before.
15
38
  - `attached.inputs` and `attached.output` take a zod object as well as a shape (`attached.inputs(z.object({ table: z.string() }))`), as 2.x's `registerTool` does. Shapes work as before.
16
39
 
@@ -32,6 +55,8 @@ What changed in each release of `@wakeshift/sdk`. 0.4.0 is the first published r
32
55
 
33
56
  ## 0.5.1 (2026-10-30)
34
57
 
58
+ Never published to npm. Everything below first shipped in 0.5.2.
59
+
35
60
  ### Added
36
61
 
37
62
  - **Clients that speak MCP Events can poll.** `attach` now answers `events/poll`, and `events/list` offers `poll` beside `webhook` for every event. A client that can't take webhooks (an agent with no public URL, or Mastra in its poll mode) subscribes with its first poll, then each poll returns what happened since its cursor, in order. A poll's cursor acknowledges everything before it, so an event isn't lost when a poll fails: the next poll returns it again. Your `authorize` runs on every poll. Wakeshift keeps the subscription while the client polls (it lapses after 10 minutes without a poll), lists it with the user's other subscriptions (`list_subscriptions` says "polled by an MCP client"), and answers the client's next poll with `Forbidden` once the user stops it. Nothing to change in your code.
package/README.md CHANGED
@@ -2,15 +2,15 @@
2
2
 
3
3
  Let agents subscribe to things in your app. You define the events your app offers, serve them on your MCP server, and emit them when they happen; Wakeshift wakes every subscribed agent, whether it's Claude Code, Codex, ChatGPT or another. Your events follow MCP Events: clients that speak it subscribe natively, and Wakeshift bridges every other agent. You don't deal with harnesses.
4
4
 
5
- Node 22 or later. The package is ESM-only: load it with `import`, from an ES module (`"type": "module"`, or a `.mjs` or `.mts` file). It has no CommonJS build, so `require("@wakeshift/sdk")` works only where Node can require ES modules. It works with either MCP TypeScript SDK: `@modelcontextprotocol/sdk` 1.x (1.23 or later: the first that takes zod 4) or `@modelcontextprotocol/server` 2.x ([below](#using-modelcontextprotocolserver-2x)). Its peer dependencies are the one you use and `zod` (3.25 or later, or 4; 4.2 or later with 2.x). Import `z` from `"zod"`, not `"zod/v4"`: the SDK's schemas are built from `"zod"`, and the MCP SDK refuses a schema that mixes the two.
5
+ Node 22 or later. The package is ESM-only: load it with `import`, from an ES module (`"type": "module"`, or a `.mjs` or `.mts` file). It has no CommonJS build, so `require("@wakeshift/sdk")` works only where Node can require ES modules. It works with either MCP TypeScript SDK: `@modelcontextprotocol/server` 2.x (2.0 or later), which this page uses, or `@modelcontextprotocol/sdk` 1.x (1.23 or later: the first that takes zod 4; [below](#using-modelcontextprotocolsdk-1x)). Its peer dependencies are the one you use and `zod`: 4.2 or later with 2.x, 3.25 or later with 1.x. Import `z` from `"zod"`, not `"zod/v4"`: the SDK's schemas are built from `"zod"`, and the MCP SDK refuses a schema that mixes the two.
6
6
 
7
7
  ## Install
8
8
 
9
9
  ```sh
10
- npm install @wakeshift/sdk @modelcontextprotocol/sdk zod
10
+ npm install @wakeshift/sdk @modelcontextprotocol/server @modelcontextprotocol/node zod
11
11
  ```
12
12
 
13
- On `@modelcontextprotocol/server` 2.x, install that instead of `@modelcontextprotocol/sdk`.
13
+ `@modelcontextprotocol/node` serves your MCP server on Node's `http`. On `@modelcontextprotocol/sdk` 1.x, install `@wakeshift/sdk @modelcontextprotocol/sdk zod` instead ([below](#using-modelcontextprotocolsdk-1x)).
14
14
 
15
15
  Install the package, not a checkout of this repository (a `file:` dependency): a `file:` dependency links the directory along with its `node_modules`, so the SDK can load its own development copy of zod instead of yours, and the MCP SDK then fails with "Mixed Zod versions detected in object shape". To try a change of your own, `npm pack` and install the tarball it writes.
16
16
 
@@ -19,7 +19,9 @@ Install the package, not a checkout of this repository (a `file:` dependency): a
19
19
  You have an MCP server and a GitHub webhook handler. Register an app in the Wakeshift console (Build), and put its key in `WAKESHIFT_KEY` (`wst_` and 50 characters). The SDK talks to `https://app.wakeshift.ai` unless `WAKESHIFT_CORE` names another address. Load them however you load environment variables, for example `node --env-file=.env server.ts` (Node 22.18 or later runs a `.ts` file directly).
20
20
 
21
21
  ```ts
22
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
22
+ import { createServer } from "node:http"
23
+ import { McpServer, createMcpHandler } from "@modelcontextprotocol/server"
24
+ import { toNodeHandler } from "@modelcontextprotocol/node"
23
25
  import { z } from "zod"
24
26
  import { Wakeshift } from "@wakeshift/sdk"
25
27
 
@@ -33,10 +35,15 @@ const ciFinished = wake.defineEvent("ci.finished", {
33
35
  text: (run, { owner, repo }) => `CI ${run.conclusion} on ${owner}/${repo} ${run.branch}: ${run.name}`,
34
36
  })
35
37
 
36
- // One McpServer per MCP session; serve the events before connecting it.
37
- const server = new McpServer({ name: "ci-bot", version: "1.0.0" })
38
- // myUser: your own sign-in's ID for the person behind the request.
39
- wake.attach(server, { identify: (extra) => ({ user: myUser(extra) }) })
38
+ // createMcpHandler makes a server for each request: attach in the function that makes it.
39
+ const mcp = createMcpHandler(() => {
40
+ const server = new McpServer({ name: "ci-bot", version: "1.0.0" })
41
+ // myUser: your own sign-in's ID for the person behind the request.
42
+ wake.attach(server, { identify: (ctx) => ({ user: myUser(ctx.http?.authInfo) }) })
43
+ return server
44
+ })
45
+
46
+ createServer(toNodeHandler(mcp)).listen(3000)
40
47
  ```
41
48
 
42
49
  ```ts
@@ -56,14 +63,15 @@ That's all. An agent subscribes with the `subscribe_events` tool `attach` adds (
56
63
 
57
64
  - `owner` and `repo` are required, so they route occurrences; their values are lowercase IDs (`a-z 0-9 . _ -`). The optional `branch` filters on the payload's `branch`.
58
65
  - `attach` adds `subscribe_events`, `unsubscribe_events` and `list_subscriptions`, and touches no tool of yours you don't wrap (below).
59
- - Native subscriptions (ChatGPT) and personal events need a signed-in user: your app signs its users in and passes `identify` to `attach`.
66
+ - Native subscriptions (ChatGPT) and personal events need a signed-in user: your app signs its users in and passes `identify` to `attach`. `identify` gets the request's context, `ctx`: the verified token is `ctx.http?.authInfo` (`toNodeHandler` passes on the `req.auth` your auth middleware sets), and the HTTP request is `ctx.http?.req`.
67
+ - `createMcpHandler` answers clients on MCP protocol 2026-07-28 and on older revisions, so every client connects with nothing more to set up. Wakeshift lists its tools for each request's client, so `tools/list` is cached by that client only, whatever cache hint you set for it.
60
68
 
61
69
  ## Events
62
70
 
63
71
  Events follow MCP Events (SEP-3415): you define the event types your app offers, serve them, and emit occurrences. Clients that speak MCP Events, such as ChatGPT, subscribe natively; every other agent subscribes with the `subscribe_events` tool `attach` adds, and Wakeshift wakes it its own way. You write no subscribe tool and handle no harnesses.
64
72
 
65
73
  ```ts
66
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
74
+ import { McpServer } from "@modelcontextprotocol/server"
67
75
  import { z } from "zod"
68
76
  import { Wakeshift } from "@wakeshift/sdk"
69
77
 
@@ -86,10 +94,10 @@ const turn = wake.defineEvent("game.turn", {
86
94
  personal: true,
87
95
  })
88
96
 
89
- // One server per connection; attach once, before connecting it.
97
+ // In the function that makes each server (createMcpHandler's, above), attach first.
90
98
  const server = new McpServer({ name: "chat", version: "1.0.0" })
91
99
  // myUser: your own sign-in's ID for the person behind the request.
92
- const attached = wake.attach(server, { identify: (extra) => ({ user: myUser(extra) }) })
100
+ const attached = wake.attach(server, { identify: (ctx) => ({ user: myUser(ctx.http?.authInfo) }) })
93
101
 
94
102
  // When something happens:
95
103
  await message.emit({ channel: "eng" }, { from: "ada", text: "shipped it" })
@@ -98,7 +106,7 @@ await turn.emit({ table: "juniper" }, { hole: ["As", "Kd"], toCall: 2 }, { to: "
98
106
 
99
107
  - **`wake.defineEvent(name, { description, args, payload, text, authorize?, personal?, deleteOnDelivery? })`** returns the event. `args` is what a subscriber picks; its required string, integer and boolean fields route occurrences, so their values are lowercase IDs (a-z 0-9 . _ -), and an optional one filters on the payload field of the same name. `authorize(subscriber, args)` decides who may subscribe. With `deleteOnDelivery: true`, Wakeshift deletes each occurrence's text and payload once it's delivered. Define every event before `attach`, which throws with none.
100
108
  - **`event.emit(args, payload, { id?, to? })`** checks the payload against its schema and sends it to everyone subscribed with those arguments. A personal event goes to its recipients, `to`: users (the `user` attach's `identify` gives for them), `{ user, subscription }` (exactly one conversation's subscription, the `id` `call.subscribe` returned) and `{ native: user }` (only that user's own MCP Events subscriptions), mixable, up to 1,000. It returns `{ matched, duplicate, unmatched, ended }`: `matched` counts the conversations woken, `unmatched` the users with nothing subscribed, `ended` the subscriptions no longer live (forget them). `id` makes retries emit once; a retry must name the same recipients.
101
- - **`wake.attach(server, { identify?, tools?, events? })`** attaches Wakeshift to your MCP server and returns the handle your own tools use (below). It serves `events/list`, `events/subscribe` and `events/unsubscribe` to clients that speak MCP Events, and adds `subscribe_events` (for everyone else), `unsubscribe_events` and `list_subscriptions` (for every client, so a person can always see and stop what they're subscribed to); `tools` names fewer of them (`BUILTIN_TOOLS`; `[]` for none). `identify(extra)` says who is behind a request, as `{ user, client? }`: `user` is your app's ID for the person, from your own sign-in; without it, requests have no user, so personal events and `list_subscriptions` refuse. On a stateless server, which never sees `initialize`, `client` is the MCP client's name as your sign-in knows it (an OAuth registration's `client_name`, say): Wakeshift reads it to tell which program the agent runs in. A name the request carries wins. On a stateless server it also serves `wakeshift_wait` to clients that take MCP tasks (a recent Claude Code in the terminal): Wakeshift tells those conversations to call it, and it wakes them when your events arrive, with nothing to install; other clients don't see it. It also sends your app's manifest, with your events, the first time they're needed: it adds them and never removes one. A separate process that only emits never sends it.
109
+ - **`wake.attach(server, { identify?, tools?, events? })`** attaches Wakeshift to your MCP server and returns the handle your own tools use (below). It serves `events/list`, `events/subscribe` and `events/unsubscribe` to clients that speak MCP Events, and adds `subscribe_events` (for everyone else), `unsubscribe_events` and `list_subscriptions` (for every client, so a person can always see and stop what they're subscribed to); `tools` names fewer of them (`BUILTIN_TOOLS`; `[]` for none). `identify(ctx)` says who is behind a request, as `{ user, client? }`: `user` is your app's ID for the person, from your own sign-in; without it, requests have no user, so personal events and `list_subscriptions` refuse. On a stateless server, which never sees `initialize` (`createMcpHandler`'s is one), `client` is the MCP client's name as your sign-in knows it (an OAuth registration's `client_name`, say): Wakeshift reads it to tell which program the agent runs in. A name the request carries wins. On a stateless server it also serves `wakeshift_wait` to clients that take MCP tasks (a recent Claude Code in the terminal): Wakeshift tells those conversations to call it, and it wakes them when your events arrive, with nothing to install; other clients don't see it. It also sends your app's manifest, with your events, the first time they're needed: it adds them and never removes one. A separate process that only emits never sends it.
102
110
  - **`event.end(user, id)`** ends one of a user's subscriptions by its ID, outside a tool (a player who timed out, a member removed).
103
111
  - **`event.status(args, { to? })`** counts the conversations subscribed with those arguments (a personal event: one user's), how many are connected now, and how: `{ subscriptions, connected, via: { stream, poll, hooks, webhook } }`. **`event.statusOf([{ user, subscription }, …])`** reads up to 1,000 subscriptions by ID: `[{ id, user, live, connected, via? }]`. Both only read.
104
112
  - **`event.publishUrl(args, { to? })`** returns `{ id, event, url }`, a capability URL that emits the event with those arguments (a personal event: to one user) for services that only send webhooks. A POST's JSON body is the payload, checked against the event's schema. `wake.revokePublishUrl(id)` stops it.
@@ -111,7 +119,7 @@ A tool like "join" subscribes the conversation calling it: wrap its callback wit
111
119
  // attached: what wake.attach(server, …) returned, above.
112
120
  server.registerTool(
113
121
  "join",
114
- { description: "Take a seat at a table", inputSchema: attached.inputs({ table: z.string() }) },
122
+ { description: "Take a seat at a table", inputSchema: attached.inputs(z.object({ table: z.string() })) },
115
123
  attached.events("join", async ({ table }, call) => {
116
124
  const sub = await call.subscribe(turn, { table }, { reason: `Playing at ${table}` })
117
125
  // Keep sub.user and sub.id to reach this conversation later:
@@ -121,21 +129,21 @@ server.registerTool(
121
129
  )
122
130
  ```
123
131
 
124
- - **`attached.inputs(shape)`** is `shape` plus Wakeshift's three optional arguments, `session_key`, `harness` and `conversation_id`. A tool wrapped with `attached.events` needs them: without them it doesn't compile. An argument a tool doesn't declare is ignored, not refused.
132
+ - **`attached.inputs(schema)`** is your zod object plus Wakeshift's three optional arguments, `session_key`, `harness` and `conversation_id`. A tool wrapped with `attached.events` needs them: without them it doesn't compile. An argument a tool doesn't declare is ignored, not refused.
125
133
  - **`attached.events(name, handler)`** wraps the callback of the tool registered as `name` (the MCP SDK doesn't tell a callback its tool's name; the first call checks it). The handler gets the tool's own arguments and a `call`, and returns a string or a `CallToolResult`.
126
- - **`call`**: `user` (from `identify`), `tool`, `native` and `nativeConversation`, `session` (the `session_key` passed; after a subscribe, the conversation's session), `recognized` (which program Wakeshift recognized the agent as, `null` before a subscribe) and `extra` (MCP's own). It works until the handler returns.
134
+ - **`call`**: `user` (from `identify`), `tool`, `native` and `nativeConversation`, `session` (the `session_key` passed; after a subscribe, the conversation's session), `recognized` (which program Wakeshift recognized the agent as, `null` before a subscribe) and `extra` (the request's context, `ctx`). It works until the handler returns.
127
135
  - **`await call.subscribe(event, args, { ttl?, reason?, label? })`** subscribes the calling conversation, or renews its subscription, and resolves to `{ id, user, created, session, recognized, native }`. `reason` is shown in your console. Your tool decides who may (`authorize` isn't consulted). **`await call.unsubscribe(event, args)`** ends it; **`await call.resolveSession()`** finds or starts the conversation's session without subscribing it.
128
136
  - **A failed call**: one that throws, returns `isError`, returns output its `outputSchema` refuses, or is canceled, ends every subscription it made, and its reply gets nothing from Wakeshift.
129
- - **Clients that subscribe themselves** (MCP Events: ChatGPT and others) aren't subscribed: `call.native` is true, and the reply tells them which event to subscribe to. `attach`'s `events.native(extra)` tells the SDK about callers it can't recognize, such as your own agents.
130
- - **Structured output**: some hosts show the model only `structuredContent`. A wrapped tool's `structuredContent` carries what Wakeshift adds in its `wakeshift` field too, and the tool's listed output schema names that field. Make the `outputSchema` with `attached.output(shape)` to declare it in your own schema and types; it's optional, but needed with a schema that refuses other fields (`z.strictObject`).
137
+ - **Clients that subscribe themselves** (MCP Events: ChatGPT and others) aren't subscribed: `call.native` is true, and the reply tells them which event to subscribe to. `attach`'s `events.native(ctx)` tells the SDK about callers it can't recognize, such as your own agents.
138
+ - **Structured output**: some hosts show the model only `structuredContent`. A wrapped tool's `structuredContent` carries what Wakeshift adds in its `wakeshift` field too, and the tool's listed output schema names that field. Make the `outputSchema` with `attached.output(schema)` to declare it in your own schema and types; it's optional, but needed with a schema that refuses other fields (`z.strictObject`).
131
139
 
132
140
  ```ts
133
141
  server.registerTool(
134
142
  "look",
135
143
  {
136
144
  description: "Look at a table",
137
- inputSchema: attached.inputs({ table: z.string() }),
138
- outputSchema: attached.output({ seats: z.number() }),
145
+ inputSchema: attached.inputs(z.object({ table: z.string() })),
146
+ outputSchema: attached.output(z.object({ seats: z.number() })),
139
147
  },
140
148
  attached.events("look", async ({ table }) => ({
141
149
  content: [{ type: "text", text: `3 seats at ${table}.` }],
@@ -145,7 +153,7 @@ server.registerTool(
145
153
 
146
154
  server.registerTool(
147
155
  "leave",
148
- { description: "Leave a table", inputSchema: attached.inputs({ table: z.string() }) },
156
+ { description: "Leave a table", inputSchema: attached.inputs(z.object({ table: z.string() })) },
149
157
  attached.events("leave", async ({ table }, call) => {
150
158
  const left = await call.unsubscribe(turn, { table })
151
159
  return left ? `You left ${table}.` : `You weren't at ${table}.`
@@ -155,20 +163,16 @@ server.registerTool(
155
163
 
156
164
  A wrapped tool that subscribes to nothing, like `look`, still gets what Wakeshift has for the conversation calling it: occurrences held for an agent Wakeshift can't wake, or what it needs to reconnect. Usually that's nothing.
157
165
 
158
- ## Using @modelcontextprotocol/server 2.x
166
+ ## Using @modelcontextprotocol/sdk 1.x
159
167
 
160
- `attach` takes an `McpServer` from either MCP TypeScript SDK: `@modelcontextprotocol/sdk` 1.x or `@modelcontextprotocol/server` 2.x (2.0 or later). It tells them apart by the server you pass, and everything on this page works the same on both.
168
+ `attach` takes an `McpServer` from either MCP TypeScript SDK and tells them apart by the server you pass, so everything on this page works the same on `@modelcontextprotocol/sdk` 1.x (1.23 or later), with zod 3.25 or later.
161
169
 
162
170
  ```sh
163
- npm install @wakeshift/sdk @modelcontextprotocol/server @modelcontextprotocol/node zod
171
+ npm install @wakeshift/sdk @modelcontextprotocol/sdk zod
164
172
  ```
165
173
 
166
- 2.x serves HTTP with `createMcpHandler`, which makes a server for each request, so attach in the function that makes it. Define your events once, outside it:
167
-
168
174
  ```ts
169
- import { createServer } from "node:http"
170
- import { McpServer, createMcpHandler } from "@modelcontextprotocol/server"
171
- import { toNodeHandler } from "@modelcontextprotocol/node"
175
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"
172
176
  import { z } from "zod"
173
177
  import { Wakeshift } from "@wakeshift/sdk"
174
178
 
@@ -181,30 +185,115 @@ const ciFinished = wake.defineEvent("ci.finished", {
181
185
  text: (run, { owner, repo }) => `CI ${run.conclusion} on ${owner}/${repo} ${run.branch}`,
182
186
  })
183
187
 
184
- const mcp = createMcpHandler(() => {
185
- const server = new McpServer({ name: "ci-bot", version: "1.0.0" })
186
- // identify gets 2.x's request context: your auth middleware's verified token is ctx.http?.authInfo.
187
- const attached = wake.attach(server, { identify: (ctx) => ({ user: myUser(ctx.http?.authInfo) }) })
188
- server.registerTool(
189
- "watch_ci",
190
- { description: "Watch CI on a repo", inputSchema: attached.inputs(z.object({ owner: z.string(), repo: z.string() })) },
191
- attached.events("watch_ci", async ({ owner, repo }, call) => {
192
- await call.subscribe(ciFinished, { owner, repo }, { reason: `Watching ${owner}/${repo}` })
193
- return `Watching CI on ${owner}/${repo}.`
194
- }),
195
- )
196
- return server
188
+ // One McpServer per MCP session; attach before connecting it to its transport.
189
+ const server = new McpServer({ name: "ci-bot", version: "1.0.0" })
190
+ const attached = wake.attach(server, { identify: (extra) => ({ user: myUser(extra.authInfo) }) })
191
+ server.registerTool(
192
+ "watch_ci",
193
+ { description: "Watch CI on a repo", inputSchema: attached.inputs({ owner: z.string(), repo: z.string() }) },
194
+ attached.events("watch_ci", async ({ owner, repo }, call) => {
195
+ await call.subscribe(ciFinished, { owner, repo }, { reason: `Watching ${owner}/${repo}` })
196
+ return `Watching CI on ${owner}/${repo}.`
197
+ }),
198
+ )
199
+ ```
200
+
201
+ What's different on 1.x:
202
+
203
+ - `identify`, `events.native` and a wrapped tool's `call.extra` get 1.x's `extra` instead of `ctx`: the verified token is `extra.authInfo` (1.x's Streamable HTTP transport copies `req.auth` into it), and the HTTP headers are `extra.requestInfo?.headers`.
204
+ - `attached.inputs` and `attached.output` take a zod shape, as 1.x's `registerTool` does: `attached.inputs({ … })`.
205
+ - You serve it with 1.x's `StreamableHTTPServerTransport`. With sessions, give each session its own `McpServer`; clients connect with `initialize`. Clients that speak only MCP protocol 2026-07-28, such as OpenClaw, connect only to a stateless transport (no `sessionIdGenerator`), where `attach` answers that revision for 1.x.
206
+
207
+ ### Moving from 1.x to 2.x
208
+
209
+ Moving a server from 1.x to 2.x moves where your own code reads a request's fields, in `identify`, `events.native` and your tool callbacks. Fields read the 1.x way don't fail on 2.x. They come back `undefined`, and code that doesn't type its context as 2.x's still compiles, so a user your sign-in recognized on 1.x silently goes unrecognized. Check each read:
210
+
211
+ | What you read | On 1.x (`extra`) | On 2.x (`ctx`) |
212
+ | --- | --- | --- |
213
+ | The verified token | `extra.authInfo` | `ctx.http?.authInfo` |
214
+ | An HTTP header | `extra.requestInfo?.headers[name]` | `ctx.http?.req?.headers.get(name)` |
215
+ | Your own `_meta` keys | `extra._meta` | `ctx.mcpReq._meta` |
216
+ | The reserved `io.modelcontextprotocol/*` `_meta` keys (protocol version, client info, client capabilities) | `extra._meta` | `ctx.mcpReq.envelope`, on protocol 2026-07-28: 2.x moves them out of `ctx.mcpReq._meta` |
217
+ | The request's cancellation | `extra.signal` | `ctx.mcpReq.signal` |
218
+
219
+ Wakeshift reads both shapes itself, so what it sees doesn't change: only your own reads move. Write your schemas, events' included, with zod 4 (4.2 or later), which 2.x requires.
220
+
221
+ ## Mastra
222
+
223
+ Mastra's `MCPServer` (`@mastra/mcp` 2.7 or later) builds its MCP server itself, so it has its own entry point, `@wakeshift/sdk/mastra` (0.7.0 or later). Wrap the `execute` of each Mastra tool that subscribes, and serve your `MCPServer` with it:
224
+
225
+ ```ts
226
+ import { createTool } from "@mastra/core/tools"
227
+ import { MCPServer } from "@mastra/mcp"
228
+ import { z } from "zod"
229
+ import { Wakeshift } from "@wakeshift/sdk"
230
+ import { attachMastra } from "@wakeshift/sdk/mastra"
231
+
232
+ const wake = new Wakeshift({ key: process.env.WAKESHIFT_KEY! })
233
+ const ciFinished = wake.defineEvent("ci.finished", {
234
+ description: "CI finished on a GitHub repo",
235
+ args: z.object({ owner: z.string(), repo: z.string() }),
236
+ payload: z.object({ branch: z.string(), conclusion: z.string() }),
237
+ text: (run, { owner, repo }) => `CI ${run.conclusion} on ${owner}/${repo} ${run.branch}`,
197
238
  })
198
239
 
199
- createServer(toNodeHandler(mcp)).listen(3000)
240
+ // identify gets the request's MCP context: the verified token is ctx.http?.authInfo.
241
+ const attached = attachMastra(wake, { identify: (ctx) => ({ user: myUser(ctx.http?.authInfo) }) })
242
+
243
+ const watchCi = createTool({
244
+ id: "watch_ci",
245
+ description: "Watch CI on a repo",
246
+ inputSchema: attached.inputs(z.object({ owner: z.string(), repo: z.string() })),
247
+ execute: attached.events("watch_ci", async ({ owner, repo }: { owner: string; repo: string }, call) => {
248
+ await call.subscribe(ciFinished, { owner, repo }, { reason: `Watching ${owner}/${repo}` })
249
+ return `Watching CI on ${owner}/${repo}.`
250
+ }),
251
+ })
252
+
253
+ const server = attached.serve(new MCPServer({ id: "ci-bot", name: "CI bot", version: "1.0.0", tools: { watch_ci: watchCi } }))
200
254
  ```
201
255
 
202
- What's different on 2.x:
256
+ Serve it as Mastra's docs do (`server.startHTTP`, `server.startStdio`, or registered on your `Mastra` instance). It gets everything `attach` gives: the built-in tools, MCP Events for clients that speak it, Claude Code's `wakeshift_wait`, and Wakeshift's addition in your wrapped tools' results.
257
+
258
+ - `attached.events(name, handler)` wraps a tool's `execute` for the tool your `MCPServer` serves under `name` (its key in `tools`). `handler(args, call, context)` returns what a Mastra tool returns: a string, or your output. Run outside an MCP client's call (by an agent, a workflow or Studio), `call.subscribe` throws.
259
+ - Wrapped tools answer in one call: a Mastra task or a request for input from one ends what it subscribed.
260
+ - Your own MCP Events (`MCPServer`'s `events`) keep working: `events/list` lists yours and Wakeshift's, and each request goes to whichever serves the event it names.
261
+ - `attached.serve` checks your `@mastra/mcp` version and throws for one outside 2.7–2.x.
262
+
263
+ ## xmcp
264
+
265
+ xmcp builds its MCP server from your tool files, so Wakeshift comes in through them, with `@wakeshift/sdk/xmcp` (0.7.0 or later):
266
+
267
+ ```ts
268
+ import { z } from "zod"
269
+ import { Wakeshift } from "@wakeshift/sdk"
270
+ import { attachXmcp } from "@wakeshift/sdk/xmcp"
271
+
272
+ // src/wake.ts
273
+ const wake = new Wakeshift({ key: process.env.WAKESHIFT_KEY! })
274
+ const ciFinished = wake.defineEvent("ci.finished", {
275
+ description: "CI finished on a GitHub repo",
276
+ args: z.object({ owner: z.string(), repo: z.string() }),
277
+ payload: z.object({ branch: z.string(), conclusion: z.string() }),
278
+ text: (run, { owner, repo }) => `CI ${run.conclusion} on ${owner}/${repo} ${run.branch}`,
279
+ })
280
+ // identify gets xmcp's tool context: the verified token is extra.authInfo.
281
+ const attached = attachXmcp(wake, { identify: (extra) => ({ user: myUser(extra.authInfo) }) })
282
+
283
+ // src/tools/watch_ci.ts: export these three
284
+ const schema = attached.inputs({ owner: z.string(), repo: z.string() })
285
+ const metadata = { name: "watch_ci", description: "Watch CI on a repo" }
286
+ const handler = attached.events("watch_ci", async ({ owner, repo }: { owner: string; repo: string }, call) => {
287
+ await call.subscribe(ciFinished, { owner, repo }, { reason: `Watching ${owner}/${repo}` })
288
+ return `Watching CI on ${owner}/${repo}.`
289
+ })
290
+
291
+ // src/tools/subscribe_events.ts, and the same for unsubscribe_events and list_subscriptions
292
+ const tool = attached.tool("subscribe_events")
293
+ // export const schema = tool.schema; export const metadata = tool.metadata; export default tool.handler
294
+ ```
203
295
 
204
- - `identify`, `events.native` and a wrapped tool's `call.extra` get 2.x's request context (`ctx`) instead of 1.x's `extra`: the verified token is `ctx.http?.authInfo`, and the HTTP request is `ctx.http?.req`.
205
- - `attached.inputs` and `attached.output` take a zod object, as 2.x's `registerTool` does: `attached.inputs(z.object({ … }))`. A bare shape works too.
206
- - Write your schemas, events' included, with zod 4 (4.2 or later), which 2.x requires.
207
- - 2.x speaks MCP protocol 2026-07-28 itself, and `createMcpHandler` answers older clients too, so clients on either revision connect with nothing more to set up. Wakeshift lists its tools for each request's client, so `tools/list` is cached by that client only, whatever cache hint you set for it.
296
+ Each wrapped tool's result gets Wakeshift's addition in its content and `_meta` (not in `structuredContent`, which xmcp checks strictly against your output schema). xmcp can't serve MCP methods of an app's own, so on xmcp there are no MCP Events (ChatGPT and other clients that subscribe natively can't subscribe) and no `wakeshift_wait`: Claude Code is woken its other ways, and every other agent as usual.
208
297
 
209
298
  ## Reference
210
299
 
package/dist/discover.js CHANGED
@@ -125,4 +125,3 @@ export function serveStateless(host, opts = {}) {
125
125
  return out;
126
126
  });
127
127
  }
128
- //# sourceMappingURL=discover.js.map
package/dist/errors.js CHANGED
@@ -25,4 +25,3 @@ export class WakeshiftError extends Error {
25
25
  export function isCode(err, code) {
26
26
  return err instanceof WakeshiftError && err.code === code;
27
27
  }
28
- //# sourceMappingURL=errors.js.map
package/dist/events.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // https://wakeshift.ai/docs/events has the guide.
9
9
  import { RpcError } from "./mcp.js";
10
10
  import { isSchema, objectFromShape, safeParse, toJsonSchema } from "./zod.js";
11
- import { hostOf } from "./host.js";
11
+ import { capabilitiesError, hostOf } from "./host.js";
12
12
  import { WakeshiftError } from "./errors.js";
13
13
  import { DELIVERY, EXTENSION_KEY, HEADER_MISMATCH, INTERNAL_ERROR, INVALID_PARAMS, TOP_LEVEL_KEY, NAME_HEADER, RESULT_TYPE, codesFor, dialectOf, headerValue, } from "./spec.js";
14
14
  import { toolCallOf, userAgentOf } from "./wakeshift.js";
@@ -395,13 +395,20 @@ export const LIST_TOOL_DESCRIPTION = EVENT_TOOL_TEXT.list_description;
395
395
  const MAX_LISTED_EVENTS = 20;
396
396
  /** @internal The events part of Wakeshift.attach. */
397
397
  export function attachEvents(wake, server, events, opts = {}) {
398
+ checkAttach(events, opts);
399
+ return attachHost(wake, hostOf(server), events, opts);
400
+ }
401
+ /** @internal What attach refuses before it touches a server: no events, or an own tool it doesn't have. */
402
+ export function checkAttach(events, opts, what = "attach") {
398
403
  if (events.length === 0)
399
- throw new Error("wakeshift: attach needs at least one event (wake.defineEvent) defined first");
404
+ throw new Error(`wakeshift: ${what} needs at least one event (wake.defineEvent) defined first`);
400
405
  for (const t of opts.tools ?? []) {
401
406
  if (!BUILTIN_TOOLS.includes(t))
402
- throw new Error(`wakeshift: attach's tools names "${t}": its own tools are ${BUILTIN_TOOLS.join(", ")}`);
407
+ throw new Error(`wakeshift: ${what}'s tools names "${t}": its own tools are ${BUILTIN_TOOLS.join(", ")}`);
403
408
  }
404
- const host = hostOf(server);
409
+ }
410
+ /** @internal attach on a Host: an McpServer's, or a framework adapter's (mastra.ts, xmcp.ts). */
411
+ export function attachHost(wake, host, events, opts = {}) {
405
412
  const tools = new Set(opts.tools ?? BUILTIN_TOOLS);
406
413
  // raw is the MCP SDK's own request context (1.x's extra, 2.x's ctx): what identify gets.
407
414
  const identityOf = (raw) => opts.identify?.(raw) ?? undefined;
@@ -423,9 +430,9 @@ export function attachEvents(wake, server, events, opts = {}) {
423
430
  host.registerCapabilities({ [TOP_LEVEL_KEY]: { listChanged: false }, extensions: { [EXTENSION_KEY]: { listChanged: false } } });
424
431
  }
425
432
  catch (err) {
426
- throw new Error("wakeshift: call attach before connecting the server to a transport", { cause: err });
433
+ throw capabilitiesError(host, err);
427
434
  }
428
- if (!host.handlers()) {
435
+ if (!host.handlers() && !host.framework) {
429
436
  // An MCP SDK release that keeps its request handlers elsewhere: the native
430
437
  // events and the built-in tools still work, but not what wraps the SDK's own methods.
431
438
  wake.warnOnce("handlers", "wakeshift: this MCP SDK keeps its request handlers where Wakeshift can't reach them, so wakeshift_wait, the 2026-07-28 tools/list changes and wrapped tools' output schemas are off: check for a newer @wakeshift/sdk");
@@ -611,7 +618,8 @@ export function attachEvents(wake, server, events, opts = {}) {
611
618
  name: ev.name,
612
619
  arguments: args,
613
620
  });
614
- return result(fill(out.ended ? "unsubscribed" : "not_subscribed", { event: ev.name, arguments: canonical(args) }));
621
+ // addition: the conversation's session key stays the same.
622
+ return result(fill(out.ended ? "unsubscribed" : "not_subscribed", { event: ev.name, arguments: canonical(args) }), out.addition);
615
623
  }
616
624
  catch (err) {
617
625
  return fail(err instanceof ArgsError ? err.detail : fill("unsubscribe_failed", { detail: detailOf(err) }));
@@ -704,4 +712,3 @@ export function rpcErrorOf(err, codes) {
704
712
  }
705
713
  return new RpcError(INTERNAL_ERROR, "Wakeshift refused the subscription: " + err.detail);
706
714
  }
707
- //# sourceMappingURL=events.js.map
package/dist/harness.js CHANGED
@@ -75,4 +75,3 @@ function slug(name) {
75
75
  id = id.slice(0, 40).replace(/^-+|-+$/g, "");
76
76
  return id.length < 2 ? "agent" : id;
77
77
  }
78
- //# sourceMappingURL=harness.js.map
package/dist/host.d.ts CHANGED
@@ -1,2 +1,4 @@
1
+ /** Where the docs list the MCP server frameworks Wakeshift works with, and how. */
2
+ export declare const FRAMEWORKS_DOCS = "https://wakeshift.ai/docs/frameworks";
1
3
  /** The protocol revision that has server/discover, MCP Events and the tasks extension. */
2
4
  export declare const STATELESS_VERSION = "2026-07-28";