@wakeshift/sdk 0.2.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 +182 -0
- package/README.md +81 -64
- 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 +92 -48
- package/dist/events.js +217 -76
- package/dist/events.js.map +1 -1
- package/dist/harness.js +2 -2
- package/dist/harness.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/key.d.ts +1 -6
- 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 -241
- package/dist/wakeshift.js +157 -247
- package/dist/wakeshift.js.map +1 -1
- package/package.json +17 -3
- package/dist/guest.d.ts +0 -119
- package/dist/guest.js +0 -506
- package/dist/guest.js.map +0 -1
- package/dist/limits.d.ts +0 -19
- package/dist/limits.js +0 -105
- package/dist/limits.js.map +0 -1
- package/dist/sealed.d.ts +0 -4
- package/dist/sealed.js +0 -38
- package/dist/sealed.js.map +0 -1
package/dist/events.js
CHANGED
|
@@ -11,9 +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 {
|
|
15
|
-
import {
|
|
16
|
-
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";
|
|
17
17
|
const nameRE = /^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*){0,4}$/;
|
|
18
18
|
const argNameRE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
19
19
|
// jsonSchemaOf is a zod schema as JSON Schema, without $schema (Wakeshift
|
|
@@ -21,8 +21,42 @@ const argNameRE = /^[a-z][a-z0-9_]{0,63}$/;
|
|
|
21
21
|
function jsonSchemaOf(s) {
|
|
22
22
|
const out = toJsonSchemaCompat(s);
|
|
23
23
|
delete out.$schema;
|
|
24
|
+
normalize(out);
|
|
24
25
|
return out;
|
|
25
26
|
}
|
|
27
|
+
// normalize puts a schema in the form all three SDKs give for the same
|
|
28
|
+
// definition (test/events-list.json): required in name order, whatever
|
|
29
|
+
// order the fields were defined in, and an integer without the bounds zod
|
|
30
|
+
// adds for .int() (JavaScript's safe integers), which the other SDKs don't.
|
|
31
|
+
function normalize(s) {
|
|
32
|
+
if (Array.isArray(s.required))
|
|
33
|
+
s.required = [...s.required].sort();
|
|
34
|
+
if (s.type === "integer") {
|
|
35
|
+
if (s.minimum === Number.MIN_SAFE_INTEGER)
|
|
36
|
+
delete s.minimum;
|
|
37
|
+
if (s.maximum === Number.MAX_SAFE_INTEGER)
|
|
38
|
+
delete s.maximum;
|
|
39
|
+
}
|
|
40
|
+
if (s.properties && typeof s.properties === "object") {
|
|
41
|
+
for (const p of Object.values(s.properties))
|
|
42
|
+
if (p && typeof p === "object")
|
|
43
|
+
normalize(p);
|
|
44
|
+
}
|
|
45
|
+
if (s.items && typeof s.items === "object" && !Array.isArray(s.items))
|
|
46
|
+
normalize(s.items);
|
|
47
|
+
for (const k of ["anyOf", "oneOf", "allOf"]) {
|
|
48
|
+
if (Array.isArray(s[k]))
|
|
49
|
+
for (const b of s[k])
|
|
50
|
+
if (b && typeof b === "object")
|
|
51
|
+
normalize(b);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// canonical is JSON with every object's keys in name order, as Wakeshift
|
|
55
|
+
// keeps a subscription's arguments: how the tools' results echo them.
|
|
56
|
+
function canonical(v) {
|
|
57
|
+
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;
|
|
58
|
+
return JSON.stringify(sortKeys(v) ?? {});
|
|
59
|
+
}
|
|
26
60
|
function isSchema(v) {
|
|
27
61
|
return !!v && typeof v === "object" && ("_def" in v || "_zod" in v);
|
|
28
62
|
}
|
|
@@ -82,7 +116,7 @@ export class WakeEvent {
|
|
|
82
116
|
// ev/<name>/{a}/{b}: one segment per required scalar argument; an
|
|
83
117
|
// optional one filters on the payload field of its name.
|
|
84
118
|
const segs = ["ev", name];
|
|
85
|
-
filter = filter
|
|
119
|
+
filter = { ...filter }; // the caller's object stays as it was
|
|
86
120
|
for (const arg of Object.keys(props).sort()) {
|
|
87
121
|
if (!argNameRE.test(arg))
|
|
88
122
|
throw new Error(`wakeshift: event ${name}: argument ${JSON.stringify(arg)}: lowercase letters, digits and _`);
|
|
@@ -94,6 +128,9 @@ export class WakeEvent {
|
|
|
94
128
|
segs.push(`{${arg}}`);
|
|
95
129
|
}
|
|
96
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)`);
|
|
97
134
|
filter[arg] = `{${arg}}`;
|
|
98
135
|
}
|
|
99
136
|
else {
|
|
@@ -140,24 +177,65 @@ export class WakeEvent {
|
|
|
140
177
|
d.title = this.title;
|
|
141
178
|
return d;
|
|
142
179
|
}
|
|
180
|
+
/** @internal The client the event was defined on. */
|
|
181
|
+
owner() {
|
|
182
|
+
return this.wake;
|
|
183
|
+
}
|
|
143
184
|
/**
|
|
144
185
|
* Sends one occurrence: to everyone subscribed with these arguments, or,
|
|
145
|
-
* for a personal event, to the
|
|
186
|
+
* for a personal event, to the recipients `to` names. The payload is
|
|
146
187
|
* checked against the event's schema first.
|
|
147
188
|
*/
|
|
148
189
|
async emit(args, payload, opts = {}) {
|
|
149
190
|
const a = this.parseArgs(args);
|
|
150
191
|
const p = safeParse(this.payloadSchema, payload);
|
|
151
192
|
if (!p.success)
|
|
152
|
-
throw new
|
|
153
|
-
const to = opts.to
|
|
193
|
+
throw new ArgsError(`payload for ${this.name}: ${issues(p.error)}`);
|
|
194
|
+
const to = this.wireTo(opts.to);
|
|
154
195
|
if (this.personal && !to)
|
|
155
|
-
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`);
|
|
156
197
|
if (!this.personal && to)
|
|
157
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`);
|
|
158
199
|
const text = this.opts.text(p.data, a);
|
|
159
200
|
return this.wake.emitEvent({ name: this.name, arguments: a, payload: p.data, text, id: opts.id, to });
|
|
160
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
|
+
}
|
|
161
239
|
/**
|
|
162
240
|
* A capability URL that emits this event with these arguments (a personal
|
|
163
241
|
* event: to the one subscriber `to` names), for services that can only
|
|
@@ -168,6 +246,9 @@ export class WakeEvent {
|
|
|
168
246
|
*/
|
|
169
247
|
async publishUrl(args, opts = {}) {
|
|
170
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
|
+
}
|
|
171
252
|
const to = opts.to?.trim();
|
|
172
253
|
if (this.personal && !to)
|
|
173
254
|
throw new WakeshiftError(400, "bad_arguments", `${this.name} is personal: pass { to }, the one subscriber it's for`);
|
|
@@ -179,63 +260,86 @@ export class WakeEvent {
|
|
|
179
260
|
return this.wake.requestApp("POST", "/events/publish-urls", body);
|
|
180
261
|
}
|
|
181
262
|
/**
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
* clients subscribe themselves. Your tool decides who may: authorize isn't
|
|
186
|
-
* consulted here. user is the app's ID for the person (required for a
|
|
187
|
-
* personal event); wakeArgs are your tool's wake.args (session_key,
|
|
188
|
-
* harness, conversation_id).
|
|
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.
|
|
189
266
|
*/
|
|
190
|
-
async
|
|
267
|
+
async status(args, opts = {}) {
|
|
191
268
|
const a = this.parseArgs(args);
|
|
192
|
-
if (
|
|
193
|
-
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`);
|
|
194
271
|
}
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
agent: out.agent ?? { harness: null, via: null, claimed: null },
|
|
206
|
-
result: (text) => applyAddition(addition, text),
|
|
207
|
-
};
|
|
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 } };
|
|
208
282
|
}
|
|
209
283
|
/**
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
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.
|
|
213
287
|
*/
|
|
214
|
-
async
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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;
|
|
220
306
|
});
|
|
221
|
-
return !!out.ended;
|
|
222
307
|
}
|
|
223
|
-
/**
|
|
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
|
+
*/
|
|
224
313
|
async end(user, id) {
|
|
225
314
|
const out = await this.wake.requestApp("POST", "/event-subscriptions/end", { user, id });
|
|
226
315
|
return !!out.ended;
|
|
227
316
|
}
|
|
228
317
|
}
|
|
229
|
-
/**
|
|
230
|
-
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
|
+
}
|
|
231
330
|
}
|
|
232
|
-
|
|
233
|
-
|
|
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) {
|
|
234
336
|
const fromInit = server.server.getClientVersion()?.name;
|
|
235
337
|
if (fromInit)
|
|
236
338
|
return fromInit;
|
|
237
339
|
const info = meta?.["io.modelcontextprotocol/clientInfo"];
|
|
238
|
-
|
|
340
|
+
if (typeof info?.name === "string" && info.name)
|
|
341
|
+
return info.name;
|
|
342
|
+
return identified ?? "";
|
|
239
343
|
}
|
|
240
344
|
function header(extra, name) {
|
|
241
345
|
const h = extra?.requestInfo?.headers;
|
|
@@ -245,9 +349,10 @@ function header(extra, name) {
|
|
|
245
349
|
return (Array.isArray(v) ? v[0] : v) ?? "";
|
|
246
350
|
}
|
|
247
351
|
/** A client that speaks MCP Events: it declared the extension, or it's one known to (ChatGPT). */
|
|
352
|
+
/** @internal */
|
|
248
353
|
export function speaksEvents(capabilities, clientName) {
|
|
249
354
|
const ext = capabilities?.extensions?.[EXTENSION_KEY];
|
|
250
|
-
return !!ext ||
|
|
355
|
+
return !!ext || clientName.toLowerCase() === "openai-mcp";
|
|
251
356
|
}
|
|
252
357
|
// The fixed wording of the tools attach adds (tested: bench S1–S9 run
|
|
253
358
|
// through them), and of the refusals of a native subscribe. The Go SDK
|
|
@@ -300,12 +405,29 @@ const MAX_LISTED_EVENTS = 20;
|
|
|
300
405
|
export function attachEvents(wake, server, events, opts = {}) {
|
|
301
406
|
if (events.length === 0)
|
|
302
407
|
throw new Error("wakeshift: attach needs at least one event (wake.defineEvent) defined first");
|
|
303
|
-
const
|
|
304
|
-
|
|
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
|
+
});
|
|
305
427
|
const byName = new Map(events.map((e) => [e.name, e]));
|
|
306
428
|
const low = server.server;
|
|
307
429
|
try {
|
|
308
|
-
low.registerCapabilities({ [
|
|
430
|
+
low.registerCapabilities({ [TOP_LEVEL_KEY]: { listChanged: false }, extensions: { [EXTENSION_KEY]: { listChanged: false } } });
|
|
309
431
|
}
|
|
310
432
|
catch (err) {
|
|
311
433
|
throw new Error("wakeshift: call attach before connecting the server to a transport", { cause: err });
|
|
@@ -322,7 +444,8 @@ export function attachEvents(wake, server, events, opts = {}) {
|
|
|
322
444
|
const native = async (req, extra, unsubscribe) => {
|
|
323
445
|
const p = (req.params ?? {});
|
|
324
446
|
const meta = (p._meta ?? {});
|
|
325
|
-
const
|
|
447
|
+
const id = identityOf(extra);
|
|
448
|
+
const client = clientInfoName(server, meta, id?.client);
|
|
326
449
|
const ua = header(extra, "user-agent");
|
|
327
450
|
const dialect = dialectOf(client, ua);
|
|
328
451
|
const codes = codesFor(dialect);
|
|
@@ -341,9 +464,9 @@ export function attachEvents(wake, server, events, opts = {}) {
|
|
|
341
464
|
args = ev.parseArgs(p.arguments ?? {});
|
|
342
465
|
}
|
|
343
466
|
catch (err) {
|
|
344
|
-
throw new McpError(INVALID_PARAMS, err.message);
|
|
467
|
+
throw new McpError(INVALID_PARAMS, err instanceof WakeshiftError ? err.detail : err.message);
|
|
345
468
|
}
|
|
346
|
-
const user =
|
|
469
|
+
const user = id?.user;
|
|
347
470
|
if (!user)
|
|
348
471
|
throw new McpError(codes.forbidden, fill("native_no_user"));
|
|
349
472
|
const call = { user, client, meta, ...(ua ? { user_agent: ua } : {}) };
|
|
@@ -388,17 +511,19 @@ export function attachEvents(wake, server, events, opts = {}) {
|
|
|
388
511
|
const listed = events
|
|
389
512
|
.slice(0, MAX_LISTED_EVENTS)
|
|
390
513
|
.map((e) => {
|
|
391
|
-
const props = Object.keys(e.manifest.arguments.properties ?? {});
|
|
514
|
+
const props = Object.keys(e.manifest.arguments.properties ?? {}).sort();
|
|
392
515
|
return fill("event_line", { name: e.name, arguments: props.join(", "), description: e.description });
|
|
393
516
|
})
|
|
394
517
|
.join("\n");
|
|
395
518
|
const more = events.length > MAX_LISTED_EVENTS ? fill("more", { n: events.length - MAX_LISTED_EVENTS }) : "";
|
|
396
519
|
const argsInput = z.record(z.string(), z.any()).optional().describe(EVENT_TOOL_TEXT.arguments_argument);
|
|
397
|
-
const result = (text, addition) =>
|
|
520
|
+
const result = (text, addition) => withAddition(text, addition ?? {});
|
|
398
521
|
const fail = (text) => ({ isError: true, content: [{ type: "text", text }] });
|
|
399
|
-
|
|
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", {
|
|
400
525
|
description: `${EVENT_TOOL_TEXT.subscribe_description}${EVENT_TOOL_TEXT.events_heading}${listed}${more}`,
|
|
401
|
-
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 },
|
|
402
527
|
}, async (input, extra) => {
|
|
403
528
|
const { event, arguments: raw, ...rest } = input;
|
|
404
529
|
const ev = byName.get(event);
|
|
@@ -409,21 +534,21 @@ export function attachEvents(wake, server, events, opts = {}) {
|
|
|
409
534
|
args = ev.parseArgs(raw ?? {});
|
|
410
535
|
}
|
|
411
536
|
catch (err) {
|
|
412
|
-
return fail(err.message);
|
|
537
|
+
return fail(err instanceof WakeshiftError ? err.detail : err.message);
|
|
413
538
|
}
|
|
414
|
-
const user =
|
|
539
|
+
const { user, client } = callerOf(extra);
|
|
415
540
|
if (ev.personal && !user)
|
|
416
541
|
return fail(fill("personal_no_user", { event: ev.name }));
|
|
417
|
-
if (!(await ev.allows({ user, client
|
|
542
|
+
if (!(await ev.allows({ user, client, native: false }, args))) {
|
|
418
543
|
return fail(fill("refused", { event: ev.name }));
|
|
419
544
|
}
|
|
420
545
|
try {
|
|
421
546
|
const out = await wake.requestApp("POST", "/event-subscriptions", {
|
|
422
|
-
call:
|
|
547
|
+
call: toolCallOf(extra, client, user, rest, "subscribe_events"),
|
|
423
548
|
name: ev.name,
|
|
424
549
|
arguments: args,
|
|
425
550
|
});
|
|
426
|
-
const what = { event: ev.name, arguments:
|
|
551
|
+
const what = { event: ev.name, arguments: canonical(args) };
|
|
427
552
|
const lead = fill(out.event_subscription.created ? "subscribed" : "renewed", what);
|
|
428
553
|
return result(lead, out.addition);
|
|
429
554
|
}
|
|
@@ -431,17 +556,17 @@ export function attachEvents(wake, server, events, opts = {}) {
|
|
|
431
556
|
return fail(fill("subscribe_failed", { detail: detailOf(err) }));
|
|
432
557
|
}
|
|
433
558
|
});
|
|
434
|
-
|
|
559
|
+
register("unsubscribe_events", {
|
|
435
560
|
description: EVENT_TOOL_TEXT.unsubscribe_description,
|
|
436
561
|
inputSchema: {
|
|
437
562
|
event: z.enum(names).optional().describe(EVENT_TOOL_TEXT.event_argument),
|
|
438
563
|
arguments: argsInput,
|
|
439
564
|
id: z.string().optional().describe(EVENT_TOOL_TEXT.id_argument),
|
|
440
|
-
...
|
|
565
|
+
...toolArgs,
|
|
441
566
|
},
|
|
442
567
|
}, async (input, extra) => {
|
|
443
568
|
const { event, arguments: raw, id, ...rest } = input;
|
|
444
|
-
const user =
|
|
569
|
+
const { user, client } = callerOf(extra);
|
|
445
570
|
try {
|
|
446
571
|
if (id) {
|
|
447
572
|
if (!user)
|
|
@@ -454,18 +579,21 @@ export function attachEvents(wake, server, events, opts = {}) {
|
|
|
454
579
|
return fail(fill("need"));
|
|
455
580
|
const args = ev.parseArgs(raw ?? {});
|
|
456
581
|
const out = await wake.requestApp("POST", "/event-subscriptions/unsubscribe", {
|
|
457
|
-
call:
|
|
582
|
+
call: toolCallOf(extra, client, user, rest, "unsubscribe_events"),
|
|
458
583
|
name: ev.name,
|
|
459
584
|
arguments: args,
|
|
460
585
|
});
|
|
461
|
-
return result(fill(out.ended ? "unsubscribed" : "not_subscribed", { event: ev.name, arguments:
|
|
586
|
+
return result(fill(out.ended ? "unsubscribed" : "not_subscribed", { event: ev.name, arguments: canonical(args) }));
|
|
462
587
|
}
|
|
463
588
|
catch (err) {
|
|
464
|
-
return fail(err instanceof ArgsError ? err.
|
|
589
|
+
return fail(err instanceof ArgsError ? err.detail : fill("unsubscribe_failed", { detail: detailOf(err) }));
|
|
465
590
|
}
|
|
466
591
|
});
|
|
467
|
-
|
|
468
|
-
|
|
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;
|
|
469
597
|
if (!user)
|
|
470
598
|
return fail(fill("list_no_user"));
|
|
471
599
|
try {
|
|
@@ -489,27 +617,40 @@ export function attachEvents(wake, server, events, opts = {}) {
|
|
|
489
617
|
low.oninitialized = () => {
|
|
490
618
|
// Set both ways: a server reconnected to another client decides again.
|
|
491
619
|
if (speaksEvents(low.getClientCapabilities(), low.getClientVersion()?.name ?? ""))
|
|
492
|
-
subscribeTool
|
|
620
|
+
subscribeTool?.disable();
|
|
493
621
|
else
|
|
494
|
-
subscribeTool
|
|
622
|
+
subscribeTool?.enable();
|
|
495
623
|
prev?.();
|
|
496
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;
|
|
497
635
|
}
|
|
498
636
|
// rpcErrorOf is the JSON-RPC error for a failed native call, in the
|
|
499
637
|
// client's dialect's codes. A conversation that isn't the caller's is
|
|
500
638
|
// Forbidden, whether it exists or not.
|
|
501
|
-
|
|
639
|
+
/** @internal */
|
|
640
|
+
export function rpcErrorOf(err, codes) {
|
|
502
641
|
if (err instanceof McpError)
|
|
503
642
|
return err;
|
|
504
|
-
if (!(err instanceof WakeshiftError))
|
|
643
|
+
if (!(err instanceof WakeshiftError) || err.code === "unreachable")
|
|
505
644
|
return new McpError(INTERNAL_ERROR, "Wakeshift couldn't be reached");
|
|
506
645
|
switch (err.code) {
|
|
507
646
|
case "callback_failed":
|
|
508
|
-
return new McpError(codes.callbackEndpoint, err.detail, { reason: err.reason });
|
|
647
|
+
return new McpError(codes.callbackEndpoint, err.detail, { reason: err.reason ?? "" });
|
|
509
648
|
case "unknown_event":
|
|
510
649
|
return new McpError(codes.notFound, err.detail, { kind: "event" });
|
|
511
650
|
case "unsupported":
|
|
512
651
|
return new McpError(codes.unsupported, err.detail, { feature: "deliveryMode" });
|
|
652
|
+
case "subscription_limit":
|
|
653
|
+
return new McpError(codes.resourceExhausted, err.detail, { kind: "subscriptions" });
|
|
513
654
|
case "bad_request":
|
|
514
655
|
case "bad_arguments":
|
|
515
656
|
return new McpError(INVALID_PARAMS, err.detail);
|