@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/CHANGELOG.md +13 -0
- package/README.md +77 -0
- package/dist/discover.js +0 -1
- package/dist/errors.js +0 -1
- package/dist/events.js +13 -7
- package/dist/harness.js +0 -1
- package/dist/host.d.ts +2 -0
- package/dist/host.js +181 -4
- package/dist/index.js +0 -1
- package/dist/key.js +0 -1
- package/dist/mastra.d.ts +79 -0
- package/dist/mastra.js +248 -0
- package/dist/mcp.js +0 -1
- package/dist/retry.js +0 -1
- package/dist/spec.js +0 -1
- package/dist/tasks.js +0 -1
- package/dist/toolcall.d.ts +1 -0
- package/dist/toolcall.js +44 -12
- package/dist/wakeshift.js +9 -1
- package/dist/xmcp.d.ts +73 -0
- package/dist/xmcp.js +125 -0
- package/dist/zod.js +0 -1
- package/package.json +22 -4
- package/dist/discover.js.map +0 -1
- package/dist/errors.js.map +0 -1
- package/dist/events.js.map +0 -1
- package/dist/harness.js.map +0 -1
- package/dist/host.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/key.js.map +0 -1
- package/dist/mcp.js.map +0 -1
- package/dist/retry.js.map +0 -1
- package/dist/spec.js.map +0 -1
- package/dist/tasks.js.map +0 -1
- package/dist/toolcall.js.map +0 -1
- package/dist/wakeshift.js.map +0 -1
- package/dist/zod.js.map +0 -1
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
package/dist/retry.js
CHANGED
package/dist/spec.js
CHANGED
package/dist/tasks.js
CHANGED
package/dist/toolcall.d.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wakeshift/sdk",
|
|
3
|
-
"version": "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": {
|