@wakeshift/sdk 0.5.2 → 0.6.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
@@ -6,14 +6,13 @@
6
6
  // extension, and through Wakeshift's wake paths for everyone else, who
7
7
  // subscribe with the subscribe_events tool attach adds.
8
8
  // https://wakeshift.ai/docs/events has the guide.
9
- import { z } from "zod";
10
- import { McpError } from "@modelcontextprotocol/sdk/types.js";
11
- import { toJsonSchemaCompat } from "@modelcontextprotocol/sdk/server/zod-json-schema-compat.js";
12
- import { objectFromShape, safeParse } from "@modelcontextprotocol/sdk/server/zod-compat.js";
9
+ import { RpcError } from "./mcp.js";
10
+ import { isSchema, objectFromShape, safeParse, toJsonSchema } from "./zod.js";
11
+ import { hostOf } from "./host.js";
13
12
  import { WakeshiftError } from "./errors.js";
14
13
  import { DELIVERY, 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";
14
+ import { toolCallOf, userAgentOf } from "./wakeshift.js";
15
+ import { Attached, wakeInputsFor } from "./toolcall.js";
17
16
  import { serveStateless } from "./discover.js";
18
17
  import { serveTasks } from "./tasks.js";
19
18
  const nameRE = /^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*){0,4}$/;
@@ -21,7 +20,7 @@ const argNameRE = /^[a-z][a-z0-9_]{0,63}$/;
21
20
  // jsonSchemaOf is a zod schema as JSON Schema, without $schema (Wakeshift
22
21
  // reads it as 2020-12).
