@wakeshift/sdk 0.3.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.
package/dist/events.js CHANGED
@@ -11,8 +11,9 @@ import { McpError } from "@modelcontextprotocol/sdk/types.js";
11
11
  import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
12
12
  import { objectFromShape, safeParse } from "@modelcontextprotocol/sdk/server/zod-compat.js";
13
13
  import { WakeshiftError } from "./errors.js";
14
- import { EXTENSION_KEY, HEADER_MISMATCH, INTERNAL_ERROR, INVALID_PARAMS, LEGACY_KEY, NAME_HEADER, RESULT_TYPE, codesFor, dialectOf, headerValue, } from "./spec.js";
15
- import { applyAddition } from "./wakeshift.js";
14
+ import { EXTENSION_KEY, HEADER_MISMATCH, INTERNAL_ERROR, INVALID_PARAMS, TOP_LEVEL_KEY, NAME_HEADER, RESULT_TYPE, codesFor, dialectOf, headerValue, } from "./spec.js";
15
+ import { toolCallOf } from "./wakeshift.js";
16
+ import { Attached, toolArgs } from "./toolcall.js";
16
17
  const nameRE = /^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*){0,4}$/;
17
18
  const argNameRE = /^[a-z][a-z0-9_]{0,63}$/;
18
19
  // jsonSchemaOf is a zod schema as JSON Schema, without $schema (Wakeshift
