@wakeshift/sdk 0.6.0 → 0.7.0

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/dist/mastra.js ADDED
@@ -0,0 +1,248 @@
1
+ // @wakeshift/sdk/mastra: Wakeshift in a Mastra MCPServer (@mastra/mcp 2.x).
2
+ //
3
+ // MCPServer builds a low-level MCP Server (@modelcontextprotocol/server
4
+ // 2.x) for each request and fills it in itself; it never hands it to the
5
+ // app, and its public extension points (Mastra tools, its own events) can't
6
+ // carry what Wakeshift needs: a tool's result is Mastra's conversion of
7
+ // what the tool returned, without _meta, which Codex's and Claude Code's
8
+ // hooks read Wakeshift's beacon from; and there's no way to add a method
9
+ // (tasks/get for the wait tool) or answer a tool's call another way.
10
+ //
11
+ // So serve() takes the one seam there is: MCPServer's createServerInstance,
12
+ // a private method every transport (HTTP, stdio) builds its servers with.
13
+ // Each server it builds gets what attach gives an McpServer, on that
14
+ // server's request handlers (host.ts's lowServerHost): attach's own tools,
15
+ // MCP Events, the wait tool and the 2026-07-28 tools/list changes; and the
16
+ // tools wrapped with attached.events get Wakeshift's block in their results.
17
+ // serve() checks @mastra/mcp's version and that the seam is there, and the
18
+ // compatibility suite (sdk/compat, ts-mastra) runs it against Mastra's
19
+ // newest release nightly, so a Mastra release that moves it shows up there.
20
+ //
21
+ // Mastra's own MCP Events (MCPServer's events option) keep working beside
22
+ // Wakeshift's: events/list lists both, and every other events/* request
23
+ // goes to whichever serves the event it names (Wakeshift's, if both do).
24
+ //
25
+ // const attached = attachMastra(wake, { identify: (ctx) => ({ user: userOf(ctx.http?.authInfo) }) })
26
+ // const watchCi = createTool({
27
+ // id: "watch_ci",
28
+ // description: "Watch CI on a repo",
29
+ // inputSchema: attached.inputs(z.object({ owner: z.string(), repo: z.string() })),
30
+ // execute: attached.events("watch_ci", async ({ owner, repo }, call) => {
31
+ // await call.subscribe(ciFinished, { owner, repo })
32
+ // return `Watching CI on ${owner}/${repo}.`
33
+ // }),
34
+ // })
35
+ // const server = attached.serve(new MCPServer({ id: "ci", name: "CI", version: "1.0.0", tools: { watch_ci: watchCi } }))
36
+ import { createRequire } from "node:module";
37
+ import { attachHost, checkAttach } from "./events.js";
38
+ import { lowServerHost, toolsOnlyHost, v2Req } from "./host.js";
39
+ import { Attached, CallEnded } from "./toolcall.js";
40
+ /** The @mastra/mcp releases serve() is built for: 2.x, from 2.7. */
41
+ export const MASTRA_MCP_RANGE = ">=2.7.0 <3.0.0";
42
+ const EVENTS_METHODS = ["events/list", "events/subscribe", "events/unsubscribe", "events/poll", "events/stream"];
43
+ /** @internal Whether a version of @mastra/mcp is one serve() is built for. */
44
+ export function supportedMastra(version) {
45
+ const m = /^(\d+)\.(\d+)\./.exec(version);
46
+ return !!m && Number(m[1]) === 2 && Number(m[2]) >= 7;
47
+ }
48
+ // installedMastra is the @mastra/mcp version this process loads, if it can
49
+ // tell (a bundled app may not ship package.json files).
50
+ function installedMastra() {
51
+ try {
52
+ return createRequire(import.meta.url)("@mastra/mcp/package.json").version;
53
+ }
54
+ catch {
55
+ return undefined;
56
+ }
57
+ }
58
+ /** Wakeshift in a Mastra MCPServer: make it with attachMastra, then serve your MCPServer with it. */
59
+ export class MastraAttached {
60
+ wake;
61
+ opts;
62
+ wrapped = new Set();
63
+ // The call each wrapped tool's execute belongs to, by its request (ctx.mcpReq,
64
+ // which Mastra passes the tool as context.mcp.extra.mcpReq).
65
+ scopes = new WeakMap();
66
+ // Only for inputs and output, which need no server.
67
+ shapes;
68
+ served = new WeakSet();
69
+ /** @internal Use attachMastra. */
70
+ constructor(wake, opts) {
71
+ this.wake = wake;
72
+ this.opts = opts;
73
+ const host = toolsOnlyHost({ framework: "mastra", req: v2Req, registerTool: () => { } });
74
+ this.shapes = new Attached({ wake, host, identify: () => undefined, clientOf: () => "", events: {}, warnOnce: () => { } });
75
+ }
76
+ inputs(schema) {
77
+ return this.shapes.inputs(schema);
78
+ }
79
+ output(schema) {
80
+ return this.shapes.output(schema);
81
+ }
82
+ /**
83
+ * Wraps a Mastra tool's execute, for the tool your MCPServer serves under
84
+ * the same name (its key in `tools`): `execute: attached.events("watch_ci",
85
+ * async (args, call) => …)`. handler gets your own arguments, a ToolCall
86
+ * and Mastra's tool context, and returns what a Mastra tool returns (a
87
+ * string, or your output). The reply an MCP client gets carries what
88
+ * Wakeshift adds for the conversation; a call that fails ends the
89
+ * subscriptions it made. Its inputSchema must come from attached.inputs.
90
+ * Run outside an MCP request (by an agent, a workflow or Studio), the
91
+ * handler gets a call whose subscribe throws.
92
+ */
93
+ events(name, handler) {
94
+ if (!name || typeof name !== "string")
95
+ throw new Error("wakeshift: attached.events needs the tool's name, as your MCPServer serves it");
96
+ if (typeof handler !== "function")
97
+ throw new Error("wakeshift: attached.events needs a handler");
98
+ this.wrapped.add(name);
99
+ return async (input, context) => {
100
+ const key = context?.mcp?.extra?.mcpReq;
101
+ const scope = key && typeof key === "object" ? this.scopes.get(key) : undefined;
102
+ const args = {};
103
+ for (const [k, v] of Object.entries((input ?? {}))) {
104
+ if (k !== "session_key" && k !== "harness" && k !== "conversation_id")
105
+ args[k] = v;
106
+ }
107
+ const call = scope && scope.call.tool === name ? scope.call : detachedCall(name, context);
108
+ return handler(args, call, context ?? {});
109
+ };
110
+ }
111
+ /**
112
+ * Serves Wakeshift from your MCPServer (@mastra/mcp 2.7 or later, 2.x),
113
+ * after your last defineEvent: attach's own tools, MCP Events for clients
114
+ * that speak it, the wait tool, and your wrapped tools' blocks. Returns
115
+ * the server. Call it once, before the server takes requests.
116
+ */
117
+ serve(server) {
118
+ const version = installedMastra();
119
+ if (version && !supportedMastra(version)) {
120
+ throw new Error(`wakeshift: attachMastra serves @mastra/mcp ${MASTRA_MCP_RANGE}, and this is ${version}: check for an @wakeshift/sdk that supports it`);
121
+ }
122
+ const s = server;
123
+ if (typeof s.createServerInstance !== "function") {
124
+ throw new Error(`wakeshift: attached.serve takes Mastra's MCPServer (@mastra/mcp ${MASTRA_MCP_RANGE}), and this one has no createServerInstance${version ? ` (@mastra/mcp ${version})` : ""}: check for a newer @wakeshift/sdk`);
125
+ }
126
+ if (this.served.has(server))
127
+ return server;
128
+ this.served.add(server);
129
+ const build = s.createServerInstance;
130
+ const self = this;
131
+ s.createServerInstance = function (...args) {
132
+ const low = build.apply(this, args);
133
+ self.attachTo(low);
134
+ return low;
135
+ };
136
+ return server;
137
+ }
138
+ // attachTo gives one server MCPServer built what attach gives an McpServer.
139
+ // A failure leaves the request served without Wakeshift, and says why once.
140
+ attachTo(low) {
141
+ try {
142
+ const host = lowServerHost(low, "Mastra's MCPServer");
143
+ const handlers = host.handlers();
144
+ const theirs = new Map();
145
+ for (const m of EVENTS_METHODS) {
146
+ const h = handlers.get(m);
147
+ if (h)
148
+ theirs.set(m, h);
149
+ }
150
+ const events = this.wake.serveEvents();
151
+ const attached = attachHost(this.wake, host, events, this.opts);
152
+ for (const name of this.wrapped)
153
+ attached.markWrapped(name);
154
+ if (theirs.size > 0)
155
+ shareEvents(handlers, theirs, new Set(events.map((e) => e.name)));
156
+ this.wrapCalls(handlers, attached);
157
+ }
158
+ catch (err) {
159
+ this.wake.warnOnce("mastra-attach", `wakeshift: couldn't attach to the server Mastra's MCPServer built, so its requests are served without Wakeshift: ${String(err?.message ?? err)}`);
160
+ }
161
+ }
162
+ // wrapCalls runs each wrapped tool's call in a scope its execute finds, and
163
+ // adds Wakeshift's block to the result Mastra makes of what it returned.
164
+ wrapCalls(handlers, attached) {
165
+ const inner = handlers.get("tools/call");
166
+ if (!inner)
167
+ return;
168
+ Map.prototype.set.call(handlers, "tools/call", async (request, ctx) => {
169
+ const name = request?.params?.name;
170
+ if (typeof name !== "string" || !this.wrapped.has(name))
171
+ return inner(request, ctx);
172
+ const { call } = attached.begin(name, (request?.params?.arguments ?? {}), ctx);
173
+ const key = ctx?.mcpReq && typeof ctx.mcpReq === "object" ? ctx.mcpReq : undefined;
174
+ if (key)
175
+ this.scopes.set(key, { call });
176
+ let result;
177
+ try {
178
+ result = await inner(request, ctx);
179
+ }
180
+ catch (err) {
181
+ await attached.fail(call);
182
+ throw err;
183
+ }
184
+ finally {
185
+ if (key)
186
+ this.scopes.delete(key);
187
+ }
188
+ if (!result || typeof result !== "object" || !Array.isArray(result.content)) {
189
+ // Not a tool's answer (a task handle, an input request): no block to
190
+ // add, so nothing the call subscribed stays.
191
+ await attached.fail(call);
192
+ this.wake.warnOnce("mastra-result:" + name, `wakeshift: ${name} answered with something other than a tool result (a Mastra task, or a request for input), so what it subscribed was rolled back: a wrapped tool must answer in one call`);
193
+ return result;
194
+ }
195
+ return attached.finish(call, result, undefined);
196
+ });
197
+ }
198
+ }
199
+ // shareEvents serves Mastra's own MCP Events beside Wakeshift's: events/list
200
+ // lists both (Wakeshift's first, on the first page), and every other method
201
+ // goes to whichever serves the event it names.
202
+ function shareEvents(handlers, theirs, ours) {
203
+ for (const [method, their] of theirs) {
204
+ const mine = handlers.get(method);
205
+ if (!mine || mine === their)
206
+ continue;
207
+ if (method === "events/list") {
208
+ Map.prototype.set.call(handlers, method, async (request, ctx) => {
209
+ if (request?.params?.cursor !== undefined)
210
+ return their(request, ctx);
211
+ const [a, b] = await Promise.all([mine(request, ctx), their(request, ctx)]);
212
+ const listed = [...(a?.events ?? []), ...(b?.events ?? []).filter((e) => !ours.has(e?.name ?? ""))];
213
+ return { ...b, ...a, events: listed, ...(b?.nextCursor !== undefined ? { nextCursor: b.nextCursor } : {}) };
214
+ });
215
+ continue;
216
+ }
217
+ Map.prototype.set.call(handlers, method, (request, ctx) => (ours.has(request?.params?.name) ? mine(request, ctx) : their(request, ctx)));
218
+ }
219
+ }
220
+ // detachedCall is the call a wrapped tool's handler gets outside an MCP
221
+ // tools/call (an agent, a workflow or Studio ran the tool): nothing to subscribe.
222
+ function detachedCall(tool, context) {
223
+ const refuse = () => Promise.reject(new Error(`wakeshift: ${tool} ran outside an MCP client's call to a server attachMastra serves (from an agent, a workflow or Studio?), so there's no conversation to subscribe`));
224
+ return {
225
+ user: undefined,
226
+ tool,
227
+ native: false,
228
+ nativeConversation: null,
229
+ session: null,
230
+ recognized: null,
231
+ extra: (context?.mcp?.extra ?? {}),
232
+ subscribe: refuse,
233
+ unsubscribe: refuse,
234
+ resolveSession: refuse,
235
+ };
236
+ }
237
+ /**
238
+ * Wakeshift for a Mastra MCPServer (@mastra/mcp 2.7 or later). Returns the
239
+ * handle your tools use (attached.inputs, attached.events) and that serves
240
+ * your MCPServer (attached.serve). Options are attach's; identify and
241
+ * events.native get the request's MCP context (ctx: the verified token is
242
+ * ctx.http?.authInfo).
243
+ */
244
+ export function attachMastra(wake, opts = {}) {
245
+ checkAttach(wake.serveEvents(), opts, "attachMastra");
246
+ return new MastraAttached(wake, opts);
247
+ }
248
+ export { CallEnded };
package/dist/mcp.js CHANGED
@@ -19,4 +19,3 @@ export class RpcError extends Error {
19
19
  this.data = data;
20
20
  }
