@wakeshift/sdk 0.2.0 → 0.4.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.
@@ -0,0 +1,558 @@
1
+ // One way to subscribe from a tool (docs/subscribe-from-tools.md): a tool
2
+ // callback wrapped by the handle attach returns (attached.events) gets a
3
+ // ToolCall, and subscribes the calling conversation with call.subscribe
4
+ // (ends it with call.unsubscribe). The wrapper adds one Wakeshift block to
5
+ // whatever the handler returns, so a conversation can't be left unwakeable
6
+ // by a forgotten argument, a wrong tool name or an unreturned result; and a
7
+ // call that fails ends the subscriptions it made. A port of the Go SDK's
8
+ // appkit.Events (pkg/appkit/toolcall.go, attachcall.go).
9
+ //
10
+ // const attached = wake.attach(server, { identify: (extra) => ({ user: userOf(extra) }) })
11
+ // server.registerTool("join",
12
+ // { description: "…", inputSchema: attached.inputs({ table: z.string() }) },
13
+ // attached.events("join", async ({ table }, call) => {
14
+ // const sub = await call.subscribe(turn, { table }, { reason: `Playing at ${table}` })
15
+ // await casino.seat(call.user!, table, sub.id)
16
+ // return `You're seated at ${table}.`
17
+ // }))
18
+ import { z } from "zod";
19
+ import * as z3 from "zod/v3";
20
+ import * as z4 from "zod/v4";
21
+ import { getObjectShape, isZ4Schema, normalizeObjectSchema, safeParseAsync } from "@modelcontextprotocol/sdk/server/zod-compat.js";
22
+ import { WakeshiftError } from "./errors.js";
23
+ import { EXTENSION_KEY, dialectOf } from "./spec.js";
24
+ import { packCall } from "./wakeshift.js";
25
+ // Wakeshift's three arguments (wakeshift.ts's SESSION_ARG, HARNESS_ARG and
26
+ // CONVERSATION_ARG, repeated: that module imports this one).
27
+ const SESSION_ARG = "session_key";
28
+ const HARNESS_ARG = "harness";
29
+ const CONVERSATION_ARG = "conversation_id";
30
+ // What the agent is told each of Wakeshift's arguments is: the Go SDK's
31
+ // WakeArgs, word for word.
32
+ const SESSION_KEY_DESCRIPTION = "This conversation's session_key, from an earlier result (a secret token, or a handle such as claude-code#2). Keep it to this conversation; don't share or log it.";
33
+ const HARNESS_DESCRIPTION = "The program you're running in: codex, claude-code, cursor (local CLI or IDE), cursor-cloud (Cursor cloud agent), grok-bot, copilot-cli, opencode, antigravity, openclaw (even when it runs your turns in Codex), hermes, chatgpt, or your program's name if it isn't listed";
34
+ const CONVERSATION_DESCRIPTION = "Your own ID for this conversation, if your program gives you one. Cursor and Grok Bot: the output of `printenv CURSOR_CONVERSATION_ID`. Codex and Antigravity send it automatically; others leave it empty";
35
+ const WAKESHIFT_OUTPUT_DESCRIPTION = "What Wakeshift adds for this conversation: how it's woken by this app's events";
36
+ const wakeInputs = {
37
+ [SESSION_ARG]: z.string().optional().describe(SESSION_KEY_DESCRIPTION),
38
+ [HARNESS_ARG]: z.string().optional().describe(HARNESS_DESCRIPTION),
39
+ [CONVERSATION_ARG]: z.string().optional().describe(CONVERSATION_DESCRIPTION),
40
+ };
41
+ /** @internal Wakeshift's three arguments as attach's own tools take them: worded as a wrapped tool's (test/events-list.json pins it). */
42
+ export const toolArgs = wakeInputs;
43
+ const wakeOutput = { wakeshift: z.string().optional().describe(WAKESHIFT_OUTPUT_DESCRIPTION) };
44
+ // The same in each of zod's two APIs: MCP refuses a shape that mixes them,
45
+ // so a shape written with zod/v3 (or zod/v4) gets Wakeshift's fields in its
46
+ // own (zod 3.25 and later carry both).
47
+ const byVersion = {
48
+ v3: {
49
+ inputs: {
50
+ [SESSION_ARG]: z3.string().optional().describe(SESSION_KEY_DESCRIPTION),
51
+ [HARNESS_ARG]: z3.string().optional().describe(HARNESS_DESCRIPTION),
52
+ [CONVERSATION_ARG]: z3.string().optional().describe(CONVERSATION_DESCRIPTION),
53
+ },
54
+ output: { wakeshift: z3.string().optional().describe(WAKESHIFT_OUTPUT_DESCRIPTION) },
55
+ },
56
+ v4: {
57
+ inputs: {
58
+ [SESSION_ARG]: z4.string().optional().describe(SESSION_KEY_DESCRIPTION),
59
+ [HARNESS_ARG]: z4.string().optional().describe(HARNESS_DESCRIPTION),
60
+ [CONVERSATION_ARG]: z4.string().optional().describe(CONVERSATION_DESCRIPTION),
61
+ },
62
+ output: { wakeshift: z4.string().optional().describe(WAKESHIFT_OUTPUT_DESCRIPTION) },
63
+ },
64
+ };
65
+ // fieldsFor is Wakeshift's fields in the zod API shape's own fields use
66
+ // (the default import's for an empty shape).
67
+ function fieldsFor(shape, kind) {
68
+ const first = Object.values(shape ?? {})[0];
69
+ const fields = !first ? (kind === "inputs" ? wakeInputs : wakeOutput) : byVersion[isZ4Schema(first) ? "v4" : "v3"][kind];
70
+ return fields;
71
+ }
72
+ const WAKE_KEYS = [SESSION_ARG, HARNESS_ARG, CONVERSATION_ARG];
73
+ /**
74
+ * @internal The tools/call _meta keys a harness names its conversation in
75
+ * (the core's harnesses ConversationMeta, dotted for nested objects):
76
+ * test/conversation-meta.json, which a Go test keeps current.
77
+ */
78
+ export const CONVERSATION_META = [
79
+ "agent-session-id",
80
+ "antigravity.google/conversation_id",
81
+ "vscode.conversationId",
82
+ "wakeshift/conversation",
83
+ "x-codex-turn-metadata.thread_id",
84
+ ];
85
+ /** A ToolCall used after its handler returned. */
86
+ export class CallEnded extends Error {
87
+ constructor() {
88
+ super("wakeshift: the tool call has ended: subscribe before the handler returns");
89
+ this.name = "CallEnded";
90
+ }
91
+ }
92
+ // The line a text-carried beacon is on (the core's beacon.Text), which a
93
+ // harness's hook reads only as the last line of a result's text.
94
+ const BEACON_LINE_PREFIX = "(For Wakeshift's hook on this computer, not for you: wakeshift-beacon: ";
95
+ const CONNECTOR_META_KEY = "wakeshift/connector";
96
+ // The most native lines Wakeshift takes for one call.
97
+ const MAX_NATIVE_LINES = 20;
98
+ // takeTrailer takes a beacon line off the end of the result's text
99
+ // (dropping its block if nothing else is in it), returning it ("" if
100
+ // there's none).
101
+ function takeTrailer(content) {
102
+ const last = content.at(-1);
103
+ if (!last || last.type !== "text")
104
+ return "";
105
+ const text = last.text.replace(/[ \t\r\n]+$/, "");
106
+ const i = text.lastIndexOf("\n");
107
+ const line = i >= 0 ? text.slice(i + 1) : text;
108
+ if (!line.startsWith(BEACON_LINE_PREFIX) || !line.endsWith(")"))
109
+ return "";
110
+ const rest = i >= 0 ? text.slice(0, i).replace(/[ \t\r\n]+$/, "") : "";
111
+ if (rest === "")
112
+ content.pop();
113
+ else
114
+ content[content.length - 1] = { ...last, text: rest };
115
+ return line;
116
+ }
117
+ /**
118
+ * @internal Adds the call's block to a wrapped handler's result: Wakeshift's
119
+ * text as a block after the app's content, then a text-carried beacon last,
120
+ * and _meta; a string result stays one text block. A result that already
121
+ * carries a beacon keeps it, last, and gets no second one. With wakeOutput,
122
+ * structuredContent's wakeshift is the block's text. test/wrapped-block.json
123
+ * is the shape, shared with the other SDKs.
124
+ */
125
+ export function mergeBlock(result, add, wakeOutput) {
126
+ const isString = typeof result === "string";
127
+ const base = isString ? { content: [{ type: "text", text: result }] } : result;
128
+ const out = { ...base, content: [...(base.content ?? [])] };
129
+ const text = (add.text ?? "").trim();
130
+ const metaBeacon = !!out._meta && CONNECTOR_META_KEY in out._meta;
131
+ const existing = takeTrailer(out.content);
132
+ if (text)
133
+ out.content.push({ type: "text", text });
134
+ const has = metaBeacon || existing !== "";
135
+ if (existing)
136
+ out.content.push({ type: "text", text: existing });
137
+ else if (!has && add.trailer)
138
+ out.content.push({ type: "text", text: add.trailer });
139
+ for (const [k, v] of Object.entries(add.meta ?? {})) {
140
+ if (k === CONNECTOR_META_KEY && has)
141
+ continue;
142
+ out._meta = { ...(out._meta ?? {}), [k]: v };
143
+ }
144
+ if (isString) {
145
+ const texts = out.content.map((c) => (c.type === "text" ? c.text : "")).filter((t) => t !== "");
146
+ out.content = [{ type: "text", text: texts.join("\n\n") }];
147
+ }
148
+ if (text && wakeOutput && out.structuredContent && typeof out.structuredContent === "object" && !Array.isArray(out.structuredContent)) {
149
+ out.structuredContent = { ...out.structuredContent, wakeshift: text };
150
+ }
151
+ return out;
152
+ }
153
+ // metaString is the string at key in a tool call's _meta: the key itself,
154
+ // else a dotted path into nested objects (the core's harnesses.metaString).
155
+ function metaString(meta, key) {
156
+ if (!meta)
157
+ return "";
158
+ const limit = (s) => (typeof s === "string" && s.length <= 200 ? s : "");
159
+ if (typeof meta[key] === "string")
160
+ return limit(meta[key]);
161
+ let cur = meta;
162
+ for (const part of key.split(".")) {
163
+ if (!cur || typeof cur !== "object" || Array.isArray(cur))
164
+ return "";
165
+ cur = cur[part];
166
+ }
167
+ return limit(cur);
168
+ }
169
+ /** @internal Whether a tool call's _meta names its conversation, the way a harness puts it there (test/conversation-meta.json). */
170
+ export function namesConversation(meta) {
171
+ return CONVERSATION_META.some((k) => metaString(meta, k) !== "");
172
+ }
173
+ function sameJSON(a, b) {
174
+ const norm = (v) => v === null || v === undefined
175
+ ? {}
176
+ : Array.isArray(v)
177
+ ? v.map(norm)
178
+ : typeof v === "object"
179
+ ? Object.fromEntries(Object.keys(v).sort().map((k) => [k, norm(v[k])]))
180
+ : v;
181
+ return JSON.stringify(norm(a)) === JSON.stringify(norm(b));
182
+ }
183
+ function detailOf(err) {
184
+ return err instanceof WakeshiftError ? err.detail : String(err?.message ?? err);
185
+ }
186
+ function registeredTool(server, name) {
187
+ const tools = server._registeredTools;
188
+ if (!tools || typeof tools !== "object")
189
+ return null; // an MCP SDK that keeps them elsewhere
190
+ return tools[name];
191
+ }
192
+ /** One tools/call of a wrapped tool: what its handler subscribed, for the block and the rollback. */
193
+ class CallScope {
194
+ at;
195
+ args;
196
+ user;
197
+ tool;
198
+ native = false;
199
+ nativeConversation = null;
200
+ session;
201
+ recognised = null;
202
+ extra;
203
+ ended = false;
204
+ pending = "";
205
+ armed = false;
206
+ created = [];
207
+ nativeLines = [];
208
+ inflight = new Set();
209
+ queue = Promise.resolve();
210
+ // The client's name identify gave, for a request that names none.
211
+ identified;
212
+ constructor(at, tool, extra, args) {
213
+ this.at = at;
214
+ this.args = args;
215
+ this.tool = tool;
216
+ this.extra = extra;
217
+ const id = at.identify(extra);
218
+ this.user = id?.user;
219
+ this.identified = id?.client;
220
+ this.session = args.session || null;
221
+ const n = nativeOf(at, extra, this.identified);
222
+ this.native = n.native;
223
+ this.nativeConversation = n.conversation;
224
+ }
225
+ // wire is the call as Wakeshift takes it: the request's client, _meta and
226
+ // User-Agent, the agent's harness and conversation_id arguments, the
227
+ // conversation's session, and who's calling.
228
+ wire(session = this.session) {
229
+ const meta = this.extra._meta;
230
+ return packCall({
231
+ meta,
232
+ clientName: this.at.clientOf(meta, this.identified),
233
+ user: this.user,
234
+ session: session ?? undefined,
235
+ userAgent: userAgentOf(this.extra),
236
+ harness: this.args.harness,
237
+ conversation: this.args.conversation,
238
+ tool: this.tool,
239
+ });
240
+ }
241
+ // request runs one request of the call to Wakeshift, after any before it
242
+ // (one at a time per call), refusing once the handler has returned.
243
+ request(fn) {
244
+ if (this.ended)
245
+ return Promise.reject(new CallEnded());
246
+ const run = this.queue.then(() => (this.ended ? Promise.reject(new CallEnded()) : fn()));
247
+ this.queue = run.catch(() => undefined);
248
+ this.inflight.add(run);
249
+ run.then(() => this.inflight.delete(run), () => this.inflight.delete(run));
250
+ return run;
251
+ }
252
+ checkEvent(event) {
253
+ if (!event || typeof event.owner !== "function" || event.owner() !== this.at.wake) {
254
+ throw new WakeshiftError(404, "unknown_event", `${event?.name ?? "that"} is another app's event: this app doesn't offer it`);
255
+ }
256
+ }
257
+ subscribe(event, args, opts = {}) {
258
+ return this.request(async () => {
259
+ this.checkEvent(event);
260
+ const known = event.parseArgs(args);
261
+ if (this.native) {
262
+ this.addNative(event.name, known);
263
+ return { id: "", user: this.user ?? "", created: false, session: "", recognised: { harness: null, via: null, claimed: null }, native: true };
264
+ }
265
+ if (event.personal && !this.user) {
266
+ throw new WakeshiftError(400, "bad_arguments", `${event.name} is personal: call.subscribe needs a user (attach's identify), who the subscription belongs to`);
267
+ }
268
+ const body = { call: this.wire(), name: event.name, arguments: known, defer_addition: true };
269
+ if (this.pending)
270
+ body.pending = this.pending;
271
+ if (opts.ttl !== undefined)
272
+ body.ttl = opts.ttl;
273
+ if (opts.reason)
274
+ body.reason = opts.reason;
275
+ if (opts.label)
276
+ body.label = opts.label;
277
+ const out = await this.at.wake.requestApp("POST", "/event-subscriptions", body);
278
+ if (out.pending)
279
+ this.pending = out.pending;
280
+ this.armed = true;
281
+ this.session = out.session;
282
+ const rec = recognisedOf(out.agent);
283
+ this.recognised = rec;
284
+ const es = out.event_subscription;
285
+ if (es.created)
286
+ this.created.push({ name: event.name, arguments: known, session: out.session, pending: out.pending ?? this.pending });
287
+ return { id: es.id, user: es.user ?? this.user ?? "", created: !!es.created, session: out.session, recognised: rec, native: false };
288
+ });
289
+ }
290
+ unsubscribe(event, args) {
291
+ return this.request(async () => {
292
+ this.checkEvent(event);
293
+ const known = event.parseArgs(args);
294
+ if (this.native)
295
+ return this.endNative(event.name, known);
296
+ const out = await this.at.wake.requestApp("POST", "/event-subscriptions/unsubscribe", { call: this.wire(), name: event.name, arguments: known });
297
+ return !!out.ended;
298
+ });
299
+ }
300
+ resolveSession() {
301
+ return this.request(async () => {
302
+ if (this.pending && this.session)
303
+ return this.session;
304
+ const out = await this.at.wake.requestApp("POST", "/calls/session", { call: this.wire() });
305
+ this.pending = out.pending;
306
+ this.session = out.session;
307
+ this.recognised = recognisedOf(out.agent);
308
+ return out.session;
309
+ });
310
+ }
311
+ // endNative ends the user's webhook subscriptions to event with these arguments.
312
+ async endNative(name, args) {
313
+ if (!this.user)
314
+ return false;
315
+ const out = await this.at.wake.requestApp("POST", "/event-subscriptions/list", {
316
+ user: this.user,
317
+ });
318
+ let ended = false;
319
+ for (const s of out.subscriptions ?? []) {
320
+ if (s.event !== name || s.destination !== "webhook" || !sameJSON(s.arguments, args))
321
+ continue;
322
+ const r = await this.at.wake.requestApp("POST", "/event-subscriptions/end", { user: this.user, id: s.id });
323
+ ended = ended || !!r.ended;
324
+ }
325
+ return ended;
326
+ }
327
+ // addNative adds a native line, once per event and arguments.
328
+ addNative(name, args) {
329
+ if (this.nativeLines.some((l) => l.name === name && sameJSON(l.arguments, args)))
330
+ return;
331
+ if (this.nativeLines.length === MAX_NATIVE_LINES) {
332
+ this.at.warnOnce("native:" + this.tool, `wakeshift: the wrapped tool ${this.tool} subscribed a native client to more than ${MAX_NATIVE_LINES} events in one call; only ${MAX_NATIVE_LINES} are named`);
333
+ return;
334
+ }
335
+ this.nativeLines.push({ name, arguments: args });
336
+ }
337
+ // end closes the call to further requests, after waiting for any in
338
+ // flight (warning once per tool: the handler should have waited for them).
339
+ async end() {
340
+ if (this.inflight.size > 0) {
341
+ this.at.warnOnce("inflight:" + this.tool, `wakeshift: the wrapped tool ${this.tool} returned while a subscribe was in flight; it was waited for: await call.subscribe, call.unsubscribe and call.resolveSession before returning`);
342
+ await Promise.allSettled([...this.inflight]);
343
+ }
344
+ this.ended = true;
345
+ }
346
+ // rollback ends every subscription the call created and no other call
347
+ // has renewed since (the core checks the pending record that made it),
348
+ // with reason rolled_back (audited as such, no "ended" notice), whatever
349
+ // became of the request.
350
+ async rollback() {
351
+ for (const c of this.created) {
352
+ try {
353
+ await this.at.wake.requestApp("POST", "/event-subscriptions/unsubscribe", { call: this.wire(c.session), name: c.name, arguments: c.arguments, reason: "rolled_back", pending: c.pending });
354
+ }
355
+ catch (err) {
356
+ this.at.wake.warn(`wakeshift: rolling back a failed tool call's subscription (${this.tool}, ${c.name}): ${detailOf(err)}`);
357
+ }
358
+ }
359
+ this.created = [];
360
+ }
361
+ // addition asks Wakeshift for the call's block: what the conversation
362
+ // needs to be woken (from the call's pending-addition record, if a
363
+ // subscribe or resolveSession made one; otherwise what any other tool
364
+ // result of the app's carries), and a line per event a native caller
365
+ // should subscribe to. A call that names no conversation, and subscribed
366
+ // nothing, gets nothing (null). Only a failure for a call that subscribed
367
+ // is thrown: without a subscription, the block is a courtesy.
368
+ async addition() {
369
+ const wire = this.wire();
370
+ if (!this.pending && this.nativeLines.length === 0 && !wire.session && !wire.conversation && !namesConversation(wire.meta))
371
+ return null;
372
+ const body = { call: wire };
373
+ if (this.pending)
374
+ body.pending = this.pending;
375
+ if (this.nativeLines.length > 0)
376
+ body.native = this.nativeLines;
377
+ try {
378
+ return await this.at.wake.requestApp("POST", "/calls/addition", body);
379
+ }
380
+ catch (err) {
381
+ if (this.armed || this.nativeLines.length > 0)
382
+ throw err;
383
+ this.at.wake.warn(`wakeshift: Wakeshift's addition to a result of ${this.tool}: ${detailOf(err)}`);
384
+ return null;
385
+ }
386
+ }
387
+ }
388
+ function recognisedOf(a) {
389
+ return { harness: a?.harness ?? null, via: a?.via ?? null, claimed: a?.claimed ?? null };
390
+ }
391
+ function headerOf(extra, name) {
392
+ const h = extra?.requestInfo?.headers;
393
+ if (!h)
394
+ return "";
395
+ const v = h[name] ?? h[name.toLowerCase()];
396
+ return (Array.isArray(v) ? v[0] : v) ?? "";
397
+ }
398
+ function userAgentOf(extra) {
399
+ return headerOf(extra, "user-agent") || headerOf(extra, "User-Agent");
400
+ }
401
+ function hasKey(o, key) {
402
+ return !!o && typeof o === "object" && !Array.isArray(o) && o[key] !== undefined;
403
+ }
404
+ /**
405
+ * @internal Whether a call comes from a client that subscribes to the app's
406
+ * events itself, on stateless servers too (which never see initialize), in
407
+ * this order: the app's override (events.native); ChatGPT's
408
+ * _meta["openai/session"]; a client name (initialize's, or _meta's, else
409
+ * identify's) or User-Agent of a known native client; a client that
410
+ * declares the MCP Events extension (initialize's capabilities, or
411
+ * _meta's). conversation is ChatGPT's conversation, when it names one.
412
+ */
413
+ export function nativeOf(at, extra, identified) {
414
+ const meta = (extra._meta ?? {});
415
+ const conversation = typeof meta["openai/session"] === "string" && meta["openai/session"] ? meta["openai/session"] : null;
416
+ if (at.events.native?.(extra))
417
+ return { native: true, conversation };
418
+ if (conversation)
419
+ return { native: true, conversation };
420
+ if (dialectOf(at.clientOf(meta, identified), userAgentOf(extra)) === "chatgpt")
421
+ return { native: true, conversation: null };
422
+ const caps = at.server.server.getClientCapabilities();
423
+ if (hasKey(caps?.extensions, EXTENSION_KEY))
424
+ return { native: true, conversation: null };
425
+ const metaCaps = meta["io.modelcontextprotocol/clientCapabilities"];
426
+ if (hasKey(metaCaps?.extensions, EXTENSION_KEY))
427
+ return { native: true, conversation: null };
428
+ return { native: false, conversation: null };
429
+ }
430
+ /**
431
+ * Wakeshift attached to an MCP server: the handle `wake.attach` returns.
432
+ * Wrap a tool's callback with events, and give its input schema
433
+ * Wakeshift's three arguments with inputs.
434
+ */
435
+ export class Attached {
436
+ at;
437
+ /** @internal */
438
+ wrapped = new Map();
439
+ /** @internal Use wake.attach. */
440
+ constructor(at) {
441
+ this.at = at;
442
+ }
443
+ /**
444
+ * shape plus Wakeshift's three optional arguments (session_key, harness
445
+ * and conversation_id), for a wrapped tool's inputSchema:
446
+ * `inputSchema: attached.inputs({ table: z.string() })`.
447
+ */
448
+ inputs(shape) {
449
+ for (const k of WAKE_KEYS)
450
+ if (k in (shape ?? {}))
451
+ throw new Error(`wakeshift: attached.inputs adds ${k} itself: leave it out of the shape`);
452
+ return { ...shape, ...fieldsFor(shape, "inputs") };
453
+ }
454
+ /**
455
+ * shape plus an optional wakeshift string, for a wrapped tool's
456
+ * outputSchema: the wrapper sets it to the block's text (without the
457
+ * beacon line), for hosts that show the model only structuredContent.
458
+ * The block stays in the content too.
459
+ */
460
+ output(shape) {
461
+ if ("wakeshift" in (shape ?? {}))
462
+ throw new Error("wakeshift: attached.output adds wakeshift itself: leave it out of the shape");
463
+ return { ...shape, ...fieldsFor(shape, "output") };
464
+ }
465
+ /**
466
+ * Wraps a tool's callback, for `server.registerTool(name, …)` with the
467
+ * same name (MCP doesn't tell a callback its tool's name): the handler
468
+ * gets the tool's own arguments and a ToolCall, and returns a string or
469
+ * a CallToolResult; the reply gets Wakeshift's block. The tool's
470
+ * inputSchema must come from attached.inputs (a type error otherwise).
471
+ * Register it on the server passed to attach.
472
+ */
473
+ events(name, handler) {
474
+ if (!name || typeof name !== "string")
475
+ throw new Error("wakeshift: attached.events needs the tool's name, as registered");
476
+ if (typeof handler !== "function")
477
+ throw new Error("wakeshift: attached.events needs a handler");
478
+ const cb = (async (args, extra) => this.run(name, cb, handler, args ?? {}, extra));
479
+ this.wrapped.set(name, cb);
480
+ return cb;
481
+ }
482
+ async run(name, cb, handler, input, extra) {
483
+ const tool = registeredTool(this.at.server, name);
484
+ if (tool === undefined || (tool && (tool.handler ?? tool.callback) !== cb)) {
485
+ throw new Error(`wakeshift: attached.events("${name}", …) isn't the tool named ${name} on the server passed to attach: register it with the same name, on that server`);
486
+ }
487
+ let wakeOutput = false;
488
+ let outputSchema;
489
+ if (tool) {
490
+ const shape = getObjectShape(normalizeObjectSchema(tool.inputSchema));
491
+ if (!shape || WAKE_KEYS.some((k) => !(k in shape))) {
492
+ throw new Error(`wakeshift: the tool ${name} doesn't take Wakeshift's arguments: make its inputSchema with attached.inputs(…)`);
493
+ }
494
+ outputSchema = tool.outputSchema;
495
+ const out = getObjectShape(normalizeObjectSchema(outputSchema));
496
+ wakeOutput = !!out && "wakeshift" in out;
497
+ }
498
+ else {
499
+ 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");
500
+ }
501
+ const str = (k) => (typeof input[k] === "string" && input[k] ? input[k] : undefined);
502
+ const call = new CallScope(this.at, name, extra, { session: str(SESSION_ARG), harness: str(HARNESS_ARG), conversation: str(CONVERSATION_ARG) });
503
+ const appArgs = {};
504
+ for (const [k, v] of Object.entries(input))
505
+ if (!WAKE_KEYS.includes(k))
506
+ appArgs[k] = v;
507
+ let result;
508
+ try {
509
+ result = await handler(appArgs, call);
510
+ }
511
+ catch (err) {
512
+ await call.end();
513
+ await call.rollback();
514
+ throw err;
515
+ }
516
+ await call.end();
517
+ // Rollback is decided from the final result, the one the client would
518
+ // receive: MCP validates the output after this callback returns, so
519
+ // the wrapper checks it the same way first.
520
+ if (result === null || result === undefined || (typeof result !== "string" && typeof result !== "object")) {
521
+ await call.rollback();
522
+ return result;
523
+ }
524
+ if ((typeof result === "object" && result.isError) || extra?.signal?.aborted || !(await validOutput(outputSchema, result))) {
525
+ await call.rollback();
526
+ return typeof result === "string" ? { content: [{ type: "text", text: result }] } : result;
527
+ }
528
+ let add;
529
+ try {
530
+ add = await call.addition();
531
+ }
532
+ catch (err) {
533
+ // The conversation is subscribed but wouldn't learn how it's woken:
534
+ // the call fails, as one whose handler failed does.
535
+ await call.rollback();
536
+ this.at.wake.warn(`wakeshift: Wakeshift's block for a result of ${name} failed, so the call's subscriptions were rolled back: ${detailOf(err)}`);
537
+ return { isError: true, content: [{ type: "text", text: `Wakeshift couldn't finish setting up this conversation's wakes (${detailOf(err)}). Try again.` }] };
538
+ }
539
+ const out = mergeBlock(result, add ?? {}, wakeOutput);
540
+ if ((add?.text ?? "").trim() && !wakeOutput && out.structuredContent) {
541
+ this.at.warnOnce("output:" + name, `wakeshift: the wrapped tool ${name} returns structuredContent without attached.output: hosts that show only structuredContent won't see how this conversation is woken`);
542
+ }
543
+ return out;
544
+ }
545
+ }
546
+ // validOutput mirrors McpServer's own check of a result against the tool's
547
+ // outputSchema (validateToolOutput): one it would refuse fails the call.
548
+ async function validOutput(outputSchema, result) {
549
+ if (!outputSchema)
550
+ return true;
551
+ if (typeof result === "string" || !result.structuredContent)
552
+ return false;
553
+ const obj = normalizeObjectSchema(outputSchema);
554
+ if (!obj)
555
+ return true;
556
+ return (await safeParseAsync(obj, result.structuredContent)).success;
557
+ }
558
+ //# sourceMappingURL=toolcall.js.map