@@ -115,7 +116,7 @@ export class WakeEvent {
115
116
  // ev/<name>/{a}/{b}: one segment per required scalar argument; an
116
117
  // optional one filters on the payload field of its name.
117
118
  const segs = ["ev", name];
118
- filter = filter ?? {};
119
+ filter = { ...filter }; // the caller's object stays as it was
119
120
  for (const arg of Object.keys(props).sort()) {
120
121
  if (!argNameRE.test(arg))
121
122
  throw new Error(`wakeshift: event ${name}: argument ${JSON.stringify(arg)}: lowercase letters, digits and _`);
@@ -127,6 +128,9 @@ export class WakeEvent {
127
128
  segs.push(`{${arg}}`);
128
129
  }
129
130
  else if (scalar && arg in payloadProps) {
131
+ // Both on the same field is refused: neither can quietly win.
132
+ if (opts.filter && Object.prototype.hasOwnProperty.call(opts.filter, arg))
133
+ throw new Error(`wakeshift: event ${name}: filter on ${arg} conflicts with optional argument ${arg}, which already filters that payload field (remove it from filter, or pass topic and filter)`);
130
134
  filter[arg] = `{${arg}}`;
131
135
  }
132
136
  else {
@@ -173,24 +177,65 @@ export class WakeEvent {
173
177
  d.title = this.title;
174
178
  return d;
175
179
  }
180
+ /** @internal The client the event was defined on. */
181
+ owner() {
182
+ return this.wake;
183
+ }
176
184
  /**
177
185
  * Sends one occurrence: to everyone subscribed with these arguments, or,
178
- * for a personal event, to the one subscriber `to` names. The payload is
186
+ * for a personal event, to the recipients `to` names. The payload is
179
187
  * checked against the event's schema first.
180
188
  */
181
189
  async emit(args, payload, opts = {}) {
182
190
  const a = this.parseArgs(args);
183
191
  const p = safeParse(this.payloadSchema, payload);
184
192
  if (!p.success)
185
- throw new WakeshiftError(400, "bad_payload", `payload for ${this.name}: ${issues(p.error)}`);
186
- const to = opts.to?.trim();
193
+ throw new ArgsError(`payload for ${this.name}: ${issues(p.error)}`);
194
+ const to = this.wireTo(opts.to);
187
195
  if (this.personal && !to)
188
- throw new WakeshiftError(400, "bad_arguments", `${this.name} is personal: pass { to }, the one subscriber it's for`);
196
+ throw new WakeshiftError(400, "bad_arguments", `${this.name} is personal: pass { to }, the subscribers it's for`);
189
197
  if (!this.personal && to)
190
198
  throw new WakeshiftError(400, "bad_arguments", `${this.name} isn't personal: it goes to everyone subscribed with these arguments, so it takes no to`);
191
199
  const text = this.opts.text(p.data, a);
192
200
  return this.wake.emitEvent({ name: this.name, arguments: a, payload: p.data, text, id: opts.id, to });
193
201
  }
202
+ // wireTo is what the core takes for to: one plain user as a string,
203
+ // anything else as a list of "user", { user, subscription } and
204
+ // { native: user }, each name trimmed of the whitespace around it, as every
205
+ // SDK does (test/emit-recipients.json). undefined: no recipients.
206
+ wireTo(to) {
207
+ if (to === undefined)
208
+ return undefined;
209
+ const empty = (what) => new WakeshiftError(400, "bad_arguments", `${this.name}: a recipient's ${what} is empty`);
210
+ const out = [];
211
+ for (const r of Array.isArray(to) ? to : [to]) {
212
+ if (typeof r === "string") {
213
+ if (!r.trim())
214
+ throw empty("user");
215
+ out.push(r.trim());
216
+ }
217
+ else if (r && typeof r === "object" && "native" in r && typeof r.native === "string") {
218
+ if (!r.native.trim())
219
+ throw empty("user");
220
+ out.push({ native: r.native.trim() });
221
+ }
222
+ else if (r && typeof r === "object" && "user" in r && typeof r.user === "string" && typeof r.subscription === "string") {
223
+ if (!r.user.trim())
224
+ throw empty("user");
225
+ if (!r.subscription.trim())
226
+ throw empty("subscription");
227
+ out.push({ user: r.user.trim(), subscription: r.subscription.trim() });
228
+ }
229
+ else {
230
+ throw new WakeshiftError(400, "bad_arguments", `${this.name}: a recipient is a user, { user, subscription } or { native: user }`);
231
+ }
232
+ }
233
+ if (out.length === 0)
234
+ return undefined;
235
+ if (out.length === 1 && typeof out[0] === "string")
236
+ return out[0];
237
+ return out;
238
+ }
194
239
  /**
195
240
  * A capability URL that emits this event with these arguments (a personal
196
241
  * event: to the one subscriber `to` names), for services that can only
@@ -201,6 +246,9 @@ export class WakeEvent {
201
246
  */
202
247
  async publishUrl(args, opts = {}) {
203
248
  const a = this.parseArgs(args);
249
+ if (opts.to !== undefined && typeof opts.to !== "string") {
250
+ throw new WakeshiftError(400, "bad_arguments", `${this.name}: a publish URL's to is exactly one user`);
251
+ }
204
252
  const to = opts.to?.trim();
205
253
  if (this.personal && !to)
206
254
  throw new WakeshiftError(400, "bad_arguments", `${this.name} is personal: pass { to }, the one subscriber it's for`);
@@ -212,64 +260,86 @@ export class WakeEvent {
212
260
  return this.wake.requestApp("POST", "/events/publish-urls", body);
213
261
  }
214
262
  /**
215
- * Subscribes the conversation calling your tool, the way subscribe_events
216
- * would, so a tool like "join" can set up its own wakes in one step. For
217
- * clients that subscribe through Wakeshift's wake paths; native MCP Events
218
- * clients subscribe themselves. Your tool decides who may: authorize isn't
219
- * consulted here. user is the app's ID for the person (required for a
220
- * personal event); wakeArgs are your tool's wake.args (session_key,
221
- * harness, conversation_id); tool is your tool's name (Wakeshift's hooks
222
- * act on a connector beacon only from the result of the tool it names).
263
+ * How many conversations are subscribed to this event with these
264
+ * arguments (a personal event: those of the one user `to` names), how
265
+ * many are connected now, and how. Only reads.
223
266
  */
224
- async subscribe(extra, args, opts) {
267
+ async status(args, opts = {}) {
225
268
  const a = this.parseArgs(args);
226
- if (this.personal && !opts.user) {
227
- throw new WakeshiftError(400, "bad_arguments", `${this.name} is personal: pass user, who the subscription belongs to`);
269
+ if (opts.to !== undefined && typeof opts.to !== "string") {
270
+ throw new WakeshiftError(400, "bad_arguments", `${this.name}: status's to is exactly one user`);
228
271
  }
229
- const out = await this.wake.requestApp("POST", "/event-subscriptions", {
230
- call: this.wake.call(extra, { server: opts.server, client: opts.client, user: opts.user, args: opts.wakeArgs ?? {}, tool: opts.tool }),
231
- name: this.name,
232
- arguments: a,
233
- });
234
- const addition = out.addition ?? {};
235
- return {
236
- id: out.event_subscription.id,
237
- created: out.event_subscription.created,
238
- session: out.session,
239
- agent: out.agent ?? { harness: null, via: null, claimed: null },
240
- result: (text) => applyAddition(addition, text),
241
- };
272
+ const to = opts.to?.trim();
273
+ if (this.personal && !to)
274
+ throw new WakeshiftError(400, "bad_arguments", `${this.name} is personal: pass { to }, the one user to count for`);
275
+ if (!this.personal && to)
276
+ throw new WakeshiftError(400, "bad_arguments", `${this.name} isn't personal: status takes no to`);
277
+ const body = { name: this.name, arguments: a };
278
+ if (to)
279
+ body.to = to;
280
+ const out = await this.wake.requestApp("POST", "/events/status", body);
281
+ return { subscriptions: out.subscriptions, connected: out.connected, via: { stream: 0, poll: 0, hooks: 0, webhook: 0, ...out.via } };
242
282
  }
243
283
  /**
244
- * Ends the calling conversation's subscription with these arguments, the
245
- * way unsubscribe_events would: for a tool like "unwatch". False if it
246
- * wasn't subscribed.
284
+ * Reads subscriptions to this event by ID, each with the user
285
+ * `call.subscribe` made it for, up to 1,000 at once: whether each is live
286
+ * and its conversation connected now, and how. Only reads.
247
287
  */
248
- async unsubscribe(extra, args, opts) {
249
- const a = this.parseArgs(args);
250
- const out = await this.wake.requestApp("POST", "/event-subscriptions/unsubscribe", {
251
- call: this.wake.call(extra, { server: opts.server, client: opts.client, user: opts.user, args: opts.wakeArgs ?? {}, tool: opts.tool }),
252
- name: this.name,
253
- arguments: a,
288
+ async statusOf(subs) {
289
+ if (subs.length === 0)
290
+ return [];
291
+ if (subs.length > MAX_STATUS_SUBSCRIPTIONS) {
292
+ throw new WakeshiftError(400, "bad_request", `${this.name}: at most ${MAX_STATUS_SUBSCRIPTIONS} subscriptions in one statusOf`);
293
+ }
294
+ const refs = subs.map((s) => {
295
+ if (!s || typeof s.user !== "string" || !s.user.trim() || typeof s.subscription !== "string" || !s.subscription.trim()) {
296
+ throw new WakeshiftError(400, "bad_request", `${this.name}: statusOf needs each subscription's user and subscription`);
297
+ }
298
+ return { user: s.user.trim(), id: s.subscription.trim() };
299
+ });
300
+ const out = await this.wake.requestApp("POST", "/events/status", { name: this.name, subscriptions: refs });
301
+ return (out.subscriptions ?? []).map((s) => {
302
+ const r = { id: s.id, user: s.user, live: !!s.live, connected: !!s.connected };
303
+ if (s.via)
304
+ r.via = s.via;
305
+ return r;
254
306
  });
255
- return !!out.ended;
256
307
  }
257
- /** Ends one of user's subscriptions to this app's events, by its ID (from subscribe or list_subscriptions). False if there was none. */
308
+ /**
309
+ * Ends one of user's subscriptions to this app's events, by its ID (a
310
+ * Subscription's id from call.subscribe, or list_subscriptions'), outside
311
+ * a tool: a player who timed out, a member removed. False if there was none.
312
+ */
258
313
  async end(user, id) {
259
314
  const out = await this.wake.requestApp("POST", "/event-subscriptions/end", { user, id });
260
315
  return !!out.ended;
261
316
  }
262
317
  }
263
- /** A subscriber's arguments don't match the event's schema. */
264
- export class ArgsError extends Error {
318
+ /** The most subscriptions one `statusOf` reads. */
319
+ export const MAX_STATUS_SUBSCRIPTIONS = 1000;
320
+ /**
321
+ * An event's arguments or payload don't match its schema: a WakeshiftError
322
+ * with code bad_arguments (status 400), as Wakeshift answers the same
323
+ * mistake, thrown before asking it.
324
+ */
325
+ export class ArgsError extends WakeshiftError {
326
+ constructor(message) {
327
+ super(400, "bad_arguments", message);
328
+ this.name = "ArgsError";
329
+ }
265
330
  }
266
- // What a client says about itself on initialize, or a request's _meta.
267
- function clientInfoName(server, meta) {
331
+ /** The tools attach adds, unless AttachOptions.tools names fewer. */
332
+ export const BUILTIN_TOOLS = ["subscribe_events", "unsubscribe_events", "list_subscriptions"];
333
+ // What a client says about itself on initialize, or a request's _meta;
334
+ // else the name identify gave (identified).
335
+ function clientInfoName(server, meta, identified) {
268
336
  const fromInit = server.server.getClientVersion()?.name;
269
337
  if (fromInit)
270
338
  return fromInit;
271
339
  const info = meta?.["io.modelcontextprotocol/clientInfo"];
272
- return typeof info?.name === "string" ? info.name : "";
340
+ if (typeof info?.name === "string" && info.name)
341
+ return info.name;
342
+ return identified ?? "";
273
343
  }
274
344
  function header(extra, name) {
275
345
  const h = extra?.requestInfo?.headers;
@@ -279,9 +349,10 @@ function header(extra, name) {
279
349
  return (Array.isArray(v) ? v[0] : v) ?? "";
280
350
  }
281
351
  /** A client that speaks MCP Events: it declared the extension, or it's one known to (ChatGPT). */
352
+ /** @internal */
282
353
  export function speaksEvents(capabilities, clientName) {
283
354
  const ext = capabilities?.extensions?.[EXTENSION_KEY];
284
- return !!ext || !!capabilities?.[LEGACY_KEY] || clientName.toLowerCase() === "openai-mcp";
355
+ return !!ext || clientName.toLowerCase() === "openai-mcp";
285
356
  }
286
357
  // The fixed wording of the tools attach adds (tested: bench S1–S9 run
287
358
  // through them), and of the refusals of a native subscribe. The Go SDK
@@ -334,11 +405,29 @@ const MAX_LISTED_EVENTS = 20;
334
405
  export function attachEvents(wake, server, events, opts = {}) {
335
406
  if (events.length === 0)
336
407
  throw new Error("wakeshift: attach needs at least one event (wake.defineEvent) defined first");
337
- const userOf = (extra) => opts.identify?.(extra)?.user;
408
+ for (const t of opts.tools ?? []) {
409
+ if (!BUILTIN_TOOLS.includes(t))
410
+ throw new Error(`wakeshift: attach's tools names "${t}": its own tools are ${BUILTIN_TOOLS.join(", ")}`);
411
+ }
412
+ const tools = new Set(opts.tools ?? BUILTIN_TOOLS);
413
+ const identityOf = (extra) => opts.identify?.(extra) ?? undefined;
414
+ // Who's calling: the user, and the client's name (the request's, else identify's).
415
+ const callerOf = (extra) => {
416
+ const id = extra ? identityOf(extra) : undefined;
417
+ return { user: id?.user, client: clientInfoName(server, extra?._meta, id?.client) };
418
+ };
419
+ const attached = new Attached({
420
+ wake,
421
+ server,
422
+ identify: (extra) => identityOf(extra),
423
+ clientOf: (meta, identified) => clientInfoName(server, meta, identified),
424
+ events: opts.events ?? {},
425
+ warnOnce: (key, message) => wake.warnOnce(key, message),
426
+ });
338
427
  const byName = new Map(events.map((e) => [e.name, e]));
339
428
  const low = server.server;
340
429
  try {
341
- low.registerCapabilities({ [LEGACY_KEY]: { listChanged: false }, extensions: { [EXTENSION_KEY]: { listChanged: false } } });
430
+ low.registerCapabilities({ [TOP_LEVEL_KEY]: { listChanged: false }, extensions: { [EXTENSION_KEY]: { listChanged: false } } });
342
431
  }
343
432
  catch (err) {
344
433
  throw new Error("wakeshift: call attach before connecting the server to a transport", { cause: err });
@@ -355,7 +444,8 @@ export function attachEvents(wake, server, events, opts = {}) {
355
444
  const native = async (req, extra, unsubscribe) => {
356
445
  const p = (req.params ?? {});
357
446
  const meta = (p._meta ?? {});
358
- const client = clientInfoName(server, meta);
447
+ const id = identityOf(extra);
448
+ const client = clientInfoName(server, meta, id?.client);
359
449
  const ua = header(extra, "user-agent");
360
450
  const dialect = dialectOf(client, ua);
361
451
  const codes = codesFor(dialect);
@@ -374,9 +464,9 @@ export function attachEvents(wake, server, events, opts = {}) {
374
464
  args = ev.parseArgs(p.arguments ?? {});
375
465
  }
376
466
  catch (err) {
377
- throw new McpError(INVALID_PARAMS, err.message);
467
+ throw new McpError(INVALID_PARAMS, err instanceof WakeshiftError ? err.detail : err.message);
378
468
  }
379
- const user = userOf(extra);
469
+ const user = id?.user;
380
470
  if (!user)
381
471
  throw new McpError(codes.forbidden, fill("native_no_user"));
382
472
  const call = { user, client, meta, ...(ua ? { user_agent: ua } : {}) };
@@ -427,11 +517,13 @@ export function attachEvents(wake, server, events, opts = {}) {
427
517
  .join("\n");
428
518
  const more = events.length > MAX_LISTED_EVENTS ? fill("more", { n: events.length - MAX_LISTED_EVENTS }) : "";
429
519
  const argsInput = z.record(z.string(), z.any()).optional().describe(EVENT_TOOL_TEXT.arguments_argument);
430
- const result = (text, addition) => applyAddition(addition ?? {}, text);
520
+ const result = (text, addition) => withAddition(text, addition ?? {});
431
521
  const fail = (text) => ({ isError: true, content: [{ type: "text", text }] });
432
- const subscribeTool = server.registerTool("subscribe_events", {
522
+ // register adds one of the tools, if the app wants it (attach's tools).
523
+ const register = (name, config, cb) => (tools.has(name) ? server.registerTool(name, config, cb) : undefined);
524
+ const subscribeTool = register("subscribe_events", {
433
525
  description: `${EVENT_TOOL_TEXT.subscribe_description}${EVENT_TOOL_TEXT.events_heading}${listed}${more}`,
434
- inputSchema: { event: z.enum(names).describe(EVENT_TOOL_TEXT.event_argument), arguments: argsInput, ...wake.args },
526
+ inputSchema: { event: z.enum(names).describe(EVENT_TOOL_TEXT.event_argument), arguments: argsInput, ...toolArgs },
435
527
  }, async (input, extra) => {
436
528
  const { event, arguments: raw, ...rest } = input;
437
529
  const ev = byName.get(event);
@@ -442,17 +534,17 @@ export function attachEvents(wake, server, events, opts = {}) {
442
534
  args = ev.parseArgs(raw ?? {});
443
535
  }
444
536
  catch (err) {
445
- return fail(err.message);
537
+ return fail(err instanceof WakeshiftError ? err.detail : err.message);
446
538
  }
447
- const user = userOf(extra);
539
+ const { user, client } = callerOf(extra);
448
540
  if (ev.personal && !user)
449
541
  return fail(fill("personal_no_user", { event: ev.name }));
450
- if (!(await ev.allows({ user, client: clientInfoName(server, extra?._meta), native: false }, args))) {
542
+ if (!(await ev.allows({ user, client, native: false }, args))) {
451
543
  return fail(fill("refused", { event: ev.name }));
452
544
  }
453
545
  try {
454
546
  const out = await wake.requestApp("POST", "/event-subscriptions", {
455
- call: wake.call(extra, { server, user, args: rest, tool: "subscribe_events" }),
547
+ call: toolCallOf(extra, client, user, rest, "subscribe_events"),
456
548
  name: ev.name,
457
549
  arguments: args,
458
550
  });
@@ -464,17 +556,17 @@ export function attachEvents(wake, server, events, opts = {}) {
464
556
  return fail(fill("subscribe_failed", { detail: detailOf(err) }));
465
557
  }
466
558
  });
467
- server.registerTool("unsubscribe_events", {
559
+ register("unsubscribe_events", {
468
560
  description: EVENT_TOOL_TEXT.unsubscribe_description,
469
561
  inputSchema: {
470
562
  event: z.enum(names).optional().describe(EVENT_TOOL_TEXT.event_argument),
471
563
  arguments: argsInput,
472
564
  id: z.string().optional().describe(EVENT_TOOL_TEXT.id_argument),
473
- ...wake.args,
565
+ ...toolArgs,
474
566
  },
475
567
  }, async (input, extra) => {
476
568
  const { event, arguments: raw, id, ...rest } = input;
477
- const user = userOf(extra);
569
+ const { user, client } = callerOf(extra);
478
570
  try {
479
571
  if (id) {
480
572
  if (!user)
@@ -487,18 +579,21 @@ export function attachEvents(wake, server, events, opts = {}) {
487
579
  return fail(fill("need"));
488
580
  const args = ev.parseArgs(raw ?? {});
489
581
  const out = await wake.requestApp("POST", "/event-subscriptions/unsubscribe", {
490
- call: wake.call(extra, { server, user, args: rest, tool: "unsubscribe_events" }),
582
+ call: toolCallOf(extra, client, user, rest, "unsubscribe_events"),
491
583
  name: ev.name,
492
584
  arguments: args,
493
585
  });
494
586
  return result(fill(out.ended ? "unsubscribed" : "not_subscribed", { event: ev.name, arguments: canonical(args) }));
495
587
  }
496
588
  catch (err) {
497
- return fail(err instanceof ArgsError ? err.message : fill("unsubscribe_failed", { detail: detailOf(err) }));
589
+ return fail(err instanceof ArgsError ? err.detail : fill("unsubscribe_failed", { detail: detailOf(err) }));
498
590
  }
499
591
  });
500
- server.registerTool("list_subscriptions", { description: EVENT_TOOL_TEXT.list_description, inputSchema: { ...wake.args } }, async (_input, extra) => {
501
- const user = userOf(extra);
592
+ // list_subscriptions only reads: marked so (readOnlyHint), clients that
593
+ // approve read-only tools without asking (Codex, by default) don't ask; and
594
+ // idempotent, as in every SDK (test/events-list.json).
595
+ register("list_subscriptions", { description: EVENT_TOOL_TEXT.list_description, inputSchema: { ...toolArgs }, annotations: { readOnlyHint: true, idempotentHint: true } }, async (_input, extra) => {
596
+ const user = identityOf(extra)?.user;
502
597
  if (!user)
503
598
  return fail(fill("list_no_user"));
504
599
  try {
@@ -522,23 +617,34 @@ export function attachEvents(wake, server, events, opts = {}) {
522
617
  low.oninitialized = () => {
523
618
  // Set both ways: a server reconnected to another client decides again.
524
619
  if (speaksEvents(low.getClientCapabilities(), low.getClientVersion()?.name ?? ""))
525
- subscribeTool.disable();
620
+ subscribeTool?.disable();
526
621
  else
527
- subscribeTool.enable();
622
+ subscribeTool?.enable();
528
623
  prev?.();
529
624
  };
625
+ return attached;
626
+ }
627
+ // withAddition is one of attach's own tools' results: its text, then
628
+ // Wakeshift's, with Wakeshift's _meta.
629
+ function withAddition(base, a) {
630
+ const text = (base.trim() + "\n\n" + (a.text ?? "")).trim();
631
+ const out = { content: text ? [{ type: "text", text }] : [] };
632
+ if (a.meta && Object.keys(a.meta).length > 0)
633
+ out._meta = { ...a.meta };
634
+ return out;
530
635
  }
531
636
  // rpcErrorOf is the JSON-RPC error for a failed native call, in the
532
637
  // client's dialect's codes. A conversation that isn't the caller's is
533
638
  // Forbidden, whether it exists or not.
534
- function rpcErrorOf(err, codes) {
639
+ /** @internal */
640
+ export function rpcErrorOf(err, codes) {
535
641
  if (err instanceof McpError)
536
642
  return err;
537
- if (!(err instanceof WakeshiftError))
643
+ if (!(err instanceof WakeshiftError) || err.code === "unreachable")
538
644
  return new McpError(INTERNAL_ERROR, "Wakeshift couldn't be reached");
539
645
  switch (err.code) {
540
646
  case "callback_failed":
541
- return new McpError(codes.callbackEndpoint, err.detail, { reason: err.reason });
647
+ return new McpError(codes.callbackEndpoint, err.detail, { reason: err.reason ?? "" });
542
648
  case "unknown_event":
543
649
  return new McpError(codes.notFound, err.detail, { kind: "event" });
544
650
  case "unsupported":