21
21
  }
22
- //# sourceMappingURL=mcp.js.map
package/dist/retry.js CHANGED
@@ -55,4 +55,3 @@ function pause(ms, signal) {
55
55
  signal?.addEventListener("abort", stop, { once: true });
56
56
  });
57
57
  }
58
- //# sourceMappingURL=retry.js.map
package/dist/spec.js CHANGED
@@ -42,4 +42,3 @@ export function headerValue(h) {
42
42
  return null;
43
43
  }
44
44
  }
45
- //# sourceMappingURL=spec.js.map
package/dist/tasks.js CHANGED
@@ -156,4 +156,3 @@ export function serveTasks(wake, host, callerOf) {
156
156
  function detailOf(err) {
157
157
  return err instanceof WakeshiftError ? err.detail : String(err?.message ?? err);
158
158
  }
159
- //# sourceMappingURL=tasks.js.map
@@ -202,5 +202,6 @@ export declare class Attached<C = any> {
202
202
  */
203
203
  events<A extends WakeArgs>(name: string, handler: (args: Omit<A, WakeKey>, call: ToolCall<C>) => WrappedResult | Promise<WrappedResult>): WrappedCallback<A, C>;
204
204
  private run;
205
+ amendToolsList(): void;
205
206
  private amendTools;
206
207
  }
package/dist/toolcall.js CHANGED
@@ -224,8 +224,8 @@ function sameJSON(a, b) {
224
224
  function detailOf(err) {
225
225
  return err instanceof WakeshiftError ? err.detail : String(err?.message ?? err);
226
226
  }
227
- /** One tools/call of a wrapped tool: what its handler subscribed, for the block and the rollback. */
228
- class CallScope {
227
+ /** @internal One tools/call of a wrapped tool: what its handler subscribed, for the block and the rollback. */
228
+ export class CallScope {
229
229
  at;
230
230
  args;
231
231
  user;
@@ -441,6 +441,9 @@ function isRecord(v) {
441
441
  * _meta's). conversation is ChatGPT's conversation, when it names one.
442
442
  */
443
443
  export function nativeOf(at, extra, identified) {
444
+ // A server that doesn't serve MCP Events (an xmcp app's) has no native callers.
445
+ if (!at.host.servesEvents)
446
+ return { native: false, conversation: null };
444
447
  const req = at.host.req(extra);
445
448
  const meta = (req.meta ?? {});
446
449
  const conversation = typeof meta["openai/session"] === "string" && meta["openai/session"] ? meta["openai/session"] : null;
@@ -532,24 +535,49 @@ export class Attached {
532
535
  }
533
536
  outputSchema = tool.outputSchema;
534
537
  }
535
- else {
538
+ else if (!host.framework) {
536
539
  this.at.warnOnce("internals", "wakeshift: this MCP SDK keeps its tools where Wakeshift can't read them, so wrapped tools' registration and output aren't checked");
537
540
  }
538
- const str = (k) => (typeof input[k] === "string" && input[k] ? input[k] : undefined);
539
- const call = new CallScope(this.at, name, extra, { session: str(SESSION_ARG), harness: str(HARNESS_ARG), conversation: str(CONVERSATION_ARG) });
540
- const appArgs = {};
541
- for (const [k, v] of Object.entries(input))
542
- if (!WAKE_KEYS.includes(k))
543
- appArgs[k] = v;
541
+ const { call, appArgs } = this.begin(name, input, extra);
544
542
  let result;
545
543
  try {
546
544
  result = await handler(appArgs, call);
547
545
  }
548
546
  catch (err) {
549
- await call.end();
550
- await call.rollback();
547
+ await this.fail(call);
551
548
  throw err;
552
549
  }
550
+ return this.finish(call, result, outputSchema);
551
+ }
552
+ /**
553
+ * @internal The first half of a wrapped call: the call's scope, from the
554
+ * tool's arguments (Wakeshift's three among them) and the request's
555
+ * context, and the app's own arguments. finish (or fail) ends it.
556
+ */
557
+ begin(name, input, extra) {
558
+ const str = (k) => (typeof input[k] === "string" && input[k] ? input[k] : undefined);
559
+ const call = new CallScope(this.at, name, extra, { session: str(SESSION_ARG), harness: str(HARNESS_ARG), conversation: str(CONVERSATION_ARG) });
560
+ const appArgs = {};
561
+ for (const [k, v] of Object.entries(input ?? {}))
562
+ if (!WAKE_KEYS.includes(k))
563
+ appArgs[k] = v;
564
+ return { call, appArgs };
565
+ }
566
+ /** @internal Ends a call whose handler threw: what it subscribed is rolled back. */
567
+ async fail(call) {
568
+ await call.end();
569
+ await call.rollback();
570
+ }
571
+ /**
572
+ * @internal The second half of a wrapped call: the handler's result with
573
+ * Wakeshift's block, or rolled back (a failed result, a canceled call, an
574
+ * output its schema refuses). outputSchema is the tool's, as its MCP SDK
575
+ * checks it (undefined: not checked here).
576
+ */
577
+ async finish(call, result, outputSchema) {
578
+ const host = this.at.host;
579
+ const name = call.tool;
580
+ const extra = call.extra;
553
581
  await call.end();
554
582
  // Rollback is decided from the final result, the one the client would
555
583
  // receive: MCP validates the output after this callback returns, so
@@ -589,6 +617,11 @@ export class Attached {
589
617
  * sets the handler when the first tool is registered, perhaps after
590
618
  * attach, so a later set is wrapped too (as tasks.ts does).
591
619
  */
620
+ /** @internal Marks name as a wrapped tool, for tools/list (a framework adapter's tool, wrapped outside this handle). */
621
+ markWrapped(name) {
622
+ if (!this.wrapped.has(name))
623
+ this.wrapped.set(name, true);
624
+ }
592
625
  amendToolsList() {
593
626
  const handlers = this.at.host.handlers();
594
627
  if (!handlers)
@@ -616,4 +649,3 @@ export class Attached {
616
649
  }
617
650
  }
618
651
  }
619
- //# sourceMappingURL=toolcall.js.map
package/dist/wakeshift.js CHANGED
@@ -304,6 +304,15 @@ export class Wakeshift {
304
304
  this.serving = true; // the manifest is sent automatically from now on
305
305
  return attached;
306
306
  }
307
+ /**
308
+ * @internal For a framework adapter (@wakeshift/sdk/mastra, /xmcp): the
309
+ * defined events, and from now on this process serves the app's tools,
310
+ * so the manifest is sent automatically, as after attach.
311
+ */
312
+ serveEvents() {
313
+ this.serving = true;
314
+ return [...this.events.values()];
315
+ }
307
316
  /**
308
317
  * @internal Sends the manifest with the defined events, once (again after
309
318
  * a defineEvent), from a process that serves the app's tools (attach). A
@@ -410,4 +419,3 @@ export class Wakeshift {
410
419
  return new WakeshiftError(0, "unreachable", `can't reach Wakeshift at ${this.core} (${why}). Check that WAKESHIFT_CORE, or the core option, is the address of the console your key came from`, { cause: err });
411
420
  }
412
421
  }
413
- //# sourceMappingURL=wakeshift.js.map
package/dist/xmcp.d.ts ADDED
@@ -0,0 +1,73 @@
1
+ import type { AuthInfo, CallToolResult } from "./mcp.js";
2
+ import type { ZodRawShapeCompat } from "./zod.js";
3
+ import { BUILTIN_TOOLS, type BuiltinTool, type Identity } from "./events.js";
4
+ import { type ToolCall, type WakeArgs, type WakeInputs, type WrappedResult } from "./toolcall.js";
5
+ import type { Wakeshift } from "./wakeshift.js";
6
+ /** The request context xmcp passes a tool's handler, its second argument: what Wakeshift reads of it. */
7
+ export interface XmcpExtra {
8
+ signal?: AbortSignal;
9
+ authInfo?: AuthInfo;
10
+ sessionId?: string;
11
+ _meta?: Record<string, unknown>;
12
+ requestId?: string | number;
13
+ requestInfo?: {
14
+ headers?: Record<string, string | string[] | undefined>;
15
+ };
16
+ clientInfo?: {
17
+ name?: string;
18
+ version?: string;
19
+ };
20
+ [k: string]: any;
21
+ }
22
+ /** attachXmcp's options. */
23
+ export interface XmcpAttachOptions {
24
+ /**
25
+ * Who is behind a request: { user }, your own ID for the person, from your
26
+ * own sign-in (the verified token is extra.authInfo). Without it, requests
27
+ * have no user, so personal events and list_subscriptions refuse.
28
+ */
29
+ identify?(extra: XmcpExtra): Identity | undefined;
30
+ /** Which of attach's own tools attached.tool gives (default: all three). */
31
+ tools?: readonly BuiltinTool[];
32
+ }
33
+ /** One of attach's own tools as an xmcp tool file exports it. */
34
+ export interface XmcpTool {
35
+ /** The tool's input, a zod 4 shape: `export const schema = tool.schema`. */
36
+ schema: ZodRawShapeCompat;
37
+ /** Its name, description and annotations: `export const metadata = tool.metadata`. */
38
+ metadata: {
39
+ name: string;
40
+ description: string;
41
+ annotations?: Record<string, unknown>;
42
+ };
43
+ /** Its handler: `export default tool.handler`. */
44
+ handler: (args: Record<string, unknown>, extra: XmcpExtra) => Promise<CallToolResult>;
45
+ }
46
+ /** Wakeshift in an xmcp app: what your tool files use. Make it with attachXmcp. */
47
+ export declare class XmcpAttached {
48
+ private readonly attached;
49
+ private readonly own;
50
+ /**
51
+ * A tool's schema plus Wakeshift's three optional arguments (session_key,
52
+ * harness and conversation_id): `export const schema = attached.inputs({ … })`.
53
+ */
54
+ inputs<S extends ZodRawShapeCompat>(shape: S): S & WakeInputs;
55
+ /**
56
+ * Wraps a tool's handler, for the tool file whose metadata names the same
57
+ * tool: `export default attached.events("watch_ci", async (args, call) => …)`.
58
+ * handler gets your own arguments and a ToolCall, and returns a string or
59
+ * a CallToolResult; the reply gets what Wakeshift adds for the
60
+ * conversation, and a call that fails ends the subscriptions it made. Its
61
+ * schema must come from attached.inputs.
62
+ */
63
+ events<P extends Record<string, any> = Record<string, any>>(name: string, handler: (args: P, call: ToolCall<XmcpExtra>) => WrappedResult | Promise<WrappedResult>): (args: P & WakeArgs, extra: XmcpExtra) => Promise<CallToolResult>;
64
+ /** One of attach's own tools, for a tool file of its own (src/tools/<name>.ts): its schema, metadata and handler. */
65
+ tool(name: BuiltinTool): XmcpTool;
66
+ }
67
+ /**
68
+ * Attaches Wakeshift to an xmcp app, after your last defineEvent. Returns
69
+ * what your tool files use: attached.inputs and attached.events for your
70
+ * own tools that subscribe, and attached.tool for attach's own three.
71
+ */
72
+ export declare function attachXmcp(wake: Wakeshift, opts?: XmcpAttachOptions): XmcpAttached;
73
+ export { BUILTIN_TOOLS, type BuiltinTool, type Identity, type ToolCall, type WakeArgs, type WrappedResult };
package/dist/xmcp.js ADDED
@@ -0,0 +1,125 @@
1
+ // @wakeshift/sdk/xmcp: Wakeshift in an xmcp app (https://xmcp.dev).
2
+ //
3
+ // xmcp builds its MCP server itself, from the files in src/tools, and never
4
+ // hands it to the app, so there's nothing for attach to take. What it does
5
+ // hand the app is each tool: a file's schema, metadata and default-exported
6
+ // handler. So the adapter works through tools alone, xmcp's supported way
7
+ // in:
8
+ //
9
+ // - a tool that subscribes is a tool file whose schema comes from
10
+ // attached.inputs and whose handler is attached.events(name, handler),
11
+ // which returns the result with Wakeshift's block in it (text and
12
+ // _meta), as attach's wrapper does;
13
+ // - attach's own tools (subscribe_events, unsubscribe_events,
14
+ // list_subscriptions) are tool files too, one line each from
15
+ // attached.tool(name);
16
+ // - who's calling is identify(extra), xmcp's handler context.
17
+ //
18
+ // xmcp's middleware sees only the methods it lists (tools/*, prompts/*,
19
+ // resources/*), and only tools it has, so a server can't answer MCP Events
20
+ // (events/*) or the wait tool's tasks/get here: clients that subscribe
21
+ // natively (ChatGPT) aren't served, and Claude Code gets its usual wake
22
+ // path instead of wakeshift_wait. Nothing here imports xmcp.
23
+ //
24
+ // // src/wake.ts
25
+ // export const wake = new Wakeshift({ key: process.env.WAKESHIFT_KEY! })
26
+ // export const ciFinished = wake.defineEvent("ci.finished", { … })
27
+ // export const attached = attachXmcp(wake, { identify: (extra) => ({ user: userOf(extra.authInfo) }) })
28
+ //
29
+ // // src/tools/watch_ci.ts
30
+ // export const schema = attached.inputs({ owner: z.string(), repo: z.string() })
31
+ // export const metadata = { name: "watch_ci", description: "Watch CI on a repo" }
32
+ // export default attached.events("watch_ci", async ({ owner, repo }, call) => { … })
33
+ //
34
+ // // src/tools/subscribe_events.ts (and the same for the other two)
35
+ // const tool = attached.tool("subscribe_events")
36
+ // export const schema = tool.schema
37
+ // export const metadata = tool.metadata
38
+ // export default tool.handler
39
+ import { BUILTIN_TOOLS, attachHost, checkAttach } from "./events.js";
40
+ import { toolsOnlyHost, v1Req } from "./host.js";
41
+ import { WAKESHIFT_FIELD } from "./toolcall.js";
42
+ const CLIENT_INFO_META = "io.modelcontextprotocol/clientInfo";
43
+ // xmcpReq reads xmcp's handler context: 1.x-shaped (_meta, requestInfo's
44
+ // headers, authInfo, signal), with the client's info beside it rather than
45
+ // in _meta, where Wakeshift looks for it.
46
+ function xmcpReq(raw) {
47
+ const req = v1Req(raw);
48
+ const info = raw?.clientInfo;
49
+ if (!info?.name || (req.meta && CLIENT_INFO_META in req.meta))
50
+ return req;
51
+ return { ...req, meta: { ...(req.meta ?? {}), [CLIENT_INFO_META]: info } };
52
+ }
53
+ /** Wakeshift in an xmcp app: what your tool files use. Make it with attachXmcp. */
54
+ export class XmcpAttached {
55
+ attached;
56
+ own;
57
+ /** @internal */
58
+ constructor(attached, own) {
59
+ this.attached = attached;
60
+ this.own = own;
61
+ }
62
+ /**
63
+ * A tool's schema plus Wakeshift's three optional arguments (session_key,
64
+ * harness and conversation_id): `export const schema = attached.inputs({ … })`.
65
+ */
66
+ inputs(shape) {
67
+ return this.attached.inputs(shape);
68
+ }
69
+ /**
70
+ * Wraps a tool's handler, for the tool file whose metadata names the same
71
+ * tool: `export default attached.events("watch_ci", async (args, call) => …)`.
72
+ * handler gets your own arguments and a ToolCall, and returns a string or
73
+ * a CallToolResult; the reply gets what Wakeshift adds for the
74
+ * conversation, and a call that fails ends the subscriptions it made. Its
75
+ * schema must come from attached.inputs.
76
+ */
77
+ events(name, handler) {
78
+ const cb = this.attached.events(name, handler);
79
+ return async (args, extra) => {
80
+ const out = (await cb(args, extra));
81
+ // xmcp checks structuredContent against the tool's output schema after
82
+ // the handler, refusing fields it doesn't name: the block stays in the
83
+ // content and _meta only.
84
+ const sc = out?.structuredContent;
85
+ if (sc && typeof sc === "object" && !Array.isArray(sc) && WAKESHIFT_FIELD in sc) {
86
+ const { [WAKESHIFT_FIELD]: _block, ...rest } = sc;
87
+ return { ...out, structuredContent: rest };
88
+ }
89
+ return out;
90
+ };
91
+ }
92
+ /** One of attach's own tools, for a tool file of its own (src/tools/<name>.ts): its schema, metadata and handler. */
93
+ tool(name) {
94
+ const t = this.own.get(name);
95
+ if (!t) {
96
+ throw new Error(BUILTIN_TOOLS.includes(name)
97
+ ? `wakeshift: attachXmcp's tools leave out ${name}`
98
+ : `wakeshift: attached.tool("${name}"): attach's own tools are ${BUILTIN_TOOLS.join(", ")}`);
99
+ }
100
+ return t;
101
+ }
102
+ }
103
+ /**
104
+ * Attaches Wakeshift to an xmcp app, after your last defineEvent. Returns
105
+ * what your tool files use: attached.inputs and attached.events for your
106
+ * own tools that subscribe, and attached.tool for attach's own three.
107
+ */
108
+ export function attachXmcp(wake, opts = {}) {
109
+ const events = wake.serveEvents();
110
+ checkAttach(events, opts, "attachXmcp");
111
+ const own = new Map();
112
+ const host = toolsOnlyHost({
113
+ framework: "xmcp",
114
+ req: xmcpReq,
115
+ registerTool(name, config, cb) {
116
+ const metadata = { name, description: config.description };
117
+ if (config.annotations)
118
+ metadata.annotations = config.annotations;
119
+ own.set(name, { schema: config.inputSchema, metadata, handler: (args, extra) => cb(args ?? {}, extra) });
120
+ },
121
+ });
122
+ const attached = attachHost(wake, host, events, { identify: opts.identify, tools: opts.tools });
123
+ return new XmcpAttached(attached, own);
124
+ }
125
+ export { BUILTIN_TOOLS };
package/dist/zod.js CHANGED
@@ -92,4 +92,3 @@ export function toJsonSchema(schema) {
92
92
  }
93
93
  return v1JsonSchema.toJsonSchemaCompat(schema);
94
94
  }
95
- //# sourceMappingURL=zod.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wakeshift/sdk",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
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>",
@@ -11,7 +11,9 @@
11
11
  "agents",
12
12
  "events",
13
13
  "webhooks",
14
- "wakeshift"
14
+ "wakeshift",
15
+ "mastra",
16
+ "xmcp"
15
17
  ],
16
18
  "homepage": "https://github.com/wakeshift/wakeshift-node#readme",
17
19
  "repository": {
@@ -28,6 +30,14 @@
28
30
  ".": {
29
31
  "types": "./dist/index.d.ts",
30
32
  "import": "./dist/index.js"
33
+ },
34
+ "./mastra": {
35
+ "types": "./dist/mastra.d.ts",
36
+ "import": "./dist/mastra.js"
37
+ },
38
+ "./xmcp": {
39
+ "types": "./dist/xmcp.d.ts",
40
+ "import": "./dist/xmcp.js"
31
41
  }
32
42
  },
33
43
  "files": [
@@ -42,7 +52,7 @@
42
52
  "access": "public"
43
53
  },
44
54
  "scripts": {
45
- "build": "tsc -p tsconfig.json",
55
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
46
56
  "typecheck": "tsc -p tsconfig.test.json --noEmit",
47
57
  "test": "tsc -p tsconfig.test.json && node --test 'build-test/test/*.test.js' && npm run examples",
48
58
  "examples": "node scripts/check-examples.mjs",
@@ -54,7 +64,9 @@
54
64
  "peerDependencies": {
55
65
  "@modelcontextprotocol/sdk": "^1.23.0",
56
66
  "@modelcontextprotocol/server": "^2.0.0",
57
- "zod": "^3.25.0 || ^4.0.0"
67
+ "zod": "^3.25.0 || ^4.0.0",
68
+ "@mastra/mcp": "^2.7.0",
69
+ "xmcp": "^1.9.0"
58
70
  },
59
71
  "peerDependenciesMeta": {
60
72
  "@modelcontextprotocol/sdk": {
@@ -62,6 +74,12 @@
62
74
  },
63
75
  "@modelcontextprotocol/server": {
64
76
  "optional": true
77
+ },
78
+ "@mastra/mcp": {
79
+ "optional": true
80
+ },
81
+ "xmcp": {
82
+ "optional": true
65
83
  }
66
84
  },
67
85
  "devDependencies": {