@wakeshift/sdk 0.7.0 → 0.7.2
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 +20 -1
- package/README.md +64 -50
- package/dist/events.js +2 -1
- package/dist/host.js +23 -1
- package/dist/tasks.js +4 -0
- package/package.json +2 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,23 @@
|
|
|
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.2 (2026-11-02)
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **`@modelcontextprotocol/server` 2.3.0 or later.** The peer dependency was `^2.0.0`, but on 2.0, 2.1 and 2.2 Claude Code's `wakeshift_wait` never completes: those releases answer `tasks/get` and `tasks/cancel` on protocol 2026-07-28 with "Method not found" themselves, before `attach` sees them ([modelcontextprotocol/typescript-sdk#2598](https://github.com/modelcontextprotocol/typescript-sdk/issues/2598), fixed in 2.3.0 by [#2599](https://github.com/modelcontextprotocol/typescript-sdk/pull/2599)). So conversations told to use the wait tool weren't woken. Everything else (wrapped tools, the built-in tools, MCP Events with `events/poll` and webhooks) works on 2.0 and later. The peer range is now `^2.3.0`: upgrade with `npm install @modelcontextprotocol/server@^2.3.0`. `@modelcontextprotocol/sdk` 1.x is unaffected.
|
|
10
|
+
- **`attach` warns on an older 2.x.** Given a server from `@modelcontextprotocol/server` before 2.3.0, `attach` warns once (through `onWarning`), naming the version to upgrade to.
|
|
11
|
+
|
|
12
|
+
## 0.7.1 (2026-11-01)
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- **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`.
|
|
17
|
+
|
|
18
|
+
### Changed
|
|
19
|
+
|
|
20
|
+
- The README and docs lead with `@modelcontextprotocol/server` 2.x; 1.x is the alternative. Nothing changes in the code.
|
|
21
|
+
|
|
5
22
|
## 0.7.0 (2026-10-31)
|
|
6
23
|
|
|
7
24
|
### Added
|
|
@@ -23,7 +40,7 @@ What changed in each release of `@wakeshift/sdk`. 0.4.0 is the first published r
|
|
|
23
40
|
|
|
24
41
|
### Added
|
|
25
42
|
|
|
26
|
-
- **`@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 [
|
|
43
|
+
- **`@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).
|
|
27
44
|
- 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.
|
|
28
45
|
- `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.
|
|
29
46
|
|
|
@@ -45,6 +62,8 @@ What changed in each release of `@wakeshift/sdk`. 0.4.0 is the first published r
|
|
|
45
62
|
|
|
46
63
|
## 0.5.1 (2026-10-30)
|
|
47
64
|
|
|
65
|
+
Never published to npm. Everything below first shipped in 0.5.2.
|
|
66
|
+
|
|
48
67
|
### Added
|
|
49
68
|
|
|
50
69
|
- **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
|
|
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.3 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/
|
|
10
|
+
npm install @wakeshift/sdk @modelcontextprotocol/server @modelcontextprotocol/node zod
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
On `@modelcontextprotocol/
|
|
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 {
|
|
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
|
-
//
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
|
|
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/
|
|
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
|
-
//
|
|
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: (
|
|
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(
|
|
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(
|
|
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` (
|
|
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(
|
|
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(
|
|
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/
|
|
166
|
+
## Using @modelcontextprotocol/sdk 1.x
|
|
159
167
|
|
|
160
|
-
`attach` takes an `McpServer` from either MCP TypeScript SDK
|
|
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/
|
|
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 {
|
|
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,38 @@ 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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
)
|
|
196
|
-
return server
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
createServer(toNodeHandler(mcp)).listen(3000)
|
|
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
|
+
)
|
|
200
199
|
```
|
|
201
200
|
|
|
202
|
-
What's different on
|
|
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.
|
|
203
206
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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.
|
|
208
220
|
|
|
209
221
|
## Mastra
|
|
210
222
|
|
|
@@ -336,6 +348,8 @@ npm run test:v2 # the same unit tests on @modelcontextprotocol/server 2.x
|
|
|
336
348
|
npm run build # dist/
|
|
337
349
|
```
|
|
338
350
|
|
|
351
|
+
`sh scripts/mcp-floor.sh` installs the oldest `@modelcontextprotocol/server` the peer range allows (unsaved), for running `npm run test:v2` on it; `npm ci` puts the locked versions back.
|
|
352
|
+
|
|
339
353
|
The end-to-end tests (`test/e2e*.test.ts`) run against a Wakeshift server, so they run in Wakeshift's own repository and skip here.
|
|
340
354
|
|
|
341
355
|
## Contributing and security
|
package/dist/events.js
CHANGED
|
@@ -618,7 +618,8 @@ export function attachHost(wake, host, events, opts = {}) {
|
|
|
618
618
|
name: ev.name,
|
|
619
619
|
arguments: args,
|
|
620
620
|
});
|
|
621
|
-
|
|
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);
|
|
622
623
|
}
|
|
623
624
|
catch (err) {
|
|
624
625
|
return fail(err instanceof ArgsError ? err.detail : fill("unsubscribe_failed", { detail: detailOf(err) }));
|
package/dist/host.js
CHANGED
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
// and a params schema on 2.x;
|
|
16
16
|
// - tasks/get and tasks/cancel: 2.x answers them on protocol 2026-07-28
|
|
17
17
|
// only for a handler registered with a params schema (the tasks
|
|
18
|
-
// extension's methods), so setTaskMethod registers them that way
|
|
18
|
+
// extension's methods), so setTaskMethod registers them that way.
|
|
19
|
+
// Before 2.3.0 it refused them there whatever was registered
|
|
20
|
+
// (modelcontextprotocol/typescript-sdk#2598, fixed by #2599), so the
|
|
21
|
+
// peer range starts at 2.3.0 and routesTasks tells attach to warn;
|
|
19
22
|
// - protocol 2026-07-28: 1.x doesn't speak it, so discover.ts teaches it;
|
|
20
23
|
// 2.x speaks it itself (server/discover, resultType, ttlMs and
|
|
21
24
|
// cacheScope).
|
|
@@ -29,6 +32,12 @@ import { z } from "zod";
|
|
|
29
32
|
import * as z4 from "zod/v4";
|
|
30
33
|
import { normalizeObjectSchema, safeParseAsync } from "./zod.js";
|
|
31
34
|
const VERSION_META = "io.modelcontextprotocol/protocolVersion";
|
|
35
|
+
/**
|
|
36
|
+
* @internal The oldest @modelcontextprotocol/server that serves tasks/get
|
|
37
|
+
* and tasks/cancel on protocol 2026-07-28 (modelcontextprotocol/typescript-sdk#2599):
|
|
38
|
+
* the floor of the SDK's peer range for it.
|
|
39
|
+
*/
|
|
40
|
+
export const TASKS_MIN_SERVER = "2.3.0";
|
|
32
41
|
function lowOf(server) {
|
|
33
42
|
return server.server;
|
|
34
43
|
}
|
|
@@ -210,6 +219,9 @@ class V1Host extends BaseHost {
|
|
|
210
219
|
// setRequestHandler would ask for its own tasks capability, an older
|
|
211
220
|
// revision's, never advertised.)
|
|
212
221
|
}
|
|
222
|
+
routesTasks() {
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
213
225
|
async validOutput(outputSchema, result) {
|
|
214
226
|
// McpServer 1.x's validateToolOutput: an object schema, and structured content that parses.
|
|
215
227
|
if (!outputSchema)
|
|
@@ -243,6 +255,15 @@ class V2Host extends BaseHost {
|
|
|
243
255
|
throw Object.assign(new Error("Method not found"), { code: -32601 });
|
|
244
256
|
});
|
|
245
257
|
}
|
|
258
|
+
routesTasks() {
|
|
259
|
+
// 2.3.0's fix keeps the methods registered with a schema in this set,
|
|
260
|
+
// and lets them past the era gate; 2.0 to 2.2 have no such set. 2.x
|
|
261
|
+
// doesn't expose its version (nor its package.json), so the fix is
|
|
262
|
+
// what's checked, private like _requestHandlers. A release that renames
|
|
263
|
+
// it would only warn wrongly, and fails v2.test.ts's check that the
|
|
264
|
+
// installed 2.x doesn't warn, on the Dependabot bump that brings it.
|
|
265
|
+
return this.low._customSchemaRequestMethods instanceof Set;
|
|
266
|
+
}
|
|
246
267
|
async validOutput(outputSchema, result) {
|
|
247
268
|
// McpServer 2.x's validateToolOutput: structured content (any JSON
|
|
248
269
|
// value) that the output schema (a Standard Schema) accepts.
|
|
@@ -362,6 +383,7 @@ export function toolsOnlyHost(opts) {
|
|
|
362
383
|
},
|
|
363
384
|
setMethod: noop,
|
|
364
385
|
setTaskMethod: noop,
|
|
386
|
+
routesTasks: () => true,
|
|
365
387
|
handlers: () => null,
|
|
366
388
|
clientName: () => undefined,
|
|
367
389
|
clientCapabilities: () => undefined,
|
package/dist/tasks.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// zero-install path (Claude Code's Monitor). Everything else is unchanged.
|
|
17
17
|
import { WakeshiftError } from "./errors.js";
|
|
18
18
|
import { TASKS_EXTENSION, takesTasks } from "./discover.js";
|
|
19
|
+
import { TASKS_MIN_SERVER } from "./host.js";
|
|
19
20
|
import { RpcError } from "./mcp.js";
|
|
20
21
|
import { WAKE_ARG_DESCRIPTIONS } from "./toolcall.js";
|
|
21
22
|
import { toolCallOf } from "./wakeshift.js";
|
|
@@ -52,6 +53,9 @@ export function serveTasks(wake, host, callerOf) {
|
|
|
52
53
|
const handlers = host.handlers();
|
|
53
54
|
if (!handlers)
|
|
54
55
|
return; // a future SDK: nothing to hook (no wait tool)
|
|
56
|
+
if (!host.routesTasks()) {
|
|
57
|
+
wake.warnOnce("tasks-routing", `wakeshift: this server's @modelcontextprotocol/server is older than ${TASKS_MIN_SERVER} (2.0 to 2.2 answer tasks/get with "Method not found" on protocol 2026-07-28 themselves), so Claude Code's ${WAIT_TOOL} never completes and conversations that use it aren't woken. Upgrade @modelcontextprotocol/server to ${TASKS_MIN_SERVER} or later.`);
|
|
58
|
+
}
|
|
55
59
|
const call = (raw, args) => {
|
|
56
60
|
const { user, client } = callerOf(raw);
|
|
57
61
|
return toolCallOf(host.req(raw), client, user, args, WAIT_TOOL);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wakeshift/sdk",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Let agents subscribe to things in your app: Wakeshift wakes them when you emit an event.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Clawvisor Inc. <support@wakeshift.ai>",
|
|
@@ -56,14 +56,13 @@
|
|
|
56
56
|
"typecheck": "tsc -p tsconfig.test.json --noEmit",
|
|
57
57
|
"test": "tsc -p tsconfig.test.json && node --test 'build-test/test/*.test.js' && npm run examples",
|
|
58
58
|
"examples": "node scripts/check-examples.mjs",
|
|
59
|
-
"e2e": "sh scripts/e2e.sh",
|
|
60
59
|
"prepack": "npm run build",
|
|
61
60
|
"pack-check": "sh scripts/pack-check.sh",
|
|
62
61
|
"test:v2": "tsc -p tsconfig.test.json && WAKESHIFT_TEST_MCP=v2 node --test 'build-test/test/*.test.js'"
|
|
63
62
|
},
|
|
64
63
|
"peerDependencies": {
|
|
65
64
|
"@modelcontextprotocol/sdk": "^1.23.0",
|
|
66
|
-
"@modelcontextprotocol/server": "^2.
|
|
65
|
+
"@modelcontextprotocol/server": "^2.3.0",
|
|
67
66
|
"zod": "^3.25.0 || ^4.0.0",
|
|
68
67
|
"@mastra/mcp": "^2.7.0",
|
|
69
68
|
"xmcp": "^1.9.0"
|