23
22
  function jsonSchemaOf(s) {
24
- const out = toJsonSchemaCompat(s);
23
+ const out = toJsonSchema(s);
25
24
  delete out.$schema;
26
25
  normalize(out);
27
26
  return out;
@@ -59,9 +58,6 @@ function canonical(v) {
59
58
  const sortKeys = (x) => Array.isArray(x) ? x.map(sortKeys) : x && typeof x === "object" ? Object.fromEntries(Object.keys(x).sort().map((k) => [k, sortKeys(x[k])])) : x;
60
59
  return JSON.stringify(sortKeys(v) ?? {});
61
60
  }
62
- function isSchema(v) {
63
- return !!v && typeof v === "object" && ("_def" in v || "_zod" in v);
64
- }
65
61
  // detailOf is an error's message for an agent: Wakeshift's own words.
66
62
  function detailOf(err) {
67
63
  return err instanceof WakeshiftError ? err.detail : String(err?.message ?? err);
@@ -332,10 +328,10 @@ export class ArgsError extends WakeshiftError {
332
328
  }
333
329
  /** The tools attach adds, unless AttachOptions.tools names fewer. */
334
330
  export const BUILTIN_TOOLS = ["subscribe_events", "unsubscribe_events", "list_subscriptions"];
335
- // What a client says about itself on initialize, or a request's _meta;
336
- // else the name identify gave (identified).
337
- function clientInfoName(server, meta, identified) {
338
- const fromInit = server.server.getClientVersion()?.name;
331
+ // What a client says about itself on initialize (2.x: or the request's
332
+ // envelope), or a request's _meta; else the name identify gave (identified).
333
+ function clientInfoName(host, meta, identified) {
334
+ const fromInit = host.clientName();
339
335
  if (fromInit)
340
336
  return fromInit;
341
337
  const info = meta?.["io.modelcontextprotocol/clientInfo"];
@@ -343,13 +339,6 @@ function clientInfoName(server, meta, identified) {
343
339
  return info.name;
344
340
  return identified ?? "";
345
341
  }
346
- function header(extra, name) {
347
- const h = extra?.requestInfo?.headers;
348
- if (!h)
349
- return "";
350
- const v = h[name] ?? h[name.toLowerCase()];
351
- return (Array.isArray(v) ? v[0] : v) ?? "";
352
- }
353
342
  /** A client that speaks MCP Events: it declared the extension, or it's one known to (ChatGPT). */
354
343
  /** @internal */
355
344
  export function speaksEvents(capabilities, clientName) {
@@ -412,79 +401,85 @@ export function attachEvents(wake, server, events, opts = {}) {
412
401
  if (!BUILTIN_TOOLS.includes(t))
413
402
  throw new Error(`wakeshift: attach's tools names "${t}": its own tools are ${BUILTIN_TOOLS.join(", ")}`);
414
403
  }
404
+ const host = hostOf(server);
415
405
  const tools = new Set(opts.tools ?? BUILTIN_TOOLS);
416
- const identityOf = (extra) => opts.identify?.(extra) ?? undefined;
406
+ // raw is the MCP SDK's own request context (1.x's extra, 2.x's ctx): what identify gets.
407
+ const identityOf = (raw) => opts.identify?.(raw) ?? undefined;
417
408
  // Who's calling: the user, and the client's name (the request's, else identify's).
418
- const callerOf = (extra) => {
419
- const id = extra ? identityOf(extra) : undefined;
420
- return { user: id?.user, client: clientInfoName(server, extra?._meta, id?.client) };
409
+ const callerOf = (raw) => {
410
+ const id = raw ? identityOf(raw) : undefined;
411
+ return { user: id?.user, client: clientInfoName(host, raw ? host.req(raw).meta : undefined, id?.client) };
421
412
  };
422
413
  const attached = new Attached({
423
414
  wake,
424
- server,
425
- identify: (extra) => identityOf(extra),
426
- clientOf: (meta, identified) => clientInfoName(server, meta, identified),
415
+ host,
416
+ identify: (raw) => identityOf(raw),
417
+ clientOf: (meta, identified) => clientInfoName(host, meta, identified),
427
418
  events: opts.events ?? {},
428
419
  warnOnce: (key, message) => wake.warnOnce(key, message),
429
420
  });
430
421
  const byName = new Map(events.map((e) => [e.name, e]));
431
- const low = server.server;
432
422
  try {
433
- low.registerCapabilities({ [TOP_LEVEL_KEY]: { listChanged: false }, extensions: { [EXTENSION_KEY]: { listChanged: false } } });
423
+ host.registerCapabilities({ [TOP_LEVEL_KEY]: { listChanged: false }, extensions: { [EXTENSION_KEY]: { listChanged: false } } });
434
424
  }
435
425
  catch (err) {
436
426
  throw new Error("wakeshift: call attach before connecting the server to a transport", { cause: err });
437
427
  }
438
- // Clients that speak only protocol 2026-07-28 (OpenClaw), on a stateless
439
- // transport: server/discover and resultType (discover.ts).
440
- serveStateless(server, { bridgedOnly: ["subscribe_events"] });
428
+ if (!host.handlers()) {
429
+ // An MCP SDK release that keeps its request handlers elsewhere: the native
430
+ // events and the built-in tools still work, but not what wraps the SDK's own methods.
431
+ wake.warnOnce("handlers", "wakeshift: this MCP SDK keeps its request handlers where Wakeshift can't reach them, so wakeshift_wait, the 2026-07-28 tools/list changes and wrapped tools' output schemas are off: check for a newer @wakeshift/sdk");
432
+ }
433
+ // Clients that speak only protocol 2026-07-28 (OpenClaw): on the MCP SDK
434
+ // 1.x, on a stateless transport, server/discover and resultType
435
+ // (discover.ts); on 2.x, its own.
436
+ serveStateless(host, { bridgedOnly: ["subscribe_events"] });
441
437
  // Clients that take MCP tasks there (Claude Code): Wakeshift's wait tool,
442
438
  // answered with a task (tasks.ts).
443
- serveTasks(wake, server, (extra) => callerOf(extra));
439
+ serveTasks(wake, host, callerOf);
444
440
  // tools/list: a wrapped tool's output schema names wakeshift, which its
445
441
  // structured results carry (toolcall.ts).
446
442
  attached.amendToolsList();
447
443
  // ── Native: events/list, events/subscribe, events/unsubscribe ─────────
448
- const method = (m) => z.object({ method: z.literal(m), params: z.any().optional() });
449
- low.setRequestHandler(method("events/list"), async () => ({ resultType: RESULT_TYPE, events: events.map((e) => e.definition()) }));
450
- low.setRequestHandler(method("events/stream"), async (_req, extra) => {
451
- const codes = codesFor(dialectOf(clientInfoName(server, undefined), header(extra, "user-agent")));
452
- throw new McpError(codes.unsupported, "push delivery isn't offered: use a webhook (events/subscribe) or poll (events/poll)", { feature: "deliveryMode", value: "stream" });
444
+ host.setMethod("events/list", async () => ({ resultType: RESULT_TYPE, events: events.map((e) => e.definition()) }));
445
+ host.setMethod("events/stream", async (_params, raw) => {
446
+ const codes = codesFor(dialectOf(clientInfoName(host, undefined), userAgentOf(host.req(raw))));
447
+ throw new RpcError(codes.unsupported, "push delivery isn't offered: use a webhook (events/subscribe) or poll (events/poll)", { feature: "deliveryMode", value: "stream" });
453
448
  });
454
- const native = async (req, extra, m) => {
449
+ const native = async (p, raw, m) => {
455
450
  const unsubscribe = m === "unsubscribe";
456
- const p = (req.params ?? {});
457
- const meta = (p._meta ?? {});
458
- const id = identityOf(extra);
459
- const client = clientInfoName(server, meta, id?.client);
460
- const ua = header(extra, "user-agent");
451
+ const req = host.req(raw);
452
+ const meta = (req.meta ?? {});
453
+ const id = identityOf(raw);
454
+ const client = clientInfoName(host, meta, id?.client);
455
+ const ua = userAgentOf(req);
461
456
  const dialect = dialectOf(client, ua);
462
457
  const codes = codesFor(dialect);
463
- const nameHeader = header(extra, NAME_HEADER);
458
+ const nameHeader = req.header(NAME_HEADER);
464
459
  if (nameHeader && headerValue(nameHeader) !== p.name) {
465
- throw new McpError(HEADER_MISMATCH, "Header mismatch: Mcp-Name doesn't match params.name");
460
+ throw new RpcError(HEADER_MISMATCH, "Header mismatch: Mcp-Name doesn't match params.name");
466
461
  }
467
462
  const ev = byName.get(p.name);
468
463
  if (!ev)
469
- throw new McpError(codes.notFound, fill("native_unknown_event", { event: JSON.stringify(p.name) }), { kind: "event" });
464
+ throw new RpcError(codes.notFound, fill("native_unknown_event", { event: JSON.stringify(p.name) }), { kind: "event" });
470
465
  if (m === "subscribe" && p.delivery?.mode !== "webhook") {
471
- throw new McpError(codes.unsupported, 'delivery.mode must be "webhook": the only delivery offered', { feature: "deliveryMode", value: p.delivery?.mode });
466
+ throw new RpcError(codes.unsupported, 'delivery.mode must be "webhook": the only delivery offered', { feature: "deliveryMode", value: p.delivery?.mode });
472
467
  }
473
468
  let args;
474
469
  try {
475
470
  args = ev.parseArgs(p.arguments ?? {});
476
471
  }
477
472
  catch (err) {
478
- throw new McpError(INVALID_PARAMS, err instanceof WakeshiftError ? err.detail : err.message);
473
+ throw new RpcError(INVALID_PARAMS, err instanceof WakeshiftError ? err.detail : err.message);
479
474
  }
480
475
  const user = id?.user;
481
476
  if (!user)
482
- throw new McpError(codes.forbidden, fill("native_no_user"));
477
+ throw new RpcError(codes.forbidden, fill("native_no_user"));
483
478
  const call = { user, client, meta, ...(ua ? { user_agent: ua } : {}) };
484
479
  // A poll has no subscribe step (each poll can start the subscription),
485
480
  // so every poll asks authorize: access is checked again each time.
486
481
  if (!unsubscribe && !(await ev.allows({ user, client, native: true }, args))) {
487
- throw new McpError(codes.forbidden, fill("native_refused", { event: ev.name }));
482
+ throw new RpcError(codes.forbidden, fill("native_refused", { event: ev.name }));
488
483
  }
489
484
  if (m === "poll") {
490
485
  const body = { call, name: ev.name, arguments: args };
@@ -506,7 +501,7 @@ export function attachEvents(wake, server, events, opts = {}) {
506
501
  if (unsubscribe) {
507
502
  const out = await wake.requestApp("POST", "/events/unsubscribe", body);
508
503
  if (!out.ended && dialect !== "chatgpt") {
509
- throw new McpError(codes.notFound, "no subscription matches that event, arguments and delivery URL", { kind: "subscription" });
504
+ throw new RpcError(codes.notFound, "no subscription matches that event, arguments and delivery URL", { kind: "subscription" });
510
505
  }
511
506
  return { resultType: RESULT_TYPE };
512
507
  }
@@ -533,10 +528,13 @@ export function attachEvents(wake, server, events, opts = {}) {
533
528
  throw rpcErrorOf(err, codes);
534
529
  }
535
530
  };
536
- low.setRequestHandler(method("events/subscribe"), (req, extra) => native(req, extra, "subscribe"));
537
- low.setRequestHandler(method("events/unsubscribe"), (req, extra) => native(req, extra, "unsubscribe"));
538
- low.setRequestHandler(method("events/poll"), (req, extra) => native(req, extra, "poll"));
531
+ host.setMethod("events/subscribe", (p, raw) => native(p, raw, "subscribe"));
532
+ host.setMethod("events/unsubscribe", (p, raw) => native(p, raw, "unsubscribe"));
533
+ host.setMethod("events/poll", (p, raw) => native(p, raw, "poll"));
539
534
  // ── Bridged: subscribe_events, unsubscribe_events, list_subscriptions ─
535
+ // Written in the zod the MCP SDK takes (2.x: zod 4 only).
536
+ const z = host.zod;
537
+ const toolArgs = wakeInputsFor(host);
540
538
  const names = events.map((e) => e.name);
541
539
  const listed = events
542
540
  .slice(0, MAX_LISTED_EVENTS)
@@ -550,23 +548,23 @@ export function attachEvents(wake, server, events, opts = {}) {
550
548
  const result = (text, addition) => withAddition(text, addition ?? {});
551
549
  const fail = (text) => ({ isError: true, content: [{ type: "text", text }] });
552
550
  // register adds one of the tools, if the app wants it (attach's tools).
553
- const register = (name, config, cb) => (tools.has(name) ? server.registerTool(name, config, cb) : undefined);
551
+ const register = (name, config, cb) => tools.has(name) ? host.registerTool(name, config, cb) : undefined;
554
552
  const subscribeTool = register("subscribe_events", {
555
553
  description: `${EVENT_TOOL_TEXT.subscribe_description}${EVENT_TOOL_TEXT.events_heading}${listed}${more}`,
556
554
  inputSchema: { event: z.enum(names).describe(EVENT_TOOL_TEXT.event_argument), arguments: argsInput, ...toolArgs },
557
- }, async (input, extra) => {
558
- const { event, arguments: raw, ...rest } = input;
555
+ }, async (input, raw) => {
556
+ const { event, arguments: rawArgs, ...rest } = input;
559
557
  const ev = byName.get(event);
560
558
  if (!ev)
561
559
  return fail(fill("unknown_event", { event }));
562
560
  let args;
563
561
  try {
564
- args = ev.parseArgs(raw ?? {});
562
+ args = ev.parseArgs(rawArgs ?? {});
565
563
  }
566
564
  catch (err) {
567
565
  return fail(err instanceof WakeshiftError ? err.detail : err.message);
568
566
  }
569
- const { user, client } = callerOf(extra);
567
+ const { user, client } = callerOf(raw);
570
568
  if (ev.personal && !user)
571
569
  return fail(fill("personal_no_user", { event: ev.name }));
572
570
  if (!(await ev.allows({ user, client, native: false }, args))) {
@@ -574,7 +572,7 @@ export function attachEvents(wake, server, events, opts = {}) {
574
572
  }
575
573
  try {
576
574
  const out = await wake.requestApp("POST", "/event-subscriptions", {
577
- call: toolCallOf(extra, client, user, rest, "subscribe_events"),
575
+ call: toolCallOf(host.req(raw), client, user, rest, "subscribe_events"),
578
576
  name: ev.name,
579
577
  arguments: args,
580
578
  });
@@ -594,9 +592,9 @@ export function attachEvents(wake, server, events, opts = {}) {
594
592
  id: z.string().optional().describe(EVENT_TOOL_TEXT.id_argument),
595
593
  ...toolArgs,
596
594
  },
597
- }, async (input, extra) => {
598
- const { event, arguments: raw, id, ...rest } = input;
599
- const { user, client } = callerOf(extra);
595
+ }, async (input, raw) => {
596
+ const { event, arguments: rawArgs, id, ...rest } = input;
597
+ const { user, client } = callerOf(raw);
600
598
  try {
601
599
  if (id) {
602
600
  if (!user)
@@ -607,9 +605,9 @@ export function attachEvents(wake, server, events, opts = {}) {
607
605
  const ev = event ? byName.get(event) : undefined;
608
606
  if (!ev)
609
607
  return fail(fill("need"));
610
- const args = ev.parseArgs(raw ?? {});
608
+ const args = ev.parseArgs(rawArgs ?? {});
611
609
  const out = await wake.requestApp("POST", "/event-subscriptions/unsubscribe", {
612
- call: toolCallOf(extra, client, user, rest, "unsubscribe_events"),
610
+ call: toolCallOf(host.req(raw), client, user, rest, "unsubscribe_events"),
613
611
  name: ev.name,
614
612
  arguments: args,
615
613
  });
@@ -622,8 +620,8 @@ export function attachEvents(wake, server, events, opts = {}) {
622
620
  // list_subscriptions only reads: marked so (readOnlyHint), clients that
623
621
  // approve read-only tools without asking (Codex, by default) don't ask; and
624
622
  // idempotent, as in every SDK (test/events-list.json).
625
- register("list_subscriptions", { description: EVENT_TOOL_TEXT.list_description, inputSchema: { ...toolArgs }, annotations: { readOnlyHint: true, idempotentHint: true } }, async (_input, extra) => {
626
- const user = identityOf(extra)?.user;
623
+ register("list_subscriptions", { description: EVENT_TOOL_TEXT.list_description, inputSchema: { ...toolArgs }, annotations: { readOnlyHint: true, idempotentHint: true } }, async (_input, raw) => {
624
+ const user = identityOf(raw)?.user;
627
625
  if (!user)
628
626
  return fail(fill("list_no_user"));
629
627
  try {
@@ -647,15 +645,13 @@ export function attachEvents(wake, server, events, opts = {}) {
647
645
  // Per-connection negotiation: a client that speaks MCP Events subscribes
648
646
  // natively, so it isn't offered subscribe_events (a server made per
649
647
  // request never sees initialize, and keeps offering it).
650
- const prev = low.oninitialized;
651
- low.oninitialized = () => {
648
+ host.onInitialized(() => {
652
649
  // Set both ways: a server reconnected to another client decides again.
653
- if (speaksEvents(low.getClientCapabilities(), low.getClientVersion()?.name ?? ""))
650
+ if (speaksEvents(host.clientCapabilities(), host.clientName() ?? ""))
654
651
  subscribeTool?.disable();
655
652
  else
656
653
  subscribeTool?.enable();
657
- prev?.();
658
- };
654
+ });
659
655
  return attached;
660
656
  }
661
657
  // withAddition is one of attach's own tools' results: its text, then
@@ -683,29 +679,29 @@ export function pollResult(out) {
683
679
  // Forbidden, whether it exists or not.
684
680
  /** @internal */
685
681
  export function rpcErrorOf(err, codes) {
686
- if (err instanceof McpError)
682
+ if (err instanceof RpcError)
687
683
  return err;
688
684
  if (!(err instanceof WakeshiftError) || err.code === "unreachable")
689
- return new McpError(INTERNAL_ERROR, "Wakeshift couldn't be reached");
685
+ return new RpcError(INTERNAL_ERROR, "Wakeshift couldn't be reached");
690
686
  switch (err.code) {
691
687
  case "callback_failed":
692
- return new McpError(codes.callbackEndpoint, err.detail, { reason: err.reason ?? "" });
688
+ return new RpcError(codes.callbackEndpoint, err.detail, { reason: err.reason ?? "" });
693
689
  case "unknown_event":
694
- return new McpError(codes.notFound, err.detail, { kind: "event" });
690
+ return new RpcError(codes.notFound, err.detail, { kind: "event" });
695
691
  case "unsupported":
696
- return new McpError(codes.unsupported, err.detail, { feature: "deliveryMode" });
692
+ return new RpcError(codes.unsupported, err.detail, { feature: "deliveryMode" });
697
693
  case "subscription_limit":
698
- return new McpError(codes.resourceExhausted, err.detail, { limit: "subscriptions" });
694
+ return new RpcError(codes.resourceExhausted, err.detail, { limit: "subscriptions" });
699
695
  case "subscription_stopped":
700
- return new McpError(codes.forbidden, err.detail, { reason: "stopped" });
696
+ return new RpcError(codes.forbidden, err.detail, { reason: "stopped" });
701
697
  case "bad_request":
702
698
  case "bad_arguments":
703
- return new McpError(INVALID_PARAMS, err.detail);
699
+ return new RpcError(INVALID_PARAMS, err.detail);
704
700
  case "unknown_session":
705
701
  case "other_client":
706
702
  case "unknown_conversation":
707
- return new McpError(codes.forbidden, "another client of yours holds that subscription");
703
+ return new RpcError(codes.forbidden, "another client of yours holds that subscription");
708
704
  }
709
- return new McpError(INTERNAL_ERROR, "Wakeshift refused the subscription: " + err.detail);
705
+ return new RpcError(INTERNAL_ERROR, "Wakeshift refused the subscription: " + err.detail);
710
706
  }
711
707
  //# sourceMappingURL=events.js.map