@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/CHANGELOG.md +17 -0
- package/README.md +56 -2
- package/dist/discover.d.ts +2 -2
- package/dist/discover.js +67 -46
- package/dist/discover.js.map +1 -1
- package/dist/events.d.ts +13 -9
- package/dist/events.js +80 -84
- package/dist/events.js.map +1 -1
- package/dist/host.d.ts +2 -0
- package/dist/host.js +203 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp.d.ts +92 -0
- package/dist/mcp.js +22 -0
- package/dist/mcp.js.map +1 -0
- package/dist/tasks.js +50 -38
- package/dist/tasks.js.map +1 -1
- package/dist/toolcall.d.ts +71 -24
- package/dist/toolcall.js +59 -60
- package/dist/toolcall.js.map +1 -1
- package/dist/wakeshift.d.ts +7 -3
- package/dist/wakeshift.js +12 -11
- package/dist/wakeshift.js.map +1 -1
- package/dist/zod.d.ts +16 -0
- package/dist/zod.js +95 -0
- package/dist/zod.js.map +1 -0
- package/package.json +15 -2
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 {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
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,
|
|
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 =
|
|
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
|
|
336
|
-
// else the name identify gave (identified).
|
|
337
|
-
function clientInfoName(
|
|
338
|
-
const fromInit =
|
|
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
|
-
|
|
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 = (
|
|
419
|
-
const id =
|
|
420
|
-
return { user: id?.user, client: clientInfoName(
|
|
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
|
-
|
|
425
|
-
identify: (
|
|
426
|
-
clientOf: (meta, identified) => clientInfoName(
|
|
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
|
-
|
|
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
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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,
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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 (
|
|
449
|
+
const native = async (p, raw, m) => {
|
|
455
450
|
const unsubscribe = m === "unsubscribe";
|
|
456
|
-
const
|
|
457
|
-
const meta = (
|
|
458
|
-
const id = identityOf(
|
|
459
|
-
const client = clientInfoName(
|
|
460
|
-
const ua =
|
|
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(
|
|
458
|
+
const nameHeader = req.header(NAME_HEADER);
|
|
464
459
|
if (nameHeader && headerValue(nameHeader) !== p.name) {
|
|
465
|
-
throw new
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
537
|
-
|
|
538
|
-
|
|
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) =>
|
|
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,
|
|
558
|
-
const { event, arguments:
|
|
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(
|
|
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(
|
|
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(
|
|
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,
|
|
598
|
-
const { event, arguments:
|
|
599
|
-
const { user, client } = callerOf(
|
|
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(
|
|
608
|
+
const args = ev.parseArgs(rawArgs ?? {});
|
|
611
609
|
const out = await wake.requestApp("POST", "/event-subscriptions/unsubscribe", {
|
|
612
|
-
call: toolCallOf(
|
|
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,
|
|
626
|
-
const user = identityOf(
|
|
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
|
-
|
|
651
|
-
low.oninitialized = () => {
|
|
648
|
+
host.onInitialized(() => {
|
|
652
649
|
// Set both ways: a server reconnected to another client decides again.
|
|
653
|
-
if (speaksEvents(
|
|
650
|
+
if (speaksEvents(host.clientCapabilities(), host.clientName() ?? ""))
|
|
654
651
|
subscribeTool?.disable();
|
|
655
652
|
else
|
|
656
653
|
subscribeTool?.enable();
|
|
657
|
-
|
|
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
|
|
682
|
+
if (err instanceof RpcError)
|
|
687
683
|
return err;
|
|
688
684
|
if (!(err instanceof WakeshiftError) || err.code === "unreachable")
|
|
689
|
-
return new
|
|
685
|
+
return new RpcError(INTERNAL_ERROR, "Wakeshift couldn't be reached");
|
|
690
686
|
switch (err.code) {
|
|
691
687
|
case "callback_failed":
|
|
692
|
-
return new
|
|
688
|
+
return new RpcError(codes.callbackEndpoint, err.detail, { reason: err.reason ?? "" });
|
|
693
689
|
case "unknown_event":
|
|
694
|
-
return new
|
|
690
|
+
return new RpcError(codes.notFound, err.detail, { kind: "event" });
|
|
695
691
|
case "unsupported":
|
|
696
|
-
return new
|
|
692
|
+
return new RpcError(codes.unsupported, err.detail, { feature: "deliveryMode" });
|
|
697
693
|
case "subscription_limit":
|
|
698
|
-
return new
|
|
694
|
+
return new RpcError(codes.resourceExhausted, err.detail, { limit: "subscriptions" });
|
|
699
695
|
case "subscription_stopped":
|
|
700
|
-
return new
|
|
696
|
+
return new RpcError(codes.forbidden, err.detail, { reason: "stopped" });
|
|
701
697
|
case "bad_request":
|
|
702
698
|
case "bad_arguments":
|
|
703
|
-
return new
|
|
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
|
|
703
|
+
return new RpcError(codes.forbidden, "another client of yours holds that subscription");
|
|
708
704
|
}
|
|
709
|
-
return new
|
|
705
|
+
return new RpcError(INTERNAL_ERROR, "Wakeshift refused the subscription: " + err.detail);
|
|
710
706
|
}
|
|
711
707
|
//# sourceMappingURL=events.js.map
|