@wakeshift/sdk 0.6.0 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -1
- package/README.md +137 -48
- package/dist/discover.js +0 -1
- package/dist/errors.js +0 -1
- package/dist/events.js +15 -8
- 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 -5
- 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/host.js
CHANGED
|
@@ -37,11 +37,83 @@ function lowOf(server) {
|
|
|
37
37
|
* Server has projectCallToolResult, which 1.x's lacks.
|
|
38
38
|
*/
|
|
39
39
|
export function hostOf(server) {
|
|
40
|
+
if (!isMcpServer(server))
|
|
41
|
+
throw new Error(unsupportedServer(server));
|
|
42
|
+
const low = server.server;
|
|
43
|
+
return typeof low.projectCallToolResult === "function" ? new V2Host(server) : new V1Host(server);
|
|
44
|
+
}
|
|
45
|
+
/** Where the docs list the MCP server frameworks Wakeshift works with, and how. */
|
|
46
|
+
export const FRAMEWORKS_DOCS = "https://wakeshift.ai/docs/frameworks";
|
|
47
|
+
// isMcpServer: an McpServer from either MCP SDK (its low-level server, and registerTool).
|
|
48
|
+
function isMcpServer(server) {
|
|
40
49
|
const low = server?.server;
|
|
41
|
-
|
|
42
|
-
|
|
50
|
+
return !!low && typeof low.setRequestHandler === "function" && typeof server.registerTool === "function";
|
|
51
|
+
}
|
|
52
|
+
const TAKES = "wakeshift: attach takes an McpServer, from @modelcontextprotocol/server 2.x or @modelcontextprotocol/sdk 1.x (@modelcontextprotocol/sdk/server/mcp.js)";
|
|
53
|
+
/**
|
|
54
|
+
* @internal What attach says when it's given something other than an
|
|
55
|
+
* McpServer: what it got, what it takes, and, for a framework it knows,
|
|
56
|
+
* what to do instead.
|
|
57
|
+
*/
|
|
58
|
+
export function unsupportedServer(server) {
|
|
59
|
+
const got = describe(server);
|
|
60
|
+
const hint = frameworkHint(server);
|
|
61
|
+
return `${TAKES}, and got ${got}.${hint ? " " + hint : ""} Frameworks Wakeshift works with, and how: ${FRAMEWORKS_DOCS}`;
|
|
62
|
+
}
|
|
63
|
+
function has(o, ...fns) {
|
|
64
|
+
return !!o && (typeof o === "object" || typeof o === "function") && fns.every((f) => typeof o[f] === "function");
|
|
65
|
+
}
|
|
66
|
+
function ctorName(o) {
|
|
67
|
+
const name = o?.constructor?.name;
|
|
68
|
+
return typeof name === "string" && name && name !== "Object" ? name : "";
|
|
69
|
+
}
|
|
70
|
+
// describe names what attach was given: its class, else its type.
|
|
71
|
+
function describe(server) {
|
|
72
|
+
if (server === null)
|
|
73
|
+
return "null";
|
|
74
|
+
if (server === undefined)
|
|
75
|
+
return "undefined";
|
|
76
|
+
if (typeof server !== "object")
|
|
77
|
+
return `a ${typeof server}`;
|
|
78
|
+
const name = ctorName(server);
|
|
79
|
+
return name ? `a ${name}` : "an object";
|
|
80
|
+
}
|
|
81
|
+
// frameworkHint says what to do for a server from a framework attach knows
|
|
82
|
+
// but can't take: "" for any other.
|
|
83
|
+
function frameworkHint(server) {
|
|
84
|
+
if (!server || typeof server !== "object")
|
|
85
|
+
return "";
|
|
86
|
+
// Mastra's MCPServer (@mastra/mcp): builds its MCP server per request, inside.
|
|
87
|
+
if (has(server, "startHTTP", "convertTools") || (has(server, "startHTTP", "startStdio") && "toolActions" in server)) {
|
|
88
|
+
return "This looks like Mastra's MCPServer (@mastra/mcp), which builds its MCP server inside: use attachMastra from @wakeshift/sdk/mastra.";
|
|
43
89
|
}
|
|
44
|
-
|
|
90
|
+
// punkpeye's fastmcp: its FastMCP, or a session of it.
|
|
91
|
+
if (has(server, "addTool", "start") && ctorName(server) === "FastMCP") {
|
|
92
|
+
return "This looks like fastmcp's FastMCP (punkpeye), which Wakeshift doesn't support: it refuses MCP protocol 2026-07-28.";
|
|
93
|
+
}
|
|
94
|
+
if (ctorName(server) === "FastMCPSession") {
|
|
95
|
+
return "This looks like a fastmcp session (punkpeye), which Wakeshift doesn't support: fastmcp refuses MCP protocol 2026-07-28.";
|
|
96
|
+
}
|
|
97
|
+
// A low-level Server, not the McpServer around it.
|
|
98
|
+
if (has(server, "setRequestHandler", "connect") && !has(server, "registerTool")) {
|
|
99
|
+
return "This is a low-level MCP Server: pass the McpServer instead (new McpServer(…), whose .server it is).";
|
|
100
|
+
}
|
|
101
|
+
// Cloudflare's McpAgent: its server is the McpServer.
|
|
102
|
+
if ("server" in server && has(server, "init") && isMcpServer(server.server)) {
|
|
103
|
+
return "This looks like a Cloudflare McpAgent: pass its McpServer (this.server).";
|
|
104
|
+
}
|
|
105
|
+
return "";
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* @internal attach's error when the server refused Wakeshift's
|
|
109
|
+
* capabilities: an McpServer already connected to a transport, or
|
|
110
|
+
* whatever else its MCP SDK said.
|
|
111
|
+
*/
|
|
112
|
+
export function capabilitiesError(host, err) {
|
|
113
|
+
const low = host.server.server;
|
|
114
|
+
if (low?.transport)
|
|
115
|
+
return new Error("wakeshift: call attach before connecting the server to a transport", { cause: err });
|
|
116
|
+
return new Error(`wakeshift: attach couldn't add Wakeshift's capabilities to this server: ${String(err?.message ?? err)}`, { cause: err });
|
|
45
117
|
}
|
|
46
118
|
// headerOf reads a header from a plain record (1.x's requestInfo.headers).
|
|
47
119
|
function headerOf(headers, name) {
|
|
@@ -85,6 +157,7 @@ export function v2Req(raw) {
|
|
|
85
157
|
}
|
|
86
158
|
class BaseHost {
|
|
87
159
|
server;
|
|
160
|
+
servesEvents = true;
|
|
88
161
|
constructor(server) {
|
|
89
162
|
this.server = server;
|
|
90
163
|
}
|
|
@@ -192,6 +265,111 @@ function objectFromV4Shape(shape) {
|
|
|
192
265
|
return z4.object({});
|
|
193
266
|
return z4.object(shape);
|
|
194
267
|
}
|
|
268
|
+
// ── Framework adapters (mastra.ts, xmcp.ts) ─────────────────────────────
|
|
269
|
+
/**
|
|
270
|
+
* @internal The Host for a low-level Server from @modelcontextprotocol/server
|
|
271
|
+
* 2.x, as a framework builds one for each request and fills in itself
|
|
272
|
+
* (Mastra's MCPServer). It has no McpServer to register attach's own
|
|
273
|
+
* tools on, so they're kept here and served from its handler map, beneath
|
|
274
|
+
* every other wrapper attach adds: tools/list lists them after the
|
|
275
|
+
* framework's own, and tools/call answers them. Everything else is
|
|
276
|
+
* V2Host's, on the same map.
|
|
277
|
+
*/
|
|
278
|
+
export function lowServerHost(low, framework) {
|
|
279
|
+
if (!low || typeof low.setRequestHandler !== "function" || !(low._requestHandlers instanceof Map)) {
|
|
280
|
+
throw new Error(`wakeshift: ${framework} built an MCP server Wakeshift can't serve (no request handlers it can wrap): check for a newer @wakeshift/sdk`);
|
|
281
|
+
}
|
|
282
|
+
return new LowV2Host(new ToolRegistry(low), framework);
|
|
283
|
+
}
|
|
284
|
+
// ToolRegistry stands in for an McpServer around a low-level Server: what
|
|
285
|
+
// attach registers on it is listed and called through the server's handlers.
|
|
286
|
+
class ToolRegistry {
|
|
287
|
+
server;
|
|
288
|
+
tools = new Map();
|
|
289
|
+
constructor(server) {
|
|
290
|
+
this.server = server;
|
|
291
|
+
const handlers = server._requestHandlers;
|
|
292
|
+
const list = handlers.get("tools/list");
|
|
293
|
+
const call = handlers.get("tools/call");
|
|
294
|
+
if (list) {
|
|
295
|
+
Map.prototype.set.call(handlers, "tools/list", async (request, raw) => {
|
|
296
|
+
const out = await list(request, raw);
|
|
297
|
+
const own = [...this.tools].filter(([, t]) => t.enabled);
|
|
298
|
+
if (own.length === 0 || !out || !Array.isArray(out.tools))
|
|
299
|
+
return out;
|
|
300
|
+
const listed = new Set(out.tools.map((t) => t?.name));
|
|
301
|
+
return { ...out, tools: [...out.tools, ...own.filter(([name]) => !listed.has(name)).map(([name, t]) => listedTool(name, t))] };
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
if (call) {
|
|
305
|
+
Map.prototype.set.call(handlers, "tools/call", async (request, raw) => {
|
|
306
|
+
const t = this.tools.get(request?.params?.name);
|
|
307
|
+
if (!t || !t.enabled)
|
|
308
|
+
return call(request, raw);
|
|
309
|
+
const parsed = await t.schema.safeParseAsync(request?.params?.arguments ?? {});
|
|
310
|
+
if (!parsed.success) {
|
|
311
|
+
return { isError: true, content: [{ type: "text", text: `Invalid arguments for tool ${request.params.name}: ${parsed.error.message}` }] };
|
|
312
|
+
}
|
|
313
|
+
return t.cb(parsed.data, raw);
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
registerTool(name, config, cb) {
|
|
318
|
+
const t = { description: config.description, annotations: config.annotations, schema: config.inputSchema, cb, enabled: true };
|
|
319
|
+
this.tools.set(name, t);
|
|
320
|
+
return {
|
|
321
|
+
enable: () => void (t.enabled = true),
|
|
322
|
+
disable: () => void (t.enabled = false),
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
function listedTool(name, t) {
|
|
327
|
+
const out = { name, description: t.description, inputSchema: z4.toJSONSchema(t.schema, { io: "input" }) };
|
|
328
|
+
if (t.annotations)
|
|
329
|
+
out.annotations = t.annotations;
|
|
330
|
+
return out;
|
|
331
|
+
}
|
|
332
|
+
class LowV2Host extends V2Host {
|
|
333
|
+
framework;
|
|
334
|
+
constructor(registry, framework) {
|
|
335
|
+
super(registry);
|
|
336
|
+
this.framework = framework;
|
|
337
|
+
}
|
|
338
|
+
registeredTool() {
|
|
339
|
+
return null; // the framework's own tools are its own
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* @internal The Host for a framework that keeps its MCP server to itself
|
|
344
|
+
* (xmcp): attach's own tools go to the adapter, the request is read its
|
|
345
|
+
* way, and there are no handlers to wrap, so no MCP Events, no wait tool
|
|
346
|
+
* and no tools/list changes. Its tools are on @modelcontextprotocol/server
|
|
347
|
+
* 2.x, so they're written in zod 4.
|
|
348
|
+
*/
|
|
349
|
+
export function toolsOnlyHost(opts) {
|
|
350
|
+
const noop = () => { };
|
|
351
|
+
return {
|
|
352
|
+
version: 2,
|
|
353
|
+
framework: opts.framework,
|
|
354
|
+
servesEvents: false,
|
|
355
|
+
server: { server: {}, registerTool: noop },
|
|
356
|
+
zod: z4,
|
|
357
|
+
req: opts.req,
|
|
358
|
+
registerCapabilities: noop,
|
|
359
|
+
registerTool(name, config, cb) {
|
|
360
|
+
opts.registerTool(name, config, cb);
|
|
361
|
+
return { enable: noop, disable: noop };
|
|
362
|
+
},
|
|
363
|
+
setMethod: noop,
|
|
364
|
+
setTaskMethod: noop,
|
|
365
|
+
handlers: () => null,
|
|
366
|
+
clientName: () => undefined,
|
|
367
|
+
clientCapabilities: () => undefined,
|
|
368
|
+
onInitialized: noop,
|
|
369
|
+
registeredTool: () => null,
|
|
370
|
+
validOutput: async () => true,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
195
373
|
/** The protocol revision that has server/discover, MCP Events and the tasks extension. */
|
|
196
374
|
export const STATELESS_VERSION = "2026-07-28";
|
|
197
375
|
/** @internal Whether a request is protocol 2026-07-28: its _meta names it, or its MCP-Protocol-Version header does. */
|
|
@@ -200,4 +378,3 @@ export function is2026(req) {
|
|
|
200
378
|
return true;
|
|
201
379
|
return req.header("mcp-protocol-version") === STATELESS_VERSION;
|
|
202
380
|
}
|
|
203
|
-
//# sourceMappingURL=host.js.map
|
package/dist/index.js
CHANGED
|
@@ -5,4 +5,3 @@ export { parseKey, signAssertion } from "./key.js";
|
|
|
5
5
|
export { WakeEvent, ArgsError, EVENT_TOOL_TEXT, SUBSCRIBE_TOOL_DESCRIPTION, UNSUBSCRIBE_TOOL_DESCRIPTION, LIST_TOOL_DESCRIPTION, BUILTIN_TOOLS, MAX_STATUS_SUBSCRIPTIONS, } from "./events.js";
|
|
6
6
|
export { CallEnded, } from "./toolcall.js";
|
|
7
7
|
export { EXTENSION_KEY, TEXT_META, REVISION as EVENTS_REVISION } from "./spec.js";
|
|
8
|
-
//# sourceMappingURL=index.js.map
|
package/dist/key.js
CHANGED
|
@@ -108,4 +108,3 @@ export function signAssertion(k, core, now = Date.now(), appId) {
|
|
|
108
108
|
const input = b64({ alg: "EdDSA", typ: "app+jwt", kid: k.kid }) + "." + b64(claims);
|
|
109
109
|
return input + "." + sign(null, Buffer.from(input), k.privateKey).toString("base64url");
|
|
110
110
|
}
|
|
111
|
-
//# sourceMappingURL=key.js.map
|
package/dist/mastra.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type * as z3 from "zod/v3";
|
|
2
|
+
import type * as z4 from "zod/v4";
|
|
3
|
+
import type { ToolCallExtra } from "./mcp.js";
|
|
4
|
+
import type { ZodRawShapeCompat } from "./zod.js";
|
|
5
|
+
import { type AttachOptions, type BuiltinTool, type Identity } from "./events.js";
|
|
6
|
+
import { CallEnded, type ToolCall, type WakeArgs, type WakeInputs, type WakeInputsV3, type WakeInputsV4, type WakeOutputShape, type WakeOutputV3, type WakeOutputV4 } from "./toolcall.js";
|
|
7
|
+
import type { Wakeshift } from "./wakeshift.js";
|
|
8
|
+
/** The @mastra/mcp releases serve() is built for: 2.x, from 2.7. */
|
|
9
|
+
export declare const MASTRA_MCP_RANGE = ">=2.7.0 <3.0.0";
|
|
10
|
+
/**
|
|
11
|
+
* The request context Wakeshift reads in Mastra: the MCP SDK 2.x's
|
|
12
|
+
* ServerContext (`ctx`) of the request, the verified token in
|
|
13
|
+
* `ctx.http?.authInfo`. A Mastra tool sees the same as `context.mcp.extra`.
|
|
14
|
+
*/
|
|
15
|
+
export type MastraContext = ToolCallExtra;
|
|
16
|
+
/** attachMastra's options: attach's, with identify and events.native getting the request's ctx. */
|
|
17
|
+
export type MastraAttachOptions = AttachOptions<MastraContext>;
|
|
18
|
+
/** The second argument Mastra passes a tool's execute: what Wakeshift reads of it. */
|
|
19
|
+
export interface MastraToolContext {
|
|
20
|
+
mcp?: {
|
|
21
|
+
extra?: {
|
|
22
|
+
mcpReq?: object;
|
|
23
|
+
[k: string]: any;
|
|
24
|
+
};
|
|
25
|
+
[k: string]: any;
|
|
26
|
+
};
|
|
27
|
+
[k: string]: any;
|
|
28
|
+
}
|
|
29
|
+
/** Wakeshift in a Mastra MCPServer: make it with attachMastra, then serve your MCPServer with it. */
|
|
30
|
+
export declare class MastraAttached {
|
|
31
|
+
private readonly wake;
|
|
32
|
+
private readonly opts;
|
|
33
|
+
private readonly wrapped;
|
|
34
|
+
private readonly scopes;
|
|
35
|
+
private readonly shapes;
|
|
36
|
+
private readonly served;
|
|
37
|
+
/**
|
|
38
|
+
* A tool's input schema plus Wakeshift's three optional arguments
|
|
39
|
+
* (session_key, harness and conversation_id), for a wrapped tool's
|
|
40
|
+
* inputSchema: `inputSchema: attached.inputs(z.object({ … }))`.
|
|
41
|
+
*/
|
|
42
|
+
inputs<O extends z4.ZodObject>(schema: O): z4.ZodObject<O["shape"] & WakeInputsV4, O["_zod"]["config"]>;
|
|
43
|
+
inputs<O extends z3.AnyZodObject>(schema: O): z3.ZodObject<O["shape"] & WakeInputsV3, O["_def"]["unknownKeys"], O["_def"]["catchall"]>;
|
|
44
|
+
inputs<S extends ZodRawShapeCompat>(shape: S): S & WakeInputs;
|
|
45
|
+
/** A tool's output schema plus an optional wakeshift string, for a wrapped tool's outputSchema. Optional: see attach's. */
|
|
46
|
+
output<O extends z4.ZodObject>(schema: O): z4.ZodObject<O["shape"] & WakeOutputV4, O["_zod"]["config"]>;
|
|
47
|
+
output<O extends z3.AnyZodObject>(schema: O): z3.ZodObject<O["shape"] & WakeOutputV3, O["_def"]["unknownKeys"], O["_def"]["catchall"]>;
|
|
48
|
+
output<S extends ZodRawShapeCompat>(shape: S): S & WakeOutputShape;
|
|
49
|
+
/**
|
|
50
|
+
* Wraps a Mastra tool's execute, for the tool your MCPServer serves under
|
|
51
|
+
* the same name (its key in `tools`): `execute: attached.events("watch_ci",
|
|
52
|
+
* async (args, call) => …)`. handler gets your own arguments, a ToolCall
|
|
53
|
+
* and Mastra's tool context, and returns what a Mastra tool returns (a
|
|
54
|
+
* string, or your output). The reply an MCP client gets carries what
|
|
55
|
+
* Wakeshift adds for the conversation; a call that fails ends the
|
|
56
|
+
* subscriptions it made. Its inputSchema must come from attached.inputs.
|
|
57
|
+
* Run outside an MCP request (by an agent, a workflow or Studio), the
|
|
58
|
+
* handler gets a call whose subscribe throws.
|
|
59
|
+
*/
|
|
60
|
+
events<P extends Record<string, any> = Record<string, any>, R = unknown>(name: string, handler: (args: P, call: ToolCall<MastraContext>, context: MastraToolContext) => R | Promise<R>): (input: P & WakeArgs, context?: any) => Promise<R>;
|
|
61
|
+
/**
|
|
62
|
+
* Serves Wakeshift from your MCPServer (@mastra/mcp 2.7 or later, 2.x),
|
|
63
|
+
* after your last defineEvent: attach's own tools, MCP Events for clients
|
|
64
|
+
* that speak it, the wait tool, and your wrapped tools' blocks. Returns
|
|
65
|
+
* the server. Call it once, before the server takes requests.
|
|
66
|
+
*/
|
|
67
|
+
serve<S extends object>(server: S): S;
|
|
68
|
+
private attachTo;
|
|
69
|
+
private wrapCalls;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Wakeshift for a Mastra MCPServer (@mastra/mcp 2.7 or later). Returns the
|
|
73
|
+
* handle your tools use (attached.inputs, attached.events) and that serves
|
|
74
|
+
* your MCPServer (attached.serve). Options are attach's; identify and
|
|
75
|
+
* events.native get the request's MCP context (ctx: the verified token is
|
|
76
|
+
* ctx.http?.authInfo).
|
|
77
|
+
*/
|
|
78
|
+
export declare function attachMastra(wake: Wakeshift, opts?: MastraAttachOptions): MastraAttached;
|
|
79
|
+
export { CallEnded, type BuiltinTool, type Identity, type ToolCall, type WakeArgs };
|
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
|
}
|