@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/CHANGELOG.md +153 -0
- package/README.md +74 -48
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/events.d.ts +91 -51
- package/dist/events.js +178 -72
- package/dist/events.js.map +1 -1
- package/dist/harness.js +1 -1
- package/dist/harness.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/key.d.ts +0 -5
- package/dist/key.js +9 -5
- package/dist/key.js.map +1 -1
- package/dist/retry.js +1 -1
- package/dist/retry.js.map +1 -1
- package/dist/spec.d.ts +2 -2
- package/dist/spec.js +2 -2
- package/dist/spec.js.map +1 -1
- package/dist/toolcall.d.ts +154 -0
- package/dist/toolcall.js +558 -0
- package/dist/toolcall.js.map +1 -0
- package/dist/wakeshift.d.ts +75 -253
- package/dist/wakeshift.js +156 -244
- package/dist/wakeshift.js.map +1 -1
- package/package.json +16 -2
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,
|
|
15
|
-
import {
|
|
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
|
|
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
|
|
186
|
-
const to = opts.to
|
|
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
|
|
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
|
-
*
|
|
216
|
-
*
|
|
217
|
-
*
|
|
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
|
|
267
|
+
async status(args, opts = {}) {
|
|
225
268
|
const a = this.parseArgs(args);
|
|
226
|
-
if (
|
|
227
|
-
throw new WakeshiftError(400, "bad_arguments", `${this.name}
|
|
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
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
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
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
264
|
-
export
|
|
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
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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 ||
|
|
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
|
|
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({ [
|
|
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
|
|
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 =
|
|
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) =>
|
|
520
|
+
const result = (text, addition) => withAddition(text, addition ?? {});
|
|
431
521
|
const fail = (text) => ({ isError: true, content: [{ type: "text", text }] });
|
|
432
|
-
|
|
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, ...
|
|
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 =
|
|
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
|
|
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:
|
|
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
|
-
|
|
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
|
-
...
|
|
565
|
+
...toolArgs,
|
|
474
566
|
},
|
|
475
567
|
}, async (input, extra) => {
|
|
476
568
|
const { event, arguments: raw, id, ...rest } = input;
|
|
477
|
-
const user =
|
|
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:
|
|
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.
|
|
589
|
+
return fail(err instanceof ArgsError ? err.detail : fill("unsubscribe_failed", { detail: detailOf(err) }));
|
|
498
590
|
}
|
|
499
591
|
});
|
|
500
|
-
|
|
501
|
-
|
|
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
|
|
620
|
+
subscribeTool?.disable();
|
|
526
621
|
else
|
|
527
|
-
subscribeTool
|
|
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
|
-
|
|
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":
|