@wakeshift/sdk 0.5.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/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 { capabilitiesError, 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) {
@@ -406,85 +395,98 @@ export const LIST_TOOL_DESCRIPTION = EVENT_TOOL_TEXT.list_description;
406
395
  const MAX_LISTED_EVENTS = 20;
407
396
  /** @internal The events part of Wakeshift.attach. */
408
397
  export function attachEvents(wake, server, events, opts = {}) {
398
+ checkAttach(events, opts);
399
+ return attachHost(wake, hostOf(server), events, opts);
400
+ }
401
+ /** @internal What attach refuses before it touches a server: no events, or an own tool it doesn't have. */
402
+ export function checkAttach(events, opts, what = "attach") {
409
403
  if (events.length === 0)
410
- throw new Error("wakeshift: attach needs at least one event (wake.defineEvent) defined first");
404
+ throw new Error(`wakeshift: ${what} needs at least one event (wake.defineEvent) defined first`);
411
405
  for (const t of opts.tools ?? []) {
412
406
  if (!BUILTIN_TOOLS.includes(t))
413
- throw new Error(`wakeshift: attach's tools names "${t}": its own tools are ${BUILTIN_TOOLS.join(", ")}`);
407
+ throw new Error(`wakeshift: ${what}'s tools names "${t}": its own tools are ${BUILTIN_TOOLS.join(", ")}`);
414
408
  }
409
+ }
410
+ /** @internal attach on a Host: an McpServer's, or a framework adapter's (mastra.ts, xmcp.ts). */
411
+ export function attachHost(wake, host, events, opts = {}) {
415
412
  const tools = new Set(opts.tools ?? BUILTIN_TOOLS);
416
- const identityOf = (extra) => opts.identify?.(extra) ?? undefined;
413
+ // raw is the MCP SDK's own request context (1.x's extra, 2.x's ctx): what identify gets.
414
+ const identityOf = (raw) => opts.identify?.(raw) ?? undefined;
417
415
  // 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) };
416
+ const callerOf = (raw) => {
417
+ const id = raw ? identityOf(raw) : undefined;
418
+ return { user: id?.user, client: clientInfoName(host, raw ? host.req(raw).meta : undefined, id?.client) };
421
419
  };
422
420
  const attached = new Attached({
423
421
  wake,
424
- server,
425
- identify: (extra) => identityOf(extra),
426
- clientOf: (meta, identified) => clientInfoName(server, meta, identified),
422
+ host,
423
+ identify: (raw) => identityOf(raw),
424
+ clientOf: (meta, identified) => clientInfoName(host, meta, identified),
427
425
  events: opts.events ?? {},
428
426
  warnOnce: (key, message) => wake.warnOnce(key, message),
429
427
  });
430
428
  const byName = new Map(events.map((e) => [e.name, e]));
431
- const low = server.server;
432
429
  try {
433
- low.registerCapabilities({ [TOP_LEVEL_KEY]: { listChanged: false }, extensions: { [EXTENSION_KEY]: { listChanged: false } } });
430
+ host.registerCapabilities({ [TOP_LEVEL_KEY]: { listChanged: false }, extensions: { [EXTENSION_KEY]: { listChanged: false } } });
434
431
  }
435
432
  catch (err) {
436
- throw new Error("wakeshift: call attach before connecting the server to a transport", { cause: err });
433
+ throw capabilitiesError(host, err);
437
434
  }
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"] });
435
+ if (!host.handlers() && !host.framework) {
436
+ // An MCP SDK release that keeps its request handlers elsewhere: the native
437
+ // events and the built-in tools still work, but not what wraps the SDK's own methods.
438
+ 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");
439
+ }
440
+ // Clients that speak only protocol 2026-07-28 (OpenClaw): on the MCP SDK
441
+ // 1.x, on a stateless transport, server/discover and resultType
442
+ // (discover.ts); on 2.x, its own.
443
+ serveStateless(host, { bridgedOnly: ["subscribe_events"] });
441
444
  // Clients that take MCP tasks there (Claude Code): Wakeshift's wait tool,
442
445
  // answered with a task (tasks.ts).
443
- serveTasks(wake, server, (extra) => callerOf(extra));
446
+ serveTasks(wake, host, callerOf);
444
447
  // tools/list: a wrapped tool's output schema names wakeshift, which its
445
448
  // structured results carry (toolcall.ts).
446
449
  attached.amendToolsList();
447
450
  // ── 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" });
451
+ host.setMethod("events/list", async () => ({ resultType: RESULT_TYPE, events: events.map((e) => e.definition()) }));
452
+ host.setMethod("events/stream", async (_params, raw) => {
453
+ const codes = codesFor(dialectOf(clientInfoName(host, undefined), userAgentOf(host.req(raw))));
454
+ throw new RpcError(codes.unsupported, "push delivery isn't offered: use a webhook (events/subscribe) or poll (events/poll)", { feature: "deliveryMode", value: "stream" });
453
455
  });
454
- const native = async (req, extra, m) => {
456
+ const native = async (p, raw, m) => {
455
457
  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");
458
+ const req = host.req(raw);
459
+ const meta = (req.meta ?? {});
460
+ const id = identityOf(raw);
461
+ const client = clientInfoName(host, meta, id?.client);
462
+ const ua = userAgentOf(req);
461
463
  const dialect = dialectOf(client, ua);
462
464
  const codes = codesFor(dialect);
463
- const nameHeader = header(extra, NAME_HEADER);
465
+ const nameHeader = req.header(NAME_HEADER);
464
466
  if (nameHeader && headerValue(nameHeader) !== p.name) {
465
- throw new McpError(HEADER_MISMATCH, "Header mismatch: Mcp-Name doesn't match params.name");
467
+ throw new RpcError(HEADER_MISMATCH, "Header mismatch: Mcp-Name doesn't match params.name");
466
468
  }
467
469
  const ev = byName.get(p.name);
468
470
  if (!ev)
469
- throw new McpError(codes.notFound, fill("native_unknown_event", { event: JSON.stringify(p.name) }), { kind: "event" });
471
+ throw new RpcError(codes.notFound, fill("native_unknown_event", { event: JSON.stringify(p.name) }), { kind: "event" });
470
472
  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 });
473
+ throw new RpcError(codes.unsupported, 'delivery.mode must be "webhook": the only delivery offered', { feature: "deliveryMode", value: p.delivery?.mode });
472
474
  }
473
475
  let args;
474
476
  try {
475
477
  args = ev.parseArgs(p.arguments ?? {});
476
478
  }
477
479
  catch (err) {
478
- throw new McpError(INVALID_PARAMS, err instanceof WakeshiftError ? err.detail : err.message);
480
+ throw new RpcError(INVALID_PARAMS, err instanceof WakeshiftError ? err.detail : err.message);
479
481
  }
480
482
  const user = id?.user;
481
483
  if (!user)
482
- throw new McpError(codes.forbidden, fill("native_no_user"));
484
+ throw new RpcError(codes.forbidden, fill("native_no_user"));
483
485
  const call = { user, client, meta, ...(ua ? { user_agent: ua } : {}) };
484
486
  // A poll has no subscribe step (each poll can start the subscription),
485
487
  // so every poll asks authorize: access is checked again each time.
486
488
  if (!unsubscribe && !(await ev.allows({ user, client, native: true }, args))) {
487
- throw new McpError(codes.forbidden, fill("native_refused", { event: ev.name }));
489
+ throw new RpcError(codes.forbidden, fill("native_refused", { event: ev.name }));
488
490
  }
489
491
  if (m === "poll") {
490
492
  const body = { call, name: ev.name, arguments: args };
@@ -506,7 +508,7 @@ export function attachEvents(wake, server, events, opts = {}) {
506
508
  if (unsubscribe) {
507
509
  const out = await wake.requestApp("POST", "/events/unsubscribe", body);
508
510
  if (!out.ended && dialect !== "chatgpt") {
509
- throw new McpError(codes.notFound, "no subscription matches that event, arguments and delivery URL", { kind: "subscription" });
511
+ throw new RpcError(codes.notFound, "no subscription matches that event, arguments and delivery URL", { kind: "subscription" });
510
512
  }
511
513
  return { resultType: RESULT_TYPE };
512
514
  }
@@ -533,10 +535,13 @@ export function attachEvents(wake, server, events, opts = {}) {
533
535
  throw rpcErrorOf(err, codes);
534
536
  }
535
537
  };
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"));
538
+ host.setMethod("events/subscribe", (p, raw) => native(p, raw, "subscribe"));
539
+ host.setMethod("events/unsubscribe", (p, raw) => native(p, raw, "unsubscribe"));
540
+ host.setMethod("events/poll", (p, raw) => native(p, raw, "poll"));
539
541
  // ── Bridged: subscribe_events, unsubscribe_events, list_subscriptions ─
542
+ // Written in the zod the MCP SDK takes (2.x: zod 4 only).
543
+ const z = host.zod;
544
+ const toolArgs = wakeInputsFor(host);
540
545
  const names = events.map((e) => e.name);
541
546
  const listed = events
542
547
  .slice(0, MAX_LISTED_EVENTS)
@@ -550,23 +555,23 @@ export function attachEvents(wake, server, events, opts = {}) {
550
555
  const result = (text, addition) => withAddition(text, addition ?? {});
551
556
  const fail = (text) => ({ isError: true, content: [{ type: "text", text }] });
552
557
  // 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);
558
+ const register = (name, config, cb) => tools.has(name) ? host.registerTool(name, config, cb) : undefined;
554
559
  const subscribeTool = register("subscribe_events", {
555
560
  description: `${EVENT_TOOL_TEXT.subscribe_description}${EVENT_TOOL_TEXT.events_heading}${listed}${more}`,
556
561
  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;
562
+ }, async (input, raw) => {
563
+ const { event, arguments: rawArgs, ...rest } = input;
559
564
  const ev = byName.get(event);
560
565
  if (!ev)
561
566
  return fail(fill("unknown_event", { event }));
562
567
  let args;
563
568
  try {
564
- args = ev.parseArgs(raw ?? {});
569
+ args = ev.parseArgs(rawArgs ?? {});
565
570
  }
566
571
  catch (err) {
567
572
  return fail(err instanceof WakeshiftError ? err.detail : err.message);
568
573
  }
569
- const { user, client } = callerOf(extra);
574
+ const { user, client } = callerOf(raw);
570
575
  if (ev.personal && !user)
571
576
  return fail(fill("personal_no_user", { event: ev.name }));
572
577
  if (!(await ev.allows({ user, client, native: false }, args))) {
@@ -574,7 +579,7 @@ export function attachEvents(wake, server, events, opts = {}) {
574
579
  }
575
580
  try {
576
581
  const out = await wake.requestApp("POST", "/event-subscriptions", {
577
- call: toolCallOf(extra, client, user, rest, "subscribe_events"),
582
+ call: toolCallOf(host.req(raw), client, user, rest, "subscribe_events"),
578
583
  name: ev.name,
579
584
  arguments: args,
580
585
  });
@@ -594,9 +599,9 @@ export function attachEvents(wake, server, events, opts = {}) {
594
599
  id: z.string().optional().describe(EVENT_TOOL_TEXT.id_argument),
595
600
  ...toolArgs,
596
601
  },
597
- }, async (input, extra) => {
598
- const { event, arguments: raw, id, ...rest } = input;
599
- const { user, client } = callerOf(extra);
602
+ }, async (input, raw) => {
603
+ const { event, arguments: rawArgs, id, ...rest } = input;
604
+ const { user, client } = callerOf(raw);
600
605
  try {
601
606
  if (id) {
602
607
  if (!user)
@@ -607,9 +612,9 @@ export function attachEvents(wake, server, events, opts = {}) {
607
612
  const ev = event ? byName.get(event) : undefined;
608
613
  if (!ev)
609
614
  return fail(fill("need"));
610
- const args = ev.parseArgs(raw ?? {});
615
+ const args = ev.parseArgs(rawArgs ?? {});
611
616
  const out = await wake.requestApp("POST", "/event-subscriptions/unsubscribe", {
612
- call: toolCallOf(extra, client, user, rest, "unsubscribe_events"),
617
+ call: toolCallOf(host.req(raw), client, user, rest, "unsubscribe_events"),
613
618
  name: ev.name,
614
619
  arguments: args,
615
620
  });
@@ -622,8 +627,8 @@ export function attachEvents(wake, server, events, opts = {}) {
622
627
  // list_subscriptions only reads: marked so (readOnlyHint), clients that
623
628
  // approve read-only tools without asking (Codex, by default) don't ask; and
624
629
  // 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;
630
+ register("list_subscriptions", { description: EVENT_TOOL_TEXT.list_description, inputSchema: { ...toolArgs }, annotations: { readOnlyHint: true, idempotentHint: true } }, async (_input, raw) => {
631
+ const user = identityOf(raw)?.user;
627
632
  if (!user)
628
633
  return fail(fill("list_no_user"));
629
634
  try {
@@ -647,15 +652,13 @@ export function attachEvents(wake, server, events, opts = {}) {
647
652
  // Per-connection negotiation: a client that speaks MCP Events subscribes
648
653
  // natively, so it isn't offered subscribe_events (a server made per
649
654
  // request never sees initialize, and keeps offering it).
650
- const prev = low.oninitialized;
651
- low.oninitialized = () => {
655
+ host.onInitialized(() => {
652
656
  // Set both ways: a server reconnected to another client decides again.
653
- if (speaksEvents(low.getClientCapabilities(), low.getClientVersion()?.name ?? ""))
657
+ if (speaksEvents(host.clientCapabilities(), host.clientName() ?? ""))
654
658
  subscribeTool?.disable();
655
659
  else
656
660
  subscribeTool?.enable();
657
- prev?.();
658
- };
661
+ });
659
662
  return attached;
660
663
  }
661
664
  // withAddition is one of attach's own tools' results: its text, then
@@ -683,29 +686,28 @@ export function pollResult(out) {
683
686
  // Forbidden, whether it exists or not.
684
687
  /** @internal */
685
688
  export function rpcErrorOf(err, codes) {
686
- if (err instanceof McpError)
689
+ if (err instanceof RpcError)
687
690
  return err;
688
691
  if (!(err instanceof WakeshiftError) || err.code === "unreachable")
689
- return new McpError(INTERNAL_ERROR, "Wakeshift couldn't be reached");
692
+ return new RpcError(INTERNAL_ERROR, "Wakeshift couldn't be reached");
690
693
  switch (err.code) {
691
694
  case "callback_failed":
692
- return new McpError(codes.callbackEndpoint, err.detail, { reason: err.reason ?? "" });
695
+ return new RpcError(codes.callbackEndpoint, err.detail, { reason: err.reason ?? "" });
693
696
  case "unknown_event":
694
- return new McpError(codes.notFound, err.detail, { kind: "event" });
697
+ return new RpcError(codes.notFound, err.detail, { kind: "event" });
695
698
  case "unsupported":
696
- return new McpError(codes.unsupported, err.detail, { feature: "deliveryMode" });
699
+ return new RpcError(codes.unsupported, err.detail, { feature: "deliveryMode" });
697
700
  case "subscription_limit":
698
- return new McpError(codes.resourceExhausted, err.detail, { limit: "subscriptions" });
701
+ return new RpcError(codes.resourceExhausted, err.detail, { limit: "subscriptions" });
699
702
  case "subscription_stopped":
700
- return new McpError(codes.forbidden, err.detail, { reason: "stopped" });
703
+ return new RpcError(codes.forbidden, err.detail, { reason: "stopped" });
701
704
  case "bad_request":
702
705
  case "bad_arguments":
703
- return new McpError(INVALID_PARAMS, err.detail);
706
+ return new RpcError(INVALID_PARAMS, err.detail);
704
707
  case "unknown_session":
705
708
  case "other_client":
706
709
  case "unknown_conversation":
707
- return new McpError(codes.forbidden, "another client of yours holds that subscription");
710
+ return new RpcError(codes.forbidden, "another client of yours holds that subscription");
708
711
  }
709
- return new McpError(INTERNAL_ERROR, "Wakeshift refused the subscription: " + err.detail);
712
+ return new RpcError(INTERNAL_ERROR, "Wakeshift refused the subscription: " + err.detail);
710
713
  }
711
- //# sourceMappingURL=events.js.map
package/dist/harness.js CHANGED
@@ -75,4 +75,3 @@ function slug(name) {
75
75
  id = id.slice(0, 40).replace(/^-+|-+$/g, "");
76
76
  return id.length < 2 ? "agent" : id;
77
77
  }
78
- //# sourceMappingURL=harness.js.map
package/dist/host.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ /** Where the docs list the MCP server frameworks Wakeshift works with, and how. */
2
+ export declare const FRAMEWORKS_DOCS = "https://wakeshift.ai/docs/frameworks";
3
+ /** The protocol revision that has server/discover, MCP Events and the tasks extension. */
4
+ export declare const STATELESS_VERSION = "2026-07-28";