@fruggr/zendesk-mcp-server 3.3.1 → 3.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/dist/THIRD-PARTY-NOTICES.md +1 -1
- package/dist/{http-BYYYoQaK.js → http-DU1dSrPe.js} +340 -28
- package/dist/index.js +2988 -529
- package/dist/sbom.cdx.json +7 -7
- package/dist/{token-persistence-BUHh23BM.js → token-persistence-r0v9RO3q.js} +3 -3
- package/dist/{token-store-BLAkLLQO.js → token-store-DyO12sbo.js} +2 -2
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { parseArgs } from "node:util";
|
|
4
|
-
import { randomUUID } from "node:crypto";
|
|
4
|
+
import { randomBytes, randomUUID } from "node:crypto";
|
|
5
5
|
import minpath, { dirname, join } from "node:path";
|
|
6
6
|
import { fileURLToPath, fileURLToPath as urlToPath, pathToFileURL } from "node:url";
|
|
7
7
|
import minproc, { default as process$2 } from "node:process";
|
|
@@ -39,7 +39,7 @@ var __toESM$1 = (mod, isNodeMode, target) => (target = mod != null ? __create$1(
|
|
|
39
39
|
}) : target, mod));
|
|
40
40
|
var __require = /* #__PURE__ */ (() => createRequire(import.meta.url))();
|
|
41
41
|
//#endregion
|
|
42
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
42
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/util.js
|
|
43
43
|
function getEnumValues(entries) {
|
|
44
44
|
const numericValues = Object.values(entries).filter((v) => typeof v === "number");
|
|
45
45
|
return Object.entries(entries).filter(([k, _]) => numericValues.indexOf(+k) === -1).map(([_, v]) => v);
|
|
@@ -189,7 +189,7 @@ const allowsEval = /* @__PURE__*/ cached$1(() => {
|
|
|
189
189
|
return false;
|
|
190
190
|
}
|
|
191
191
|
});
|
|
192
|
-
function isPlainObject$
|
|
192
|
+
function isPlainObject$9(o) {
|
|
193
193
|
if (isObject(o) === false) return false;
|
|
194
194
|
const ctor = o.constructor;
|
|
195
195
|
if (ctor === void 0) return true;
|
|
@@ -200,7 +200,7 @@ function isPlainObject$3(o) {
|
|
|
200
200
|
return true;
|
|
201
201
|
}
|
|
202
202
|
function shallowClone(o) {
|
|
203
|
-
if (isPlainObject$
|
|
203
|
+
if (isPlainObject$9(o)) return { ...o };
|
|
204
204
|
if (Array.isArray(o)) return [...o];
|
|
205
205
|
if (o instanceof Map) return new Map(o);
|
|
206
206
|
if (o instanceof Set) return new Set(o);
|
|
@@ -288,7 +288,7 @@ function omit(schema, mask) {
|
|
|
288
288
|
}));
|
|
289
289
|
}
|
|
290
290
|
function extend$1(schema, shape) {
|
|
291
|
-
if (!isPlainObject$
|
|
291
|
+
if (!isPlainObject$9(shape)) throw new Error("Invalid input to extend: expected a plain object");
|
|
292
292
|
const checks = schema._zod.def.checks;
|
|
293
293
|
if (checks && checks.length > 0) {
|
|
294
294
|
const existingShape = sourceShape(schema);
|
|
@@ -303,7 +303,7 @@ function extended(schema, shape) {
|
|
|
303
303
|
return newShape;
|
|
304
304
|
}
|
|
305
305
|
function safeExtend(schema, shape) {
|
|
306
|
-
if (!isPlainObject$
|
|
306
|
+
if (!isPlainObject$9(shape)) throw new Error("Invalid input to safeExtend: expected a plain object");
|
|
307
307
|
return clone$1(schema, mergeDefs(schema._zod.def, { shape: extended(schema, shape) }));
|
|
308
308
|
}
|
|
309
309
|
function merge$2(a, b) {
|
|
@@ -364,6 +364,17 @@ function prefixIssues(path, issues) {
|
|
|
364
364
|
function unwrapMessage(message) {
|
|
365
365
|
return typeof message === "string" ? message : message?.message;
|
|
366
366
|
}
|
|
367
|
+
/** The leading size checks of `defChecks` that set an upper bound, each paired with it. */
|
|
368
|
+
function sizeCaps(defChecks) {
|
|
369
|
+
const caps = [];
|
|
370
|
+
for (const ch of defChecks ?? []) {
|
|
371
|
+
const d = ch._zod.def;
|
|
372
|
+
if (!/_(length|size)$|_equals$/.test(d.check)) break;
|
|
373
|
+
const max = d.maximum ?? d.length ?? d.size;
|
|
374
|
+
if (max >= 0) caps.push([max, ch]);
|
|
375
|
+
}
|
|
376
|
+
return caps;
|
|
377
|
+
}
|
|
367
378
|
function attachSchema(issues, start, inst) {
|
|
368
379
|
var _a;
|
|
369
380
|
for (let i = start; i < issues.length; i++) (_a = issues[i]).schema ?? (_a.schema = inst);
|
|
@@ -580,7 +591,7 @@ function constantCatch(value) {
|
|
|
580
591
|
return fn;
|
|
581
592
|
}
|
|
582
593
|
//#endregion
|
|
583
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
594
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/core.js
|
|
584
595
|
var _a$4;
|
|
585
596
|
/** A special constant with type `never` */
|
|
586
597
|
const NEVER = /*@__PURE__*/ Object.freeze({ status: "aborted" });
|
|
@@ -688,7 +699,7 @@ function config(newConfig) {
|
|
|
688
699
|
return globalConfig;
|
|
689
700
|
}
|
|
690
701
|
//#endregion
|
|
691
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
702
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/doc.js
|
|
692
703
|
var Doc = class {
|
|
693
704
|
constructor(args = [], closed = {}) {
|
|
694
705
|
this.content = [];
|
|
@@ -722,7 +733,7 @@ var Doc = class {
|
|
|
722
733
|
}
|
|
723
734
|
};
|
|
724
735
|
//#endregion
|
|
725
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
736
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/memoizer.js
|
|
726
737
|
var $ZodCyclicError = class extends Error {
|
|
727
738
|
constructor() {
|
|
728
739
|
super(`Cannot parse a reference cycle that closes through a transform`);
|
|
@@ -984,7 +995,7 @@ function isBackEdge(ctx, value) {
|
|
|
984
995
|
return backEdges !== void 0 && isRef(value) && backEdges.has(value);
|
|
985
996
|
}
|
|
986
997
|
//#endregion
|
|
987
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
998
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/regexes.js
|
|
988
999
|
/**
|
|
989
1000
|
* @deprecated CUID v1 is deprecated by its authors due to information leakage
|
|
990
1001
|
* (timestamps embedded in the id). Use {@link cuid2} instead.
|
|
@@ -1059,7 +1070,7 @@ const _null$2 = /^null$/i;
|
|
|
1059
1070
|
const lowercase = /^[^A-Z]*$/;
|
|
1060
1071
|
const uppercase = /^[^a-z]*$/;
|
|
1061
1072
|
//#endregion
|
|
1062
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
1073
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/checks.js
|
|
1063
1074
|
const $ZodCheck = /*@__PURE__*/ $constructor("$ZodCheck", (inst, def) => {
|
|
1064
1075
|
var _a;
|
|
1065
1076
|
inst._zod ?? (inst._zod = {});
|
|
@@ -1355,7 +1366,7 @@ const $ZodCheckOverwrite = /*@__PURE__*/ $constructor("$ZodCheckOverwrite", (ins
|
|
|
1355
1366
|
};
|
|
1356
1367
|
});
|
|
1357
1368
|
//#endregion
|
|
1358
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
1369
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/errors.js
|
|
1359
1370
|
function _getMessage() {
|
|
1360
1371
|
const internals = this._zod;
|
|
1361
1372
|
internals.message ?? (internals.message = JSON.stringify(internals.def, jsonStringifyReplacer, 2));
|
|
@@ -1471,7 +1482,7 @@ function formatError(error, mapper = (issue) => issue.message) {
|
|
|
1471
1482
|
return fieldErrors;
|
|
1472
1483
|
}
|
|
1473
1484
|
//#endregion
|
|
1474
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
1485
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/parse.js
|
|
1475
1486
|
function finalizeParams(callee, params) {
|
|
1476
1487
|
return {
|
|
1477
1488
|
callee: params?.callee ?? callee,
|
|
@@ -1674,14 +1685,14 @@ const _safeDecodeAsync = (_Err) => async (schema, value, _ctx) => {
|
|
|
1674
1685
|
return _safeParseAsync(_Err)(schema, value, _ctx);
|
|
1675
1686
|
};
|
|
1676
1687
|
//#endregion
|
|
1677
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
1688
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/versions.js
|
|
1678
1689
|
const version = {
|
|
1679
1690
|
major: 4,
|
|
1680
1691
|
minor: 6,
|
|
1681
|
-
patch:
|
|
1692
|
+
patch: 6
|
|
1682
1693
|
};
|
|
1683
1694
|
//#endregion
|
|
1684
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
1695
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/schemas.js
|
|
1685
1696
|
const $ZodType = /*@__PURE__*/ $constructor("$ZodType", (inst, def) => {
|
|
1686
1697
|
var _a;
|
|
1687
1698
|
inst ?? (inst = {});
|
|
@@ -2243,6 +2254,26 @@ const $ZodNever = /*@__PURE__*/ $constructor("$ZodNever", (inst, def) => {
|
|
|
2243
2254
|
return payload;
|
|
2244
2255
|
};
|
|
2245
2256
|
});
|
|
2257
|
+
function capSize(inst, size) {
|
|
2258
|
+
const caps = sizeCaps(inst._zod.def.checks);
|
|
2259
|
+
if (!caps.length) return;
|
|
2260
|
+
const cap = Math.min(...caps.map((c) => c[0]));
|
|
2261
|
+
const parse = inst._zod.parse;
|
|
2262
|
+
inst._zod.parse = (payload, ctx) => {
|
|
2263
|
+
const n = size(payload.value);
|
|
2264
|
+
if (n > cap && !ctx?.skipChecks && ctx?.direction !== "backward") {
|
|
2265
|
+
for (const [max, ch] of sizeCaps(inst._zod.def.checks)) if (n > max && ch._zod.def.when(payload)) {
|
|
2266
|
+
const start = payload.issues.length;
|
|
2267
|
+
ch._zod.check(payload);
|
|
2268
|
+
payload.issues[start].continue = false;
|
|
2269
|
+
attachSchema(payload.issues, start, inst);
|
|
2270
|
+
return payload;
|
|
2271
|
+
}
|
|
2272
|
+
}
|
|
2273
|
+
return parse(payload, ctx);
|
|
2274
|
+
};
|
|
2275
|
+
}
|
|
2276
|
+
const arraySize = (input) => Array.isArray(input) ? input.length : -1;
|
|
2246
2277
|
function handleArrayResult(result, final, index) {
|
|
2247
2278
|
if (result.issues.length) final.issues.push(...prefixIssues(index, result.issues));
|
|
2248
2279
|
final.value[index] = result.value;
|
|
@@ -2280,6 +2311,7 @@ const $ZodArray = /*@__PURE__*/ $constructor("$ZodArray", (inst, def) => {
|
|
|
2280
2311
|
if (proms.length) return Promise.all(proms).then(() => payload);
|
|
2281
2312
|
return payload;
|
|
2282
2313
|
};
|
|
2314
|
+
capSize(inst, arraySize);
|
|
2283
2315
|
});
|
|
2284
2316
|
function handlePropertyResult(result, final, key, input, optin, optout) {
|
|
2285
2317
|
const isPresent = key in input;
|
|
@@ -2388,7 +2420,7 @@ const $ZodObject = /*@__PURE__*/ $constructor("$ZodObject", (inst, def) => {
|
|
|
2388
2420
|
}
|
|
2389
2421
|
return propValues;
|
|
2390
2422
|
});
|
|
2391
|
-
const isObject$
|
|
2423
|
+
const isObject$1 = isObject;
|
|
2392
2424
|
const catchall = def.catchall;
|
|
2393
2425
|
let value;
|
|
2394
2426
|
const memo = globalConfig.memoizer;
|
|
@@ -2396,7 +2428,7 @@ const $ZodObject = /*@__PURE__*/ $constructor("$ZodObject", (inst, def) => {
|
|
|
2396
2428
|
inst._zod.parse = (payload, ctx) => {
|
|
2397
2429
|
value ?? (value = _normalized.value);
|
|
2398
2430
|
const input = payload.value;
|
|
2399
|
-
if (!isObject$
|
|
2431
|
+
if (!isObject$1(input)) {
|
|
2400
2432
|
payload.issues.push({
|
|
2401
2433
|
expected: "object",
|
|
2402
2434
|
code: "invalid_type",
|
|
@@ -2526,7 +2558,7 @@ const $ZodObjectJIT = /*@__PURE__*/ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
2526
2558
|
return doc.compile();
|
|
2527
2559
|
};
|
|
2528
2560
|
let fastpass;
|
|
2529
|
-
const isObject$
|
|
2561
|
+
const isObject$2 = isObject;
|
|
2530
2562
|
const jit = !globalConfig.jitless;
|
|
2531
2563
|
const fastEnabled = jit && allowsEval.value;
|
|
2532
2564
|
const catchall = def.catchall;
|
|
@@ -2534,7 +2566,7 @@ const $ZodObjectJIT = /*@__PURE__*/ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
2534
2566
|
inst._zod.parse = (payload, ctx) => {
|
|
2535
2567
|
value ?? (value = _normalized.value);
|
|
2536
2568
|
const input = payload.value;
|
|
2537
|
-
if (!isObject$
|
|
2569
|
+
if (!isObject$2(input)) {
|
|
2538
2570
|
payload.issues.push({
|
|
2539
2571
|
expected: "object",
|
|
2540
2572
|
code: "invalid_type",
|
|
@@ -2698,7 +2730,7 @@ function mergeValues(a, b) {
|
|
|
2698
2730
|
valid: true,
|
|
2699
2731
|
data: a
|
|
2700
2732
|
};
|
|
2701
|
-
if (isPlainObject$
|
|
2733
|
+
if (isPlainObject$9(a) && isPlainObject$9(b)) {
|
|
2702
2734
|
const bKeys = Object.keys(b);
|
|
2703
2735
|
const sharedKeys = Object.keys(a).filter((key) => bKeys.indexOf(key) !== -1);
|
|
2704
2736
|
const newObj = {
|
|
@@ -2791,7 +2823,7 @@ const $ZodRecord = /*@__PURE__*/ $constructor("$ZodRecord", (inst, def) => {
|
|
|
2791
2823
|
memo?.attach(inst);
|
|
2792
2824
|
inst._zod.parse = (payload, ctx) => {
|
|
2793
2825
|
const input = payload.value;
|
|
2794
|
-
if (!isPlainObject$
|
|
2826
|
+
if (!isPlainObject$9(input)) {
|
|
2795
2827
|
payload.issues.push({
|
|
2796
2828
|
expected: "record",
|
|
2797
2829
|
code: "invalid_type",
|
|
@@ -3198,7 +3230,7 @@ function handleRefineResult(result, payload, input, inst) {
|
|
|
3198
3230
|
}
|
|
3199
3231
|
}
|
|
3200
3232
|
//#endregion
|
|
3201
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
3233
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/compile.js
|
|
3202
3234
|
/** @internal Sentinel the compiled fast path returns when validation fails. */
|
|
3203
3235
|
const INVALID = Symbol.for("zod.compile.invalid");
|
|
3204
3236
|
const FALLBACK_FLAG = Symbol.for("zod.compile.fallback");
|
|
@@ -4183,9 +4215,13 @@ function generateNullableCheck(doc, ctx, schema, accessor, buildsValue = true) {
|
|
|
4183
4215
|
doc.write(`}`);
|
|
4184
4216
|
return outputVar;
|
|
4185
4217
|
}
|
|
4218
|
+
function generateSizeGate(doc, schema, size) {
|
|
4219
|
+
for (const [max, check] of sizeCaps(schema._zod.def.checks)) doc.write(`if (${size} > ${numericOperand(max, check._zod.def.check)}) return INVALID;`);
|
|
4220
|
+
}
|
|
4186
4221
|
function generateArrayCheck(doc, ctx, schema, accessor, buildsValue = true) {
|
|
4187
4222
|
const def = schema._zod.def;
|
|
4188
4223
|
doc.write(`if (!Array.isArray(${accessor})) return INVALID;`);
|
|
4224
|
+
generateSizeGate(doc, schema, `${accessor}.length`);
|
|
4189
4225
|
const outputVar = buildsValue ? newVar(ctx) : null;
|
|
4190
4226
|
const iVar = newVar(ctx);
|
|
4191
4227
|
const elemVar = newVar(ctx);
|
|
@@ -4432,7 +4468,7 @@ function generateIntersectionCheck(doc, ctx, schema, accessor) {
|
|
|
4432
4468
|
}
|
|
4433
4469
|
function generateRecordCheck(doc, ctx, schema, accessor) {
|
|
4434
4470
|
const def = schema._zod.def;
|
|
4435
|
-
const isPlainObjectConst = addConstant(ctx, isPlainObject$
|
|
4471
|
+
const isPlainObjectConst = addConstant(ctx, isPlainObject$9);
|
|
4436
4472
|
doc.write(`if (!${isPlainObjectConst}(${accessor})) return INVALID;`);
|
|
4437
4473
|
const outputVar = newVar(ctx);
|
|
4438
4474
|
const kVar = newVar(ctx);
|
|
@@ -4522,6 +4558,7 @@ function literalPropertyKey(ctx, key) {
|
|
|
4522
4558
|
function generateMapCheck(doc, ctx, schema, accessor) {
|
|
4523
4559
|
const def = schema._zod.def;
|
|
4524
4560
|
doc.write(`if (!(${accessor} instanceof Map)) return INVALID;`);
|
|
4561
|
+
generateSizeGate(doc, schema, `${accessor}.size`);
|
|
4525
4562
|
const outputVar = newVar(ctx);
|
|
4526
4563
|
const kVar = newVar(ctx);
|
|
4527
4564
|
const valVar = newVar(ctx);
|
|
@@ -4538,6 +4575,7 @@ function generateMapCheck(doc, ctx, schema, accessor) {
|
|
|
4538
4575
|
function generateSetCheck(doc, ctx, schema, accessor) {
|
|
4539
4576
|
const def = schema._zod.def;
|
|
4540
4577
|
doc.write(`if (!(${accessor} instanceof Set)) return INVALID;`);
|
|
4578
|
+
generateSizeGate(doc, schema, `${accessor}.size`);
|
|
4541
4579
|
const outputVar = newVar(ctx);
|
|
4542
4580
|
const valVar = newVar(ctx);
|
|
4543
4581
|
doc.write(`const ${outputVar} = new Set();`);
|
|
@@ -4677,7 +4715,7 @@ function generateTransformCheck(doc, ctx, schema, accessor) {
|
|
|
4677
4715
|
return accessor;
|
|
4678
4716
|
}
|
|
4679
4717
|
//#endregion
|
|
4680
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
4718
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/locales/en.js
|
|
4681
4719
|
const error = () => {
|
|
4682
4720
|
const Sizable = {
|
|
4683
4721
|
string: {
|
|
@@ -4785,7 +4823,7 @@ function en_default() {
|
|
|
4785
4823
|
return { localeError: error() };
|
|
4786
4824
|
}
|
|
4787
4825
|
//#endregion
|
|
4788
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
4826
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/registries.js
|
|
4789
4827
|
var _a$3;
|
|
4790
4828
|
var $ZodRegistry = class {
|
|
4791
4829
|
constructor() {
|
|
@@ -4832,7 +4870,7 @@ function registry() {
|
|
|
4832
4870
|
(_a$3 = globalThis).__zod_globalRegistry ?? (_a$3.__zod_globalRegistry = registry());
|
|
4833
4871
|
const globalRegistry = globalThis.__zod_globalRegistry;
|
|
4834
4872
|
//#endregion
|
|
4835
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
4873
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/api.js
|
|
4836
4874
|
function snapshotChecks(def) {
|
|
4837
4875
|
if (def.checks) def.checks = [...def.checks];
|
|
4838
4876
|
return def;
|
|
@@ -5361,7 +5399,7 @@ function _check(fn, params) {
|
|
|
5361
5399
|
return ch;
|
|
5362
5400
|
}
|
|
5363
5401
|
//#endregion
|
|
5364
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
5402
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/to-json-schema.js
|
|
5365
5403
|
function assignProps(target, ...sources) {
|
|
5366
5404
|
for (const source of sources) for (const key of Reflect.ownKeys(source)) if (Object.prototype.propertyIsEnumerable.call(source, key)) assignProp(target, key, source[key]);
|
|
5367
5405
|
return target;
|
|
@@ -5805,7 +5843,7 @@ const createStandardJSONSchemaMethod = (schema, io, processors = {}) => (params)
|
|
|
5805
5843
|
return finalize(ctx, schema);
|
|
5806
5844
|
};
|
|
5807
5845
|
//#endregion
|
|
5808
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
5846
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/core/json-schema-processors.js
|
|
5809
5847
|
const narrowMin = (agg, key, value) => {
|
|
5810
5848
|
if (agg[key] === void 0 || value > agg[key]) agg[key] = value;
|
|
5811
5849
|
};
|
|
@@ -6451,7 +6489,7 @@ function toJSONSchema(input, params) {
|
|
|
6451
6489
|
return finalize(ctx, input);
|
|
6452
6490
|
}
|
|
6453
6491
|
//#endregion
|
|
6454
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
6492
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/compile.js
|
|
6455
6493
|
let compiling = false;
|
|
6456
6494
|
globalConfig.postProcessor = (inst) => {
|
|
6457
6495
|
if (compiling) return;
|
|
@@ -6480,7 +6518,7 @@ globalConfig.postProcessor = (inst) => {
|
|
|
6480
6518
|
inst._zod.run = shim;
|
|
6481
6519
|
};
|
|
6482
6520
|
//#endregion
|
|
6483
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
6521
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/errors.js
|
|
6484
6522
|
const _installedErrorProtos = /* @__PURE__ */ new WeakSet([Object.prototype, Error.prototype]);
|
|
6485
6523
|
function _lazyMethod(proto, key, make) {
|
|
6486
6524
|
Object.defineProperty(proto, key, {
|
|
@@ -6530,7 +6568,7 @@ const initializer = (inst, issues) => {
|
|
|
6530
6568
|
};
|
|
6531
6569
|
const ZodRealError = /*@__PURE__*/ $constructor("ZodError", initializer, void 0, { Parent: Error });
|
|
6532
6570
|
//#endregion
|
|
6533
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
6571
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/parse.js
|
|
6534
6572
|
const parse$10 = /* @__PURE__ */ _parse(ZodRealError);
|
|
6535
6573
|
const parseAsync = /* @__PURE__ */ _parseAsync(ZodRealError);
|
|
6536
6574
|
const safeParse = /* @__PURE__ */ _safeParse(ZodRealError);
|
|
@@ -6544,7 +6582,7 @@ const safeDecode = /* @__PURE__ */ _safeDecode(ZodRealError);
|
|
|
6544
6582
|
const safeEncodeAsync = /* @__PURE__ */ _safeEncodeAsync(ZodRealError);
|
|
6545
6583
|
const safeDecodeAsync = /* @__PURE__ */ _safeDecodeAsync(ZodRealError);
|
|
6546
6584
|
//#endregion
|
|
6547
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
6585
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/schemas.js
|
|
6548
6586
|
function _ensureDefaultLocale() {
|
|
6549
6587
|
if (!globalConfig.localeError) config(en_default());
|
|
6550
6588
|
}
|
|
@@ -7479,7 +7517,7 @@ function preprocess$1(fn, schema) {
|
|
|
7479
7517
|
});
|
|
7480
7518
|
}
|
|
7481
7519
|
//#endregion
|
|
7482
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
7520
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/compat.js
|
|
7483
7521
|
/** @deprecated Use the raw string literal codes instead, e.g. "invalid_type". */
|
|
7484
7522
|
const ZodIssueCode = {
|
|
7485
7523
|
invalid_type: "invalid_type",
|
|
@@ -7498,7 +7536,7 @@ const ZodIssueCode = {
|
|
|
7498
7536
|
var ZodFirstPartyTypeKind;
|
|
7499
7537
|
ZodFirstPartyTypeKind || (ZodFirstPartyTypeKind = {});
|
|
7500
7538
|
//#endregion
|
|
7501
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
7539
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/iso.js
|
|
7502
7540
|
function datetime(params) {
|
|
7503
7541
|
return /* @__PURE__ */ _isoDateTime(ZodISODateTime, params);
|
|
7504
7542
|
}
|
|
@@ -7506,7 +7544,7 @@ function date(params) {
|
|
|
7506
7544
|
return /* @__PURE__ */ _isoDate(ZodISODate, params);
|
|
7507
7545
|
}
|
|
7508
7546
|
//#endregion
|
|
7509
|
-
//#region node_modules/.pnpm/zod@4.6.
|
|
7547
|
+
//#region node_modules/.pnpm/zod@4.6.6/node_modules/zod/v4/classic/coerce.js
|
|
7510
7548
|
function number$1(params) {
|
|
7511
7549
|
return /* @__PURE__ */ _coercedNumber(ZodNumber, params);
|
|
7512
7550
|
}
|
|
@@ -7933,8 +7971,8 @@ const loadConfig = (argv = process.argv.slice(2)) => {
|
|
|
7933
7971
|
readOnly: cli.readOnly ?? false,
|
|
7934
7972
|
namespaces,
|
|
7935
7973
|
tools: cli.tools,
|
|
7936
|
-
topology: cli.topology
|
|
7937
|
-
promotedArticles: cli.promotedArticles
|
|
7974
|
+
topology: cli.topology,
|
|
7975
|
+
promotedArticles: cli.promotedArticles,
|
|
7938
7976
|
hcResourceScheme,
|
|
7939
7977
|
dev: cli.dev ?? false,
|
|
7940
7978
|
printTools: cli.printTools ?? false,
|
|
@@ -7944,388 +7982,6 @@ const loadConfig = (argv = process.argv.slice(2)) => {
|
|
|
7944
7982
|
});
|
|
7945
7983
|
};
|
|
7946
7984
|
//#endregion
|
|
7947
|
-
//#region src/constants.ts
|
|
7948
|
-
const positiveIntEnv = (name, fallback) => {
|
|
7949
|
-
const raw = readEnv(name).value;
|
|
7950
|
-
if (raw === void 0 || raw.trim() === "") return fallback;
|
|
7951
|
-
const parsed = Number(raw);
|
|
7952
|
-
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
7953
|
-
};
|
|
7954
|
-
const CHARACTER_LIMIT = positiveIntEnv("RESPONSE_CHARACTER_LIMIT", 25e3);
|
|
7955
|
-
const ARTICLE_RESOURCES_SCAN_MAX_PAGES = positiveIntEnv("ARTICLE_RESOURCES_SCAN_MAX_PAGES", 20);
|
|
7956
|
-
const DEFAULT_CALLBACK_PORT = 27439;
|
|
7957
|
-
const MAX_ATTACHMENT_BYTES = positiveIntEnv("ATTACHMENT_MAX_BYTES", 5242880);
|
|
7958
|
-
const MAX_EMBEDDED_IMAGE_COUNT = positiveIntEnv("EMBEDDED_IMAGES_MAX", 10);
|
|
7959
|
-
const STDIO_MAX_MESSAGE_BYTES = 10485760;
|
|
7960
|
-
const MESSAGE_CONTENT_BUDGET_BYTES = 10420224;
|
|
7961
|
-
const MAX_RESPONSE_BYTES = Math.min(positiveIntEnv("RESPONSE_MAX_BYTES", MESSAGE_CONTENT_BUDGET_BYTES), MESSAGE_CONTENT_BUDGET_BYTES);
|
|
7962
|
-
const MAX_BASE64_INPUT_CHARS = MESSAGE_CONTENT_BUDGET_BYTES;
|
|
7963
|
-
const MAX_BASE64_INPUT_MB = Number.parseFloat((MAX_BASE64_INPUT_CHARS / 4 * 3 / 1048576).toFixed(2));
|
|
7964
|
-
const MAX_COMMENT_PAGES = positiveIntEnv("COMMENT_MAX_PAGES", 10);
|
|
7965
|
-
const TICKET_FIELD_SCAN_MAX_PAGES = positiveIntEnv("TICKET_FIELD_SCAN_MAX_PAGES", 10);
|
|
7966
|
-
const REORDER_CONFIRM_THRESHOLD = positiveIntEnv("REORDER_CONFIRM_THRESHOLD", 20);
|
|
7967
|
-
const getBaseUrl = (subdomain) => `https://${subdomain}.zendesk.com/api/v2`;
|
|
7968
|
-
const getHelpCenterBaseUrl = (subdomain) => `https://${subdomain}.zendesk.com/api/v2/help_center`;
|
|
7969
|
-
const getOAuthUrls = (subdomain) => ({
|
|
7970
|
-
authorizeUrl: `https://${subdomain}.zendesk.com/oauth/authorizations/new`,
|
|
7971
|
-
tokenUrl: `https://${subdomain}.zendesk.com/oauth/tokens`
|
|
7972
|
-
});
|
|
7973
|
-
//#endregion
|
|
7974
|
-
//#region src/client/retry.ts
|
|
7975
|
-
const MAX_ATTEMPTS = 3;
|
|
7976
|
-
const BASE_DELAY_MS = 250;
|
|
7977
|
-
const MAX_DELAY_MS = 4e3;
|
|
7978
|
-
const REQUEST_TIMEOUT_MS = 3e4;
|
|
7979
|
-
const TRANSFER_TIMEOUT_MS = 12e4;
|
|
7980
|
-
/**
|
|
7981
|
-
* A deadline for one attempt. The abort surfaces as a `TimeoutError` whose `code`
|
|
7982
|
-
* is numeric, not a syscall string, so it classifies as `unknown`: a GET is
|
|
7983
|
-
* retried, a write is not — the request may have arrived before the deadline.
|
|
7984
|
-
*/
|
|
7985
|
-
const deadlineSignal = (timeoutMs) => AbortSignal.timeout(timeoutMs);
|
|
7986
|
-
const MAX_RETRY_AFTER_MS = 5e3;
|
|
7987
|
-
const POLICIES = {
|
|
7988
|
-
GET: {
|
|
7989
|
-
network: "any",
|
|
7990
|
-
serverErrors: true
|
|
7991
|
-
},
|
|
7992
|
-
DELETE: {
|
|
7993
|
-
network: "pre-send",
|
|
7994
|
-
serverErrors: false
|
|
7995
|
-
},
|
|
7996
|
-
POST: {
|
|
7997
|
-
network: "pre-send",
|
|
7998
|
-
serverErrors: false
|
|
7999
|
-
},
|
|
8000
|
-
PUT: {
|
|
8001
|
-
network: "pre-send",
|
|
8002
|
-
serverErrors: false
|
|
8003
|
-
}
|
|
8004
|
-
};
|
|
8005
|
-
const DELAY_SECONDS = /^\d+$/;
|
|
8006
|
-
const HTTP_DATE_START = /^[A-Za-z]/;
|
|
8007
|
-
const NON_ASCII = /[^ -~]/g;
|
|
8008
|
-
const TOKEN_SEGMENT = /\/token\/[^/]+/;
|
|
8009
|
-
const URL_IN_TEXT = /[a-z][a-z0-9+.-]*:\/\/\S+/gi;
|
|
8010
|
-
const PRE_SEND_CODES = /* @__PURE__ */ new Set([
|
|
8011
|
-
"ENOTFOUND",
|
|
8012
|
-
"EAI_AGAIN",
|
|
8013
|
-
"ECONNREFUSED",
|
|
8014
|
-
"UND_ERR_CONNECT_TIMEOUT"
|
|
8015
|
-
]);
|
|
8016
|
-
const defaultRetryDeps = {
|
|
8017
|
-
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
8018
|
-
random: Math.random
|
|
8019
|
-
};
|
|
8020
|
-
/** Non-ASCII bytes break `node:http` headers, so client error text stays ASCII. */
|
|
8021
|
-
const toAscii = (text) => text.replace(NON_ASCII, "?");
|
|
8022
|
-
/**
|
|
8023
|
-
* Identifies the request without leaking a credential: uploads carry an upload
|
|
8024
|
-
* token in the query string, and an attachment `content_url` carries a download
|
|
8025
|
-
* token as a path segment (`/attachments/token/<token>/`).
|
|
8026
|
-
*/
|
|
8027
|
-
const describeTarget = (url) => {
|
|
8028
|
-
const { origin, pathname } = new URL(url);
|
|
8029
|
-
return `${origin}${pathname.replace(TOKEN_SEGMENT, "/token/[redacted]")}`;
|
|
8030
|
-
};
|
|
8031
|
-
/** First `code` in the cause chain, inspecting 5 levels so a cycle cannot hang. */
|
|
8032
|
-
const errorCode = (err) => {
|
|
8033
|
-
let current = err;
|
|
8034
|
-
for (let depth = 0; depth < 5 && current !== null && typeof current === "object"; depth += 1) {
|
|
8035
|
-
const { code, cause } = current;
|
|
8036
|
-
if (typeof code === "string") return code;
|
|
8037
|
-
current = cause;
|
|
8038
|
-
}
|
|
8039
|
-
};
|
|
8040
|
-
const classifyNetworkError = (err) => PRE_SEND_CODES.has(errorCode(err)) ? "pre-send" : "unknown";
|
|
8041
|
-
const UNINFORMATIVE_NAMES = /* @__PURE__ */ new Set(["Error", "TypeError"]);
|
|
8042
|
-
/**
|
|
8043
|
-
* The client refuses to replay a write that may have landed — but the caller is
|
|
8044
|
-
* an LLM whose reflex on a failed tool call is to try again, which would undo
|
|
8045
|
-
* that. So a failed write says whether it may have taken effect. ASCII only,
|
|
8046
|
-
* same rule as the rest of the message.
|
|
8047
|
-
*/
|
|
8048
|
-
const MAY_HAVE_APPLIED_NOTE = "The write may already have been applied. Check the current state before retrying, or you may duplicate it.";
|
|
8049
|
-
const NEVER_SENT_NOTE = "The request never reached Zendesk, so nothing was applied. Retrying is safe.";
|
|
8050
|
-
const REFUSED_NOTE = "Zendesk refused the request, so nothing was applied. Retrying is safe.";
|
|
8051
|
-
/** Only a write can be duplicated by a replay, so only a write carries a note. */
|
|
8052
|
-
const writeNote = (method, note) => method === "GET" ? "" : ` ${note}`;
|
|
8053
|
-
/**
|
|
8054
|
-
* A cause message can quote the URL it failed on — Node's `fetch` refuses a URL
|
|
8055
|
-
* carrying credentials by printing the whole thing, query string included — which
|
|
8056
|
-
* would smuggle back exactly what `describeTarget` drops. Same treatment for both,
|
|
8057
|
-
* so one function owns what a URL may look like in our output.
|
|
8058
|
-
*/
|
|
8059
|
-
const redactUrls = (text) => text.replace(URL_IN_TEXT, (found) => {
|
|
8060
|
-
try {
|
|
8061
|
-
return new URL(found).origin === "null" ? "[url]" : describeTarget(found);
|
|
8062
|
-
} catch {
|
|
8063
|
-
return "[url]";
|
|
8064
|
-
}
|
|
8065
|
-
});
|
|
8066
|
-
const failureDetail = (cause) => {
|
|
8067
|
-
if (!(cause instanceof Error)) return redactUrls(String(cause));
|
|
8068
|
-
const message = redactUrls(cause.message);
|
|
8069
|
-
const code = errorCode(cause);
|
|
8070
|
-
if (code !== void 0) return `${code}: ${message}`;
|
|
8071
|
-
return UNINFORMATIVE_NAMES.has(cause.name) ? message : `${cause.name}: ${message}`;
|
|
8072
|
-
};
|
|
8073
|
-
const networkErrorMessage = (method, target, attempts, cause) => {
|
|
8074
|
-
const base = `Network error on ${method} ${target} after ${attempts === 1 ? "1 attempt" : `${attempts} attempts`}: ${failureDetail(cause)}`;
|
|
8075
|
-
const note = classifyNetworkError(cause) === "pre-send" ? NEVER_SENT_NOTE : MAY_HAVE_APPLIED_NOTE;
|
|
8076
|
-
return toAscii(method === "GET" ? base : `${base}.${writeNote(method, note)}`);
|
|
8077
|
-
};
|
|
8078
|
-
const createZendeskNetworkError = (method, target, attempts, cause) => Object.assign(new Error(networkErrorMessage(method, target, attempts, cause), { cause }), {
|
|
8079
|
-
name: "ZendeskNetworkError",
|
|
8080
|
-
method,
|
|
8081
|
-
target,
|
|
8082
|
-
attempts
|
|
8083
|
-
});
|
|
8084
|
-
/** Exponential backoff with equal jitter: half the window fixed, half random. */
|
|
8085
|
-
const computeBackoffMs = (attempt, random) => {
|
|
8086
|
-
const window = Math.min(BASE_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS);
|
|
8087
|
-
return Math.round(window / 2 + window * random() / 2);
|
|
8088
|
-
};
|
|
8089
|
-
/** `Retry-After` in ms — delay-seconds or HTTP-date. */
|
|
8090
|
-
const parseRetryAfter = (header, now = Date.now()) => {
|
|
8091
|
-
if (header === null) return void 0;
|
|
8092
|
-
const value = header.trim();
|
|
8093
|
-
if (DELAY_SECONDS.test(value)) return Number(value) * 1e3;
|
|
8094
|
-
if (!HTTP_DATE_START.test(value)) return void 0;
|
|
8095
|
-
const date = Date.parse(value);
|
|
8096
|
-
if (Number.isNaN(date)) return void 0;
|
|
8097
|
-
return Math.max(0, date - now);
|
|
8098
|
-
};
|
|
8099
|
-
/**
|
|
8100
|
-
* How long to wait before replaying this response, or undefined to accept it.
|
|
8101
|
-
* `Retry-After` wins over backoff wherever it appears — Zendesk sends it on a 503
|
|
8102
|
-
* during maintenance as well as on a 429 — and a value past the cap means the
|
|
8103
|
-
* response is surfaced rather than parking the call for that long.
|
|
8104
|
-
*/
|
|
8105
|
-
const retryDelayFor = (response, policy, attempt, random) => {
|
|
8106
|
-
if (!(response.status === 429 || response.status >= 500 && policy.serverErrors)) return void 0;
|
|
8107
|
-
const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
|
|
8108
|
-
if (retryAfter === void 0) return computeBackoffMs(attempt, random);
|
|
8109
|
-
return retryAfter > MAX_RETRY_AFTER_MS ? void 0 : retryAfter;
|
|
8110
|
-
};
|
|
8111
|
-
/**
|
|
8112
|
-
* Runs `attempt` until it succeeds, hits a terminal outcome, or spends the
|
|
8113
|
-
* attempt budget. Returns the last response for the caller to inspect (a
|
|
8114
|
-
* non-ok status is still the caller's to turn into a `ZendeskApiError`), and
|
|
8115
|
-
* throws `ZendeskNetworkError` when no response was ever obtained.
|
|
8116
|
-
*/
|
|
8117
|
-
const fetchWithRetry = async (attempt, method, target, deps = defaultRetryDeps) => {
|
|
8118
|
-
const policy = POLICIES[method];
|
|
8119
|
-
for (let tries = 1;; tries += 1) {
|
|
8120
|
-
const last = tries >= MAX_ATTEMPTS;
|
|
8121
|
-
let response;
|
|
8122
|
-
try {
|
|
8123
|
-
response = await attempt();
|
|
8124
|
-
} catch (err) {
|
|
8125
|
-
const replayable = policy.network === "any" || classifyNetworkError(err) === policy.network;
|
|
8126
|
-
if (last || !replayable) throw createZendeskNetworkError(method, target, tries, err);
|
|
8127
|
-
await deps.sleep(computeBackoffMs(tries, deps.random));
|
|
8128
|
-
continue;
|
|
8129
|
-
}
|
|
8130
|
-
if (last) return response;
|
|
8131
|
-
const delayMs = retryDelayFor(response, policy, tries, deps.random);
|
|
8132
|
-
if (delayMs === void 0) return response;
|
|
8133
|
-
await response.text().catch(() => void 0);
|
|
8134
|
-
await deps.sleep(delayMs);
|
|
8135
|
-
}
|
|
8136
|
-
};
|
|
8137
|
-
//#endregion
|
|
8138
|
-
//#region src/client/zendesk-api.ts
|
|
8139
|
-
var ZendeskApiError = class ZendeskApiError extends Error {
|
|
8140
|
-
status;
|
|
8141
|
-
statusText;
|
|
8142
|
-
body;
|
|
8143
|
-
method;
|
|
8144
|
-
constructor(status, statusText, body, method) {
|
|
8145
|
-
super(ZendeskApiError.buildMessage(status, statusText, body, method));
|
|
8146
|
-
this.status = status;
|
|
8147
|
-
this.statusText = statusText;
|
|
8148
|
-
this.body = body;
|
|
8149
|
-
this.method = method;
|
|
8150
|
-
this.name = "ZendeskApiError";
|
|
8151
|
-
}
|
|
8152
|
-
static buildMessage(status, statusText, body, method) {
|
|
8153
|
-
switch (status) {
|
|
8154
|
-
case 401: return "Authentication failed. Your Zendesk token may be expired or invalid. Re-authenticate to get a new token.";
|
|
8155
|
-
case 403: return "Permission denied. Your Zendesk account does not have access to this resource.";
|
|
8156
|
-
case 404: return `Resource not found. Please verify the ID is correct. (${statusText})`;
|
|
8157
|
-
case 422: return `Validation error: ${body}`;
|
|
8158
|
-
case 429: return `Rate limit exceeded. Please wait before making more requests.${writeNote(method, REFUSED_NOTE)}`;
|
|
8159
|
-
default: {
|
|
8160
|
-
const message = `Zendesk API error ${status}: ${statusText}. ${body}`;
|
|
8161
|
-
return status >= 500 ? `${message}${writeNote(method, MAY_HAVE_APPLIED_NOTE)}` : message;
|
|
8162
|
-
}
|
|
8163
|
-
}
|
|
8164
|
-
}
|
|
8165
|
-
};
|
|
8166
|
-
const buildAuthHeader = (token) => `Bearer ${token}`;
|
|
8167
|
-
const buildUrl = (base, path, params) => {
|
|
8168
|
-
const url = new URL(`${base}${path}`);
|
|
8169
|
-
if (params) for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
|
|
8170
|
-
return url.toString();
|
|
8171
|
-
};
|
|
8172
|
-
const performFetch = (method, url, init, timeoutMs = REQUEST_TIMEOUT_MS) => fetchWithRetry(() => fetch(url, {
|
|
8173
|
-
...init,
|
|
8174
|
-
method,
|
|
8175
|
-
signal: deadlineSignal(timeoutMs)
|
|
8176
|
-
}), method, describeTarget(url));
|
|
8177
|
-
const executeRequest = async (url, token, options = {}) => {
|
|
8178
|
-
const { method = "GET", body } = options;
|
|
8179
|
-
const headers = {
|
|
8180
|
-
Authorization: buildAuthHeader(token),
|
|
8181
|
-
Accept: "application/json"
|
|
8182
|
-
};
|
|
8183
|
-
if (body) headers["Content-Type"] = "application/json";
|
|
8184
|
-
const init = { headers };
|
|
8185
|
-
if (body) init.body = JSON.stringify(body);
|
|
8186
|
-
const response = await performFetch(method, url, init);
|
|
8187
|
-
if (!response.ok) {
|
|
8188
|
-
const responseBody = await response.text();
|
|
8189
|
-
throw new ZendeskApiError(response.status, response.statusText, responseBody, method);
|
|
8190
|
-
}
|
|
8191
|
-
if (response.status === 204) return {};
|
|
8192
|
-
return response.json();
|
|
8193
|
-
};
|
|
8194
|
-
const zendeskGet = (subdomain, token, path, params) => {
|
|
8195
|
-
const url = buildUrl(getBaseUrl(subdomain), path, params);
|
|
8196
|
-
return executeRequest(url, token);
|
|
8197
|
-
};
|
|
8198
|
-
const zendeskPost = (subdomain, token, path, body) => {
|
|
8199
|
-
const url = buildUrl(getBaseUrl(subdomain), path);
|
|
8200
|
-
return executeRequest(url, token, {
|
|
8201
|
-
method: "POST",
|
|
8202
|
-
body
|
|
8203
|
-
});
|
|
8204
|
-
};
|
|
8205
|
-
const zendeskPut = (subdomain, token, path, body) => {
|
|
8206
|
-
const url = buildUrl(getBaseUrl(subdomain), path);
|
|
8207
|
-
return executeRequest(url, token, {
|
|
8208
|
-
method: "PUT",
|
|
8209
|
-
body
|
|
8210
|
-
});
|
|
8211
|
-
};
|
|
8212
|
-
const helpCenterGet = (subdomain, token, path, params) => {
|
|
8213
|
-
const url = buildUrl(getHelpCenterBaseUrl(subdomain), path, params);
|
|
8214
|
-
return executeRequest(url, token);
|
|
8215
|
-
};
|
|
8216
|
-
const helpCenterPost = (subdomain, token, path, body) => {
|
|
8217
|
-
const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
|
|
8218
|
-
return executeRequest(url, token, {
|
|
8219
|
-
method: "POST",
|
|
8220
|
-
body
|
|
8221
|
-
});
|
|
8222
|
-
};
|
|
8223
|
-
const helpCenterPut = (subdomain, token, path, body) => {
|
|
8224
|
-
const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
|
|
8225
|
-
return executeRequest(url, token, {
|
|
8226
|
-
method: "PUT",
|
|
8227
|
-
body
|
|
8228
|
-
});
|
|
8229
|
-
};
|
|
8230
|
-
const helpCenterDelete = (subdomain, token, path) => {
|
|
8231
|
-
const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
|
|
8232
|
-
return executeRequest(url, token, { method: "DELETE" });
|
|
8233
|
-
};
|
|
8234
|
-
const fetchZendeskBinary = async (subdomain, token, contentUrl) => {
|
|
8235
|
-
const expectedHost = `${subdomain}.zendesk.com`;
|
|
8236
|
-
const headers = {};
|
|
8237
|
-
if (new URL(contentUrl).hostname === expectedHost) headers["Authorization"] = buildAuthHeader(token);
|
|
8238
|
-
const response = await performFetch("GET", contentUrl, { headers }, TRANSFER_TIMEOUT_MS);
|
|
8239
|
-
if (!response.ok) {
|
|
8240
|
-
const body = await response.text();
|
|
8241
|
-
throw new ZendeskApiError(response.status, response.statusText, body, "GET");
|
|
8242
|
-
}
|
|
8243
|
-
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
|
|
8244
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
8245
|
-
return {
|
|
8246
|
-
data: Buffer.from(arrayBuffer),
|
|
8247
|
-
contentType
|
|
8248
|
-
};
|
|
8249
|
-
};
|
|
8250
|
-
const zendeskUpload = async (subdomain, token, filename, data, contentType, uploadToken) => {
|
|
8251
|
-
const params = { filename };
|
|
8252
|
-
if (uploadToken) params["token"] = uploadToken;
|
|
8253
|
-
const url = buildUrl(getBaseUrl(subdomain), "/uploads", params);
|
|
8254
|
-
const response = await performFetch("POST", url, {
|
|
8255
|
-
headers: {
|
|
8256
|
-
Authorization: buildAuthHeader(token),
|
|
8257
|
-
"Content-Type": contentType
|
|
8258
|
-
},
|
|
8259
|
-
body: data
|
|
8260
|
-
}, TRANSFER_TIMEOUT_MS);
|
|
8261
|
-
if (!response.ok) {
|
|
8262
|
-
const responseBody = await response.text();
|
|
8263
|
-
throw new ZendeskApiError(response.status, response.statusText, responseBody, "POST");
|
|
8264
|
-
}
|
|
8265
|
-
return response.json();
|
|
8266
|
-
};
|
|
8267
|
-
const helpCenterUpload = async (subdomain, token, path, formData) => {
|
|
8268
|
-
const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
|
|
8269
|
-
const response = await performFetch("POST", url, {
|
|
8270
|
-
headers: { Authorization: buildAuthHeader(token) },
|
|
8271
|
-
body: formData
|
|
8272
|
-
}, TRANSFER_TIMEOUT_MS);
|
|
8273
|
-
if (!response.ok) {
|
|
8274
|
-
const responseBody = await response.text();
|
|
8275
|
-
throw new ZendeskApiError(response.status, response.statusText, responseBody, "POST");
|
|
8276
|
-
}
|
|
8277
|
-
return response.json();
|
|
8278
|
-
};
|
|
8279
|
-
//#endregion
|
|
8280
|
-
//#region src/client/brands.ts
|
|
8281
|
-
const BRAND_SUBDOMAIN = /^[a-z0-9][a-z0-9-]*$/;
|
|
8282
|
-
/**
|
|
8283
|
-
* Fetch every brand of the account (cursor-paginated). `page[size]` is sent on
|
|
8284
|
-
* the FIRST request too: without it Zendesk falls back to OFFSET pagination,
|
|
8285
|
-
* which answers without `meta.after_cursor`, and the loop below would stop
|
|
8286
|
-
* after page 1 — a brand past the first 100 would resolve as Unknown.
|
|
8287
|
-
*/
|
|
8288
|
-
const fetchAllBrands = async (subdomain, token) => {
|
|
8289
|
-
const brands = [];
|
|
8290
|
-
let cursor;
|
|
8291
|
-
do {
|
|
8292
|
-
const response = await zendeskGet(subdomain, token, "/brands", {
|
|
8293
|
-
"page[size]": String(100),
|
|
8294
|
-
...cursor ? { "page[after]": cursor } : {}
|
|
8295
|
-
});
|
|
8296
|
-
brands.push(...response.brands ?? []);
|
|
8297
|
-
const meta = response.meta;
|
|
8298
|
-
if (meta?.has_more) {
|
|
8299
|
-
if (!meta.after_cursor || meta.after_cursor === cursor) throw new Error("GET /brands reported more pages but returned no usable after_cursor (missing or identical to the cursor just sent). Cannot continue pagination safely.");
|
|
8300
|
-
cursor = meta.after_cursor;
|
|
8301
|
-
} else cursor = void 0;
|
|
8302
|
-
} while (cursor);
|
|
8303
|
-
return brands;
|
|
8304
|
-
};
|
|
8305
|
-
const createBrandSubdomainResolver = (subdomain, getToken) => {
|
|
8306
|
-
let cached;
|
|
8307
|
-
const brands = () => {
|
|
8308
|
-
if (!cached || Date.now() - cached.at >= 3e5) {
|
|
8309
|
-
const promise = (async () => fetchAllBrands(subdomain, await getToken()))();
|
|
8310
|
-
cached = {
|
|
8311
|
-
at: Date.now(),
|
|
8312
|
-
promise
|
|
8313
|
-
};
|
|
8314
|
-
promise.catch(() => {
|
|
8315
|
-
cached = void 0;
|
|
8316
|
-
});
|
|
8317
|
-
}
|
|
8318
|
-
return cached.promise;
|
|
8319
|
-
};
|
|
8320
|
-
return async (idOrSubdomain) => {
|
|
8321
|
-
const all = await brands();
|
|
8322
|
-
const brand = all.find((b) => String(b.id) === idOrSubdomain) ?? all.find((b) => b.subdomain.toLowerCase() === idOrSubdomain.toLowerCase());
|
|
8323
|
-
if (!brand) throw new Error(`Unknown brand "${idOrSubdomain}": no brand with that id or subdomain exists on this account; call list_brands to see the available brands.`);
|
|
8324
|
-
if (!BRAND_SUBDOMAIN.test(brand.subdomain)) throw new Error(`Brand ${brand.id} has an unexpected subdomain; refusing to send a request to it.`);
|
|
8325
|
-
return brand.subdomain;
|
|
8326
|
-
};
|
|
8327
|
-
};
|
|
8328
|
-
//#endregion
|
|
8329
7985
|
//#region node_modules/.pnpm/@modelcontextprotocol+server@2.3.1/node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
|
|
8330
7986
|
var __create = Object.create;
|
|
8331
7987
|
var __defProp = Object.defineProperty;
|
|
@@ -10433,6 +10089,26 @@ var SdkHttpError = class extends SdkError {
|
|
|
10433
10089
|
return this.data.statusText;
|
|
10434
10090
|
}
|
|
10435
10091
|
};
|
|
10092
|
+
/**
|
|
10093
|
+
* Inbound request methods whose processing structurally requires a client
|
|
10094
|
+
* capability, keyed by method, valued by the capabilities required.
|
|
10095
|
+
*
|
|
10096
|
+
* Currently empty: none of the request methods served on the 2026-07-28
|
|
10097
|
+
* registry unconditionally requires a client capability. Entries appear here
|
|
10098
|
+
* when such methods exist — for example requests whose handling embeds
|
|
10099
|
+
* elicitation or sampling input requests (the input-request engine), or
|
|
10100
|
+
* opt-in subscription delivery. Handler-conditional requirements (a specific
|
|
10101
|
+
* tool that needs sampling) are not expressible as a static method table and
|
|
10102
|
+
* are enforced at the point the requirement arises instead.
|
|
10103
|
+
*/
|
|
10104
|
+
const REQUIRED_CLIENT_CAPABILITIES_BY_METHOD = {};
|
|
10105
|
+
/**
|
|
10106
|
+
* The client capabilities a request method structurally requires, or
|
|
10107
|
+
* `undefined` when the method has no static requirement.
|
|
10108
|
+
*/
|
|
10109
|
+
function requiredClientCapabilitiesForRequest(method) {
|
|
10110
|
+
return Object.hasOwn(REQUIRED_CLIENT_CAPABILITIES_BY_METHOD, method) ? REQUIRED_CLIENT_CAPABILITIES_BY_METHOD[method] : void 0;
|
|
10111
|
+
}
|
|
10436
10112
|
function isPlainObject$7(value) {
|
|
10437
10113
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10438
10114
|
}
|
|
@@ -10515,6 +10191,13 @@ function missingClientCapabilities(required, declared) {
|
|
|
10515
10191
|
* are ISO dates, so lexicographic comparison orders them chronologically.
|
|
10516
10192
|
*/
|
|
10517
10193
|
const FIRST_MODERN_PROTOCOL_VERSION = "2026-07-28";
|
|
10194
|
+
/**
|
|
10195
|
+
* Modern-era protocol revisions this SDK can negotiate via `server/discover`.
|
|
10196
|
+
* Deliberately separate from {@linkcode SUPPORTED_PROTOCOL_VERSIONS} (the legacy
|
|
10197
|
+
* `initialize` list), so adding a revision here can never leak a modern version
|
|
10198
|
+
* string into a 2025-era handshake. Internal — not part of the public API surface.
|
|
10199
|
+
*/
|
|
10200
|
+
const SUPPORTED_MODERN_PROTOCOL_VERSIONS = [FIRST_MODERN_PROTOCOL_VERSION];
|
|
10518
10201
|
/** Whether the given protocol revision belongs to the modern (2026-07-28+) era. */
|
|
10519
10202
|
function isModernProtocolVersion(version) {
|
|
10520
10203
|
return version >= FIRST_MODERN_PROTOCOL_VERSION;
|
|
@@ -14099,6 +13782,66 @@ function isExtensionReusedRequestMethod(method) {
|
|
|
14099
13782
|
return method === "tasks/get" || method === "tasks/cancel";
|
|
14100
13783
|
}
|
|
14101
13784
|
const ALL_CODECS = [rev2025Codec, rev2026Codec];
|
|
13785
|
+
/**
|
|
13786
|
+
* Per-request `_meta` envelope claim helpers (protocol revision 2026-07-28).
|
|
13787
|
+
*
|
|
13788
|
+
* Pure, value-returning helpers used by the inbound HTTP classifier
|
|
13789
|
+
* (`classifyInboundRequest`): claim detection and envelope validation with
|
|
13790
|
+
* self-identifying issues. The envelope schema itself stays the wire layer's
|
|
13791
|
+
* single source of truth (`RequestMetaEnvelopeSchema`); this module only maps
|
|
13792
|
+
* its outcomes into the shapes the validation ladder emits.
|
|
13793
|
+
*
|
|
13794
|
+
* Claim detection is deliberately narrow: a message claims the 2026-07-28
|
|
13795
|
+
* envelope mechanism if and only if the reserved protocol-version `_meta` key
|
|
13796
|
+
* is present in `params._meta`. Other reserved keys (client info, client
|
|
13797
|
+
* capabilities, log level), a bare `progressToken`, or unrelated keys under
|
|
13798
|
+
* the `io.modelcontextprotocol/` prefix do NOT constitute a claim on their
|
|
13799
|
+
* own — but once the claim key is present, a malformed envelope is a
|
|
13800
|
+
* validation error, never a silent fall back to legacy handling.
|
|
13801
|
+
*
|
|
13802
|
+
* The wire-exact envelope schema, the required-key set, and the per-key issue
|
|
13803
|
+
* mapping live in the wire layer (the 2026-era codec's `validateEnvelopeMeta`).
|
|
13804
|
+
* This module never reaches into a per-revision wire module directly.
|
|
13805
|
+
*/
|
|
13806
|
+
function isPlainObject$3(value) {
|
|
13807
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
13808
|
+
}
|
|
13809
|
+
/** The `_meta` object of a message's params, when present. */
|
|
13810
|
+
function requestMetaOf(params) {
|
|
13811
|
+
if (!isPlainObject$3(params)) return void 0;
|
|
13812
|
+
const meta = params["_meta"];
|
|
13813
|
+
return isPlainObject$3(meta) ? meta : void 0;
|
|
13814
|
+
}
|
|
13815
|
+
/**
|
|
13816
|
+
* Whether a message's params carry the per-request envelope claim: the
|
|
13817
|
+
* reserved protocol-version `_meta` key is present (regardless of whether the
|
|
13818
|
+
* rest of the envelope is valid — validation is a separate, later step).
|
|
13819
|
+
*/
|
|
13820
|
+
function hasEnvelopeClaim(params) {
|
|
13821
|
+
const meta = requestMetaOf(params);
|
|
13822
|
+
return meta !== void 0 && "io.modelcontextprotocol/protocolVersion" in meta;
|
|
13823
|
+
}
|
|
13824
|
+
/**
|
|
13825
|
+
* The protocol version named by a message's envelope claim, when the claim is
|
|
13826
|
+
* present and carries a string value. A present claim with a non-string value
|
|
13827
|
+
* still counts as a claim ({@linkcode hasEnvelopeClaim}); it surfaces as a
|
|
13828
|
+
* validation issue instead of a version.
|
|
13829
|
+
*/
|
|
13830
|
+
function envelopeClaimVersion(params) {
|
|
13831
|
+
const value = requestMetaOf(params)?.[PROTOCOL_VERSION_META_KEY];
|
|
13832
|
+
return typeof value === "string" ? value : void 0;
|
|
13833
|
+
}
|
|
13834
|
+
/**
|
|
13835
|
+
* Validates a request's `_meta` object as a 2026-07-28 per-request envelope
|
|
13836
|
+
* and reports problems as self-identifying issues (which key, what problem).
|
|
13837
|
+
*
|
|
13838
|
+
* Returns an empty array when the envelope is valid. Missing required keys are
|
|
13839
|
+
* reported first (as `problem: 'missing'`), then schema violations inside
|
|
13840
|
+
* present keys, in a stable order.
|
|
13841
|
+
*/
|
|
13842
|
+
function validateEnvelopeMeta(meta) {
|
|
13843
|
+
return codecForVersion(MODERN_WIRE_REVISION).validateEnvelopeMeta(meta);
|
|
13844
|
+
}
|
|
14102
13845
|
var schemas_exports = /* @__PURE__ */ __exportAll({
|
|
14103
13846
|
AnnotationsSchema: () => AnnotationsSchema,
|
|
14104
13847
|
AudioContentSchema: () => AudioContentSchema,
|
|
@@ -14303,6 +14046,8 @@ function assertCompleteRequestPrompt(request) {
|
|
|
14303
14046
|
function assertCompleteRequestResourceTemplate(request) {
|
|
14304
14047
|
if (request.params.ref.type !== "ref/resource") throw new TypeError(`Expected CompleteRequestResourceTemplate, but got ${request.params.ref.type}`);
|
|
14305
14048
|
}
|
|
14049
|
+
/** The fixed prefix every custom-parameter header carries. */
|
|
14050
|
+
const MCP_PARAM_HEADER_PREFIX = "Mcp-Param-";
|
|
14306
14051
|
/** The schema-extension property name a tool's `inputSchema` carries. */
|
|
14307
14052
|
const X_MCP_HEADER_KEY = "x-mcp-header";
|
|
14308
14053
|
/**
|
|
@@ -14425,8 +14170,566 @@ const OBJECT_VALUED_SUBSCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
14425
14170
|
function pathName(path) {
|
|
14426
14171
|
return path.length === 0 ? "<root>" : path.join(".");
|
|
14427
14172
|
}
|
|
14173
|
+
const BASE64_SENTINEL_PREFIX = "=?base64?";
|
|
14174
|
+
const BASE64_SENTINEL_SUFFIX = "?=";
|
|
14175
|
+
const BASE64_CANONICAL = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/;
|
|
14176
|
+
const CANONICAL_DECIMAL = /^-?\d+(\.\d+)?$/;
|
|
14177
|
+
/**
|
|
14178
|
+
* Convert a primitive argument value to its string representation per the
|
|
14179
|
+
* spec's type-conversion rules: strings pass through, integers and numbers
|
|
14180
|
+
* become their decimal string, booleans become lowercase `'true'` / `'false'`.
|
|
14181
|
+
* Non-finite numbers and integers outside the safe range are refused (the
|
|
14182
|
+
* caller treats `undefined` as "do not emit a header for this value").
|
|
14183
|
+
*/
|
|
14184
|
+
function mcpParamPrimitiveToString(value) {
|
|
14185
|
+
if (typeof value === "string") return value;
|
|
14186
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
14187
|
+
if (typeof value === "number") {
|
|
14188
|
+
if (!Number.isFinite(value)) return void 0;
|
|
14189
|
+
if (Number.isInteger(value) && !Number.isSafeInteger(value)) return void 0;
|
|
14190
|
+
return String(value);
|
|
14191
|
+
}
|
|
14192
|
+
}
|
|
14193
|
+
function base64ToUtf8(b64) {
|
|
14194
|
+
const bin = atob(b64);
|
|
14195
|
+
const bytes = new Uint8Array(bin.length);
|
|
14196
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.codePointAt(i);
|
|
14197
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
14198
|
+
}
|
|
14199
|
+
/**
|
|
14200
|
+
* Decode an `Mcp-Param-*` header value: when it carries the Base64 sentinel,
|
|
14201
|
+
* the payload is decoded as UTF-8; otherwise the value is returned as-is.
|
|
14202
|
+
* Returns `undefined` when the sentinel is present but the payload is not
|
|
14203
|
+
* canonical Base64 (or not valid UTF-8) — the spec requires servers to reject
|
|
14204
|
+
* such values.
|
|
14205
|
+
*/
|
|
14206
|
+
function decodeMcpParamValue(value) {
|
|
14207
|
+
if (!(value.startsWith(BASE64_SENTINEL_PREFIX) && value.endsWith(BASE64_SENTINEL_SUFFIX))) return value;
|
|
14208
|
+
const b64 = value.slice(9, value.length - 2);
|
|
14209
|
+
if (!BASE64_CANONICAL.test(b64)) return void 0;
|
|
14210
|
+
try {
|
|
14211
|
+
return base64ToUtf8(b64);
|
|
14212
|
+
} catch {
|
|
14213
|
+
return;
|
|
14214
|
+
}
|
|
14215
|
+
}
|
|
14216
|
+
function valueAtPath(root, path) {
|
|
14217
|
+
let node = root;
|
|
14218
|
+
for (const key of path) {
|
|
14219
|
+
if (node === null || typeof node !== "object") return void 0;
|
|
14220
|
+
node = node[key];
|
|
14221
|
+
}
|
|
14222
|
+
return node;
|
|
14223
|
+
}
|
|
14224
|
+
/**
|
|
14225
|
+
* The header/body comparison the server performs at tool-resolution time.
|
|
14226
|
+
*
|
|
14227
|
+
* For each `x-mcp-header` declaration on the named tool: when the body
|
|
14228
|
+
* `arguments` carries a value, the matching `Mcp-Param-{Name}` header MUST be
|
|
14229
|
+
* present and decode to an equal value; when the body value is `null` or
|
|
14230
|
+
* absent the server MUST NOT expect the header (a present header is ignored).
|
|
14231
|
+
* A sentinel-carrying header whose payload is not canonical Base64 / valid
|
|
14232
|
+
* UTF-8 is rejected as invalid characters.
|
|
14233
|
+
*
|
|
14234
|
+
* Integer-typed declarations are compared numerically (the spec's SHOULD —
|
|
14235
|
+
* `42.0` and `42` are equal); everything else is compared as decoded strings.
|
|
14236
|
+
*
|
|
14237
|
+
* Returns `undefined` when every check passes, or an
|
|
14238
|
+
* {@linkcode InboundLadderRejection} carrying the same `-32020`
|
|
14239
|
+
* (`HeaderMismatch`) shape the inbound classifier emits for the
|
|
14240
|
+
* standard-header cross-checks — `400 Bad Request` with the disagreeing pair
|
|
14241
|
+
* in `data.mismatch`.
|
|
14242
|
+
*/
|
|
14243
|
+
function validateMcpParamHeaders(declarations, args, headers) {
|
|
14244
|
+
for (const decl of declarations) {
|
|
14245
|
+
const headerKey = `${MCP_PARAM_HEADER_PREFIX}${decl.headerName}`;
|
|
14246
|
+
const headerValue = headers.get(headerKey);
|
|
14247
|
+
const bodyRaw = valueAtPath(args, decl.path);
|
|
14248
|
+
if (bodyRaw === void 0 || bodyRaw === null) continue;
|
|
14249
|
+
const bodyString = mcpParamPrimitiveToString(bodyRaw);
|
|
14250
|
+
if (bodyString === void 0) continue;
|
|
14251
|
+
if (headerValue === null) return paramHeaderMismatchRejection("param-header-missing", headerKey, `the body carries ${pathName(decl.path)}=${JSON.stringify(bodyRaw)} but the ${headerKey} header is absent`);
|
|
14252
|
+
const decoded = decodeMcpParamValue(headerValue);
|
|
14253
|
+
if (decoded === void 0) return paramHeaderMismatchRejection("param-header-invalid-encoding", headerKey, `the ${headerKey} header carries an invalid Base64 sentinel value`);
|
|
14254
|
+
if (!((decl.type === "integer" || decl.type === "number") && CANONICAL_DECIMAL.test(decoded) && typeof bodyRaw === "number" ? Number(decoded) === bodyRaw : decoded === bodyString)) return paramHeaderMismatchRejection("param-header-mismatch", headerKey, `the ${headerKey} header decodes to ${JSON.stringify(decoded)} but the body carries ${pathName(decl.path)}=${JSON.stringify(bodyRaw)}`);
|
|
14255
|
+
}
|
|
14256
|
+
}
|
|
14257
|
+
/**
|
|
14258
|
+
* Build the `-32020` (`HeaderMismatch`) rejection for an `Mcp-Param-*`
|
|
14259
|
+
* disagreement. Same shape as the inbound classifier's standard-header
|
|
14260
|
+
* cross-check mismatch (HTTP `400`, `data.mismatch` naming the disagreeing
|
|
14261
|
+
* pair, `settled: true`); only the rung differs because this check runs at the
|
|
14262
|
+
* pre-dispatch step against a known tool's schema rather than at the edge.
|
|
14263
|
+
*/
|
|
14264
|
+
function paramHeaderMismatchRejection(cell, header, body) {
|
|
14265
|
+
return {
|
|
14266
|
+
kind: "reject",
|
|
14267
|
+
rung: "param-header-validation",
|
|
14268
|
+
cell,
|
|
14269
|
+
httpStatus: 400,
|
|
14270
|
+
code: HEADER_MISMATCH_ERROR_CODE,
|
|
14271
|
+
message: `Bad Request: the request headers and body disagree: ${body}`,
|
|
14272
|
+
data: { mismatch: {
|
|
14273
|
+
header,
|
|
14274
|
+
body
|
|
14275
|
+
} },
|
|
14276
|
+
settled: true
|
|
14277
|
+
};
|
|
14278
|
+
}
|
|
14279
|
+
/**
|
|
14280
|
+
* Inbound HTTP request classification and the inbound validation ladder
|
|
14281
|
+
* (protocol revision 2026-07-28).
|
|
14282
|
+
*
|
|
14283
|
+
* `classifyInboundRequest` is the body-primary era predicate for an HTTP
|
|
14284
|
+
* entry that serves both protocol eras on one endpoint. It is evaluated
|
|
14285
|
+
* exactly once, at the entry boundary, on the already-parsed request body:
|
|
14286
|
+
*
|
|
14287
|
+
* - `initialize` is a legacy-era request by definition (the modern era has no
|
|
14288
|
+
* `initialize` handshake) — unless it carries a valid envelope claim naming
|
|
14289
|
+
* a modern revision, in which case the claim wins and the request is
|
|
14290
|
+
* classified like any other enveloped request (the modern era then answers
|
|
14291
|
+
* it with method-not-found, exactly like every other method it does not
|
|
14292
|
+
* define).
|
|
14293
|
+
* - A request whose `params._meta` carries the reserved protocol-version key
|
|
14294
|
+
* claims the per-request envelope mechanism and classifies into the era the
|
|
14295
|
+
* named revision belongs to (a malformed envelope behind a present claim is
|
|
14296
|
+
* a validation error, never a silent fall back to legacy handling).
|
|
14297
|
+
* - A request without a claim is legacy-era traffic.
|
|
14298
|
+
* - The `MCP-Protocol-Version` header is a cross-check only *for
|
|
14299
|
+
* classification*: it never upgrades or downgrades a body-derived
|
|
14300
|
+
* classification, and a disagreement between header and body is an explicit
|
|
14301
|
+
* ladder outcome. Its absence likewise never changes the era — but the spec
|
|
14302
|
+
* requires the header on every modern *request* POST, so a
|
|
14303
|
+
* modern-classified request that omits it is refused one rung later, by
|
|
14304
|
+
* {@linkcode validateStandardRequestHeaders}, not here. Notification POSTs
|
|
14305
|
+
* are exempt (see the next bullet), and that rung enforces presence on
|
|
14306
|
+
* requests only.
|
|
14307
|
+
* - Notifications carry no envelope claim of their own under the current
|
|
14308
|
+
* spec, so for notification POSTs without a body claim the modern header is
|
|
14309
|
+
* determinative; the `Mcp-Method` header is validated against the body when
|
|
14310
|
+
* the message classifies modern and is never enforced on legacy traffic.
|
|
14311
|
+
* A notification that does carry a claim is treated body-primary like a
|
|
14312
|
+
* request, and a malformed claim is rejected the same way a request's
|
|
14313
|
+
* malformed claim is — never silently resolved against the header.
|
|
14314
|
+
* The notification-POST header cross-checks here are an SDK-defensive
|
|
14315
|
+
* posture, not a spec requirement: the spec leaves header rules for posted
|
|
14316
|
+
* notifications undefined (core client notifications do not occur over
|
|
14317
|
+
* Streamable HTTP); applying the request rules symmetrically is what an
|
|
14318
|
+
* ecosystem custom-notification POST expects, and the −32020 cells stay
|
|
14319
|
+
* passing for them.
|
|
14320
|
+
* - `GET`/`DELETE` (and any other non-`POST` method) are body-less 2025-era
|
|
14321
|
+
* session operations: the modern era is `POST`-only, so they are routed to
|
|
14322
|
+
* legacy serving when it is configured and rejected otherwise.
|
|
14323
|
+
* - Array (batch) bodies are classified element-wise: an array containing a
|
|
14324
|
+
* modern-claiming or invalid element is rejected, an all-legacy array is
|
|
14325
|
+
* legacy traffic unchanged, and a single-element array is still an array.
|
|
14326
|
+
*
|
|
14327
|
+
* The classifier returns plain values (it never throws and never touches a
|
|
14328
|
+
* transport): a routing outcome (`legacy`/`modern`) or a ladder rejection
|
|
14329
|
+
* carrying the JSON-RPC error to emit and the HTTP status to emit it with.
|
|
14330
|
+
* Legacy routing outcomes deliberately carry NO `MessageClassification` —
|
|
14331
|
+
* legacy and hand-wired traffic is never classified, which keeps its
|
|
14332
|
+
* dispatch behavior byte-identical to today's.
|
|
14333
|
+
*
|
|
14334
|
+
* Error codes for the modern-path rejection cells follow the published
|
|
14335
|
+
* conformance suite (and the spec text it asserts):
|
|
14336
|
+
*
|
|
14337
|
+
* - A header/body cross-check mismatch (the `MCP-Protocol-Version` header
|
|
14338
|
+
* disagreeing with the body, or the `Mcp-Method` header disagreeing with the
|
|
14339
|
+
* body method) is rejected with `-32020` (`HeaderMismatch`) on HTTP 400.
|
|
14340
|
+
* - A request whose protocol-version header names a modern revision but whose
|
|
14341
|
+
* body carries no `_meta` envelope claim — including an envelope present but
|
|
14342
|
+
* missing the required protocol-version key — is rejected with `-32602`
|
|
14343
|
+
* (invalid params) naming the missing key(s), on HTTP 400.
|
|
14344
|
+
*
|
|
14345
|
+
* Should a future spec revision or conformance release change these
|
|
14346
|
+
* assignments, the affected cells are re-derived against that release; the
|
|
14347
|
+
* `settled` flag on {@linkcode InboundLadderRejection} stays available to mark
|
|
14348
|
+
* a cell provisional again while such a change is in flight.
|
|
14349
|
+
*/
|
|
14350
|
+
/**
|
|
14351
|
+
* The error code emitted for header/body cross-check mismatches: the
|
|
14352
|
+
* `MCP-Protocol-Version` header disagreeing with the body's envelope claim (or
|
|
14353
|
+
* with the body's classification), and the `Mcp-Method` header disagreeing
|
|
14354
|
+
* with the body method.
|
|
14355
|
+
*
|
|
14356
|
+
* `-32020` is the draft schema's `HEADER_MISMATCH` constant (the SEP-2243
|
|
14357
|
+
* `HeaderMismatch` code; the spec requires HTTP 400 for it), as also asserted
|
|
14358
|
+
* by the published conformance suite for header-validation failures. It has no
|
|
14359
|
+
* {@linkcode ProtocolErrorCode} member because it is not part of the 2025-era
|
|
14360
|
+
* wire vocabulary; the validation ladder is its only emitter.
|
|
14361
|
+
*/
|
|
14362
|
+
const HEADER_MISMATCH_ERROR_CODE = -32020;
|
|
14428
14363
|
ProtocolErrorCode.InvalidRequest, ProtocolErrorCode.UnsupportedProtocolVersion, ProtocolErrorCode.InvalidParams, ProtocolErrorCode.MethodNotFound, ProtocolErrorCode.InvalidParams, ProtocolErrorCode.MissingRequiredClientCapability;
|
|
14429
|
-
|
|
14364
|
+
/**
|
|
14365
|
+
* HTTP status for ladder-originated JSON-RPC error codes.
|
|
14366
|
+
*
|
|
14367
|
+
* Keyed on origin, not on the bare code: this table only applies to errors
|
|
14368
|
+
* the ladder (or a pre-handler protocol gate) produced. Errors produced by
|
|
14369
|
+
* request handlers — whatever their code — stay in-band on HTTP 200, and are
|
|
14370
|
+
* never mapped to an HTTP status by this table; in particular `-32603` and
|
|
14371
|
+
* domain-specific codes never become a blanket 500. The single exception is
|
|
14372
|
+
* `MissingRequiredClientCapability` (-32021) — see
|
|
14373
|
+
* {@linkcode httpStatusForErrorCode}.
|
|
14374
|
+
*
|
|
14375
|
+
* `-32602` (invalid params) deliberately has NO entry: the only invalid-params
|
|
14376
|
+
* rejection that maps to HTTP 400 is the classifier's own envelope rung
|
|
14377
|
+
* short-circuit, which carries its HTTP status directly. A dispatch- or
|
|
14378
|
+
* handler-produced invalid-params error is always in-band.
|
|
14379
|
+
*/
|
|
14380
|
+
const LADDER_ERROR_HTTP_STATUS = {
|
|
14381
|
+
[ProtocolErrorCode.ParseError]: 400,
|
|
14382
|
+
[ProtocolErrorCode.InvalidRequest]: 400,
|
|
14383
|
+
[ProtocolErrorCode.MethodNotFound]: 404,
|
|
14384
|
+
[ProtocolErrorCode.UnsupportedProtocolVersion]: 400,
|
|
14385
|
+
[ProtocolErrorCode.MissingRequiredClientCapability]: 400,
|
|
14386
|
+
[HEADER_MISMATCH_ERROR_CODE]: 400
|
|
14387
|
+
};
|
|
14388
|
+
/**
|
|
14389
|
+
* The HTTP status to answer a JSON-RPC error with, keyed on the error's
|
|
14390
|
+
* origin. `in-band` errors (anything produced by a request handler) are
|
|
14391
|
+
* HTTP 200 — the JSON-RPC error response is the payload, not an HTTP
|
|
14392
|
+
* failure — with ONE exception: `MissingRequiredClientCapability` (-32021),
|
|
14393
|
+
* whose 400 the spec mandates on the error itself with no origin condition,
|
|
14394
|
+
* and which the SDK genuinely produces after dispatch (the `input_required`
|
|
14395
|
+
* capability gate). A handler relaying some downstream peer's `-32020`/`-32022`
|
|
14396
|
+
* is NOT that peer's spec error and stays in-band like every other handler
|
|
14397
|
+
* code. `ladder` errors map through {@linkcode LADDER_ERROR_HTTP_STATUS}.
|
|
14398
|
+
*
|
|
14399
|
+
* The per-request transport intentionally does NOT delegate to this function:
|
|
14400
|
+
* its `?? 400` ladder fallback is only correct for entry-gate codes known to
|
|
14401
|
+
* the table, and would wrongly map dispatch-window errors outside it (a
|
|
14402
|
+
* window `-32602` must stay in-band on 200). The transport indexes the table
|
|
14403
|
+
* directly; keep the two in agreement when editing either.
|
|
14404
|
+
*/
|
|
14405
|
+
function httpStatusForErrorCode(code, origin) {
|
|
14406
|
+
if (origin === "in-band") return code === ProtocolErrorCode.MissingRequiredClientCapability ? 400 : 200;
|
|
14407
|
+
return LADDER_ERROR_HTTP_STATUS[code] ?? 400;
|
|
14408
|
+
}
|
|
14409
|
+
function rejection(rung, cell, httpStatus, error, settled) {
|
|
14410
|
+
return {
|
|
14411
|
+
kind: "reject",
|
|
14412
|
+
rung,
|
|
14413
|
+
cell,
|
|
14414
|
+
httpStatus,
|
|
14415
|
+
code: error.code,
|
|
14416
|
+
message: error.message,
|
|
14417
|
+
...error.data !== void 0 && { data: error.data },
|
|
14418
|
+
settled
|
|
14419
|
+
};
|
|
14420
|
+
}
|
|
14421
|
+
function crossCheckMismatch(cell, header, body, rung = "era-classification") {
|
|
14422
|
+
return rejection(rung, cell, 400, new ProtocolError(HEADER_MISMATCH_ERROR_CODE, `Bad Request: the request headers and body disagree: ${body}`, { mismatch: {
|
|
14423
|
+
header,
|
|
14424
|
+
body
|
|
14425
|
+
} }), true);
|
|
14426
|
+
}
|
|
14427
|
+
/**
|
|
14428
|
+
* The methods whose body carries a `params.name` / `params.uri` /
|
|
14429
|
+
* `params.taskId` value the `Mcp-Name` header must mirror, and which body
|
|
14430
|
+
* field supplies it. The core rows come from SEP-2243 § Standard Request
|
|
14431
|
+
* Headers (`Required For` column); the `tasks/*` rows come from SEP-2663's
|
|
14432
|
+
* Streamable HTTP binding ("the client MUST set the `Mcp-Name` header to the
|
|
14433
|
+
* value of `params.taskId`" for `tasks/get` / `tasks/update` /
|
|
14434
|
+
* `tasks/cancel`, so intermediaries can route every request for a task to the
|
|
14435
|
+
* instance holding its state). Shared by the client transport (header
|
|
14436
|
+
* emission) and the server ladder (validation) so both sides derive from one
|
|
14437
|
+
* table.
|
|
14438
|
+
*/
|
|
14439
|
+
const MCP_NAME_HEADER_SOURCE = {
|
|
14440
|
+
"tools/call": "name",
|
|
14441
|
+
"prompts/get": "name",
|
|
14442
|
+
"resources/read": "uri",
|
|
14443
|
+
"tasks/get": "taskId",
|
|
14444
|
+
"tasks/update": "taskId",
|
|
14445
|
+
"tasks/cancel": "taskId"
|
|
14446
|
+
};
|
|
14447
|
+
/**
|
|
14448
|
+
* Resolve the `Mcp-Name` source for one request against
|
|
14449
|
+
* {@linkcode MCP_NAME_HEADER_SOURCE}: which body field the header mirrors for
|
|
14450
|
+
* `method`, and the string value that field carries in `params`.
|
|
14451
|
+
*
|
|
14452
|
+
* Returns `undefined` when `method` is off-table (no `Mcp-Name` is emitted or
|
|
14453
|
+
* required). Otherwise `value` is the field's string value, or `undefined`
|
|
14454
|
+
* when `params` is not a plain object or the field is absent / not a string —
|
|
14455
|
+
* the client emits no header then, and the server leaves that body to the
|
|
14456
|
+
* rungs further down the ladder.
|
|
14457
|
+
*
|
|
14458
|
+
* `method` is peer-supplied on the server and caller-supplied on the client,
|
|
14459
|
+
* so the table lookup is `Object.hasOwn`-guarded against `Object.prototype`
|
|
14460
|
+
* collisions (`constructor`, `toString`, …). Shared by the client transport
|
|
14461
|
+
* (emission) and {@linkcode validateStandardRequestHeaders} (validation) so
|
|
14462
|
+
* the extraction — not just the table — cannot drift between the two sides.
|
|
14463
|
+
*/
|
|
14464
|
+
function mcpNameSource(method, params) {
|
|
14465
|
+
const field = Object.hasOwn(MCP_NAME_HEADER_SOURCE, method) ? MCP_NAME_HEADER_SOURCE[method] : void 0;
|
|
14466
|
+
if (field === void 0) return;
|
|
14467
|
+
const raw = isPlainObject$2(params) ? params[field] : void 0;
|
|
14468
|
+
return {
|
|
14469
|
+
field,
|
|
14470
|
+
value: typeof raw === "string" ? raw : void 0
|
|
14471
|
+
};
|
|
14472
|
+
}
|
|
14473
|
+
/** Strip RFC 9110 optional whitespace (SP / HTAB) around a field value in linear time. */
|
|
14474
|
+
function stripHttpOws(value) {
|
|
14475
|
+
let start = 0;
|
|
14476
|
+
while (start < value.length) {
|
|
14477
|
+
const code = value.codePointAt(start);
|
|
14478
|
+
if (code !== 9 && code !== 32) break;
|
|
14479
|
+
start += 1;
|
|
14480
|
+
}
|
|
14481
|
+
let end = value.length;
|
|
14482
|
+
while (end > start) {
|
|
14483
|
+
const code = value.codePointAt(end - 1);
|
|
14484
|
+
if (code !== 9 && code !== 32) break;
|
|
14485
|
+
end -= 1;
|
|
14486
|
+
}
|
|
14487
|
+
return start === 0 && end === value.length ? value : value.slice(start, end);
|
|
14488
|
+
}
|
|
14489
|
+
/**
|
|
14490
|
+
* SEP-2243 standard-header server-side validation, evaluated by the HTTP
|
|
14491
|
+
* entry on a modern-classified request immediately after
|
|
14492
|
+
* {@linkcode classifyInboundRequest} returns a modern route.
|
|
14493
|
+
*
|
|
14494
|
+
* Returns the `-32020` (`HeaderMismatch`) ladder rejection (HTTP `400`,
|
|
14495
|
+
* `standard-header-validation` rung — the same shape
|
|
14496
|
+
* {@linkcode classifyInboundRequest} already emits on the edge
|
|
14497
|
+
* `era-classification` rung for the `MCP-Protocol-Version` and
|
|
14498
|
+
* `Mcp-Method` *mismatch* cells) when:
|
|
14499
|
+
*
|
|
14500
|
+
* - the required `MCP-Protocol-Version` header is absent (SEP-2243 requires it
|
|
14501
|
+
* on every modern *request* POST, and lists it first among the required
|
|
14502
|
+
* standard headers — so a request missing it *and* `Mcp-Method` is answered
|
|
14503
|
+
* by this cell);
|
|
14504
|
+
* - the required `Mcp-Method` header is absent;
|
|
14505
|
+
* - the required `Mcp-Name` header is absent on a `tools/call`,
|
|
14506
|
+
* `prompts/get`, `resources/read`, or (per SEP-2663's Streamable HTTP
|
|
14507
|
+
* binding) `tasks/get` / `tasks/update` / `tasks/cancel` request whose
|
|
14508
|
+
* body carries the `params.name` / `params.uri` / `params.taskId` value
|
|
14509
|
+
* the header mirrors;
|
|
14510
|
+
* - the `Mcp-Name` header carries an invalid `=?base64?…?=` sentinel; or
|
|
14511
|
+
* - the (decoded) `Mcp-Name` value disagrees with the body's
|
|
14512
|
+
* `params.name` / `params.uri` / `params.taskId`.
|
|
14513
|
+
*
|
|
14514
|
+
* Returns `undefined` (pass) for notifications (the spec table reads
|
|
14515
|
+
* "All requests"), for methods that have no `Mcp-Name` source, and when the
|
|
14516
|
+
* headers agree with the body. Never enforced on legacy traffic — the entry
|
|
14517
|
+
* only calls this on a modern route.
|
|
14518
|
+
*
|
|
14519
|
+
* Kept separate from {@linkcode classifyInboundRequest} so that a body-only
|
|
14520
|
+
* call to the classifier (no headers passed) keeps routing a modern request
|
|
14521
|
+
* unchanged: the classifier remains a pure body-primary router, and this
|
|
14522
|
+
* function is the presence/`Mcp-Name` half of the standard-header rung the
|
|
14523
|
+
* entry layers on top. That separation is what lets the missing
|
|
14524
|
+
* `MCP-Protocol-Version` cell live here without disturbing the body-primary
|
|
14525
|
+
* rule — classification still resolves from the body (a proxy stripping the
|
|
14526
|
+
* header must not change the era), and only this rung refuses to serve it.
|
|
14527
|
+
*/
|
|
14528
|
+
function validateStandardRequestHeaders(request, route) {
|
|
14529
|
+
if (route.messageKind !== "request") return;
|
|
14530
|
+
const method = route.message.method;
|
|
14531
|
+
if (request.protocolVersionHeader === void 0) {
|
|
14532
|
+
const claimed = route.classification.revision;
|
|
14533
|
+
return crossCheckMismatch("version-header-missing", "(missing)", claimed === void 0 ? "the body carries a modern per-request envelope but the required MCP-Protocol-Version header is absent" : `the body envelope names protocol version ${claimed} but the required MCP-Protocol-Version header is absent`, "standard-header-validation");
|
|
14534
|
+
}
|
|
14535
|
+
if (request.mcpMethodHeader === void 0) return crossCheckMismatch("method-header-missing", "(missing)", `the body names method ${method} but the required Mcp-Method header is absent`, "standard-header-validation");
|
|
14536
|
+
const source = mcpNameSource(method, route.message.params);
|
|
14537
|
+
if (source === void 0) return;
|
|
14538
|
+
const { field: sourceField, value: bodyValue } = source;
|
|
14539
|
+
if (request.mcpNameHeader === void 0) {
|
|
14540
|
+
if (bodyValue === void 0) return;
|
|
14541
|
+
return crossCheckMismatch("name-header-missing", "(missing)", `the body carries params.${sourceField}="${bodyValue}" but the required Mcp-Name header is absent`, "standard-header-validation");
|
|
14542
|
+
}
|
|
14543
|
+
const normalizedNameHeader = stripHttpOws(request.mcpNameHeader);
|
|
14544
|
+
const decoded = decodeMcpParamValue(normalizedNameHeader);
|
|
14545
|
+
if (decoded === void 0) return crossCheckMismatch("name-header-invalid-encoding", normalizedNameHeader, "the Mcp-Name header carries an invalid Base64 sentinel value", "standard-header-validation");
|
|
14546
|
+
if (bodyValue !== void 0 && decoded !== bodyValue) return crossCheckMismatch("name-header-mismatch", normalizedNameHeader, `the body carries params.${sourceField}="${bodyValue}" but the Mcp-Name header names "${decoded}"`, "standard-header-validation");
|
|
14547
|
+
}
|
|
14548
|
+
function isPlainObject$2(value) {
|
|
14549
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
14550
|
+
}
|
|
14551
|
+
function classificationForClaim(claimedVersion) {
|
|
14552
|
+
if (claimedVersion === void 0) return { era: "modern" };
|
|
14553
|
+
return {
|
|
14554
|
+
era: isModernProtocolVersion(claimedVersion) ? "modern" : "legacy",
|
|
14555
|
+
revision: claimedVersion
|
|
14556
|
+
};
|
|
14557
|
+
}
|
|
14558
|
+
/**
|
|
14559
|
+
* Whether a request's params carry a per-request envelope claim that is both
|
|
14560
|
+
* well-formed and names a modern protocol revision.
|
|
14561
|
+
*
|
|
14562
|
+
* Used by the `initialize` precedence rule: only such a claim overrides the
|
|
14563
|
+
* `initialize` ⇒ legacy-handshake classification — a request carrying a valid
|
|
14564
|
+
* modern envelope is a modern request regardless of its method name, and the
|
|
14565
|
+
* modern era then answers `initialize` exactly like any other method it does
|
|
14566
|
+
* not define (method-not-found). A malformed claim, or one naming a pre-2026
|
|
14567
|
+
* revision, keeps the legacy-handshake routing unchanged.
|
|
14568
|
+
*
|
|
14569
|
+
* Exported on the core internal barrel for the stdio serving entry, which
|
|
14570
|
+
* applies the same precedence rule to a connection's opening message; not
|
|
14571
|
+
* public API.
|
|
14572
|
+
*/
|
|
14573
|
+
function carriesValidModernEnvelopeClaim(params) {
|
|
14574
|
+
if (!hasEnvelopeClaim(params)) return false;
|
|
14575
|
+
const claimedVersion = envelopeClaimVersion(params);
|
|
14576
|
+
if (claimedVersion === void 0 || !isModernProtocolVersion(claimedVersion)) return false;
|
|
14577
|
+
const meta = requestMetaOf(params);
|
|
14578
|
+
return meta !== void 0 && validateEnvelopeMeta(meta).length === 0;
|
|
14579
|
+
}
|
|
14580
|
+
function classifyBatch(body) {
|
|
14581
|
+
if (body.length === 0) return rejection("jsonrpc-shape", "empty-batch", 400, new ProtocolError(ProtocolErrorCode.InvalidRequest, "Bad Request: empty JSON-RPC batch"), true);
|
|
14582
|
+
for (const element of body) {
|
|
14583
|
+
if (hasEnvelopeClaim(isPlainObject$2(element) ? element["params"] : void 0)) return rejection("jsonrpc-shape", "batch-with-modern-element", 400, new ProtocolError(ProtocolErrorCode.InvalidRequest, "Bad Request: JSON-RPC batches may not contain requests for protocol revision 2026-07-28 or later"), true);
|
|
14584
|
+
if (!(isJSONRPCRequest(element) || isJSONRPCNotification(element) || isJSONRPCResultResponse(element) || isJSONRPCErrorResponse(element))) return rejection("jsonrpc-shape", "batch-with-invalid-element", 400, new ProtocolError(ProtocolErrorCode.InvalidRequest, "Bad Request: JSON-RPC batch contains an invalid message"), true);
|
|
14585
|
+
}
|
|
14586
|
+
return {
|
|
14587
|
+
kind: "legacy",
|
|
14588
|
+
reason: "batch"
|
|
14589
|
+
};
|
|
14590
|
+
}
|
|
14591
|
+
function classifyRequestBody(request, body) {
|
|
14592
|
+
const params = body.params;
|
|
14593
|
+
const method = body.method;
|
|
14594
|
+
const headerVersion = request.protocolVersionHeader;
|
|
14595
|
+
const headerNamesModern = headerVersion !== void 0 && isModernProtocolVersion(headerVersion);
|
|
14596
|
+
if (method === "initialize" && !carriesValidModernEnvelopeClaim(params)) {
|
|
14597
|
+
if (headerNamesModern) return crossCheckMismatch("initialize-with-modern-header", headerVersion, "an initialize request (legacy handshake) was sent with a modern MCP-Protocol-Version header");
|
|
14598
|
+
const requestedVersion = isPlainObject$2(params) && typeof params["protocolVersion"] === "string" ? params["protocolVersion"] : void 0;
|
|
14599
|
+
return {
|
|
14600
|
+
kind: "legacy",
|
|
14601
|
+
reason: "initialize",
|
|
14602
|
+
...requestedVersion !== void 0 && { requestedVersion }
|
|
14603
|
+
};
|
|
14604
|
+
}
|
|
14605
|
+
if (hasEnvelopeClaim(params)) {
|
|
14606
|
+
const meta = requestMetaOf(params);
|
|
14607
|
+
const firstIssue = (meta === void 0 ? [] : validateEnvelopeMeta(meta))[0];
|
|
14608
|
+
if (firstIssue !== void 0) return rejection("envelope", "envelope-invalid", 400, new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid _meta envelope for protocol revision 2026-07-28: ${firstIssue.key}: ${firstIssue.problem}`, { envelope: firstIssue }), true);
|
|
14609
|
+
const claimedVersion = envelopeClaimVersion(params);
|
|
14610
|
+
if (headerVersion !== void 0 && claimedVersion !== void 0 && headerVersion !== claimedVersion) return crossCheckMismatch("header-body-version-mismatch", headerVersion, `the body envelope names protocol version ${claimedVersion} but the MCP-Protocol-Version header names ${headerVersion}`);
|
|
14611
|
+
if (request.mcpMethodHeader !== void 0 && request.mcpMethodHeader !== method) return crossCheckMismatch("method-header-mismatch", request.mcpMethodHeader, `the body names method ${method} but the Mcp-Method header names ${request.mcpMethodHeader}`);
|
|
14612
|
+
return {
|
|
14613
|
+
kind: "modern",
|
|
14614
|
+
messageKind: "request",
|
|
14615
|
+
message: body,
|
|
14616
|
+
classification: classificationForClaim(claimedVersion)
|
|
14617
|
+
};
|
|
14618
|
+
}
|
|
14619
|
+
if (headerNamesModern) {
|
|
14620
|
+
const meta = requestMetaOf(params);
|
|
14621
|
+
const missingFromEnvelope = validateEnvelopeMeta(meta ?? {}).filter((issue) => issue.problem === "missing").map((issue) => issue.key);
|
|
14622
|
+
const missing = meta === void 0 ? ["_meta"] : missingFromEnvelope.length > 0 ? missingFromEnvelope : [PROTOCOL_VERSION_META_KEY];
|
|
14623
|
+
return rejection("envelope", "modern-header-without-claim", 400, new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid params: the MCP-Protocol-Version header names protocol revision ${headerVersion}, but the request is missing the required per-request envelope key(s): ${missing.join(", ")}`, { envelope: { missing } }), true);
|
|
14624
|
+
}
|
|
14625
|
+
return {
|
|
14626
|
+
kind: "legacy",
|
|
14627
|
+
reason: "no-claim",
|
|
14628
|
+
...headerVersion !== void 0 && { requestedVersion: headerVersion }
|
|
14629
|
+
};
|
|
14630
|
+
}
|
|
14631
|
+
function classifyNotificationBody(request, body) {
|
|
14632
|
+
const params = body.params;
|
|
14633
|
+
const method = body.method;
|
|
14634
|
+
const headerVersion = request.protocolVersionHeader;
|
|
14635
|
+
const headerNamesModern = headerVersion !== void 0 && isModernProtocolVersion(headerVersion);
|
|
14636
|
+
if (hasEnvelopeClaim(params)) {
|
|
14637
|
+
const claimedVersion = envelopeClaimVersion(params);
|
|
14638
|
+
if (claimedVersion === void 0) {
|
|
14639
|
+
const meta = requestMetaOf(params);
|
|
14640
|
+
const claimIssue = (meta === void 0 ? [] : validateEnvelopeMeta(meta)).find((issue) => issue.key === "io.modelcontextprotocol/protocolVersion") ?? {
|
|
14641
|
+
key: "io.modelcontextprotocol/protocolVersion",
|
|
14642
|
+
problem: "expected a protocol version string"
|
|
14643
|
+
};
|
|
14644
|
+
return rejection("envelope", "notification-envelope-invalid", 400, new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid _meta envelope for protocol revision 2026-07-28: ${claimIssue.key}: ${claimIssue.problem}`, { envelope: claimIssue }), true);
|
|
14645
|
+
}
|
|
14646
|
+
if (headerVersion !== void 0 && headerVersion !== claimedVersion) return crossCheckMismatch("notification-header-body-version-mismatch", headerVersion, `the notification envelope names protocol version ${claimedVersion} but the MCP-Protocol-Version header names ${headerVersion}`);
|
|
14647
|
+
const classification = classificationForClaim(claimedVersion);
|
|
14648
|
+
if (classification.era === "modern" && request.mcpMethodHeader !== void 0 && request.mcpMethodHeader !== method) return crossCheckMismatch("notification-method-header-mismatch", request.mcpMethodHeader, `the notification body names method ${method} but the Mcp-Method header names ${request.mcpMethodHeader}`);
|
|
14649
|
+
return {
|
|
14650
|
+
kind: "modern",
|
|
14651
|
+
messageKind: "notification",
|
|
14652
|
+
message: body,
|
|
14653
|
+
classification
|
|
14654
|
+
};
|
|
14655
|
+
}
|
|
14656
|
+
if (headerNamesModern) {
|
|
14657
|
+
if (request.mcpMethodHeader !== void 0 && request.mcpMethodHeader !== method) return crossCheckMismatch("notification-method-header-mismatch", request.mcpMethodHeader, `the notification body names method ${method} but the Mcp-Method header names ${request.mcpMethodHeader}`);
|
|
14658
|
+
return {
|
|
14659
|
+
kind: "modern",
|
|
14660
|
+
messageKind: "notification",
|
|
14661
|
+
message: body,
|
|
14662
|
+
classification: {
|
|
14663
|
+
era: "modern",
|
|
14664
|
+
revision: headerVersion
|
|
14665
|
+
}
|
|
14666
|
+
};
|
|
14667
|
+
}
|
|
14668
|
+
return {
|
|
14669
|
+
kind: "legacy",
|
|
14670
|
+
reason: "notification",
|
|
14671
|
+
...headerVersion !== void 0 && { requestedVersion: headerVersion }
|
|
14672
|
+
};
|
|
14673
|
+
}
|
|
14674
|
+
/**
|
|
14675
|
+
* Classifies one inbound HTTP request for dual-era serving.
|
|
14676
|
+
*
|
|
14677
|
+
* The body-primary predicate, evaluated once at the entry boundary: see the
|
|
14678
|
+
* module documentation for the rules. Returns a routing outcome (`legacy` or
|
|
14679
|
+
* `modern`) or a ladder rejection; it never throws.
|
|
14680
|
+
*/
|
|
14681
|
+
function classifyInboundRequest(request) {
|
|
14682
|
+
request = {
|
|
14683
|
+
...request,
|
|
14684
|
+
...request.protocolVersionHeader !== void 0 && { protocolVersionHeader: stripHttpOws(request.protocolVersionHeader) },
|
|
14685
|
+
...request.mcpMethodHeader !== void 0 && { mcpMethodHeader: stripHttpOws(request.mcpMethodHeader) },
|
|
14686
|
+
...request.mcpNameHeader !== void 0 && { mcpNameHeader: stripHttpOws(request.mcpNameHeader) }
|
|
14687
|
+
};
|
|
14688
|
+
if (request.httpMethod.toUpperCase() !== "POST") return {
|
|
14689
|
+
kind: "legacy",
|
|
14690
|
+
reason: "http-method"
|
|
14691
|
+
};
|
|
14692
|
+
const body = request.body;
|
|
14693
|
+
if (Array.isArray(body)) return classifyBatch(body);
|
|
14694
|
+
if (isJSONRPCResultResponse(body) || isJSONRPCErrorResponse(body)) return {
|
|
14695
|
+
kind: "legacy",
|
|
14696
|
+
reason: "response"
|
|
14697
|
+
};
|
|
14698
|
+
if (isPlainObject$2(body) && isJSONRPCRequest(body)) return classifyRequestBody(request, body);
|
|
14699
|
+
if (isPlainObject$2(body) && isJSONRPCNotification(body)) return classifyNotificationBody(request, body);
|
|
14700
|
+
return rejection("jsonrpc-shape", "invalid-json-rpc-body", 400, new ProtocolError(ProtocolErrorCode.InvalidRequest, "Bad Request: the request body is not a valid JSON-RPC message"), true);
|
|
14701
|
+
}
|
|
14702
|
+
/**
|
|
14703
|
+
* The rejection a modern-only endpoint (no legacy serving configured)
|
|
14704
|
+
* answers a legacy-classified request with.
|
|
14705
|
+
*
|
|
14706
|
+
* - Envelope-less requests (including `initialize`) are answered with the
|
|
14707
|
+
* unsupported-protocol-version error carrying the endpoint's supported
|
|
14708
|
+
* versions and echoing the version the request named (when it named one —
|
|
14709
|
+
* `requested` is omitted rather than fabricated when the request named no
|
|
14710
|
+
* version at all), so a legacy client can discover what the endpoint serves
|
|
14711
|
+
* from the error alone.
|
|
14712
|
+
* - Posted responses and batch arrays are invalid requests on the modern era.
|
|
14713
|
+
* - Non-`POST` methods are not allowed.
|
|
14714
|
+
* - Legacy-classified notifications return `undefined`: the caller answers
|
|
14715
|
+
* 202 with no body and does not dispatch the notification (accept-and-drop).
|
|
14716
|
+
*/
|
|
14717
|
+
function modernOnlyStrictRejection(route, supportedVersions) {
|
|
14718
|
+
switch (route.reason) {
|
|
14719
|
+
case "http-method": return rejection("http-method", "modern-only-method-not-allowed", 405, new ProtocolError(-32e3, "Method not allowed."), true);
|
|
14720
|
+
case "batch": return rejection("jsonrpc-shape", "modern-only-batch-not-supported", 400, new ProtocolError(ProtocolErrorCode.InvalidRequest, "Bad Request: JSON-RPC batches are not supported by this endpoint"), true);
|
|
14721
|
+
case "response": return rejection("jsonrpc-shape", "modern-only-response-post", 400, new ProtocolError(ProtocolErrorCode.InvalidRequest, "Bad Request: JSON-RPC responses cannot be posted to this endpoint"), true);
|
|
14722
|
+
case "notification": return;
|
|
14723
|
+
case "initialize":
|
|
14724
|
+
case "no-claim": {
|
|
14725
|
+
const requested = route.requestedVersion;
|
|
14726
|
+
return rejection("era-classification", "modern-only-missing-envelope", 400, requested === void 0 ? new ProtocolError(ProtocolErrorCode.UnsupportedProtocolVersion, "Unsupported protocol version: the request did not name a protocol version", { supported: [...supportedVersions] }) : new UnsupportedProtocolVersionError({
|
|
14727
|
+
supported: [...supportedVersions],
|
|
14728
|
+
requested
|
|
14729
|
+
}), true);
|
|
14730
|
+
}
|
|
14731
|
+
}
|
|
14732
|
+
}
|
|
14430
14733
|
/**
|
|
14431
14734
|
* Internal Zod schema utilities for protocol handling.
|
|
14432
14735
|
* These are used internally by the SDK for protocol message validation.
|
|
@@ -15119,6 +15422,17 @@ function withRequestStateValue(ctx, value) {
|
|
|
15119
15422
|
}
|
|
15120
15423
|
};
|
|
15121
15424
|
}
|
|
15425
|
+
let writeNegotiatedProtocolVersion;
|
|
15426
|
+
/**
|
|
15427
|
+
* Package-internal write channel for a {@linkcode Protocol} instance's
|
|
15428
|
+
* negotiated protocol version, for callers outside the class hierarchy:
|
|
15429
|
+
* tests and the (future) modern-era server entry that marks a factory
|
|
15430
|
+
* instance modern at binding time. Exported on the core internal barrel
|
|
15431
|
+
* only — never public API.
|
|
15432
|
+
*/
|
|
15433
|
+
function setNegotiatedProtocolVersion(instance, version) {
|
|
15434
|
+
writeNegotiatedProtocolVersion(instance, version);
|
|
15435
|
+
}
|
|
15122
15436
|
/**
|
|
15123
15437
|
* Implements MCP protocol framing on top of a pluggable transport, including
|
|
15124
15438
|
* features like request/response linking, notifications, and progress.
|
|
@@ -15145,7 +15459,11 @@ var Protocol = class {
|
|
|
15145
15459
|
* (`Client.connect`, `Server._oninitialize`).
|
|
15146
15460
|
*/
|
|
15147
15461
|
_negotiatedProtocolVersion;
|
|
15148
|
-
static {
|
|
15462
|
+
static {
|
|
15463
|
+
writeNegotiatedProtocolVersion = (instance, version) => {
|
|
15464
|
+
instance._negotiatedProtocolVersion = version;
|
|
15465
|
+
};
|
|
15466
|
+
}
|
|
15149
15467
|
_supportedProtocolVersions;
|
|
15150
15468
|
/**
|
|
15151
15469
|
* Callback for when the connection is closed for any reason.
|
|
@@ -15872,7 +16190,7 @@ function mergeCapabilities(base, additional) {
|
|
|
15872
16190
|
}
|
|
15873
16191
|
return result;
|
|
15874
16192
|
}
|
|
15875
|
-
function isPlainObject$
|
|
16193
|
+
function isPlainObject$8(value) {
|
|
15876
16194
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15877
16195
|
}
|
|
15878
16196
|
/**
|
|
@@ -15886,12 +16204,12 @@ function isPlainObject$2(value) {
|
|
|
15886
16204
|
function partitionInputResponses(inputResponses) {
|
|
15887
16205
|
const accepted = {};
|
|
15888
16206
|
const droppedKeys = [];
|
|
15889
|
-
if (!isPlainObject$
|
|
16207
|
+
if (!isPlainObject$8(inputResponses)) return {
|
|
15890
16208
|
accepted,
|
|
15891
16209
|
droppedKeys
|
|
15892
16210
|
};
|
|
15893
16211
|
for (const [key, entry] of Object.entries(inputResponses)) {
|
|
15894
|
-
if (!isPlainObject$
|
|
16212
|
+
if (!isPlainObject$8(entry) || "method" in entry || "result" in entry) {
|
|
15895
16213
|
droppedKeys.push(key);
|
|
15896
16214
|
continue;
|
|
15897
16215
|
}
|
|
@@ -23559,6 +23877,409 @@ function armSseKeepAlive(intervalMs, onTick) {
|
|
|
23559
23877
|
timer.unref?.();
|
|
23560
23878
|
return timer;
|
|
23561
23879
|
}
|
|
23880
|
+
/**
|
|
23881
|
+
* A `ServerEventBus` backed by an in-process listener set.
|
|
23882
|
+
*
|
|
23883
|
+
* `publish()` delivers synchronously to the live listener set (a listener
|
|
23884
|
+
* unsubscribing itself mid-dispatch is safe; the entry's listen-router
|
|
23885
|
+
* listeners never unsubscribe peers). A throwing listener does not stop
|
|
23886
|
+
* delivery to the others.
|
|
23887
|
+
*/
|
|
23888
|
+
var InMemoryServerEventBus = class {
|
|
23889
|
+
_listeners = /* @__PURE__ */ new Set();
|
|
23890
|
+
/**
|
|
23891
|
+
* @param onerror - Optional callback for errors thrown by listeners
|
|
23892
|
+
* during dispatch.
|
|
23893
|
+
*/
|
|
23894
|
+
constructor(onerror) {
|
|
23895
|
+
this.onerror = onerror;
|
|
23896
|
+
}
|
|
23897
|
+
publish(event) {
|
|
23898
|
+
for (const listener of this._listeners) try {
|
|
23899
|
+
listener(event);
|
|
23900
|
+
} catch (error) {
|
|
23901
|
+
this.onerror?.(error instanceof Error ? error : new Error(String(error)));
|
|
23902
|
+
}
|
|
23903
|
+
}
|
|
23904
|
+
subscribe(listener) {
|
|
23905
|
+
this._listeners.add(listener);
|
|
23906
|
+
let live = true;
|
|
23907
|
+
return () => {
|
|
23908
|
+
if (!live) return;
|
|
23909
|
+
live = false;
|
|
23910
|
+
this._listeners.delete(listener);
|
|
23911
|
+
};
|
|
23912
|
+
}
|
|
23913
|
+
/** The number of currently registered listeners (test/introspection only — the routers track capacity via their own open-subscription set). */
|
|
23914
|
+
get listenerCount() {
|
|
23915
|
+
return this._listeners.size;
|
|
23916
|
+
}
|
|
23917
|
+
};
|
|
23918
|
+
/** Build a {@linkcode ServerNotifier} over a bus. */
|
|
23919
|
+
function createServerNotifier(bus) {
|
|
23920
|
+
return {
|
|
23921
|
+
toolsChanged: () => bus.publish({ kind: "tools_list_changed" }),
|
|
23922
|
+
promptsChanged: () => bus.publish({ kind: "prompts_list_changed" }),
|
|
23923
|
+
resourcesChanged: () => bus.publish({ kind: "resources_list_changed" }),
|
|
23924
|
+
resourceUpdated: (uri) => bus.publish({
|
|
23925
|
+
kind: "resource_updated",
|
|
23926
|
+
uri
|
|
23927
|
+
})
|
|
23928
|
+
};
|
|
23929
|
+
}
|
|
23930
|
+
/**
|
|
23931
|
+
* Whether a `subscriptions/listen` filter accepts a given change event.
|
|
23932
|
+
*
|
|
23933
|
+
* Pure: no I/O, no mutation. The filter governs ONLY the four
|
|
23934
|
+
* subscription-gated change types — non-gated notifications never reach the
|
|
23935
|
+
* bus and are not modeled here.
|
|
23936
|
+
*
|
|
23937
|
+
* `resource_updated` matches only when `resourceSubscriptions` is present and
|
|
23938
|
+
* contains the event's URI exactly (per the spec: "for these resource URIs").
|
|
23939
|
+
*/
|
|
23940
|
+
function listenFilterAccepts(filter, event) {
|
|
23941
|
+
switch (event.kind) {
|
|
23942
|
+
case "tools_list_changed": return filter.toolsListChanged === true;
|
|
23943
|
+
case "prompts_list_changed": return filter.promptsListChanged === true;
|
|
23944
|
+
case "resources_list_changed": return filter.resourcesListChanged === true;
|
|
23945
|
+
case "resource_updated": return filter.resourceSubscriptions !== void 0 && filter.resourceSubscriptions.includes(event.uri);
|
|
23946
|
+
}
|
|
23947
|
+
}
|
|
23948
|
+
/**
|
|
23949
|
+
* The honored subset of a requested filter: keeps only the fields the client
|
|
23950
|
+
* explicitly opted in to (drops `false` and absent fields), narrowed against
|
|
23951
|
+
* the server's declared capabilities when supplied. The serving entry sends
|
|
23952
|
+
* this back in `notifications/subscriptions/acknowledged` so the ack reflects
|
|
23953
|
+
* what the server can actually deliver.
|
|
23954
|
+
*
|
|
23955
|
+
* - `toolsListChanged` is honored only when `capabilities.tools.listChanged`
|
|
23956
|
+
* is advertised; likewise `promptsListChanged` / `resourcesListChanged`.
|
|
23957
|
+
* - `resourceSubscriptions` is honored only when
|
|
23958
|
+
* `capabilities.resources.subscribe` is advertised.
|
|
23959
|
+
*
|
|
23960
|
+
* `capabilities` is optional on this pure helper for test convenience only —
|
|
23961
|
+
* both wired routers REQUIRE capabilities at the call site (the HTTP router's
|
|
23962
|
+
* `serve()` takes a required parameter; `StdioListenRouter.serve()` throws
|
|
23963
|
+
* before `setServerCapabilities()` was called), so the fail-open
|
|
23964
|
+
* `undefined → honor everything` branch is never reachable on a wired entry.
|
|
23965
|
+
*/
|
|
23966
|
+
function honoredSubset(requested, capabilities) {
|
|
23967
|
+
const honored = {};
|
|
23968
|
+
const allow = (bit) => capabilities === void 0 || bit === true;
|
|
23969
|
+
if (requested.toolsListChanged === true && allow(capabilities?.tools?.listChanged)) honored.toolsListChanged = true;
|
|
23970
|
+
if (requested.promptsListChanged === true && allow(capabilities?.prompts?.listChanged)) honored.promptsListChanged = true;
|
|
23971
|
+
if (requested.resourcesListChanged === true && allow(capabilities?.resources?.listChanged)) honored.resourcesListChanged = true;
|
|
23972
|
+
if (requested.resourceSubscriptions !== void 0 && requested.resourceSubscriptions.length > 0 && allow(capabilities?.resources?.subscribe)) honored.resourceSubscriptions = [...requested.resourceSubscriptions];
|
|
23973
|
+
return honored;
|
|
23974
|
+
}
|
|
23975
|
+
/** Map a {@linkcode ServerEvent} onto its wire notification `{method, params}`. */
|
|
23976
|
+
function serverEventToNotification(event) {
|
|
23977
|
+
switch (event.kind) {
|
|
23978
|
+
case "tools_list_changed": return { method: "notifications/tools/list_changed" };
|
|
23979
|
+
case "prompts_list_changed": return { method: "notifications/prompts/list_changed" };
|
|
23980
|
+
case "resources_list_changed": return { method: "notifications/resources/list_changed" };
|
|
23981
|
+
case "resource_updated": return {
|
|
23982
|
+
method: "notifications/resources/updated",
|
|
23983
|
+
params: { uri: event.uri }
|
|
23984
|
+
};
|
|
23985
|
+
}
|
|
23986
|
+
}
|
|
23987
|
+
/** Default capacity guard: refuse a new subscription when this many are already open. */
|
|
23988
|
+
const DEFAULT_MAX_SUBSCRIPTIONS = 1024;
|
|
23989
|
+
function jsonRpcError(id, code, message) {
|
|
23990
|
+
return Response.json({
|
|
23991
|
+
jsonrpc: "2.0",
|
|
23992
|
+
error: {
|
|
23993
|
+
code,
|
|
23994
|
+
message
|
|
23995
|
+
},
|
|
23996
|
+
id
|
|
23997
|
+
}, { status: 200 });
|
|
23998
|
+
}
|
|
23999
|
+
/** Stamp the subscription id onto a notification's `_meta`. Non-mutating. */
|
|
24000
|
+
function stampSubscriptionId(notification, subscriptionId) {
|
|
24001
|
+
return {
|
|
24002
|
+
method: notification.method,
|
|
24003
|
+
params: {
|
|
24004
|
+
...notification.params,
|
|
24005
|
+
_meta: {
|
|
24006
|
+
...notification.params?._meta,
|
|
24007
|
+
[SUBSCRIPTION_ID_META_KEY]: subscriptionId
|
|
24008
|
+
}
|
|
24009
|
+
}
|
|
24010
|
+
};
|
|
24011
|
+
}
|
|
24012
|
+
/**
|
|
24013
|
+
* Read the requested filter off a `subscriptions/listen` request body.
|
|
24014
|
+
* Returns the validated filter, or `undefined` when `params.notifications`
|
|
24015
|
+
* is absent or fails the schema (the caller answers `-32602` — the spec
|
|
24016
|
+
* marks `notifications` REQUIRED on the listen request).
|
|
24017
|
+
*/
|
|
24018
|
+
function parseListenFilter(message) {
|
|
24019
|
+
const outcome = codecForVersion(MODERN_WIRE_REVISION).validateRequest("subscriptions/listen", message);
|
|
24020
|
+
return outcome.ok ? outcome.value.params?.notifications : void 0;
|
|
24021
|
+
}
|
|
24022
|
+
function createListenRouter(options) {
|
|
24023
|
+
const { bus, onerror } = options;
|
|
24024
|
+
const maxSubscriptions = options.maxSubscriptions ?? 1024;
|
|
24025
|
+
const keepAliveMs = options.keepAliveMs ?? 15e3;
|
|
24026
|
+
const open = /* @__PURE__ */ new Set();
|
|
24027
|
+
function serve(message, signal, capabilities, serverInfo) {
|
|
24028
|
+
if (open.size >= maxSubscriptions) {
|
|
24029
|
+
onerror?.(/* @__PURE__ */ new Error(`subscriptions/listen refused: subscription limit reached (${maxSubscriptions})`));
|
|
24030
|
+
return jsonRpcError(message.id, -32603, "Subscription limit reached");
|
|
24031
|
+
}
|
|
24032
|
+
const filter = parseListenFilter(message);
|
|
24033
|
+
if (filter === void 0) return jsonRpcError(message.id, -32602, "Invalid params: 'notifications' is required and must be a valid SubscriptionFilter");
|
|
24034
|
+
const honored = honoredSubset(filter, capabilities);
|
|
24035
|
+
const subscriptionId = message.id;
|
|
24036
|
+
const encoder = new TextEncoder();
|
|
24037
|
+
let controller;
|
|
24038
|
+
let closed = false;
|
|
24039
|
+
let unsubscribe;
|
|
24040
|
+
let keepAliveTimer;
|
|
24041
|
+
let abortCleanup;
|
|
24042
|
+
const writeFrame = (frame) => {
|
|
24043
|
+
if (closed) return;
|
|
24044
|
+
try {
|
|
24045
|
+
controller.enqueue(encoder.encode(frame));
|
|
24046
|
+
} catch (error) {
|
|
24047
|
+
onerror?.(error instanceof Error ? error : new Error(String(error)));
|
|
24048
|
+
}
|
|
24049
|
+
};
|
|
24050
|
+
const writeNotification = (method, params) => {
|
|
24051
|
+
writeFrame(`event: message\ndata: ${JSON.stringify({
|
|
24052
|
+
jsonrpc: "2.0",
|
|
24053
|
+
method,
|
|
24054
|
+
params
|
|
24055
|
+
})}\n\n`);
|
|
24056
|
+
};
|
|
24057
|
+
const teardown = (graceful) => {
|
|
24058
|
+
if (closed) return;
|
|
24059
|
+
if (graceful) writeFrame(`event: message\ndata: ${JSON.stringify({
|
|
24060
|
+
jsonrpc: "2.0",
|
|
24061
|
+
id: subscriptionId,
|
|
24062
|
+
result: {
|
|
24063
|
+
resultType: "complete",
|
|
24064
|
+
_meta: {
|
|
24065
|
+
[SUBSCRIPTION_ID_META_KEY]: subscriptionId,
|
|
24066
|
+
[SERVER_INFO_META_KEY]: serverInfo
|
|
24067
|
+
}
|
|
24068
|
+
}
|
|
24069
|
+
})}\n\n`);
|
|
24070
|
+
closed = true;
|
|
24071
|
+
try {
|
|
24072
|
+
unsubscribe?.();
|
|
24073
|
+
} catch (error) {
|
|
24074
|
+
onerror?.(error instanceof Error ? error : new Error(String(error)));
|
|
24075
|
+
}
|
|
24076
|
+
if (keepAliveTimer !== void 0) clearInterval(keepAliveTimer);
|
|
24077
|
+
abortCleanup?.();
|
|
24078
|
+
open.delete(teardown);
|
|
24079
|
+
try {
|
|
24080
|
+
controller.close();
|
|
24081
|
+
} catch {}
|
|
24082
|
+
};
|
|
24083
|
+
const readable = new ReadableStream({
|
|
24084
|
+
start(streamController) {
|
|
24085
|
+
controller = streamController;
|
|
24086
|
+
const ack = stampSubscriptionId({
|
|
24087
|
+
method: "notifications/subscriptions/acknowledged",
|
|
24088
|
+
params: { notifications: honored }
|
|
24089
|
+
}, subscriptionId);
|
|
24090
|
+
writeNotification(ack.method, ack.params);
|
|
24091
|
+
if (Object.keys(honored).length === 0) {
|
|
24092
|
+
teardown(true);
|
|
24093
|
+
return;
|
|
24094
|
+
}
|
|
24095
|
+
unsubscribe = bus.subscribe((event) => {
|
|
24096
|
+
if (closed || !listenFilterAccepts(honored, event)) return;
|
|
24097
|
+
const note = stampSubscriptionId(serverEventToNotification(event), subscriptionId);
|
|
24098
|
+
writeNotification(note.method, note.params);
|
|
24099
|
+
});
|
|
24100
|
+
keepAliveTimer = armSseKeepAlive(keepAliveMs, () => writeFrame(": keepalive\n\n"));
|
|
24101
|
+
open.add(teardown);
|
|
24102
|
+
},
|
|
24103
|
+
cancel() {
|
|
24104
|
+
teardown(false);
|
|
24105
|
+
}
|
|
24106
|
+
});
|
|
24107
|
+
if (signal !== void 0) if (signal.aborted) teardown(false);
|
|
24108
|
+
else {
|
|
24109
|
+
const onAbort = () => teardown(false);
|
|
24110
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
24111
|
+
abortCleanup = () => signal.removeEventListener("abort", onAbort);
|
|
24112
|
+
}
|
|
24113
|
+
return new Response(readable, {
|
|
24114
|
+
status: 200,
|
|
24115
|
+
headers: {
|
|
24116
|
+
"Content-Type": "text/event-stream",
|
|
24117
|
+
"Cache-Control": "no-cache, no-transform",
|
|
24118
|
+
Connection: "keep-alive",
|
|
24119
|
+
"X-Accel-Buffering": "no"
|
|
24120
|
+
}
|
|
24121
|
+
});
|
|
24122
|
+
}
|
|
24123
|
+
return {
|
|
24124
|
+
serve,
|
|
24125
|
+
closeAll() {
|
|
24126
|
+
for (const teardown of open) teardown(true);
|
|
24127
|
+
},
|
|
24128
|
+
get openCount() {
|
|
24129
|
+
return open.size;
|
|
24130
|
+
}
|
|
24131
|
+
};
|
|
24132
|
+
}
|
|
24133
|
+
const CHANGE_NOTIFICATION_METHODS = /* @__PURE__ */ new Set([
|
|
24134
|
+
"notifications/tools/list_changed",
|
|
24135
|
+
"notifications/prompts/list_changed",
|
|
24136
|
+
"notifications/resources/list_changed",
|
|
24137
|
+
"notifications/resources/updated"
|
|
24138
|
+
]);
|
|
24139
|
+
/**
|
|
24140
|
+
* Per-connection listen state for the stdio entry. One instance is held by
|
|
24141
|
+
* `serveStdio` for the connection lifetime; it routes inbound
|
|
24142
|
+
* `subscriptions/listen` / `notifications/cancelled` and rewrites outbound
|
|
24143
|
+
* change notifications onto the active subscriptions. No bus — the long-lived
|
|
24144
|
+
* pinned instance's existing `send*ListChanged()` calls feed straight into
|
|
24145
|
+
* `routeOutbound()`.
|
|
24146
|
+
*/
|
|
24147
|
+
var StdioListenRouter = class {
|
|
24148
|
+
/** Active subscriptions, keyed by the listen request's JSON-RPC id verbatim. */
|
|
24149
|
+
_subs = /* @__PURE__ */ new Map();
|
|
24150
|
+
/**
|
|
24151
|
+
* The serving instance's declared capabilities. Filled in by the entry
|
|
24152
|
+
* once the modern instance is constructed (the router is created before
|
|
24153
|
+
* the instance exists), so the acknowledged filter is narrowed against
|
|
24154
|
+
* what the server can actually deliver.
|
|
24155
|
+
*/
|
|
24156
|
+
_serverCapabilities;
|
|
24157
|
+
/**
|
|
24158
|
+
* The serving instance's identity, stamped onto the graceful-close
|
|
24159
|
+
* results' `_meta` (the spec's `SubscriptionsListenResultMetaObject` extends
|
|
24160
|
+
* `ResultMetaObject`). Handed over together with the capabilities.
|
|
24161
|
+
*/
|
|
24162
|
+
_serverInfo;
|
|
24163
|
+
constructor(_maxSubscriptions = DEFAULT_MAX_SUBSCRIPTIONS, serverCapabilities, serverInfo) {
|
|
24164
|
+
this._maxSubscriptions = _maxSubscriptions;
|
|
24165
|
+
this._serverCapabilities = serverCapabilities;
|
|
24166
|
+
this._serverInfo = serverInfo;
|
|
24167
|
+
}
|
|
24168
|
+
/**
|
|
24169
|
+
* Record the serving instance's declared capabilities and identity once
|
|
24170
|
+
* it has been constructed. Called by `serveStdio`'s connect path;
|
|
24171
|
+
* subsequent `serve()` calls narrow the honored filter against the
|
|
24172
|
+
* capabilities, and `teardownAll()` stamps the identity.
|
|
24173
|
+
*/
|
|
24174
|
+
setServerCapabilities(capabilities, serverInfo) {
|
|
24175
|
+
this._serverCapabilities = capabilities;
|
|
24176
|
+
if (serverInfo !== void 0) this._serverInfo = serverInfo;
|
|
24177
|
+
}
|
|
24178
|
+
/** Whether `id` is an active listen subscription on this connection. */
|
|
24179
|
+
has(id) {
|
|
24180
|
+
return this._subs.has(id);
|
|
24181
|
+
}
|
|
24182
|
+
/**
|
|
24183
|
+
* Serve one inbound `subscriptions/listen` request: registers the
|
|
24184
|
+
* subscription and returns the stamped acknowledged notification (or, on
|
|
24185
|
+
* capacity / params rejection, the in-band JSON-RPC error response).
|
|
24186
|
+
*
|
|
24187
|
+
* @throws when called before {@linkcode setServerCapabilities} (or the
|
|
24188
|
+
* constructor) has supplied the serving instance's capabilities. Honoring a
|
|
24189
|
+
* filter without knowing the server's advertised capabilities would fail
|
|
24190
|
+
* open (deliver unadvertised types); the entry guarantees capabilities are
|
|
24191
|
+
* set before any listen request is routed here.
|
|
24192
|
+
*/
|
|
24193
|
+
serve(message) {
|
|
24194
|
+
if (this._serverCapabilities === void 0) throw new Error("StdioListenRouter.serve() called before setServerCapabilities(); refusing to honor a filter without capabilities");
|
|
24195
|
+
if (this._subs.size >= this._maxSubscriptions) return {
|
|
24196
|
+
jsonrpc: "2.0",
|
|
24197
|
+
id: message.id,
|
|
24198
|
+
error: {
|
|
24199
|
+
code: -32603,
|
|
24200
|
+
message: "Subscription limit reached"
|
|
24201
|
+
}
|
|
24202
|
+
};
|
|
24203
|
+
const filter = parseListenFilter(message);
|
|
24204
|
+
if (filter === void 0) return {
|
|
24205
|
+
jsonrpc: "2.0",
|
|
24206
|
+
id: message.id,
|
|
24207
|
+
error: {
|
|
24208
|
+
code: -32602,
|
|
24209
|
+
message: "Invalid params: 'notifications' is required and must be a valid SubscriptionFilter"
|
|
24210
|
+
}
|
|
24211
|
+
};
|
|
24212
|
+
const honored = honoredSubset(filter, this._serverCapabilities);
|
|
24213
|
+
this._subs.set(message.id, honored);
|
|
24214
|
+
return stampSubscriptionId({
|
|
24215
|
+
method: "notifications/subscriptions/acknowledged",
|
|
24216
|
+
params: { notifications: honored }
|
|
24217
|
+
}, message.id);
|
|
24218
|
+
}
|
|
24219
|
+
/**
|
|
24220
|
+
* Tear down one subscription (inbound `notifications/cancelled`). Returns
|
|
24221
|
+
* `true` when a subscription was removed. After this call NOTHING further
|
|
24222
|
+
* is delivered for that subscription id (the post-cancel hardening).
|
|
24223
|
+
*/
|
|
24224
|
+
cancel(id) {
|
|
24225
|
+
return this._subs.delete(id);
|
|
24226
|
+
}
|
|
24227
|
+
/**
|
|
24228
|
+
* Route an outbound notification through the active subscriptions.
|
|
24229
|
+
*
|
|
24230
|
+
* - For a subscription-gated change notification, returns one stamped copy
|
|
24231
|
+
* per subscription that opted in to it (an empty array means it is
|
|
24232
|
+
* dropped — the modern era never delivers an un-requested change type).
|
|
24233
|
+
* - For any other outbound message, returns `'passthrough'` (the entry
|
|
24234
|
+
* forwards it as-is).
|
|
24235
|
+
*/
|
|
24236
|
+
routeOutbound(message) {
|
|
24237
|
+
if (!CHANGE_NOTIFICATION_METHODS.has(message.method)) return "passthrough";
|
|
24238
|
+
const uriParam = message.params?.["uri"];
|
|
24239
|
+
const uri = typeof uriParam === "string" ? uriParam : void 0;
|
|
24240
|
+
const event = notificationToServerEvent(message.method, uri);
|
|
24241
|
+
const out = [];
|
|
24242
|
+
for (const [subscriptionId, filter] of this._subs) if (listenFilterAccepts(filter, event)) out.push(stampSubscriptionId({
|
|
24243
|
+
method: message.method,
|
|
24244
|
+
params: message.params ?? {}
|
|
24245
|
+
}, subscriptionId));
|
|
24246
|
+
return out;
|
|
24247
|
+
}
|
|
24248
|
+
/**
|
|
24249
|
+
* Server-side graceful teardown of every active subscription: returns the
|
|
24250
|
+
* empty `subscriptions/listen` JSON-RPC result for each subscription id —
|
|
24251
|
+
* the spec's graceful-close signal, `_meta` carrying the subscription id
|
|
24252
|
+
* and the serving instance's identity — for the entry to emit before
|
|
24253
|
+
* closing the wire. Clears the set so nothing further is delivered.
|
|
24254
|
+
*/
|
|
24255
|
+
teardownAll() {
|
|
24256
|
+
const out = [];
|
|
24257
|
+
for (const id of this._subs.keys()) out.push({
|
|
24258
|
+
jsonrpc: "2.0",
|
|
24259
|
+
id,
|
|
24260
|
+
result: {
|
|
24261
|
+
resultType: "complete",
|
|
24262
|
+
_meta: {
|
|
24263
|
+
[SUBSCRIPTION_ID_META_KEY]: id,
|
|
24264
|
+
...this._serverInfo !== void 0 && { ["io.modelcontextprotocol/serverInfo"]: this._serverInfo }
|
|
24265
|
+
}
|
|
24266
|
+
}
|
|
24267
|
+
});
|
|
24268
|
+
this._subs.clear();
|
|
24269
|
+
return out;
|
|
24270
|
+
}
|
|
24271
|
+
};
|
|
24272
|
+
function notificationToServerEvent(method, uri) {
|
|
24273
|
+
switch (method) {
|
|
24274
|
+
case "notifications/tools/list_changed": return { kind: "tools_list_changed" };
|
|
24275
|
+
case "notifications/prompts/list_changed": return { kind: "prompts_list_changed" };
|
|
24276
|
+
case "notifications/resources/list_changed": return { kind: "resources_list_changed" };
|
|
24277
|
+
default: return {
|
|
24278
|
+
kind: "resource_updated",
|
|
24279
|
+
uri: uri ?? ""
|
|
24280
|
+
};
|
|
24281
|
+
}
|
|
24282
|
+
}
|
|
23562
24283
|
function comparableResource(value) {
|
|
23563
24284
|
const text = String(value);
|
|
23564
24285
|
const hash = text.indexOf("#");
|
|
@@ -23877,6 +24598,41 @@ const INPUT_REQUIRED_CAPABLE_METHODS = /* @__PURE__ */ new Set([
|
|
|
23877
24598
|
"prompts/get",
|
|
23878
24599
|
"resources/read"
|
|
23879
24600
|
]);
|
|
24601
|
+
let writeClientIdentity;
|
|
24602
|
+
let installDiscoverHandler;
|
|
24603
|
+
let readServerIdentity;
|
|
24604
|
+
/**
|
|
24605
|
+
* Package-internal: backfills the connection-scoped client-identity fields of a
|
|
24606
|
+
* per-request server instance from the request's validated `_meta` envelope, so the
|
|
24607
|
+
* (deprecated) {@linkcode Server.getClientCapabilities} / {@linkcode Server.getClientVersion}
|
|
24608
|
+
* accessors keep answering on instances that never see an `initialize` handshake.
|
|
24609
|
+
* Not public API.
|
|
24610
|
+
*/
|
|
24611
|
+
function seedClientIdentityFromEnvelope(server, identity) {
|
|
24612
|
+
writeClientIdentity(server, identity);
|
|
24613
|
+
}
|
|
24614
|
+
/**
|
|
24615
|
+
* Package-internal: installs the modern-only `server/discover` handler on an instance
|
|
24616
|
+
* the HTTP entry has marked as serving the 2026-07-28 era, and makes sure the modern
|
|
24617
|
+
* revisions the entry serves appear in the instance's supported-versions list (so the
|
|
24618
|
+
* discover advertisement and version-mismatch errors name them). Idempotent.
|
|
24619
|
+
* Hand-constructed instances are unaffected: nothing else calls this, so they keep
|
|
24620
|
+
* answering `-32601` unless their own supported-versions list opts into a modern
|
|
24621
|
+
* revision. Not public API.
|
|
24622
|
+
*/
|
|
24623
|
+
function installModernOnlyHandlers(server, servedModernVersions) {
|
|
24624
|
+
installDiscoverHandler(server, servedModernVersions);
|
|
24625
|
+
}
|
|
24626
|
+
/**
|
|
24627
|
+
* Package-internal: the instance's implementation identity, for the serving
|
|
24628
|
+
* entries to stamp onto entry-built results (the `subscriptions/listen`
|
|
24629
|
+
* graceful-close result — built outside the encode seam, but the spec's
|
|
24630
|
+
* `SubscriptionsListenResultMetaObject` extends `ResultMetaObject`, so it carries
|
|
24631
|
+
* the serverInfo SHOULD like every other result). Not public API.
|
|
24632
|
+
*/
|
|
24633
|
+
function serverIdentityOf(server) {
|
|
24634
|
+
return readServerIdentity(server);
|
|
24635
|
+
}
|
|
23880
24636
|
/**
|
|
23881
24637
|
* An MCP server on top of a pluggable transport.
|
|
23882
24638
|
*
|
|
@@ -23887,7 +24643,18 @@ const INPUT_REQUIRED_CAPABLE_METHODS = /* @__PURE__ */ new Set([
|
|
|
23887
24643
|
var Server = class extends Protocol {
|
|
23888
24644
|
_clientCapabilities;
|
|
23889
24645
|
_clientVersion;
|
|
23890
|
-
static {
|
|
24646
|
+
static {
|
|
24647
|
+
writeClientIdentity = (server, identity) => {
|
|
24648
|
+
if (identity.clientCapabilities !== void 0) server._clientCapabilities = identity.clientCapabilities;
|
|
24649
|
+
if (identity.clientInfo !== void 0) server._clientVersion = identity.clientInfo;
|
|
24650
|
+
};
|
|
24651
|
+
installDiscoverHandler = (server, servedModernVersions) => {
|
|
24652
|
+
const missing = servedModernVersions.filter((version) => !server._supportedProtocolVersions.includes(version));
|
|
24653
|
+
if (missing.length > 0) server._supportedProtocolVersions = [...server._supportedProtocolVersions, ...missing];
|
|
24654
|
+
server.setRequestHandler("server/discover", () => server._ondiscover());
|
|
24655
|
+
};
|
|
24656
|
+
readServerIdentity = (server) => server._serverInfo;
|
|
24657
|
+
}
|
|
23891
24658
|
_capabilities;
|
|
23892
24659
|
_instructions;
|
|
23893
24660
|
_jsonSchemaValidator;
|
|
@@ -25184,6 +25951,245 @@ function unwrapOptionalSchema(schema) {
|
|
|
25184
25951
|
}
|
|
25185
25952
|
//#endregion
|
|
25186
25953
|
//#region node_modules/.pnpm/@modelcontextprotocol+server@2.3.1/node_modules/@modelcontextprotocol/server/dist/index.mjs
|
|
25954
|
+
/**
|
|
25955
|
+
* The per-request micro-transport: a real, connected `Transport` whose whole
|
|
25956
|
+
* lifetime is one HTTP exchange. See the module documentation for the
|
|
25957
|
+
* response shapes it produces.
|
|
25958
|
+
*/
|
|
25959
|
+
var PerRequestHTTPServerTransport = class {
|
|
25960
|
+
onclose;
|
|
25961
|
+
onerror;
|
|
25962
|
+
onmessage;
|
|
25963
|
+
_classification;
|
|
25964
|
+
_responseMode;
|
|
25965
|
+
_started = false;
|
|
25966
|
+
_used = false;
|
|
25967
|
+
_closed = false;
|
|
25968
|
+
_terminalDelivered = false;
|
|
25969
|
+
/**
|
|
25970
|
+
* `true` only while the inbound message is being delivered synchronously
|
|
25971
|
+
* to the connected protocol layer. The pre-handler gates (the era
|
|
25972
|
+
* registry gate, the edge→instance handoff check, the missing-handler
|
|
25973
|
+
* rejection) answer inside this window; request handlers always run
|
|
25974
|
+
* after it (the protocol layer defers them to a microtask). An error
|
|
25975
|
+
* sent inside the window is therefore ladder-originated, and an error
|
|
25976
|
+
* sent after it is handler-produced.
|
|
25977
|
+
*/
|
|
25978
|
+
_dispatchWindowOpen = false;
|
|
25979
|
+
_requestId;
|
|
25980
|
+
_deferredResponse;
|
|
25981
|
+
_sse;
|
|
25982
|
+
_abortCleanup;
|
|
25983
|
+
_keepAliveMs;
|
|
25984
|
+
constructor(options) {
|
|
25985
|
+
this._classification = options.classification;
|
|
25986
|
+
this._responseMode = options.responseMode ?? "auto";
|
|
25987
|
+
this._keepAliveMs = options.keepAliveMs ?? 15e3;
|
|
25988
|
+
}
|
|
25989
|
+
async start() {
|
|
25990
|
+
if (this._started) throw new Error("PerRequestHTTPServerTransport is already started");
|
|
25991
|
+
this._started = true;
|
|
25992
|
+
}
|
|
25993
|
+
/**
|
|
25994
|
+
* Serves the single exchange: delivers the classified message to the
|
|
25995
|
+
* connected server instance and resolves with the HTTP response.
|
|
25996
|
+
*
|
|
25997
|
+
* Throws when called a second time (the transport is strictly
|
|
25998
|
+
* single-use), or before a server has been connected to the transport.
|
|
25999
|
+
* The returned promise rejects with a connection-closed error when the
|
|
26000
|
+
* transport is closed before a response was produced (for example because
|
|
26001
|
+
* the client disconnected).
|
|
26002
|
+
*/
|
|
26003
|
+
async handleMessage(message, extra) {
|
|
26004
|
+
if (this._used) throw new Error("PerRequestHTTPServerTransport serves exactly one exchange; construct a new transport per request");
|
|
26005
|
+
if (!this._started || this.onmessage === void 0) throw new Error("PerRequestHTTPServerTransport is not connected: connect a server to this transport before handling a message");
|
|
26006
|
+
if (this._closed) throw new Error("PerRequestHTTPServerTransport is closed");
|
|
26007
|
+
this._used = true;
|
|
26008
|
+
const signal = extra?.request?.signal;
|
|
26009
|
+
if (signal?.aborted) {
|
|
26010
|
+
await this.close();
|
|
26011
|
+
throw new SdkError(SdkErrorCode.ConnectionClosed, "The request was aborted before it could be handled");
|
|
26012
|
+
}
|
|
26013
|
+
const messageExtra = {
|
|
26014
|
+
classification: this._classification,
|
|
26015
|
+
...extra?.request !== void 0 && { request: extra.request },
|
|
26016
|
+
...extra?.authInfo !== void 0 && { authInfo: extra.authInfo }
|
|
26017
|
+
};
|
|
26018
|
+
if (isJSONRPCRequest(message)) {
|
|
26019
|
+
this._requestId = message.id;
|
|
26020
|
+
let resolve;
|
|
26021
|
+
let reject;
|
|
26022
|
+
const promise = new Promise((promiseResolve, promiseReject) => {
|
|
26023
|
+
resolve = promiseResolve;
|
|
26024
|
+
reject = promiseReject;
|
|
26025
|
+
});
|
|
26026
|
+
this._deferredResponse = {
|
|
26027
|
+
promise,
|
|
26028
|
+
resolve,
|
|
26029
|
+
reject,
|
|
26030
|
+
settled: false
|
|
26031
|
+
};
|
|
26032
|
+
if (signal !== void 0) {
|
|
26033
|
+
const onAbort = () => void this.close();
|
|
26034
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
26035
|
+
this._abortCleanup = () => signal.removeEventListener("abort", onAbort);
|
|
26036
|
+
}
|
|
26037
|
+
this._dispatchWindowOpen = true;
|
|
26038
|
+
try {
|
|
26039
|
+
this.onmessage(message, messageExtra);
|
|
26040
|
+
} finally {
|
|
26041
|
+
this._dispatchWindowOpen = false;
|
|
26042
|
+
}
|
|
26043
|
+
if (this._responseMode === "sse" && !this._closed && !this._deferredResponse.settled) this.upgradeToSse();
|
|
26044
|
+
return promise;
|
|
26045
|
+
}
|
|
26046
|
+
this.onmessage(message, messageExtra);
|
|
26047
|
+
return new Response(null, { status: 202 });
|
|
26048
|
+
}
|
|
26049
|
+
async send(message, options) {
|
|
26050
|
+
if (this._closed) return;
|
|
26051
|
+
const isResponse = isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message);
|
|
26052
|
+
const relatedId = isResponse ? message.id : options?.relatedRequestId;
|
|
26053
|
+
if (this._requestId === void 0 || relatedId === void 0 || relatedId !== this._requestId) {
|
|
26054
|
+
if (isResponse) this.onerror?.(/* @__PURE__ */ new Error(`Received a response for an unknown request id: ${String(message.id)}`));
|
|
26055
|
+
return;
|
|
26056
|
+
}
|
|
26057
|
+
if (isResponse) {
|
|
26058
|
+
if (this._terminalDelivered) return;
|
|
26059
|
+
this._terminalDelivered = true;
|
|
26060
|
+
const errorCode = isJSONRPCErrorResponse(message) ? message.error.code : void 0;
|
|
26061
|
+
const ladderStatus = errorCode !== void 0 && (this._dispatchWindowOpen || errorCode === ProtocolErrorCode.MissingRequiredClientCapability) ? LADDER_ERROR_HTTP_STATUS[errorCode] : void 0;
|
|
26062
|
+
if (ladderStatus !== void 0 && this._sse === void 0) {
|
|
26063
|
+
this.settleResponse(Response.json(message, {
|
|
26064
|
+
status: ladderStatus,
|
|
26065
|
+
headers: { "Content-Type": "application/json" }
|
|
26066
|
+
}));
|
|
26067
|
+
queueMicrotask(() => void this.close());
|
|
26068
|
+
return;
|
|
26069
|
+
}
|
|
26070
|
+
if (this._sse !== void 0 || this._responseMode === "sse") {
|
|
26071
|
+
if (this._sse === void 0) this.upgradeToSse();
|
|
26072
|
+
this.writeMessageFrame(message);
|
|
26073
|
+
this.finalizeStream();
|
|
26074
|
+
return;
|
|
26075
|
+
}
|
|
26076
|
+
this.settleResponse(Response.json(message, {
|
|
26077
|
+
status: 200,
|
|
26078
|
+
headers: { "Content-Type": "application/json" }
|
|
26079
|
+
}));
|
|
26080
|
+
queueMicrotask(() => void this.close());
|
|
26081
|
+
return;
|
|
26082
|
+
}
|
|
26083
|
+
if (this._responseMode === "json") return;
|
|
26084
|
+
if (this._sse === void 0) this.upgradeToSse();
|
|
26085
|
+
this.writeMessageFrame(message);
|
|
26086
|
+
}
|
|
26087
|
+
/**
|
|
26088
|
+
* Writes an SSE comment frame (a keep-alive heartbeat). Dropped when the
|
|
26089
|
+
* exchange is not currently streaming.
|
|
26090
|
+
*/
|
|
26091
|
+
writeCommentFrame(comment) {
|
|
26092
|
+
if (this._closed || this._sse === void 0 || this._sse.closed) return;
|
|
26093
|
+
const frame = comment.split("\n").map((line) => `: ${line}`).join("\n");
|
|
26094
|
+
this.writeFrame(`${frame}\n\n`);
|
|
26095
|
+
}
|
|
26096
|
+
async close() {
|
|
26097
|
+
if (this._closed) return;
|
|
26098
|
+
this._closed = true;
|
|
26099
|
+
this._abortCleanup?.();
|
|
26100
|
+
this._abortCleanup = void 0;
|
|
26101
|
+
if (this._sse?.keepAliveTimer !== void 0) clearInterval(this._sse.keepAliveTimer);
|
|
26102
|
+
if (this._sse !== void 0 && !this._sse.closed) {
|
|
26103
|
+
this._sse.closed = true;
|
|
26104
|
+
try {
|
|
26105
|
+
this._sse.controller.close();
|
|
26106
|
+
} catch {}
|
|
26107
|
+
}
|
|
26108
|
+
if (this._deferredResponse !== void 0 && !this._deferredResponse.settled) {
|
|
26109
|
+
this._deferredResponse.settled = true;
|
|
26110
|
+
this._deferredResponse.reject(new SdkError(SdkErrorCode.ConnectionClosed, "Connection closed before a response was produced"));
|
|
26111
|
+
}
|
|
26112
|
+
this.onclose?.();
|
|
26113
|
+
}
|
|
26114
|
+
settleResponse(response) {
|
|
26115
|
+
if (this._deferredResponse === void 0 || this._deferredResponse.settled) return;
|
|
26116
|
+
this._deferredResponse.settled = true;
|
|
26117
|
+
this._deferredResponse.resolve(response);
|
|
26118
|
+
}
|
|
26119
|
+
upgradeToSse() {
|
|
26120
|
+
let controller;
|
|
26121
|
+
const readable = new ReadableStream({
|
|
26122
|
+
start: (streamController) => {
|
|
26123
|
+
controller = streamController;
|
|
26124
|
+
},
|
|
26125
|
+
cancel: () => {
|
|
26126
|
+
this.close();
|
|
26127
|
+
}
|
|
26128
|
+
});
|
|
26129
|
+
this._sse = {
|
|
26130
|
+
controller,
|
|
26131
|
+
encoder: new TextEncoder(),
|
|
26132
|
+
closed: false
|
|
26133
|
+
};
|
|
26134
|
+
this._sse.keepAliveTimer = armSseKeepAlive(this._keepAliveMs, () => this.writeCommentFrame("keepalive"));
|
|
26135
|
+
this.settleResponse(new Response(readable, {
|
|
26136
|
+
status: 200,
|
|
26137
|
+
headers: {
|
|
26138
|
+
"Content-Type": "text/event-stream",
|
|
26139
|
+
"Cache-Control": "no-cache, no-transform",
|
|
26140
|
+
Connection: "keep-alive",
|
|
26141
|
+
"X-Accel-Buffering": "no"
|
|
26142
|
+
}
|
|
26143
|
+
}));
|
|
26144
|
+
}
|
|
26145
|
+
finalizeStream() {
|
|
26146
|
+
if (this._sse?.keepAliveTimer !== void 0) clearInterval(this._sse.keepAliveTimer);
|
|
26147
|
+
if (this._sse !== void 0 && !this._sse.closed) {
|
|
26148
|
+
this._sse.closed = true;
|
|
26149
|
+
try {
|
|
26150
|
+
this._sse.controller.close();
|
|
26151
|
+
} catch {}
|
|
26152
|
+
}
|
|
26153
|
+
queueMicrotask(() => void this.close());
|
|
26154
|
+
}
|
|
26155
|
+
writeMessageFrame(message) {
|
|
26156
|
+
this.writeFrame(`event: message\ndata: ${JSON.stringify(message)}\n\n`);
|
|
26157
|
+
}
|
|
26158
|
+
writeFrame(frame) {
|
|
26159
|
+
if (this._sse === void 0 || this._sse.closed) return;
|
|
26160
|
+
try {
|
|
26161
|
+
this._sse.controller.enqueue(this._sse.encoder.encode(frame));
|
|
26162
|
+
} catch (error) {
|
|
26163
|
+
this.onerror?.(/* @__PURE__ */ new Error(`Failed to write to the response stream: ${error}`));
|
|
26164
|
+
}
|
|
26165
|
+
}
|
|
26166
|
+
};
|
|
26167
|
+
/**
|
|
26168
|
+
* Serves one classified inbound message on the given server instance and
|
|
26169
|
+
* returns the HTTP response for the exchange.
|
|
26170
|
+
*
|
|
26171
|
+
* The instance is connected to a fresh single-exchange transport, the message
|
|
26172
|
+
* is injected through the normal transport message path, and whatever the
|
|
26173
|
+
* dispatch layer produces (the handler result, a protocol-level rejection, or
|
|
26174
|
+
* streamed related messages followed by the result) is captured as the
|
|
26175
|
+
* returned `Response`. For request exchanges, teardown rides the transport's
|
|
26176
|
+
* close chain once the terminal response has been delivered; notification
|
|
26177
|
+
* exchanges resolve with the 202 response immediately and do NOT run the
|
|
26178
|
+
* close chain — the transport stays connected until the caller closes it or
|
|
26179
|
+
* drops the per-request instance, which is the caller's choice either way.
|
|
26180
|
+
*/
|
|
26181
|
+
async function invoke(server, message, ctx) {
|
|
26182
|
+
const transport = new PerRequestHTTPServerTransport({
|
|
26183
|
+
classification: ctx.classification,
|
|
26184
|
+
...ctx.responseMode !== void 0 && { responseMode: ctx.responseMode },
|
|
26185
|
+
...ctx.keepAliveMs !== void 0 && { keepAliveMs: ctx.keepAliveMs }
|
|
26186
|
+
});
|
|
26187
|
+
await server.connect(transport);
|
|
26188
|
+
return transport.handleMessage(message, {
|
|
26189
|
+
...ctx.request !== void 0 && { request: ctx.request },
|
|
26190
|
+
...ctx.authInfo !== void 0 && { authInfo: ctx.authInfo }
|
|
26191
|
+
});
|
|
26192
|
+
}
|
|
25187
26193
|
/** Default upper bound, in bytes, on a request body read by the HTTP entry points (4 MiB). */
|
|
25188
26194
|
const DEFAULT_MAX_REQUEST_BODY_SIZE = 4194304;
|
|
25189
26195
|
/** Upper bound on the number of messages accepted in one JSON-RPC batch array. */
|
|
@@ -25934,6 +26940,995 @@ var WebStandardStreamableHTTPServerTransport = class {
|
|
|
25934
26940
|
}
|
|
25935
26941
|
}
|
|
25936
26942
|
};
|
|
26943
|
+
/**
|
|
26944
|
+
* The JSON-RPC id to echo on an entry-built error response: the body's `id`
|
|
26945
|
+
* when the body is a single JSON-RPC request whose id is a string or number,
|
|
26946
|
+
* `null` otherwise. Error responses must carry the id of the request they
|
|
26947
|
+
* correspond to whenever it could be read; `null` is reserved for the cases
|
|
26948
|
+
* where no single request id is determinable — unparseable bodies, body-less
|
|
26949
|
+
* methods, notifications, posted responses and batch arrays.
|
|
26950
|
+
*/
|
|
26951
|
+
function echoableRequestId(body) {
|
|
26952
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) return null;
|
|
26953
|
+
const { method, id } = body;
|
|
26954
|
+
if (typeof method !== "string") return null;
|
|
26955
|
+
return typeof id === "string" || typeof id === "number" ? id : null;
|
|
26956
|
+
}
|
|
26957
|
+
function jsonRpcErrorResponse(httpStatus, code, message, data, id = null) {
|
|
26958
|
+
return Response.json({
|
|
26959
|
+
jsonrpc: "2.0",
|
|
26960
|
+
error: {
|
|
26961
|
+
code,
|
|
26962
|
+
message,
|
|
26963
|
+
...data !== void 0 && { data }
|
|
26964
|
+
},
|
|
26965
|
+
id
|
|
26966
|
+
}, { status: httpStatus });
|
|
26967
|
+
}
|
|
26968
|
+
function rejectionResponse(rejection, id = null) {
|
|
26969
|
+
return jsonRpcErrorResponse(rejection.httpStatus, rejection.code, rejection.message, rejection.data, id);
|
|
26970
|
+
}
|
|
26971
|
+
function toError$1(value) {
|
|
26972
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
26973
|
+
}
|
|
26974
|
+
function internalServerErrorResponse(id = null) {
|
|
26975
|
+
return jsonRpcErrorResponse(500, -32603, "Internal server error", void 0, id);
|
|
26976
|
+
}
|
|
26977
|
+
/** Throws when the factory returned an instance that is still connected to another exchange. */
|
|
26978
|
+
function assertNotServing(server) {
|
|
26979
|
+
if (server.transport !== void 0) throw new Error("The factory returned an instance that is still serving another request. Return a new instance for each request.");
|
|
26980
|
+
}
|
|
26981
|
+
/**
|
|
26982
|
+
* The entry's default legacy serving (`legacy: 'stateless'`): per-request
|
|
26983
|
+
* stateless serving of 2025-era traffic using the same factory as the modern
|
|
26984
|
+
* path. Exported as a standalone building block for hand-wired compositions
|
|
26985
|
+
* (for example mounting legacy stateless serving on its own route next to a
|
|
26986
|
+
* strict modern endpoint).
|
|
26987
|
+
*
|
|
26988
|
+
* Each POST is served by a fresh instance from the factory connected to a
|
|
26989
|
+
* fresh streamable HTTP transport constructed with only
|
|
26990
|
+
* `sessionIdGenerator: undefined` — the established stateless idiom, unchanged.
|
|
26991
|
+
* Because serving is per-request and stateless, GET and DELETE (2025 session
|
|
26992
|
+
* operations) are answered with `405` / `Method not allowed.`, exactly like the
|
|
26993
|
+
* canonical stateless example.
|
|
26994
|
+
*
|
|
26995
|
+
* The optional `onerror` callback receives factory and serving failures on
|
|
26996
|
+
* this leg (reporting only — the response stays the 500 internal-error body).
|
|
26997
|
+
* The entry passes its own `onerror` here when expanding the default, so
|
|
26998
|
+
* legacy-leg failures are never silently swallowed.
|
|
26999
|
+
*/
|
|
27000
|
+
function createLegacyStatelessFallback(factory, onerror, keepAliveMs, maxRequestBodySize) {
|
|
27001
|
+
return async (request, options) => {
|
|
27002
|
+
if (request.method.toUpperCase() !== "POST") return jsonRpcErrorResponse(405, -32e3, "Method not allowed.");
|
|
27003
|
+
try {
|
|
27004
|
+
const product = await factory({
|
|
27005
|
+
era: "legacy",
|
|
27006
|
+
...options?.authInfo !== void 0 && { authInfo: options.authInfo },
|
|
27007
|
+
requestInfo: request
|
|
27008
|
+
});
|
|
27009
|
+
const transport = new WebStandardStreamableHTTPServerTransport({
|
|
27010
|
+
sessionIdGenerator: void 0,
|
|
27011
|
+
...keepAliveMs !== void 0 && { keepAliveMs },
|
|
27012
|
+
...maxRequestBodySize !== void 0 && { maxRequestBodySize }
|
|
27013
|
+
});
|
|
27014
|
+
await product.connect(transport);
|
|
27015
|
+
const teardown = () => {
|
|
27016
|
+
transport.close().catch(() => {});
|
|
27017
|
+
product.close().catch(() => {});
|
|
27018
|
+
};
|
|
27019
|
+
request.signal?.addEventListener("abort", teardown, { once: true });
|
|
27020
|
+
const response = await transport.handleRequest(request, {
|
|
27021
|
+
...options?.authInfo !== void 0 && { authInfo: options.authInfo },
|
|
27022
|
+
...options?.parsedBody !== void 0 && { parsedBody: options.parsedBody }
|
|
27023
|
+
});
|
|
27024
|
+
if (response.body === null || mediaTypeEssence(response.headers.get("content-type")) !== "text/event-stream") {
|
|
27025
|
+
teardown();
|
|
27026
|
+
return response;
|
|
27027
|
+
}
|
|
27028
|
+
const reader = response.body.getReader();
|
|
27029
|
+
let toreDown = false;
|
|
27030
|
+
const completeExchange = () => {
|
|
27031
|
+
if (!toreDown) {
|
|
27032
|
+
toreDown = true;
|
|
27033
|
+
teardown();
|
|
27034
|
+
}
|
|
27035
|
+
};
|
|
27036
|
+
const monitoredBody = new ReadableStream({
|
|
27037
|
+
pull: async (controller) => {
|
|
27038
|
+
try {
|
|
27039
|
+
const { done, value } = await reader.read();
|
|
27040
|
+
if (done) {
|
|
27041
|
+
completeExchange();
|
|
27042
|
+
controller.close();
|
|
27043
|
+
return;
|
|
27044
|
+
}
|
|
27045
|
+
if (value !== void 0) controller.enqueue(value);
|
|
27046
|
+
} catch (error) {
|
|
27047
|
+
completeExchange();
|
|
27048
|
+
controller.error(error);
|
|
27049
|
+
}
|
|
27050
|
+
},
|
|
27051
|
+
cancel: (reason) => {
|
|
27052
|
+
completeExchange();
|
|
27053
|
+
return reader.cancel(reason).catch(() => {});
|
|
27054
|
+
}
|
|
27055
|
+
});
|
|
27056
|
+
return new Response(monitoredBody, {
|
|
27057
|
+
status: response.status,
|
|
27058
|
+
statusText: response.statusText,
|
|
27059
|
+
headers: response.headers
|
|
27060
|
+
});
|
|
27061
|
+
} catch (error) {
|
|
27062
|
+
try {
|
|
27063
|
+
onerror?.(toError$1(error));
|
|
27064
|
+
} catch {}
|
|
27065
|
+
return internalServerErrorResponse(echoableRequestId(options?.parsedBody));
|
|
27066
|
+
}
|
|
27067
|
+
};
|
|
27068
|
+
}
|
|
27069
|
+
/**
|
|
27070
|
+
* Read the SEP-2243 standard request headers off the inbound request.
|
|
27071
|
+
*
|
|
27072
|
+
* Both halves of the standard-header story need them — the body-primary
|
|
27073
|
+
* classifier for its cross-check cells, and
|
|
27074
|
+
* {@linkcode validateStandardRequestHeaders} for the presence half — and a
|
|
27075
|
+
* header read at one site but not the other is precisely how a required header
|
|
27076
|
+
* goes unenforced: that divergence is what let a modern POST omitting
|
|
27077
|
+
* `MCP-Protocol-Version` be served. Read them here once so a header added to
|
|
27078
|
+
* {@linkcode InboundHttpRequest} reaches both sites together.
|
|
27079
|
+
*/
|
|
27080
|
+
function standardHeadersOf(request) {
|
|
27081
|
+
return {
|
|
27082
|
+
protocolVersionHeader: request.headers.get("mcp-protocol-version") ?? void 0,
|
|
27083
|
+
mcpMethodHeader: request.headers.get("mcp-method") ?? void 0,
|
|
27084
|
+
mcpNameHeader: request.headers.get("mcp-name") ?? void 0
|
|
27085
|
+
};
|
|
27086
|
+
}
|
|
27087
|
+
/**
|
|
27088
|
+
* The entry's classification step: read the request body exactly once (unless
|
|
27089
|
+
* a pre-parsed body is supplied) and classify the request with
|
|
27090
|
+
* {@linkcode classifyInboundRequest}. This is the single code path behind both
|
|
27091
|
+
* {@linkcode createMcpHandler}'s routing and the exported
|
|
27092
|
+
* {@linkcode isLegacyRequest} predicate, so the two can never disagree.
|
|
27093
|
+
*
|
|
27094
|
+
* Pass `needsForward: false` when the caller never reads `forwardRequest` —
|
|
27095
|
+
* the body-preserving clone is then skipped and `forwardRequest` is the
|
|
27096
|
+
* (consumed) input request.
|
|
27097
|
+
*/
|
|
27098
|
+
async function classifyEntryRequest(request, providedParsedBody, needsForward = true, maxRequestBodySize = DEFAULT_MAX_REQUEST_BODY_SIZE) {
|
|
27099
|
+
const httpMethod = request.method.toUpperCase();
|
|
27100
|
+
let body;
|
|
27101
|
+
let parsedBody = providedParsedBody;
|
|
27102
|
+
let forwardRequest = request;
|
|
27103
|
+
let unparseable = false;
|
|
27104
|
+
if (httpMethod === "POST") {
|
|
27105
|
+
if (parsedBody === void 0) {
|
|
27106
|
+
if (needsForward) forwardRequest = request.clone();
|
|
27107
|
+
let bodyText;
|
|
27108
|
+
try {
|
|
27109
|
+
const read = await readRequestBody(request, maxRequestBodySize);
|
|
27110
|
+
if (read.tooLarge) return { step: "body-too-large" };
|
|
27111
|
+
bodyText = read.text;
|
|
27112
|
+
} catch {
|
|
27113
|
+
return { step: "unreadable-body" };
|
|
27114
|
+
}
|
|
27115
|
+
try {
|
|
27116
|
+
body = bodyText.length === 0 ? void 0 : JSON.parse(bodyText);
|
|
27117
|
+
} catch {
|
|
27118
|
+
unparseable = true;
|
|
27119
|
+
}
|
|
27120
|
+
if (!unparseable && body !== void 0) parsedBody = body;
|
|
27121
|
+
} else body = parsedBody;
|
|
27122
|
+
if (unparseable || body === void 0) return {
|
|
27123
|
+
step: "no-json-body",
|
|
27124
|
+
forwardRequest
|
|
27125
|
+
};
|
|
27126
|
+
}
|
|
27127
|
+
return {
|
|
27128
|
+
step: "classified",
|
|
27129
|
+
outcome: classifyInboundRequest({
|
|
27130
|
+
httpMethod,
|
|
27131
|
+
...standardHeadersOf(request),
|
|
27132
|
+
...body !== void 0 && { body }
|
|
27133
|
+
}),
|
|
27134
|
+
body,
|
|
27135
|
+
parsedBody,
|
|
27136
|
+
forwardRequest
|
|
27137
|
+
};
|
|
27138
|
+
}
|
|
27139
|
+
/**
|
|
27140
|
+
* Whether {@linkcode createMcpHandler} would route this request to its legacy
|
|
27141
|
+
* (2025-era) serving rather than the modern (2026-07-28) path.
|
|
27142
|
+
*
|
|
27143
|
+
* Call it with just the request: `await isLegacyRequest(request)`. For a
|
|
27144
|
+
* `POST` the body is read from an internal clone, so the request you pass
|
|
27145
|
+
* stays fully readable for whichever handler you route it to — no second
|
|
27146
|
+
* argument is needed. (In a Node `(req, res)` handler, build that `Request`
|
|
27147
|
+
* with `toWebRequest(req)` from `@modelcontextprotocol/node`; behind a body
|
|
27148
|
+
* parser, which has already drained the Node stream, build it as
|
|
27149
|
+
* `toWebRequest(req, req.body)` so the bytes come from the parsed body —
|
|
27150
|
+
* either way the predicate still takes just the request.) The optional
|
|
27151
|
+
* `parsedBody` is a perf escape hatch for a body you already hold parsed:
|
|
27152
|
+
* pass it and the predicate classifies from the value directly, reading and
|
|
27153
|
+
* cloning nothing. It is needed, not just faster, when the request's own
|
|
27154
|
+
* body was already read — the internal clone is then impossible (cloning a
|
|
27155
|
+
* used body throws a `TypeError`), so such a single-argument call rejects
|
|
27156
|
+
* instead of guessing.
|
|
27157
|
+
*
|
|
27158
|
+
* This is the entry's own classification step exported as a predicate — it
|
|
27159
|
+
* runs exactly the code `createMcpHandler` runs to make the routing decision,
|
|
27160
|
+
* not a re-implementation — so a hand-wired composition that branches on it
|
|
27161
|
+
* can never disagree with the entry. It is classification only: hand-wired
|
|
27162
|
+
* compositions must validate Content-Type themselves (415 for POSTs whose
|
|
27163
|
+
* media type is not `application/json`, via {@linkcode isJsonContentType})
|
|
27164
|
+
* before dispatching either leg — routing the legacy leg into the SDK
|
|
27165
|
+
* transports gets their built-in check, but a custom modern leg has none. Use it to keep an existing legacy
|
|
27166
|
+
* deployment (for example a sessionful streamable HTTP wiring) serving 2025
|
|
27167
|
+
* traffic next to a strict modern endpoint, now that the entry has no
|
|
27168
|
+
* handler-valued `legacy` option:
|
|
27169
|
+
*
|
|
27170
|
+
* ```ts
|
|
27171
|
+
* import { createMcpHandler, isLegacyRequest } from '@modelcontextprotocol/server';
|
|
27172
|
+
*
|
|
27173
|
+
* const modern = createMcpHandler(factory, { legacy: 'reject' });
|
|
27174
|
+
*
|
|
27175
|
+
* export default {
|
|
27176
|
+
* async fetch(request: Request): Promise<Response> {
|
|
27177
|
+
* if (await isLegacyRequest(request)) {
|
|
27178
|
+
* // e.g. an existing sessionful WebStandardStreamableHTTPServerTransport wiring
|
|
27179
|
+
* return myExistingLegacyHandler(request);
|
|
27180
|
+
* }
|
|
27181
|
+
* return modern.fetch(request);
|
|
27182
|
+
* }
|
|
27183
|
+
* };
|
|
27184
|
+
* ```
|
|
27185
|
+
*
|
|
27186
|
+
* Semantics (identical to the entry's routing):
|
|
27187
|
+
*
|
|
27188
|
+
* - Returns `true` only for requests with no per-request `_meta` envelope
|
|
27189
|
+
* claim: claim-less POSTs (including the `initialize` handshake and 2025-era
|
|
27190
|
+
* notification POSTs without a modern protocol-version header), body-less
|
|
27191
|
+
* GET/DELETE session operations, all-legacy JSON-RPC batch arrays, posted
|
|
27192
|
+
* JSON-RPC responses, and POSTs whose body is empty or not valid JSON.
|
|
27193
|
+
* - Returns `false` for everything the modern path answers, including its
|
|
27194
|
+
* validation-ladder rejections: a request carrying the envelope claim (even
|
|
27195
|
+
* one naming a revision the endpoint does not serve — the modern path
|
|
27196
|
+
* answers it with the unsupported-protocol-version error), a malformed
|
|
27197
|
+
* envelope behind a present claim (answered `-32602`), a request whose
|
|
27198
|
+
* `MCP-Protocol-Version` header names a modern revision but that lacks the
|
|
27199
|
+
* envelope (`-32602`), header/body mismatches (`-32020`), and a POST body
|
|
27200
|
+
* over the size limit (413). Consumers
|
|
27201
|
+
* routing on the predicate must send `false` traffic to the modern handler,
|
|
27202
|
+
* never to a legacy handler — the modern path owns those error answers.
|
|
27203
|
+
* - `server/discover` probes sent by negotiating clients always carry the
|
|
27204
|
+
* envelope claim, so they are never legacy; a hand-built claim-less POST to
|
|
27205
|
+
* a method named `server/discover` has no claim and classifies legacy,
|
|
27206
|
+
* exactly as the entry itself routes it.
|
|
27207
|
+
*/
|
|
27208
|
+
async function isLegacyRequest(request, parsedBody, options) {
|
|
27209
|
+
const maxRequestBodySize = resolveMaxRequestBodySize(options?.maxRequestBodySize);
|
|
27210
|
+
const classified = await classifyEntryRequest(parsedBody === void 0 && request.method.toUpperCase() === "POST" ? request.clone() : request, parsedBody, false, maxRequestBodySize);
|
|
27211
|
+
return classified.step === "no-json-body" || classified.step === "classified" && classified.outcome.kind === "legacy";
|
|
27212
|
+
}
|
|
27213
|
+
/**
|
|
27214
|
+
* Creates an HTTP handler that serves the 2026-07-28 protocol revision from a
|
|
27215
|
+
* per-request server factory and, by default, falls back to old-school
|
|
27216
|
+
* stateless serving for 2025-era traffic. Pass `legacy: 'reject'` for a
|
|
27217
|
+
* modern-only strict endpoint.
|
|
27218
|
+
*
|
|
27219
|
+
* Mounting: `handler.fetch` is the web-standard face (Cloudflare Workers,
|
|
27220
|
+
* Deno, Bun, Hono's `c.req.raw`); for Express/Fastify/plain `node:http`, wrap
|
|
27221
|
+
* the handler once with `toNodeHandler(handler)` from
|
|
27222
|
+
* `@modelcontextprotocol/node`. When mounting bare on a fetch-native runtime,
|
|
27223
|
+
* put Origin/Host validation in front of the handler — the entry itself is
|
|
27224
|
+
* deliberately validation-free:
|
|
27225
|
+
*
|
|
27226
|
+
* ```ts
|
|
27227
|
+
* import { hostHeaderValidationResponse, originValidationResponse, localhostAllowedHostnames, localhostAllowedOrigins } from '@modelcontextprotocol/server';
|
|
27228
|
+
*
|
|
27229
|
+
* export default {
|
|
27230
|
+
* async fetch(request: Request): Promise<Response> {
|
|
27231
|
+
* const rejected =
|
|
27232
|
+
* hostHeaderValidationResponse(request, localhostAllowedHostnames()) ??
|
|
27233
|
+
* originValidationResponse(request, localhostAllowedOrigins());
|
|
27234
|
+
* return rejected ?? handler.fetch(request);
|
|
27235
|
+
* }
|
|
27236
|
+
* };
|
|
27237
|
+
* ```
|
|
27238
|
+
*
|
|
27239
|
+
* Use ONE factory for both legs: the same tools/resources/prompts definition
|
|
27240
|
+
* backs the modern path and the stateless legacy fallback, so the two eras can
|
|
27241
|
+
* never drift apart. To keep an existing legacy deployment (for example a
|
|
27242
|
+
* sessionful streamable HTTP wiring) serving 2025 traffic instead of the
|
|
27243
|
+
* stateless fallback, route in user land with {@linkcode isLegacyRequest} in
|
|
27244
|
+
* front of a strict handler — see that predicate's documentation for the
|
|
27245
|
+
* pattern. Power users composing transport-neutral routing can also use the
|
|
27246
|
+
* exported building blocks directly: {@linkcode classifyInboundRequest} for
|
|
27247
|
+
* the era decision and `PerRequestHTTPServerTransport` for single-exchange
|
|
27248
|
+
* serving — such compositions must reject POSTs whose Content-Type media type
|
|
27249
|
+
* is not `application/json` (415) before parsing the body, using
|
|
27250
|
+
* {@linkcode isJsonContentType}; neither building block performs this
|
|
27251
|
+
* validation itself.
|
|
27252
|
+
*
|
|
27253
|
+
* The entry performs no token verification: `authInfo` given to `fetch` is
|
|
27254
|
+
* passed through to handlers and the factory as-is and is never derived from
|
|
27255
|
+
* request headers.
|
|
27256
|
+
*/
|
|
27257
|
+
function createMcpHandler(factory, options = {}) {
|
|
27258
|
+
const { legacy, onerror, responseMode } = options;
|
|
27259
|
+
const maxRequestBodySize = resolveMaxRequestBodySize(options.maxRequestBodySize);
|
|
27260
|
+
if (typeof legacy === "function") throw new TypeError("The 'legacy' option only accepts 'stateless' or 'reject', not a handler function. To serve 2025-era traffic with your own handler, route in user land with the exported isLegacyRequest(request) predicate in front of a strict (legacy: 'reject') handler.");
|
|
27261
|
+
/** Modern per-request instances with an exchange still in flight (close() tears these down). */
|
|
27262
|
+
const inflight = /* @__PURE__ */ new Set();
|
|
27263
|
+
let closed = false;
|
|
27264
|
+
const reportError = (error) => {
|
|
27265
|
+
try {
|
|
27266
|
+
onerror?.(error);
|
|
27267
|
+
} catch {}
|
|
27268
|
+
};
|
|
27269
|
+
const bus = options.bus ?? new InMemoryServerEventBus(reportError);
|
|
27270
|
+
const notify = createServerNotifier(bus);
|
|
27271
|
+
const listenRouter = createListenRouter({
|
|
27272
|
+
bus,
|
|
27273
|
+
maxSubscriptions: options.maxSubscriptions ?? 1024,
|
|
27274
|
+
keepAliveMs: options.keepAliveMs ?? 15e3,
|
|
27275
|
+
onerror: reportError
|
|
27276
|
+
});
|
|
27277
|
+
if (responseMode === "json") console.warn("responseMode: 'json' drops mid-call notifications. subscriptions/listen streams are always served over SSE regardless; other notifications emitted before a result are dropped.");
|
|
27278
|
+
const legacyHandler = legacy === "reject" ? void 0 : createLegacyStatelessFallback(factory, reportError, options.keepAliveMs, maxRequestBodySize);
|
|
27279
|
+
async function serveModern(route, request, authInfo) {
|
|
27280
|
+
const claimedRevision = route.classification.revision;
|
|
27281
|
+
if (claimedRevision === void 0 || !SUPPORTED_MODERN_PROTOCOL_VERSIONS.includes(claimedRevision)) {
|
|
27282
|
+
const error = new UnsupportedProtocolVersionError({
|
|
27283
|
+
supported: [...SUPPORTED_MODERN_PROTOCOL_VERSIONS],
|
|
27284
|
+
requested: claimedRevision ?? "unknown"
|
|
27285
|
+
});
|
|
27286
|
+
reportError(error);
|
|
27287
|
+
return jsonRpcErrorResponse(400, error.code, error.message, error.data, echoableRequestId(route.message));
|
|
27288
|
+
}
|
|
27289
|
+
const stdHeaderRejection = validateStandardRequestHeaders({
|
|
27290
|
+
httpMethod: request.method,
|
|
27291
|
+
...standardHeadersOf(request)
|
|
27292
|
+
}, route);
|
|
27293
|
+
if (stdHeaderRejection !== void 0) {
|
|
27294
|
+
reportError(/* @__PURE__ */ new Error(`Rejected inbound request (${stdHeaderRejection.cell}): ${stdHeaderRejection.message}`));
|
|
27295
|
+
return rejectionResponse(stdHeaderRejection, echoableRequestId(route.message));
|
|
27296
|
+
}
|
|
27297
|
+
const meta = route.messageKind === "request" ? requestMetaOf(route.message.params) : void 0;
|
|
27298
|
+
const declaredClientCapabilities = meta?.[CLIENT_CAPABILITIES_META_KEY];
|
|
27299
|
+
if (route.messageKind === "request") {
|
|
27300
|
+
const required = requiredClientCapabilitiesForRequest(route.message.method);
|
|
27301
|
+
if (required !== void 0) {
|
|
27302
|
+
const missing = missingClientCapabilities(required, declaredClientCapabilities);
|
|
27303
|
+
if (missing !== void 0) {
|
|
27304
|
+
const error = new MissingRequiredClientCapabilityError({ requiredCapabilities: missing });
|
|
27305
|
+
reportError(error);
|
|
27306
|
+
return jsonRpcErrorResponse(httpStatusForErrorCode(error.code, "ladder"), error.code, error.message, error.data, route.message.id);
|
|
27307
|
+
}
|
|
27308
|
+
}
|
|
27309
|
+
}
|
|
27310
|
+
const product = await factory({
|
|
27311
|
+
era: "modern",
|
|
27312
|
+
...authInfo !== void 0 && { authInfo },
|
|
27313
|
+
requestInfo: request
|
|
27314
|
+
});
|
|
27315
|
+
const server = product instanceof McpServer ? product.server : product;
|
|
27316
|
+
assertNotServing(server);
|
|
27317
|
+
if (route.messageKind === "request" && route.message.method === "subscriptions/listen") {
|
|
27318
|
+
const capabilities = server.getCapabilities();
|
|
27319
|
+
const serverInfo = serverIdentityOf(server);
|
|
27320
|
+
product.close().catch(reportError);
|
|
27321
|
+
return listenRouter.serve(route.message, request.signal, capabilities, serverInfo);
|
|
27322
|
+
}
|
|
27323
|
+
if (route.messageKind === "request" && route.message.method === "tools/call" && product instanceof McpServer) {
|
|
27324
|
+
const callParams = route.message.params;
|
|
27325
|
+
const toolName = typeof callParams?.name === "string" ? callParams.name : void 0;
|
|
27326
|
+
const inputSchema = toolName === void 0 ? void 0 : product.toolInputSchemaJson(toolName);
|
|
27327
|
+
if (inputSchema !== void 0) {
|
|
27328
|
+
const scan = scanXMcpHeaderDeclarations(inputSchema);
|
|
27329
|
+
if (scan.valid && scan.declarations.length > 0) {
|
|
27330
|
+
const rejection = validateMcpParamHeaders(scan.declarations, callParams?.arguments, request.headers);
|
|
27331
|
+
if (rejection !== void 0) {
|
|
27332
|
+
product.close().catch(reportError);
|
|
27333
|
+
reportError(/* @__PURE__ */ new Error(`Rejected inbound request (${rejection.cell}): ${rejection.message}`));
|
|
27334
|
+
return rejectionResponse(rejection, route.message.id);
|
|
27335
|
+
}
|
|
27336
|
+
}
|
|
27337
|
+
}
|
|
27338
|
+
}
|
|
27339
|
+
if (route.messageKind === "request" && product instanceof McpServer) try {
|
|
27340
|
+
const challenge = await findScopeChallenge([route.message], authInfo, (context) => product.resolveScopeChallenge(context));
|
|
27341
|
+
if (challenge !== void 0) {
|
|
27342
|
+
product.close().catch(reportError);
|
|
27343
|
+
return createScopeChallengeResponse(challenge, scopeChallengeResourceMetadataUrl(authInfo));
|
|
27344
|
+
}
|
|
27345
|
+
} catch (error) {
|
|
27346
|
+
product.close().catch(reportError);
|
|
27347
|
+
reportError(toError$1(error));
|
|
27348
|
+
return internalServerErrorResponse(route.message.id);
|
|
27349
|
+
}
|
|
27350
|
+
assertNotServing(server);
|
|
27351
|
+
setNegotiatedProtocolVersion(server, claimedRevision);
|
|
27352
|
+
installModernOnlyHandlers(server, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
|
|
27353
|
+
if (meta !== void 0) seedClientIdentityFromEnvelope(server, {
|
|
27354
|
+
clientInfo: meta[CLIENT_INFO_META_KEY],
|
|
27355
|
+
clientCapabilities: declaredClientCapabilities
|
|
27356
|
+
});
|
|
27357
|
+
const previousOnClose = server.onclose;
|
|
27358
|
+
inflight.add(server);
|
|
27359
|
+
const onExchangeClose = () => {
|
|
27360
|
+
inflight.delete(server);
|
|
27361
|
+
if (server.onclose === onExchangeClose) server.onclose = previousOnClose;
|
|
27362
|
+
previousOnClose?.();
|
|
27363
|
+
};
|
|
27364
|
+
server.onclose = onExchangeClose;
|
|
27365
|
+
try {
|
|
27366
|
+
const response = await invoke(product, route.message, {
|
|
27367
|
+
classification: route.classification,
|
|
27368
|
+
request,
|
|
27369
|
+
...authInfo !== void 0 && { authInfo },
|
|
27370
|
+
...responseMode !== void 0 && { responseMode },
|
|
27371
|
+
...options.keepAliveMs !== void 0 && { keepAliveMs: options.keepAliveMs }
|
|
27372
|
+
});
|
|
27373
|
+
if (route.messageKind === "notification") queueMicrotask(() => void server.close().catch(() => {}));
|
|
27374
|
+
return response;
|
|
27375
|
+
} catch (error) {
|
|
27376
|
+
if (error instanceof SdkError && error.code === SdkErrorCode.ConnectionClosed) return new Response(null, { status: 499 });
|
|
27377
|
+
await server.close().catch(() => {});
|
|
27378
|
+
inflight.delete(server);
|
|
27379
|
+
reportError(toError$1(error));
|
|
27380
|
+
return internalServerErrorResponse(echoableRequestId(route.message));
|
|
27381
|
+
}
|
|
27382
|
+
}
|
|
27383
|
+
async function serveLegacyRoute(route, forwardRequest, authInfo, parsedBody) {
|
|
27384
|
+
if (legacyHandler !== void 0) return legacyHandler(forwardRequest, {
|
|
27385
|
+
...authInfo !== void 0 && { authInfo },
|
|
27386
|
+
...parsedBody !== void 0 && { parsedBody }
|
|
27387
|
+
});
|
|
27388
|
+
const strict = modernOnlyStrictRejection(route, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
|
|
27389
|
+
if (strict === void 0) return new Response(null, { status: 202 });
|
|
27390
|
+
reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only endpoint (${strict.cell}): ${strict.message}`));
|
|
27391
|
+
return rejectionResponse(strict, echoableRequestId(parsedBody));
|
|
27392
|
+
}
|
|
27393
|
+
async function handle(request, requestOptions) {
|
|
27394
|
+
const authInfo = requestOptions?.authInfo;
|
|
27395
|
+
if (request.method.toUpperCase() === "POST" && !isJsonContentType(request.headers.get("content-type"))) {
|
|
27396
|
+
reportError(/* @__PURE__ */ new Error("Unsupported Media Type: Content-Type must be application/json"));
|
|
27397
|
+
return jsonRpcErrorResponse(415, -32e3, "Unsupported Media Type: Content-Type must be application/json");
|
|
27398
|
+
}
|
|
27399
|
+
const classified = await classifyEntryRequest(request, requestOptions?.parsedBody, true, maxRequestBodySize);
|
|
27400
|
+
if (classified.step === "unreadable-body") return jsonRpcErrorResponse(400, -32700, "Parse error: the request body could not be read");
|
|
27401
|
+
if (classified.step === "body-too-large") return jsonRpcErrorResponse(413, -32e3, requestBodyTooLargeMessage(maxRequestBodySize));
|
|
27402
|
+
if (classified.step === "no-json-body") {
|
|
27403
|
+
if (legacyHandler !== void 0) return legacyHandler(classified.forwardRequest, { ...authInfo !== void 0 && { authInfo } });
|
|
27404
|
+
return jsonRpcErrorResponse(400, -32700, "Parse error: the request body is not valid JSON");
|
|
27405
|
+
}
|
|
27406
|
+
const { outcome, body, parsedBody, forwardRequest } = classified;
|
|
27407
|
+
try {
|
|
27408
|
+
switch (outcome.kind) {
|
|
27409
|
+
case "reject":
|
|
27410
|
+
reportError(/* @__PURE__ */ new Error(`Rejected inbound request (${outcome.cell}): ${outcome.message}`));
|
|
27411
|
+
return rejectionResponse(outcome, echoableRequestId(body));
|
|
27412
|
+
case "modern": return await serveModern(outcome, request, authInfo);
|
|
27413
|
+
case "legacy": return await serveLegacyRoute(outcome, forwardRequest, authInfo, parsedBody);
|
|
27414
|
+
}
|
|
27415
|
+
} catch (error) {
|
|
27416
|
+
reportError(toError$1(error));
|
|
27417
|
+
return internalServerErrorResponse(echoableRequestId(body));
|
|
27418
|
+
}
|
|
27419
|
+
}
|
|
27420
|
+
const fetchFace = async (request, requestOptions) => {
|
|
27421
|
+
if (closed) throw new Error("This MCP handler has been closed");
|
|
27422
|
+
try {
|
|
27423
|
+
return await handle(request, requestOptions);
|
|
27424
|
+
} catch (error) {
|
|
27425
|
+
reportError(toError$1(error));
|
|
27426
|
+
return internalServerErrorResponse(echoableRequestId(requestOptions?.parsedBody));
|
|
27427
|
+
}
|
|
27428
|
+
};
|
|
27429
|
+
return {
|
|
27430
|
+
fetch: fetchFace,
|
|
27431
|
+
notify,
|
|
27432
|
+
bus,
|
|
27433
|
+
close: async () => {
|
|
27434
|
+
closed = true;
|
|
27435
|
+
listenRouter.closeAll();
|
|
27436
|
+
const closing = [...inflight].map((server) => server.close().catch(() => {}));
|
|
27437
|
+
inflight.clear();
|
|
27438
|
+
await Promise.all(closing);
|
|
27439
|
+
}
|
|
27440
|
+
};
|
|
27441
|
+
}
|
|
27442
|
+
const PREFIX = "v1.";
|
|
27443
|
+
function bytesToBase64Url(bytes) {
|
|
27444
|
+
let bin = "";
|
|
27445
|
+
for (const b of bytes) bin += String.fromCodePoint(b);
|
|
27446
|
+
return btoa(bin).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
|
|
27447
|
+
}
|
|
27448
|
+
function constantTimeTagEqual(a, b) {
|
|
27449
|
+
if (a.length !== b.length) return false;
|
|
27450
|
+
let r = 0;
|
|
27451
|
+
for (let i = 0; i < a.length; i++) r |= a.codePointAt(i) ^ b.codePointAt(i);
|
|
27452
|
+
return r === 0;
|
|
27453
|
+
}
|
|
27454
|
+
function base64UrlToBytes(s) {
|
|
27455
|
+
const b64 = s.replaceAll("-", "+").replaceAll("_", "/");
|
|
27456
|
+
const bin = atob(b64);
|
|
27457
|
+
const bytes = new Uint8Array(bin.length);
|
|
27458
|
+
for (let i = 0; i < bin.length; i++) bytes[i] = bin.codePointAt(i);
|
|
27459
|
+
return bytes;
|
|
27460
|
+
}
|
|
27461
|
+
/**
|
|
27462
|
+
* Create an opt-in HMAC-SHA256 codec for the multi-round-trip `requestState`
|
|
27463
|
+
* (protocol revision 2026-07-28).
|
|
27464
|
+
*
|
|
27465
|
+
* `requestState` round-trips through the client and is attacker-controlled
|
|
27466
|
+
* input on re-entry. The SDK applies no protection of its own; this helper is
|
|
27467
|
+
* the convenience implementation of the spec's integrity MUST so authors don't
|
|
27468
|
+
* hand-roll HMAC. Wire shape:
|
|
27469
|
+
*
|
|
27470
|
+
* "v1." b64url({"p":<payload>,"exp":<unixSeconds>,"b":<bindTag>?}) "." b64url(mac)
|
|
27471
|
+
*
|
|
27472
|
+
* where `bindTag` is `b64url(HMAC(key, "mcp.requestState.bind:" + bind(ctx))[:16])`
|
|
27473
|
+
* — the binding value is never embedded raw.
|
|
27474
|
+
*
|
|
27475
|
+
* The codec is **signed, not encrypted**: the body is integrity-protected but
|
|
27476
|
+
* the client can base64url-decode it and read the payload (`p`) in clear. Do
|
|
27477
|
+
* not put secrets in the payload; use an AEAD construction if confidentiality
|
|
27478
|
+
* is required. The handler reads its payload back via the typed
|
|
27479
|
+
* `ctx.mcpReq.requestState<T>()` accessor — the seam has already run `verify`
|
|
27480
|
+
* (integrity proven, payload decoded) by the time the handler is entered.
|
|
27481
|
+
*
|
|
27482
|
+
* Verification is fail-closed and constant-time (WebCrypto `subtle.verify` for
|
|
27483
|
+
* the body MAC; a fixed-length XOR-accumulator compare for the bind tag).
|
|
27484
|
+
* See `examples/mrtr/server.ts` for a worked end-to-end example.
|
|
27485
|
+
*
|
|
27486
|
+
* Design comparison (mcp.d `secureRequestState`, the peer SDK's reference
|
|
27487
|
+
* implementation): mcp.d additionally offers an AES-256-GCM encrypted mode and
|
|
27488
|
+
* derives independent cipher / bind-HMAC sub-keys from the operator secret via
|
|
27489
|
+
* HKDF-SHA256, with an auto-generated per-process ephemeral key when none is
|
|
27490
|
+
* supplied. This codec deliberately ships only the signed mode and a single
|
|
27491
|
+
* keyed HMAC (domain-separated by input prefix) — HKDF sub-key derivation and
|
|
27492
|
+
* an encrypted mode are intentionally out of scope for the initial release.
|
|
27493
|
+
*/
|
|
27494
|
+
function createRequestStateCodec$1(options) {
|
|
27495
|
+
const subtle = globalThis.crypto?.subtle;
|
|
27496
|
+
if (subtle === void 0) throw new TypeError("createRequestStateCodec requires the Web Crypto API (globalThis.crypto.subtle); see https://ts.sdk.modelcontextprotocol.io/v2/troubleshooting for the Node.js polyfill instructions");
|
|
27497
|
+
const keyBytes = typeof options.key === "string" ? new TextEncoder().encode(options.key) : Uint8Array.from(options.key);
|
|
27498
|
+
if (keyBytes.byteLength < 32) throw new RangeError(`createRequestStateCodec: key must be at least 32 bytes (got ${keyBytes.byteLength})`);
|
|
27499
|
+
const ttlSeconds = options.ttlSeconds ?? 600;
|
|
27500
|
+
if (!Number.isFinite(ttlSeconds)) throw new RangeError("createRequestStateCodec: ttlSeconds must be a finite number");
|
|
27501
|
+
const bind = options.bind;
|
|
27502
|
+
let cryptoKey;
|
|
27503
|
+
const importedKey = () => cryptoKey ??= subtle.importKey("raw", keyBytes, {
|
|
27504
|
+
name: "HMAC",
|
|
27505
|
+
hash: "SHA-256"
|
|
27506
|
+
}, false, ["sign", "verify"]);
|
|
27507
|
+
const utf8 = new TextEncoder();
|
|
27508
|
+
const BIND_LABEL = "mcp.requestState.bind:";
|
|
27509
|
+
const bindTag = async (value) => {
|
|
27510
|
+
return bytesToBase64Url(new Uint8Array(await subtle.sign("HMAC", await importedKey(), utf8.encode(BIND_LABEL + value))).slice(0, 16));
|
|
27511
|
+
};
|
|
27512
|
+
return {
|
|
27513
|
+
async mint(payload, ctx) {
|
|
27514
|
+
const envelope = {
|
|
27515
|
+
p: payload,
|
|
27516
|
+
exp: Math.floor(Date.now() / 1e3) + ttlSeconds
|
|
27517
|
+
};
|
|
27518
|
+
if (bind !== void 0) {
|
|
27519
|
+
if (ctx === void 0) throw new TypeError("createRequestStateCodec: mint() requires ctx when a bind callback is configured");
|
|
27520
|
+
envelope.b = await bindTag(bind(ctx));
|
|
27521
|
+
}
|
|
27522
|
+
const body = bytesToBase64Url(utf8.encode(JSON.stringify(envelope)));
|
|
27523
|
+
return `${PREFIX}${body}.${bytesToBase64Url(new Uint8Array(await subtle.sign("HMAC", await importedKey(), utf8.encode(PREFIX + body))))}`;
|
|
27524
|
+
},
|
|
27525
|
+
async verify(state, ctx) {
|
|
27526
|
+
const dot = state.lastIndexOf(".");
|
|
27527
|
+
if (!state.startsWith(PREFIX) || dot <= 3) throw new Error("malformed");
|
|
27528
|
+
const body = state.slice(3, dot);
|
|
27529
|
+
let macBytes;
|
|
27530
|
+
try {
|
|
27531
|
+
macBytes = base64UrlToBytes(state.slice(dot + 1));
|
|
27532
|
+
} catch {
|
|
27533
|
+
throw new Error("malformed");
|
|
27534
|
+
}
|
|
27535
|
+
if (!await subtle.verify("HMAC", await importedKey(), macBytes, utf8.encode(PREFIX + body))) throw new Error("mac");
|
|
27536
|
+
let envelope;
|
|
27537
|
+
try {
|
|
27538
|
+
envelope = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(base64UrlToBytes(body)));
|
|
27539
|
+
} catch {
|
|
27540
|
+
throw new Error("malformed");
|
|
27541
|
+
}
|
|
27542
|
+
if (typeof envelope.exp !== "number" || envelope.exp < Math.floor(Date.now() / 1e3)) throw new Error("expired");
|
|
27543
|
+
if (bind !== void 0) {
|
|
27544
|
+
const expected = await bindTag(bind(ctx));
|
|
27545
|
+
if (envelope.b === void 0 || !constantTimeTagEqual(envelope.b, expected)) throw new Error("bind");
|
|
27546
|
+
} else if (envelope.b !== void 0) throw new Error("bind");
|
|
27547
|
+
return envelope.p;
|
|
27548
|
+
}
|
|
27549
|
+
};
|
|
27550
|
+
}
|
|
27551
|
+
//#endregion
|
|
27552
|
+
//#region src/constants.ts
|
|
27553
|
+
const positiveIntEnv = (name, fallback) => {
|
|
27554
|
+
const raw = readEnv(name).value;
|
|
27555
|
+
if (raw === void 0 || raw.trim() === "") return fallback;
|
|
27556
|
+
const parsed = Number(raw);
|
|
27557
|
+
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
27558
|
+
};
|
|
27559
|
+
const CHARACTER_LIMIT = positiveIntEnv("RESPONSE_CHARACTER_LIMIT", 25e3);
|
|
27560
|
+
const ARTICLE_RESOURCES_SCAN_MAX_PAGES = positiveIntEnv("ARTICLE_RESOURCES_SCAN_MAX_PAGES", 20);
|
|
27561
|
+
const DEFAULT_CALLBACK_PORT = 27439;
|
|
27562
|
+
const MAX_ATTACHMENT_BYTES = positiveIntEnv("ATTACHMENT_MAX_BYTES", 5242880);
|
|
27563
|
+
const MAX_EMBEDDED_IMAGE_COUNT = positiveIntEnv("EMBEDDED_IMAGES_MAX", 10);
|
|
27564
|
+
const MESSAGE_CONTENT_BUDGET_BYTES = 10420224;
|
|
27565
|
+
const MAX_RESPONSE_BYTES = Math.min(positiveIntEnv("RESPONSE_MAX_BYTES", MESSAGE_CONTENT_BUDGET_BYTES), MESSAGE_CONTENT_BUDGET_BYTES);
|
|
27566
|
+
const MAX_BASE64_INPUT_CHARS = MESSAGE_CONTENT_BUDGET_BYTES;
|
|
27567
|
+
const MAX_BASE64_INPUT_MB = Number.parseFloat((MAX_BASE64_INPUT_CHARS / 4 * 3 / 1048576).toFixed(2));
|
|
27568
|
+
const MAX_COMMENT_PAGES = positiveIntEnv("COMMENT_MAX_PAGES", 10);
|
|
27569
|
+
const TICKET_FIELD_SCAN_MAX_PAGES = positiveIntEnv("TICKET_FIELD_SCAN_MAX_PAGES", 10);
|
|
27570
|
+
const REORDER_CONFIRM_THRESHOLD = positiveIntEnv("REORDER_CONFIRM_THRESHOLD", 20);
|
|
27571
|
+
const getBaseUrl = (subdomain) => `https://${subdomain}.zendesk.com/api/v2`;
|
|
27572
|
+
const getHelpCenterBaseUrl = (subdomain) => `https://${subdomain}.zendesk.com/api/v2/help_center`;
|
|
27573
|
+
const getOAuthUrls = (subdomain) => ({
|
|
27574
|
+
authorizeUrl: `https://${subdomain}.zendesk.com/oauth/authorizations/new`,
|
|
27575
|
+
tokenUrl: `https://${subdomain}.zendesk.com/oauth/tokens`
|
|
27576
|
+
});
|
|
27577
|
+
//#endregion
|
|
27578
|
+
//#region src/client/retry.ts
|
|
27579
|
+
const MAX_ATTEMPTS = 3;
|
|
27580
|
+
const BASE_DELAY_MS = 250;
|
|
27581
|
+
const MAX_DELAY_MS = 4e3;
|
|
27582
|
+
const REQUEST_TIMEOUT_MS = 3e4;
|
|
27583
|
+
const TRANSFER_TIMEOUT_MS = 12e4;
|
|
27584
|
+
/**
|
|
27585
|
+
* A deadline for one attempt. The abort surfaces as a `TimeoutError` whose `code`
|
|
27586
|
+
* is numeric, not a syscall string, so it classifies as `unknown`: a GET is
|
|
27587
|
+
* retried, a write is not — the request may have arrived before the deadline.
|
|
27588
|
+
*/
|
|
27589
|
+
const deadlineSignal = (timeoutMs) => AbortSignal.timeout(timeoutMs);
|
|
27590
|
+
const MAX_RETRY_AFTER_MS = 5e3;
|
|
27591
|
+
const POLICIES = {
|
|
27592
|
+
GET: {
|
|
27593
|
+
network: "any",
|
|
27594
|
+
serverErrors: true
|
|
27595
|
+
},
|
|
27596
|
+
DELETE: {
|
|
27597
|
+
network: "pre-send",
|
|
27598
|
+
serverErrors: false
|
|
27599
|
+
},
|
|
27600
|
+
POST: {
|
|
27601
|
+
network: "pre-send",
|
|
27602
|
+
serverErrors: false
|
|
27603
|
+
},
|
|
27604
|
+
PUT: {
|
|
27605
|
+
network: "pre-send",
|
|
27606
|
+
serverErrors: false
|
|
27607
|
+
}
|
|
27608
|
+
};
|
|
27609
|
+
const DELAY_SECONDS = /^\d+$/;
|
|
27610
|
+
const HTTP_DATE_START = /^[A-Za-z]/;
|
|
27611
|
+
const NON_ASCII = /[^ -~]/g;
|
|
27612
|
+
const TOKEN_SEGMENT = /\/token\/[^/]+/;
|
|
27613
|
+
const URL_IN_TEXT = /[a-z][a-z0-9+.-]*:\/\/\S+/gi;
|
|
27614
|
+
const PRE_SEND_CODES = /* @__PURE__ */ new Set([
|
|
27615
|
+
"ENOTFOUND",
|
|
27616
|
+
"EAI_AGAIN",
|
|
27617
|
+
"ECONNREFUSED",
|
|
27618
|
+
"UND_ERR_CONNECT_TIMEOUT"
|
|
27619
|
+
]);
|
|
27620
|
+
const defaultRetryDeps = {
|
|
27621
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
27622
|
+
random: Math.random
|
|
27623
|
+
};
|
|
27624
|
+
/** Non-ASCII bytes break `node:http` headers, so client error text stays ASCII. */
|
|
27625
|
+
const toAscii = (text) => text.replace(NON_ASCII, "?");
|
|
27626
|
+
/**
|
|
27627
|
+
* Identifies the request without leaking a credential: uploads carry an upload
|
|
27628
|
+
* token in the query string, and an attachment `content_url` carries a download
|
|
27629
|
+
* token as a path segment (`/attachments/token/<token>/`).
|
|
27630
|
+
*/
|
|
27631
|
+
const describeTarget = (url) => {
|
|
27632
|
+
const { origin, pathname } = new URL(url);
|
|
27633
|
+
return `${origin}${pathname.replace(TOKEN_SEGMENT, "/token/[redacted]")}`;
|
|
27634
|
+
};
|
|
27635
|
+
/** First `code` in the cause chain, inspecting 5 levels so a cycle cannot hang. */
|
|
27636
|
+
const errorCode = (err) => {
|
|
27637
|
+
let current = err;
|
|
27638
|
+
for (let depth = 0; depth < 5 && current !== null && typeof current === "object"; depth += 1) {
|
|
27639
|
+
const { code, cause } = current;
|
|
27640
|
+
if (typeof code === "string") return code;
|
|
27641
|
+
current = cause;
|
|
27642
|
+
}
|
|
27643
|
+
};
|
|
27644
|
+
const classifyNetworkError = (err) => PRE_SEND_CODES.has(errorCode(err)) ? "pre-send" : "unknown";
|
|
27645
|
+
const UNINFORMATIVE_NAMES = /* @__PURE__ */ new Set(["Error", "TypeError"]);
|
|
27646
|
+
/**
|
|
27647
|
+
* The client refuses to replay a write that may have landed — but the caller is
|
|
27648
|
+
* an LLM whose reflex on a failed tool call is to try again, which would undo
|
|
27649
|
+
* that. So a failed write says whether it may have taken effect. ASCII only,
|
|
27650
|
+
* same rule as the rest of the message.
|
|
27651
|
+
*/
|
|
27652
|
+
const MAY_HAVE_APPLIED_NOTE = "The write may already have been applied. Check the current state before retrying, or you may duplicate it.";
|
|
27653
|
+
const NEVER_SENT_NOTE = "The request never reached Zendesk, so nothing was applied. Retrying is safe.";
|
|
27654
|
+
const REFUSED_NOTE = "Zendesk refused the request, so nothing was applied. Retrying is safe.";
|
|
27655
|
+
/** Only a write can be duplicated by a replay, so only a write carries a note. */
|
|
27656
|
+
const writeNote = (method, note) => method === "GET" ? "" : ` ${note}`;
|
|
27657
|
+
/**
|
|
27658
|
+
* A cause message can quote the URL it failed on — Node's `fetch` refuses a URL
|
|
27659
|
+
* carrying credentials by printing the whole thing, query string included — which
|
|
27660
|
+
* would smuggle back exactly what `describeTarget` drops. Same treatment for both,
|
|
27661
|
+
* so one function owns what a URL may look like in our output.
|
|
27662
|
+
*/
|
|
27663
|
+
const redactUrls = (text) => text.replace(URL_IN_TEXT, (found) => {
|
|
27664
|
+
try {
|
|
27665
|
+
return new URL(found).origin === "null" ? "[url]" : describeTarget(found);
|
|
27666
|
+
} catch {
|
|
27667
|
+
return "[url]";
|
|
27668
|
+
}
|
|
27669
|
+
});
|
|
27670
|
+
const failureDetail = (cause) => {
|
|
27671
|
+
if (!(cause instanceof Error)) return redactUrls(String(cause));
|
|
27672
|
+
const message = redactUrls(cause.message);
|
|
27673
|
+
const code = errorCode(cause);
|
|
27674
|
+
if (code !== void 0) return `${code}: ${message}`;
|
|
27675
|
+
return UNINFORMATIVE_NAMES.has(cause.name) ? message : `${cause.name}: ${message}`;
|
|
27676
|
+
};
|
|
27677
|
+
const networkErrorMessage = (method, target, attempts, cause) => {
|
|
27678
|
+
const base = `Network error on ${method} ${target} after ${attempts === 1 ? "1 attempt" : `${attempts} attempts`}: ${failureDetail(cause)}`;
|
|
27679
|
+
const note = classifyNetworkError(cause) === "pre-send" ? NEVER_SENT_NOTE : MAY_HAVE_APPLIED_NOTE;
|
|
27680
|
+
return toAscii(method === "GET" ? base : `${base}.${writeNote(method, note)}`);
|
|
27681
|
+
};
|
|
27682
|
+
const createZendeskNetworkError = (method, target, attempts, cause) => Object.assign(new Error(networkErrorMessage(method, target, attempts, cause), { cause }), {
|
|
27683
|
+
name: "ZendeskNetworkError",
|
|
27684
|
+
method,
|
|
27685
|
+
target,
|
|
27686
|
+
attempts
|
|
27687
|
+
});
|
|
27688
|
+
/** Exponential backoff with equal jitter: half the window fixed, half random. */
|
|
27689
|
+
const computeBackoffMs = (attempt, random) => {
|
|
27690
|
+
const window = Math.min(BASE_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS);
|
|
27691
|
+
return Math.round(window / 2 + window * random() / 2);
|
|
27692
|
+
};
|
|
27693
|
+
/** `Retry-After` in ms — delay-seconds or HTTP-date. */
|
|
27694
|
+
const parseRetryAfter = (header, now = Date.now()) => {
|
|
27695
|
+
if (header === null) return void 0;
|
|
27696
|
+
const value = header.trim();
|
|
27697
|
+
if (DELAY_SECONDS.test(value)) return Number(value) * 1e3;
|
|
27698
|
+
if (!HTTP_DATE_START.test(value)) return void 0;
|
|
27699
|
+
const date = Date.parse(value);
|
|
27700
|
+
if (Number.isNaN(date)) return void 0;
|
|
27701
|
+
return Math.max(0, date - now);
|
|
27702
|
+
};
|
|
27703
|
+
/**
|
|
27704
|
+
* How long to wait before replaying this response, or undefined to accept it.
|
|
27705
|
+
* `Retry-After` wins over backoff wherever it appears — Zendesk sends it on a 503
|
|
27706
|
+
* during maintenance as well as on a 429 — and a value past the cap means the
|
|
27707
|
+
* response is surfaced rather than parking the call for that long.
|
|
27708
|
+
*/
|
|
27709
|
+
const retryDelayFor = (response, policy, attempt, random) => {
|
|
27710
|
+
if (!(response.status === 429 || response.status >= 500 && policy.serverErrors)) return void 0;
|
|
27711
|
+
const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
|
|
27712
|
+
if (retryAfter === void 0) return computeBackoffMs(attempt, random);
|
|
27713
|
+
return retryAfter > MAX_RETRY_AFTER_MS ? void 0 : retryAfter;
|
|
27714
|
+
};
|
|
27715
|
+
/**
|
|
27716
|
+
* Runs `attempt` until it succeeds, hits a terminal outcome, or spends the
|
|
27717
|
+
* attempt budget. Returns the last response for the caller to inspect (a
|
|
27718
|
+
* non-ok status is still the caller's to turn into a `ZendeskApiError`), and
|
|
27719
|
+
* throws `ZendeskNetworkError` when no response was ever obtained.
|
|
27720
|
+
*/
|
|
27721
|
+
const fetchWithRetry = async (attempt, method, target, deps = defaultRetryDeps) => {
|
|
27722
|
+
const policy = POLICIES[method];
|
|
27723
|
+
for (let tries = 1;; tries += 1) {
|
|
27724
|
+
const last = tries >= MAX_ATTEMPTS;
|
|
27725
|
+
let response;
|
|
27726
|
+
try {
|
|
27727
|
+
response = await attempt();
|
|
27728
|
+
} catch (err) {
|
|
27729
|
+
const replayable = policy.network === "any" || classifyNetworkError(err) === policy.network;
|
|
27730
|
+
if (last || !replayable) throw createZendeskNetworkError(method, target, tries, err);
|
|
27731
|
+
await deps.sleep(computeBackoffMs(tries, deps.random));
|
|
27732
|
+
continue;
|
|
27733
|
+
}
|
|
27734
|
+
if (last) return response;
|
|
27735
|
+
const delayMs = retryDelayFor(response, policy, tries, deps.random);
|
|
27736
|
+
if (delayMs === void 0) return response;
|
|
27737
|
+
await response.text().catch(() => void 0);
|
|
27738
|
+
await deps.sleep(delayMs);
|
|
27739
|
+
}
|
|
27740
|
+
};
|
|
27741
|
+
//#endregion
|
|
27742
|
+
//#region src/client/zendesk-api.ts
|
|
27743
|
+
var ZendeskApiError = class ZendeskApiError extends Error {
|
|
27744
|
+
status;
|
|
27745
|
+
statusText;
|
|
27746
|
+
body;
|
|
27747
|
+
method;
|
|
27748
|
+
constructor(status, statusText, body, method) {
|
|
27749
|
+
super(ZendeskApiError.buildMessage(status, statusText, body, method));
|
|
27750
|
+
this.status = status;
|
|
27751
|
+
this.statusText = statusText;
|
|
27752
|
+
this.body = body;
|
|
27753
|
+
this.method = method;
|
|
27754
|
+
this.name = "ZendeskApiError";
|
|
27755
|
+
}
|
|
27756
|
+
static buildMessage(status, statusText, body, method) {
|
|
27757
|
+
switch (status) {
|
|
27758
|
+
case 401: return "Authentication failed. Your Zendesk token may be expired or invalid. Re-authenticate to get a new token.";
|
|
27759
|
+
case 403: return "Permission denied. Your Zendesk account does not have access to this resource.";
|
|
27760
|
+
case 404: return `Resource not found. Please verify the ID is correct. (${statusText})`;
|
|
27761
|
+
case 422: return `Validation error: ${body}`;
|
|
27762
|
+
case 429: return `Rate limit exceeded. Please wait before making more requests.${writeNote(method, REFUSED_NOTE)}`;
|
|
27763
|
+
default: {
|
|
27764
|
+
const message = `Zendesk API error ${status}: ${statusText}. ${body}`;
|
|
27765
|
+
return status >= 500 ? `${message}${writeNote(method, MAY_HAVE_APPLIED_NOTE)}` : message;
|
|
27766
|
+
}
|
|
27767
|
+
}
|
|
27768
|
+
}
|
|
27769
|
+
};
|
|
27770
|
+
const buildAuthHeader = (token) => `Bearer ${token}`;
|
|
27771
|
+
const buildUrl = (base, path, params) => {
|
|
27772
|
+
const url = new URL(`${base}${path}`);
|
|
27773
|
+
if (params) for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
|
|
27774
|
+
return url.toString();
|
|
27775
|
+
};
|
|
27776
|
+
const performFetch = (method, url, init, timeoutMs = REQUEST_TIMEOUT_MS) => fetchWithRetry(() => fetch(url, {
|
|
27777
|
+
...init,
|
|
27778
|
+
method,
|
|
27779
|
+
signal: deadlineSignal(timeoutMs)
|
|
27780
|
+
}), method, describeTarget(url));
|
|
27781
|
+
const executeRequest = async (url, token, options = {}) => {
|
|
27782
|
+
const { method = "GET", body } = options;
|
|
27783
|
+
const headers = {
|
|
27784
|
+
Authorization: buildAuthHeader(token),
|
|
27785
|
+
Accept: "application/json"
|
|
27786
|
+
};
|
|
27787
|
+
if (body) headers["Content-Type"] = "application/json";
|
|
27788
|
+
const init = { headers };
|
|
27789
|
+
if (body) init.body = JSON.stringify(body);
|
|
27790
|
+
const response = await performFetch(method, url, init);
|
|
27791
|
+
if (!response.ok) {
|
|
27792
|
+
const responseBody = await response.text();
|
|
27793
|
+
throw new ZendeskApiError(response.status, response.statusText, responseBody, method);
|
|
27794
|
+
}
|
|
27795
|
+
if (response.status === 204) return {};
|
|
27796
|
+
return response.json();
|
|
27797
|
+
};
|
|
27798
|
+
const zendeskGet = (subdomain, token, path, params) => {
|
|
27799
|
+
const url = buildUrl(getBaseUrl(subdomain), path, params);
|
|
27800
|
+
return executeRequest(url, token);
|
|
27801
|
+
};
|
|
27802
|
+
const zendeskPost = (subdomain, token, path, body) => {
|
|
27803
|
+
const url = buildUrl(getBaseUrl(subdomain), path);
|
|
27804
|
+
return executeRequest(url, token, {
|
|
27805
|
+
method: "POST",
|
|
27806
|
+
body
|
|
27807
|
+
});
|
|
27808
|
+
};
|
|
27809
|
+
const zendeskPut = (subdomain, token, path, body) => {
|
|
27810
|
+
const url = buildUrl(getBaseUrl(subdomain), path);
|
|
27811
|
+
return executeRequest(url, token, {
|
|
27812
|
+
method: "PUT",
|
|
27813
|
+
body
|
|
27814
|
+
});
|
|
27815
|
+
};
|
|
27816
|
+
const helpCenterGet = (subdomain, token, path, params) => {
|
|
27817
|
+
const url = buildUrl(getHelpCenterBaseUrl(subdomain), path, params);
|
|
27818
|
+
return executeRequest(url, token);
|
|
27819
|
+
};
|
|
27820
|
+
const helpCenterPost = (subdomain, token, path, body) => {
|
|
27821
|
+
const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
|
|
27822
|
+
return executeRequest(url, token, {
|
|
27823
|
+
method: "POST",
|
|
27824
|
+
body
|
|
27825
|
+
});
|
|
27826
|
+
};
|
|
27827
|
+
const helpCenterPut = (subdomain, token, path, body) => {
|
|
27828
|
+
const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
|
|
27829
|
+
return executeRequest(url, token, {
|
|
27830
|
+
method: "PUT",
|
|
27831
|
+
body
|
|
27832
|
+
});
|
|
27833
|
+
};
|
|
27834
|
+
const helpCenterDelete = (subdomain, token, path) => {
|
|
27835
|
+
const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
|
|
27836
|
+
return executeRequest(url, token, { method: "DELETE" });
|
|
27837
|
+
};
|
|
27838
|
+
const fetchZendeskBinary = async (subdomain, token, contentUrl) => {
|
|
27839
|
+
const expectedHost = `${subdomain}.zendesk.com`;
|
|
27840
|
+
const headers = {};
|
|
27841
|
+
if (new URL(contentUrl).hostname === expectedHost) headers["Authorization"] = buildAuthHeader(token);
|
|
27842
|
+
const response = await performFetch("GET", contentUrl, { headers }, TRANSFER_TIMEOUT_MS);
|
|
27843
|
+
if (!response.ok) {
|
|
27844
|
+
const body = await response.text();
|
|
27845
|
+
throw new ZendeskApiError(response.status, response.statusText, body, "GET");
|
|
27846
|
+
}
|
|
27847
|
+
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
|
|
27848
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
27849
|
+
return {
|
|
27850
|
+
data: Buffer.from(arrayBuffer),
|
|
27851
|
+
contentType
|
|
27852
|
+
};
|
|
27853
|
+
};
|
|
27854
|
+
const zendeskUpload = async (subdomain, token, filename, data, contentType, uploadToken) => {
|
|
27855
|
+
const params = { filename };
|
|
27856
|
+
if (uploadToken) params["token"] = uploadToken;
|
|
27857
|
+
const url = buildUrl(getBaseUrl(subdomain), "/uploads", params);
|
|
27858
|
+
const response = await performFetch("POST", url, {
|
|
27859
|
+
headers: {
|
|
27860
|
+
Authorization: buildAuthHeader(token),
|
|
27861
|
+
"Content-Type": contentType
|
|
27862
|
+
},
|
|
27863
|
+
body: data
|
|
27864
|
+
}, TRANSFER_TIMEOUT_MS);
|
|
27865
|
+
if (!response.ok) {
|
|
27866
|
+
const responseBody = await response.text();
|
|
27867
|
+
throw new ZendeskApiError(response.status, response.statusText, responseBody, "POST");
|
|
27868
|
+
}
|
|
27869
|
+
return response.json();
|
|
27870
|
+
};
|
|
27871
|
+
const helpCenterUpload = async (subdomain, token, path, formData) => {
|
|
27872
|
+
const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
|
|
27873
|
+
const response = await performFetch("POST", url, {
|
|
27874
|
+
headers: { Authorization: buildAuthHeader(token) },
|
|
27875
|
+
body: formData
|
|
27876
|
+
}, TRANSFER_TIMEOUT_MS);
|
|
27877
|
+
if (!response.ok) {
|
|
27878
|
+
const responseBody = await response.text();
|
|
27879
|
+
throw new ZendeskApiError(response.status, response.statusText, responseBody, "POST");
|
|
27880
|
+
}
|
|
27881
|
+
return response.json();
|
|
27882
|
+
};
|
|
27883
|
+
//#endregion
|
|
27884
|
+
//#region src/client/brands.ts
|
|
27885
|
+
const BRAND_SUBDOMAIN = /^[a-z0-9][a-z0-9-]*$/;
|
|
27886
|
+
/**
|
|
27887
|
+
* Fetch every brand of the account (cursor-paginated). `page[size]` is sent on
|
|
27888
|
+
* the FIRST request too: without it Zendesk falls back to OFFSET pagination,
|
|
27889
|
+
* which answers without `meta.after_cursor`, and the loop below would stop
|
|
27890
|
+
* after page 1 — a brand past the first 100 would resolve as Unknown.
|
|
27891
|
+
*/
|
|
27892
|
+
const fetchAllBrands = async (subdomain, token) => {
|
|
27893
|
+
const brands = [];
|
|
27894
|
+
let cursor;
|
|
27895
|
+
do {
|
|
27896
|
+
const response = await zendeskGet(subdomain, token, "/brands", {
|
|
27897
|
+
"page[size]": String(100),
|
|
27898
|
+
...cursor ? { "page[after]": cursor } : {}
|
|
27899
|
+
});
|
|
27900
|
+
brands.push(...response.brands ?? []);
|
|
27901
|
+
const meta = response.meta;
|
|
27902
|
+
if (meta?.has_more) {
|
|
27903
|
+
if (!meta.after_cursor || meta.after_cursor === cursor) throw new Error("GET /brands reported more pages but returned no usable after_cursor (missing or identical to the cursor just sent). Cannot continue pagination safely.");
|
|
27904
|
+
cursor = meta.after_cursor;
|
|
27905
|
+
} else cursor = void 0;
|
|
27906
|
+
} while (cursor);
|
|
27907
|
+
return brands;
|
|
27908
|
+
};
|
|
27909
|
+
const createBrandSubdomainResolver = (subdomain, getToken) => {
|
|
27910
|
+
let cached;
|
|
27911
|
+
const brands = () => {
|
|
27912
|
+
if (!cached || Date.now() - cached.at >= 3e5) {
|
|
27913
|
+
const promise = (async () => fetchAllBrands(subdomain, await getToken()))();
|
|
27914
|
+
cached = {
|
|
27915
|
+
at: Date.now(),
|
|
27916
|
+
promise
|
|
27917
|
+
};
|
|
27918
|
+
promise.catch(() => {
|
|
27919
|
+
cached = void 0;
|
|
27920
|
+
});
|
|
27921
|
+
}
|
|
27922
|
+
return cached.promise;
|
|
27923
|
+
};
|
|
27924
|
+
return async (idOrSubdomain) => {
|
|
27925
|
+
const all = await brands();
|
|
27926
|
+
const brand = all.find((b) => String(b.id) === idOrSubdomain) ?? all.find((b) => b.subdomain.toLowerCase() === idOrSubdomain.toLowerCase());
|
|
27927
|
+
if (!brand) throw new Error(`Unknown brand "${idOrSubdomain}": no brand with that id or subdomain exists on this account; call list_brands to see the available brands.`);
|
|
27928
|
+
if (!BRAND_SUBDOMAIN.test(brand.subdomain)) throw new Error(`Brand ${brand.id} has an unexpected subdomain; refusing to send a request to it.`);
|
|
27929
|
+
return brand.subdomain;
|
|
27930
|
+
};
|
|
27931
|
+
};
|
|
25937
27932
|
//#endregion
|
|
25938
27933
|
//#region node_modules/.pnpm/cheerio@1.2.0/node_modules/cheerio/dist/esm/options.js
|
|
25939
27934
|
const defaultOpts$2 = { _useHtmlParser2: false };
|
|
@@ -103710,7 +105705,6 @@ const countWords = (text) => {
|
|
|
103710
105705
|
return trimmed.split(WHITESPACE_RUN).length;
|
|
103711
105706
|
};
|
|
103712
105707
|
const textOf = (html) => {
|
|
103713
|
-
if (!html) return "";
|
|
103714
105708
|
return load(`<div>${html}</div>`, null, false)("div").first().text();
|
|
103715
105709
|
};
|
|
103716
105710
|
const parseSections = (html) => {
|
|
@@ -103826,14 +105820,8 @@ const htmlToMdProcessor = unified().use(rehypeParse, { fragment: true }).use(reh
|
|
|
103826
105820
|
emphasis: "_"
|
|
103827
105821
|
});
|
|
103828
105822
|
const mdToHtmlProcessor = unified().use(remarkParse).use(remarkGfm).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw).use(rehypeStringify);
|
|
103829
|
-
const htmlToMarkdown = (html) =>
|
|
103830
|
-
|
|
103831
|
-
return String(htmlToMdProcessor.processSync(html));
|
|
103832
|
-
};
|
|
103833
|
-
const markdownToHtml = (markdown) => {
|
|
103834
|
-
if (!markdown) return "";
|
|
103835
|
-
return String(mdToHtmlProcessor.processSync(markdown));
|
|
103836
|
-
};
|
|
105823
|
+
const htmlToMarkdown = (html) => String(htmlToMdProcessor.processSync(html));
|
|
105824
|
+
const markdownToHtml = (markdown) => String(mdToHtmlProcessor.processSync(markdown));
|
|
103837
105825
|
//#endregion
|
|
103838
105826
|
//#region src/utils/formatting.ts
|
|
103839
105827
|
const truncateIfNeeded = (text, advice = "Use pagination or filters to reduce results.") => {
|
|
@@ -107808,12 +109796,6 @@ const SEVERITY = {
|
|
|
107808
109796
|
warn: 2,
|
|
107809
109797
|
error: 3
|
|
107810
109798
|
};
|
|
107811
|
-
const MCP_LEVEL = {
|
|
107812
|
-
debug: "debug",
|
|
107813
|
-
info: "info",
|
|
107814
|
-
warn: "warning",
|
|
107815
|
-
error: "error"
|
|
107816
|
-
};
|
|
107817
109799
|
const REDACTED_KEYS = /* @__PURE__ */ new Set([
|
|
107818
109800
|
"token",
|
|
107819
109801
|
"accesstoken",
|
|
@@ -107862,37 +109844,21 @@ const silentLogger = {
|
|
|
107862
109844
|
debug: noop,
|
|
107863
109845
|
info: noop,
|
|
107864
109846
|
warn: noop,
|
|
107865
|
-
error: noop
|
|
107866
|
-
attachServer: noop
|
|
109847
|
+
error: noop
|
|
107867
109848
|
};
|
|
107868
109849
|
const createLogger = (level) => {
|
|
107869
109850
|
const min = SEVERITY[level];
|
|
107870
|
-
let server;
|
|
107871
109851
|
const emit = (lvl, event, fields) => {
|
|
107872
109852
|
if (SEVERITY[lvl] < min) return;
|
|
107873
|
-
const safe = sanitise(fields);
|
|
107874
109853
|
try {
|
|
107875
|
-
console.error(formatLine(lvl, event,
|
|
107876
|
-
} catch {}
|
|
107877
|
-
if (server) try {
|
|
107878
|
-
server.sendLoggingMessage({
|
|
107879
|
-
level: MCP_LEVEL[lvl],
|
|
107880
|
-
logger: "zendesk-mcp-server",
|
|
107881
|
-
data: {
|
|
107882
|
-
...safe,
|
|
107883
|
-
event
|
|
107884
|
-
}
|
|
107885
|
-
}).catch(noop);
|
|
109854
|
+
console.error(formatLine(lvl, event, sanitise(fields)));
|
|
107886
109855
|
} catch {}
|
|
107887
109856
|
};
|
|
107888
109857
|
return {
|
|
107889
109858
|
debug: (event, fields) => emit("debug", event, fields),
|
|
107890
109859
|
info: (event, fields) => emit("info", event, fields),
|
|
107891
109860
|
warn: (event, fields) => emit("warn", event, fields),
|
|
107892
|
-
error: (event, fields) => emit("error", event, fields)
|
|
107893
|
-
attachServer: (s) => {
|
|
107894
|
-
server = s;
|
|
107895
|
-
}
|
|
109861
|
+
error: (event, fields) => emit("error", event, fields)
|
|
107896
109862
|
};
|
|
107897
109863
|
};
|
|
107898
109864
|
//#endregion
|
|
@@ -107915,16 +109881,13 @@ const readPackageInfo = () => {
|
|
|
107915
109881
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
107916
109882
|
for (let depth = 0; depth < 8; depth++) {
|
|
107917
109883
|
try {
|
|
107918
|
-
const
|
|
107919
|
-
if (
|
|
107920
|
-
|
|
107921
|
-
|
|
107922
|
-
|
|
107923
|
-
|
|
107924
|
-
|
|
107925
|
-
};
|
|
107926
|
-
return cached;
|
|
107927
|
-
}
|
|
109884
|
+
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
109885
|
+
if (typeof pkg?.name === "string" && typeof pkg.version === "string") {
|
|
109886
|
+
cached = {
|
|
109887
|
+
name: pkg.name,
|
|
109888
|
+
version: pkg.version
|
|
109889
|
+
};
|
|
109890
|
+
return cached;
|
|
107928
109891
|
}
|
|
107929
109892
|
} catch {}
|
|
107930
109893
|
const parent = dirname(dir);
|
|
@@ -107935,6 +109898,19 @@ const readPackageInfo = () => {
|
|
|
107935
109898
|
return cached;
|
|
107936
109899
|
};
|
|
107937
109900
|
//#endregion
|
|
109901
|
+
//#region src/utils/request-state.ts
|
|
109902
|
+
/**
|
|
109903
|
+
* `requestState` round-trips through the client, so it comes back as
|
|
109904
|
+
* attacker-controlled input: the spec requires integrity protection and binding
|
|
109905
|
+
* to the user. Every server verifies it with this codec (`requestState.verify`)
|
|
109906
|
+
* and handlers mint state only through it. Bound to the method and the
|
|
109907
|
+
* principal, so state minted for one grant or one method is refused elsewhere.
|
|
109908
|
+
*/
|
|
109909
|
+
const createRequestStateCodec = ({ key = randomBytes(32), principal = () => "" }) => createRequestStateCodec$1({
|
|
109910
|
+
key,
|
|
109911
|
+
bind: (ctx) => `${ctx.mcpReq.method}\0${principal()}`
|
|
109912
|
+
});
|
|
109913
|
+
//#endregion
|
|
107938
109914
|
//#region src/server.ts
|
|
107939
109915
|
/**
|
|
107940
109916
|
* Invoke a tool handler, notifying `onUnauthorized` when Zendesk rejects the
|
|
@@ -107997,24 +109973,29 @@ const registerProxyTool = (server, toolName, title, tools, readOnlyMode, onUnaut
|
|
|
107997
109973
|
}, async (args) => dispatch(args));
|
|
107998
109974
|
};
|
|
107999
109975
|
/**
|
|
108000
|
-
* Build the bare `McpServer` — identity,
|
|
108001
|
-
*
|
|
108002
|
-
*
|
|
108003
|
-
*
|
|
108004
|
-
* underneath it via `registerToolset`.
|
|
109976
|
+
* Build the bare `McpServer` — identity, `instructions` — with no tools or
|
|
109977
|
+
* resources registered yet. Split out from `createMcpServer` so the dev reload
|
|
109978
|
+
* (`dev/reload.ts`) can keep this long-lived shell (and its transport/session)
|
|
109979
|
+
* alive while swapping the toolset underneath it via `registerToolset`.
|
|
108005
109980
|
*/
|
|
108006
|
-
const createServerShell = (config,
|
|
109981
|
+
const createServerShell = (config, requestStateCodec = createRequestStateCodec({})) => {
|
|
108007
109982
|
const pkg = readPackageInfo();
|
|
108008
109983
|
const instructions = buildInstructions(config);
|
|
108009
|
-
|
|
109984
|
+
return new McpServer({
|
|
108010
109985
|
name: pkg.name,
|
|
108011
109986
|
version: pkg.version
|
|
108012
109987
|
}, {
|
|
108013
|
-
|
|
108014
|
-
|
|
109988
|
+
...instructions ? { instructions } : {},
|
|
109989
|
+
requestState: { verify: (state, ctx) => requestStateCodec.verify(state, ctx) }
|
|
108015
109990
|
});
|
|
108016
|
-
|
|
108017
|
-
|
|
109991
|
+
};
|
|
109992
|
+
const createServerCaches = (config, getToken, onUnauthorized) => {
|
|
109993
|
+
const resolveBrandSubdomain = createBrandSubdomainResolver(config.subdomain, getToken);
|
|
109994
|
+
return {
|
|
109995
|
+
resolveBrandSubdomain,
|
|
109996
|
+
topology: helpCenterContextEnabled(config) ? createTopologyProvider(getToken, config.subdomain, onUnauthorized, config.brandIds, firstBrandSubdomainResolver(config, resolveBrandSubdomain)) : void 0,
|
|
109997
|
+
articles: articleResourceEnabled(config) ? createArticleResourcesProvider(getToken, config.subdomain, onUnauthorized, resourceBrandResolver(config, resolveBrandSubdomain), config.brandIds) : void 0
|
|
109998
|
+
};
|
|
108018
109999
|
};
|
|
108019
110000
|
/**
|
|
108020
110001
|
* Lazily resolve the Help Center subdomain the topology resource pins to: the
|
|
@@ -108051,8 +110032,8 @@ const resourceBrandResolver = (config, resolveBrandSubdomain) => {
|
|
|
108051
110032
|
* without dropping the transport. `tools` is passed in (not built here) so the
|
|
108052
110033
|
* reload path can hand over freshly re-imported definitions.
|
|
108053
110034
|
*/
|
|
108054
|
-
const registerToolset = (server, { config, getToken,
|
|
108055
|
-
const
|
|
110035
|
+
const registerToolset = (server, { config, getToken, caches, onUnauthorized, logger = silentLogger }, tools) => {
|
|
110036
|
+
const { topology, articles } = caches ?? createServerCaches(config, getToken, onUnauthorized);
|
|
108056
110037
|
const registered = [];
|
|
108057
110038
|
const dispose = () => {
|
|
108058
110039
|
for (const handle of registered) handle.remove();
|
|
@@ -108088,20 +110069,16 @@ const registerToolset = (server, { config, getToken, resolveBrandSubdomain, onUn
|
|
|
108088
110069
|
throw new Error(`Unsupported tool mode: ${String(unhandled)}`);
|
|
108089
110070
|
}
|
|
108090
110071
|
}
|
|
108091
|
-
if (
|
|
108092
|
-
|
|
108093
|
-
|
|
108094
|
-
|
|
108095
|
-
|
|
108096
|
-
|
|
108097
|
-
|
|
108098
|
-
|
|
108099
|
-
|
|
108100
|
-
|
|
108101
|
-
}] })));
|
|
108102
|
-
}
|
|
108103
|
-
if (articleResourceEnabled(config)) {
|
|
108104
|
-
const articles = createArticleResourcesProvider(getToken, config.subdomain, onUnauthorized, resourceBrandResolver(config, sharedResolver), config.brandIds);
|
|
110072
|
+
if (topology) registered.push(server.registerResource("help-center-topology", topologyResourceUri(config), {
|
|
110073
|
+
title: "Zendesk Help Center topology",
|
|
110074
|
+
description: "Active locales, category → section tree, visibility segments, permission groups, and your role. Useful context when creating or editing content; admin-only sections (permission groups, user segments) are marked unavailable rather than empty when your role lacks Guide-admin rights.",
|
|
110075
|
+
mimeType: "text/markdown"
|
|
110076
|
+
}, async (uri) => ({ contents: [{
|
|
110077
|
+
uri: uri.toString(),
|
|
110078
|
+
mimeType: "text/markdown",
|
|
110079
|
+
text: await topology.read()
|
|
110080
|
+
}] })));
|
|
110081
|
+
if (articles) {
|
|
108105
110082
|
const listPromotedEnabled = promotedArticlesEnabled(config);
|
|
108106
110083
|
const template = new ResourceTemplate(articleResourceUriTemplate(config), { list: async () => {
|
|
108107
110084
|
if (!listPromotedEnabled) return { resources: [] };
|
|
@@ -108144,7 +110121,7 @@ const registerToolset = (server, { config, getToken, resolveBrandSubdomain, onUn
|
|
|
108144
110121
|
dispose();
|
|
108145
110122
|
throw err;
|
|
108146
110123
|
}
|
|
108147
|
-
logger.
|
|
110124
|
+
logger.debug("tools_registered", {
|
|
108148
110125
|
count: filteredTools.length,
|
|
108149
110126
|
mode: config.mode
|
|
108150
110127
|
});
|
|
@@ -108153,24 +110130,27 @@ const registerToolset = (server, { config, getToken, resolveBrandSubdomain, onUn
|
|
|
108153
110130
|
dispose
|
|
108154
110131
|
};
|
|
108155
110132
|
};
|
|
108156
|
-
const createMcpServer = (config, getToken, logger = silentLogger, onUnauthorized) => {
|
|
108157
|
-
const server = createServerShell(config,
|
|
108158
|
-
const resolveBrandSubdomain = createBrandSubdomainResolver(config.subdomain, getToken);
|
|
110133
|
+
const createMcpServer = (config, getToken, logger = silentLogger, onUnauthorized, { caches = createServerCaches(config, getToken, onUnauthorized), requestStateCodec } = {}) => {
|
|
110134
|
+
const server = createServerShell(config, requestStateCodec);
|
|
108159
110135
|
const tools = createAllTools({
|
|
108160
110136
|
subdomain: config.subdomain,
|
|
108161
110137
|
brandIds: config.brandIds,
|
|
108162
|
-
resolveBrandSubdomain,
|
|
110138
|
+
resolveBrandSubdomain: caches.resolveBrandSubdomain,
|
|
108163
110139
|
getToken
|
|
108164
110140
|
});
|
|
108165
110141
|
registerToolset(server, {
|
|
108166
110142
|
config,
|
|
108167
110143
|
getToken,
|
|
108168
|
-
|
|
110144
|
+
caches,
|
|
108169
110145
|
onUnauthorized,
|
|
108170
110146
|
logger
|
|
108171
110147
|
}, tools);
|
|
108172
110148
|
return server;
|
|
108173
110149
|
};
|
|
110150
|
+
const defaultServerFactory = ({ config, getToken, logger, onUnauthorized, caches, requestStateCodec }) => createMcpServer(config, getToken, logger, onUnauthorized, {
|
|
110151
|
+
...caches ? { caches } : {},
|
|
110152
|
+
requestStateCodec
|
|
110153
|
+
});
|
|
108174
110154
|
//#endregion
|
|
108175
110155
|
//#region node_modules/.pnpm/@modelcontextprotocol+server@2.3.1/node_modules/@modelcontextprotocol/server/dist/stdio.mjs
|
|
108176
110156
|
/**
|
|
@@ -108296,12 +110276,485 @@ var StdioServerTransport = class {
|
|
|
108296
110276
|
});
|
|
108297
110277
|
}
|
|
108298
110278
|
};
|
|
110279
|
+
/**
|
|
110280
|
+
* How long the probe-discard path waits for the probe instance to answer the
|
|
110281
|
+
* requests it was delivered before closing it. The wait normally settles as
|
|
110282
|
+
* soon as the DiscoverResult is handed to the wire (or immediately, when a
|
|
110283
|
+
* delivered cancellation already settled the probe); the bound is a backstop
|
|
110284
|
+
* so no edge can ever hold the connection's inbound pump indefinitely behind
|
|
110285
|
+
* the discard.
|
|
110286
|
+
*/
|
|
110287
|
+
const DISCARD_ANSWER_TIMEOUT_MS = 3e3;
|
|
110288
|
+
/**
|
|
110289
|
+
* The transport a pinned instance is connected to: a thin channel that writes
|
|
110290
|
+
* through to the entry-owned wire transport and receives the messages the
|
|
110291
|
+
* entry forwards. The wire transport itself is never handed to an instance —
|
|
110292
|
+
* that is what lets the entry discard an optimistic probe instance (close the
|
|
110293
|
+
* channel) without tearing down the connection.
|
|
110294
|
+
*/
|
|
110295
|
+
var StdioConnectionChannel = class {
|
|
110296
|
+
onclose;
|
|
110297
|
+
onerror;
|
|
110298
|
+
onmessage;
|
|
110299
|
+
_closed = false;
|
|
110300
|
+
/** Request ids the entry delivered to the instance that the instance has not yet answered. */
|
|
110301
|
+
_pendingRequests = /* @__PURE__ */ new Set();
|
|
110302
|
+
_drainWaiters = [];
|
|
110303
|
+
constructor(_wire, _onInstanceClose, _outboundIntercept) {
|
|
110304
|
+
this._wire = _wire;
|
|
110305
|
+
this._onInstanceClose = _onInstanceClose;
|
|
110306
|
+
this._outboundIntercept = _outboundIntercept;
|
|
110307
|
+
}
|
|
110308
|
+
async start() {}
|
|
110309
|
+
async send(message, options) {
|
|
110310
|
+
if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
|
|
110311
|
+
const { id } = message;
|
|
110312
|
+
if (id !== void 0) this._settle(id);
|
|
110313
|
+
}
|
|
110314
|
+
if (this._closed) return;
|
|
110315
|
+
if (this._outboundIntercept?.(message) === "handled") return;
|
|
110316
|
+
return this._wire.send(message, options);
|
|
110317
|
+
}
|
|
110318
|
+
setProtocolVersion = (version) => {
|
|
110319
|
+
this._wire.setProtocolVersion?.(version);
|
|
110320
|
+
};
|
|
110321
|
+
/** Forwards one inbound message to the connected instance. */
|
|
110322
|
+
deliver(message, extra) {
|
|
110323
|
+
if (this._closed) return;
|
|
110324
|
+
if (isJSONRPCRequest(message)) this._pendingRequests.add(message.id);
|
|
110325
|
+
else if (isJSONRPCNotification(message) && message.method === "notifications/cancelled") {
|
|
110326
|
+
const cancelledId = message.params?.requestId;
|
|
110327
|
+
if (cancelledId !== void 0) this._settle(cancelledId);
|
|
110328
|
+
}
|
|
110329
|
+
this.onmessage?.(message, extra);
|
|
110330
|
+
}
|
|
110331
|
+
/**
|
|
110332
|
+
* Resolves once every request delivered to the instance has been answered
|
|
110333
|
+
* through {@linkcode send}, settled by a delivered cancellation, or the
|
|
110334
|
+
* channel has been closed and nothing further can be answered. The wait is
|
|
110335
|
+
* bounded by `timeoutMs` as a backstop so no edge can hold the caller
|
|
110336
|
+
* indefinitely; resolves `false` only when the bound elapsed with requests
|
|
110337
|
+
* still unanswered. Used by the probe-discard path so a probe request the
|
|
110338
|
+
* entry accepted is never silently dropped.
|
|
110339
|
+
*/
|
|
110340
|
+
async whenRequestsAnswered(timeoutMs) {
|
|
110341
|
+
if (this._closed || this._pendingRequests.size === 0) return true;
|
|
110342
|
+
return await new Promise((resolve) => {
|
|
110343
|
+
const waiter = () => {
|
|
110344
|
+
clearTimeout(timer);
|
|
110345
|
+
resolve(true);
|
|
110346
|
+
};
|
|
110347
|
+
const timer = setTimeout(() => {
|
|
110348
|
+
this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
|
|
110349
|
+
resolve(false);
|
|
110350
|
+
}, timeoutMs);
|
|
110351
|
+
this._drainWaiters.push(waiter);
|
|
110352
|
+
});
|
|
110353
|
+
}
|
|
110354
|
+
async close() {
|
|
110355
|
+
if (this._closed) return;
|
|
110356
|
+
this._closed = true;
|
|
110357
|
+
this._pendingRequests.clear();
|
|
110358
|
+
this._releaseDrainWaiters();
|
|
110359
|
+
try {
|
|
110360
|
+
this._onInstanceClose();
|
|
110361
|
+
} finally {
|
|
110362
|
+
this.onclose?.();
|
|
110363
|
+
}
|
|
110364
|
+
}
|
|
110365
|
+
_settle(id) {
|
|
110366
|
+
this._pendingRequests.delete(id);
|
|
110367
|
+
if (this._pendingRequests.size === 0) this._releaseDrainWaiters();
|
|
110368
|
+
}
|
|
110369
|
+
_releaseDrainWaiters() {
|
|
110370
|
+
const waiters = this._drainWaiters;
|
|
110371
|
+
this._drainWaiters = [];
|
|
110372
|
+
for (const waiter of waiters) waiter();
|
|
110373
|
+
}
|
|
110374
|
+
};
|
|
110375
|
+
/**
|
|
110376
|
+
* Classifies one message of the opening exchange with the same body-primary
|
|
110377
|
+
* rules the HTTP entry applies per request: `initialize` is the legacy
|
|
110378
|
+
* handshake unless it carries a valid modern envelope claim; a present claim
|
|
110379
|
+
* is validated (never silently ignored); a claim-less message is 2025-era
|
|
110380
|
+
* traffic. There is no header layer on stdio, so the body is the only signal.
|
|
110381
|
+
*/
|
|
110382
|
+
function classifyOpeningMessage(message) {
|
|
110383
|
+
const params = message.params;
|
|
110384
|
+
if (message.method === "initialize" && !carriesValidModernEnvelopeClaim(params)) {
|
|
110385
|
+
const requestedVersion = params !== null && typeof params === "object" && typeof params.protocolVersion === "string" ? params.protocolVersion : void 0;
|
|
110386
|
+
return {
|
|
110387
|
+
kind: "legacy",
|
|
110388
|
+
reason: "initialize",
|
|
110389
|
+
...requestedVersion !== void 0 && { requestedVersion }
|
|
110390
|
+
};
|
|
110391
|
+
}
|
|
110392
|
+
if (!hasEnvelopeClaim(params)) return {
|
|
110393
|
+
kind: "legacy",
|
|
110394
|
+
reason: "no-claim"
|
|
110395
|
+
};
|
|
110396
|
+
const meta = requestMetaOf(params);
|
|
110397
|
+
const firstIssue = (meta === void 0 ? [] : validateEnvelopeMeta(meta))[0];
|
|
110398
|
+
if (firstIssue !== void 0) return {
|
|
110399
|
+
kind: "invalid-envelope",
|
|
110400
|
+
issue: firstIssue
|
|
110401
|
+
};
|
|
110402
|
+
const claimedVersion = envelopeClaimVersion(params);
|
|
110403
|
+
if (claimedVersion === void 0 || !SUPPORTED_MODERN_PROTOCOL_VERSIONS.includes(claimedVersion)) return {
|
|
110404
|
+
kind: "unsupported-revision",
|
|
110405
|
+
requested: claimedVersion ?? "unknown"
|
|
110406
|
+
};
|
|
110407
|
+
return {
|
|
110408
|
+
kind: "modern",
|
|
110409
|
+
revision: claimedVersion,
|
|
110410
|
+
classification: {
|
|
110411
|
+
era: "modern",
|
|
110412
|
+
revision: claimedVersion
|
|
110413
|
+
}
|
|
110414
|
+
};
|
|
110415
|
+
}
|
|
110416
|
+
/**
|
|
110417
|
+
* Serves MCP over stdio from a server factory, owning the era decision for
|
|
110418
|
+
* the connection: the opening exchange selects the era, ONE instance from the
|
|
110419
|
+
* factory is pinned for the connection lifetime, and everything after passes
|
|
110420
|
+
* straight through to it. See the module documentation for the opening rules.
|
|
110421
|
+
*
|
|
110422
|
+
* ```ts
|
|
110423
|
+
* import { serveStdio } from '@modelcontextprotocol/server/stdio';
|
|
110424
|
+
*
|
|
110425
|
+
* serveStdio(() => {
|
|
110426
|
+
* const server = new McpServer({ name: 'my-server', version: '1.0.0' }, { capabilities: { tools: {} } });
|
|
110427
|
+
* // register tools/resources/prompts once — the same factory serves both eras
|
|
110428
|
+
* return server;
|
|
110429
|
+
* });
|
|
110430
|
+
* ```
|
|
110431
|
+
*/
|
|
110432
|
+
function serveStdio(factory, options = {}) {
|
|
110433
|
+
const legacyMode = options.legacy ?? "serve";
|
|
110434
|
+
const wire = options.transport ?? new StdioServerTransport();
|
|
110435
|
+
let state = { phase: "opening" };
|
|
110436
|
+
/** Channel currently being discarded (its close must not tear the connection down). */
|
|
110437
|
+
let discarding;
|
|
110438
|
+
let closing = false;
|
|
110439
|
+
/**
|
|
110440
|
+
* Whether the connection has been torn down (`handle.close()` or the wire
|
|
110441
|
+
* closing). The opening arms re-check this after every await: a close can
|
|
110442
|
+
* race factory construction, and the continuation must neither resurrect
|
|
110443
|
+
* the connection state nor keep a late-resolved instance around.
|
|
110444
|
+
*/
|
|
110445
|
+
const isTornDown = () => closing || state.phase === "closed";
|
|
110446
|
+
const reportError = (error) => {
|
|
110447
|
+
try {
|
|
110448
|
+
options.onerror?.(error);
|
|
110449
|
+
} catch {}
|
|
110450
|
+
};
|
|
110451
|
+
const writeErrorResponse = (id, code, message, data) => wire.send({
|
|
110452
|
+
jsonrpc: "2.0",
|
|
110453
|
+
id,
|
|
110454
|
+
error: {
|
|
110455
|
+
code,
|
|
110456
|
+
message,
|
|
110457
|
+
...data !== void 0 && { data }
|
|
110458
|
+
}
|
|
110459
|
+
}).catch((error) => reportError(toError(error)));
|
|
110460
|
+
/**
|
|
110461
|
+
* Entry-handled `subscriptions/listen` for this connection: holds the
|
|
110462
|
+
* active subscriptions, serves inbound listen / cancelled-of-listen
|
|
110463
|
+
* before the pinned instance is consulted, and rewrites the instance's
|
|
110464
|
+
* outbound change notifications onto the active subscriptions. Only
|
|
110465
|
+
* consulted on a modern-pinned connection — on a legacy connection
|
|
110466
|
+
* change notifications pass straight through (the 2025 unsolicited
|
|
110467
|
+
* delivery model is unchanged).
|
|
110468
|
+
*/
|
|
110469
|
+
const listenRouter = new StdioListenRouter(options.maxSubscriptions ?? 1024);
|
|
110470
|
+
/** Outbound intercept installed on a modern instance's channel. */
|
|
110471
|
+
const modernOutboundIntercept = (message) => {
|
|
110472
|
+
if (!isJSONRPCNotification(message)) return void 0;
|
|
110473
|
+
const routed = listenRouter.routeOutbound(message);
|
|
110474
|
+
if (routed === "passthrough") return void 0;
|
|
110475
|
+
for (const stamped of routed) wire.send({
|
|
110476
|
+
jsonrpc: "2.0",
|
|
110477
|
+
...stamped
|
|
110478
|
+
}).catch((error) => reportError(toError(error)));
|
|
110479
|
+
return "handled";
|
|
110480
|
+
};
|
|
110481
|
+
/**
|
|
110482
|
+
* Entry-handled inbound listen routing for a modern-pinned connection.
|
|
110483
|
+
* Returns `true` when the message was served at the entry and must NOT
|
|
110484
|
+
* be delivered to the pinned instance.
|
|
110485
|
+
*/
|
|
110486
|
+
const tryServeListen = async (message) => {
|
|
110487
|
+
if (isJSONRPCRequest(message) && message.method === "subscriptions/listen") {
|
|
110488
|
+
const meta = requestMetaOf(message.params);
|
|
110489
|
+
const issue = hasEnvelopeClaim(message.params) ? (meta === void 0 ? [] : validateEnvelopeMeta(meta))[0] : {
|
|
110490
|
+
key: "_meta",
|
|
110491
|
+
problem: "the per-request envelope is required on protocol revision 2026-07-28"
|
|
110492
|
+
};
|
|
110493
|
+
const claimedVersion = envelopeClaimVersion(message.params);
|
|
110494
|
+
let reply;
|
|
110495
|
+
if (issue !== void 0) reply = {
|
|
110496
|
+
jsonrpc: "2.0",
|
|
110497
|
+
id: message.id,
|
|
110498
|
+
error: {
|
|
110499
|
+
code: -32602,
|
|
110500
|
+
message: `Invalid _meta envelope: ${issue.key}: ${issue.problem}`
|
|
110501
|
+
}
|
|
110502
|
+
};
|
|
110503
|
+
else if (claimedVersion === void 0 || !SUPPORTED_MODERN_PROTOCOL_VERSIONS.includes(claimedVersion)) {
|
|
110504
|
+
const error = new UnsupportedProtocolVersionError({
|
|
110505
|
+
supported: [...SUPPORTED_MODERN_PROTOCOL_VERSIONS],
|
|
110506
|
+
requested: claimedVersion ?? "unknown"
|
|
110507
|
+
});
|
|
110508
|
+
reply = {
|
|
110509
|
+
jsonrpc: "2.0",
|
|
110510
|
+
id: message.id,
|
|
110511
|
+
error: {
|
|
110512
|
+
code: error.code,
|
|
110513
|
+
message: error.message,
|
|
110514
|
+
data: error.data
|
|
110515
|
+
}
|
|
110516
|
+
};
|
|
110517
|
+
} else reply = listenRouter.serve(message);
|
|
110518
|
+
await wire.send("error" in reply ? reply : {
|
|
110519
|
+
jsonrpc: "2.0",
|
|
110520
|
+
method: reply.method,
|
|
110521
|
+
params: reply.params
|
|
110522
|
+
}).catch((error) => reportError(toError(error)));
|
|
110523
|
+
return true;
|
|
110524
|
+
}
|
|
110525
|
+
if (isJSONRPCNotification(message) && message.method === "notifications/cancelled") {
|
|
110526
|
+
const cancelledId = message.params?.requestId;
|
|
110527
|
+
if (cancelledId !== void 0 && listenRouter.cancel(cancelledId)) return true;
|
|
110528
|
+
}
|
|
110529
|
+
return false;
|
|
110530
|
+
};
|
|
110531
|
+
/** Answers a 2025-era request the entry will not serve (the modern-only rejection cells). */
|
|
110532
|
+
const answerLegacyRejection = (request, reason, requestedVersion) => {
|
|
110533
|
+
const rejection = modernOnlyStrictRejection({
|
|
110534
|
+
kind: "legacy",
|
|
110535
|
+
reason,
|
|
110536
|
+
...requestedVersion !== void 0 && { requestedVersion }
|
|
110537
|
+
}, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
|
|
110538
|
+
if (rejection === void 0) return Promise.resolve();
|
|
110539
|
+
reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection.cell}): ${rejection.message}`));
|
|
110540
|
+
return writeErrorResponse(request.id, rejection.code, rejection.message, rejection.data);
|
|
110541
|
+
};
|
|
110542
|
+
const onInstanceClosed = (channel) => {
|
|
110543
|
+
if (closing || channel === discarding) return;
|
|
110544
|
+
closeAll();
|
|
110545
|
+
};
|
|
110546
|
+
const connectInstance = async (era, revision) => {
|
|
110547
|
+
const product = await factory({ era });
|
|
110548
|
+
const server = product instanceof McpServer ? product.server : product;
|
|
110549
|
+
if (era === "modern") {
|
|
110550
|
+
setNegotiatedProtocolVersion(server, revision);
|
|
110551
|
+
installModernOnlyHandlers(server, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
|
|
110552
|
+
listenRouter.setServerCapabilities(server.getCapabilities(), serverIdentityOf(server));
|
|
110553
|
+
}
|
|
110554
|
+
const channel = new StdioConnectionChannel(wire, () => onInstanceClosed(channel), era === "modern" ? modernOutboundIntercept : void 0);
|
|
110555
|
+
await product.connect(channel);
|
|
110556
|
+
return {
|
|
110557
|
+
product,
|
|
110558
|
+
channel
|
|
110559
|
+
};
|
|
110560
|
+
};
|
|
110561
|
+
/** Closes an instance whose factory resolved only after the connection was torn down. */
|
|
110562
|
+
const disposeLateInstance = (instance) => instance.product.close().catch((error) => reportError(toError(error)));
|
|
110563
|
+
const discardProbeInstance = async (instance) => {
|
|
110564
|
+
discarding = instance.channel;
|
|
110565
|
+
try {
|
|
110566
|
+
if (!await instance.channel.whenRequestsAnswered(DISCARD_ANSWER_TIMEOUT_MS)) reportError(/* @__PURE__ */ new Error(`Discarded the probe instance with requests still unanswered after ${DISCARD_ANSWER_TIMEOUT_MS}ms; continuing with the fallback`));
|
|
110567
|
+
await instance.product.close();
|
|
110568
|
+
} catch (error) {
|
|
110569
|
+
reportError(toError(error));
|
|
110570
|
+
} finally {
|
|
110571
|
+
discarding = void 0;
|
|
110572
|
+
}
|
|
110573
|
+
};
|
|
110574
|
+
const processMessage = async (message) => {
|
|
110575
|
+
if (state.phase === "closed") return;
|
|
110576
|
+
if (state.phase === "pinned") {
|
|
110577
|
+
if (state.era === "modern" && isJSONRPCRequest(message) && message.method === "initialize" && !carriesValidModernEnvelopeClaim(message.params)) {
|
|
110578
|
+
await answerLegacyRejection(message, "initialize", message.params !== null && typeof message.params === "object" && typeof message.params.protocolVersion === "string" ? message.params.protocolVersion : void 0);
|
|
110579
|
+
return;
|
|
110580
|
+
}
|
|
110581
|
+
if (state.era === "modern" && await tryServeListen(message)) return;
|
|
110582
|
+
if (isTornDown()) return;
|
|
110583
|
+
state.instance.channel.deliver(message);
|
|
110584
|
+
return;
|
|
110585
|
+
}
|
|
110586
|
+
if (!isJSONRPCRequest(message) && !isJSONRPCNotification(message)) {
|
|
110587
|
+
reportError(/* @__PURE__ */ new Error("Discarded a JSON-RPC response received before the connection negotiated an era"));
|
|
110588
|
+
return;
|
|
110589
|
+
}
|
|
110590
|
+
const opening = classifyOpeningMessage(message);
|
|
110591
|
+
switch (opening.kind) {
|
|
110592
|
+
case "invalid-envelope": {
|
|
110593
|
+
const detail = `Invalid _meta envelope for protocol revision 2026-07-28: ${opening.issue.key}: ${opening.issue.problem}`;
|
|
110594
|
+
if (isJSONRPCRequest(message)) await writeErrorResponse(message.id, ProtocolErrorCode.InvalidParams, detail, { envelope: opening.issue });
|
|
110595
|
+
else reportError(/* @__PURE__ */ new Error(`Discarded a notification with a malformed envelope: ${detail}`));
|
|
110596
|
+
return;
|
|
110597
|
+
}
|
|
110598
|
+
case "unsupported-revision":
|
|
110599
|
+
if (isJSONRPCRequest(message)) {
|
|
110600
|
+
const error = new UnsupportedProtocolVersionError({
|
|
110601
|
+
supported: [...SUPPORTED_MODERN_PROTOCOL_VERSIONS],
|
|
110602
|
+
requested: opening.requested
|
|
110603
|
+
});
|
|
110604
|
+
reportError(error);
|
|
110605
|
+
await writeErrorResponse(message.id, error.code, error.message, error.data);
|
|
110606
|
+
} else reportError(/* @__PURE__ */ new Error(`Discarded a notification claiming unsupported protocol revision ${opening.requested}`));
|
|
110607
|
+
return;
|
|
110608
|
+
case "modern":
|
|
110609
|
+
if (isJSONRPCRequest(message) && message.method === "server/discover") {
|
|
110610
|
+
if (state.phase === "probe") {
|
|
110611
|
+
state.instance.channel.deliver(message, { classification: opening.classification });
|
|
110612
|
+
return;
|
|
110613
|
+
}
|
|
110614
|
+
const instance = await connectInstance("modern", opening.revision);
|
|
110615
|
+
if (isTornDown()) {
|
|
110616
|
+
await disposeLateInstance(instance);
|
|
110617
|
+
return;
|
|
110618
|
+
}
|
|
110619
|
+
state = {
|
|
110620
|
+
phase: "probe",
|
|
110621
|
+
instance
|
|
110622
|
+
};
|
|
110623
|
+
instance.channel.deliver(message, { classification: opening.classification });
|
|
110624
|
+
return;
|
|
110625
|
+
}
|
|
110626
|
+
if (state.phase === "probe") {
|
|
110627
|
+
if (isJSONRPCNotification(message)) {
|
|
110628
|
+
state.instance.channel.deliver(message, { classification: opening.classification });
|
|
110629
|
+
return;
|
|
110630
|
+
}
|
|
110631
|
+
state = {
|
|
110632
|
+
phase: "pinned",
|
|
110633
|
+
era: "modern",
|
|
110634
|
+
instance: state.instance
|
|
110635
|
+
};
|
|
110636
|
+
} else {
|
|
110637
|
+
const instance = await connectInstance("modern", opening.revision);
|
|
110638
|
+
if (isTornDown()) {
|
|
110639
|
+
await disposeLateInstance(instance);
|
|
110640
|
+
return;
|
|
110641
|
+
}
|
|
110642
|
+
state = {
|
|
110643
|
+
phase: "pinned",
|
|
110644
|
+
era: "modern",
|
|
110645
|
+
instance
|
|
110646
|
+
};
|
|
110647
|
+
}
|
|
110648
|
+
if (await tryServeListen(message)) return;
|
|
110649
|
+
if (isTornDown()) return;
|
|
110650
|
+
state.instance.channel.deliver(message, { classification: opening.classification });
|
|
110651
|
+
return;
|
|
110652
|
+
case "legacy": {
|
|
110653
|
+
if (legacyMode === "reject") {
|
|
110654
|
+
if (isJSONRPCRequest(message)) await answerLegacyRejection(message, opening.reason, opening.requestedVersion);
|
|
110655
|
+
return;
|
|
110656
|
+
}
|
|
110657
|
+
if (state.phase === "probe") {
|
|
110658
|
+
await discardProbeInstance(state.instance);
|
|
110659
|
+
if (isTornDown()) return;
|
|
110660
|
+
state = { phase: "opening" };
|
|
110661
|
+
}
|
|
110662
|
+
const instance = await connectInstance("legacy");
|
|
110663
|
+
if (isTornDown()) {
|
|
110664
|
+
await disposeLateInstance(instance);
|
|
110665
|
+
return;
|
|
110666
|
+
}
|
|
110667
|
+
state = {
|
|
110668
|
+
phase: "pinned",
|
|
110669
|
+
era: "legacy",
|
|
110670
|
+
instance
|
|
110671
|
+
};
|
|
110672
|
+
state.instance.channel.deliver(message);
|
|
110673
|
+
return;
|
|
110674
|
+
}
|
|
110675
|
+
}
|
|
110676
|
+
};
|
|
110677
|
+
const queue = [];
|
|
110678
|
+
let pumping = false;
|
|
110679
|
+
const pump = async () => {
|
|
110680
|
+
if (pumping) return;
|
|
110681
|
+
pumping = true;
|
|
110682
|
+
try {
|
|
110683
|
+
while (queue.length > 0) {
|
|
110684
|
+
const message = queue.shift();
|
|
110685
|
+
try {
|
|
110686
|
+
await processMessage(message);
|
|
110687
|
+
} catch (error) {
|
|
110688
|
+
if (isJSONRPCRequest(message)) await writeErrorResponse(message.id, ProtocolErrorCode.InternalError, "Internal server error");
|
|
110689
|
+
reportError(toError(error));
|
|
110690
|
+
}
|
|
110691
|
+
}
|
|
110692
|
+
} finally {
|
|
110693
|
+
pumping = false;
|
|
110694
|
+
}
|
|
110695
|
+
};
|
|
110696
|
+
const closeAll = async () => {
|
|
110697
|
+
if (closing || state.phase === "closed") return;
|
|
110698
|
+
closing = true;
|
|
110699
|
+
const current = state;
|
|
110700
|
+
state = { phase: "closed" };
|
|
110701
|
+
for (const result of listenRouter.teardownAll()) await wire.send(result).catch((error) => reportError(toError(error)));
|
|
110702
|
+
if (current.phase === "probe" || current.phase === "pinned") await current.instance.product.close().catch((error) => reportError(toError(error)));
|
|
110703
|
+
await wire.close().catch((error) => reportError(toError(error)));
|
|
110704
|
+
};
|
|
110705
|
+
wire.onmessage = (message) => {
|
|
110706
|
+
queue.push(message);
|
|
110707
|
+
pump();
|
|
110708
|
+
};
|
|
110709
|
+
wire.onerror = (error) => {
|
|
110710
|
+
reportError(error);
|
|
110711
|
+
if (state.phase === "probe" || state.phase === "pinned") state.instance.channel.onerror?.(error);
|
|
110712
|
+
};
|
|
110713
|
+
wire.onclose = () => {
|
|
110714
|
+
if (closing || state.phase === "closed") return;
|
|
110715
|
+
closing = true;
|
|
110716
|
+
const current = state;
|
|
110717
|
+
state = { phase: "closed" };
|
|
110718
|
+
if (current.phase === "probe" || current.phase === "pinned") current.instance.product.close().catch((error) => reportError(toError(error)));
|
|
110719
|
+
};
|
|
110720
|
+
const started = wire.start().catch((error) => {
|
|
110721
|
+
reportError(toError(error));
|
|
110722
|
+
throw error;
|
|
110723
|
+
});
|
|
110724
|
+
started.catch(() => {});
|
|
110725
|
+
return { close: async () => {
|
|
110726
|
+
await started.catch(() => {});
|
|
110727
|
+
await closeAll();
|
|
110728
|
+
} };
|
|
110729
|
+
}
|
|
110730
|
+
function toError(value) {
|
|
110731
|
+
return value instanceof Error ? value : new Error(String(value));
|
|
110732
|
+
}
|
|
108299
110733
|
//#endregion
|
|
108300
110734
|
//#region src/transports/stdio.ts
|
|
108301
|
-
|
|
108302
|
-
|
|
108303
|
-
|
|
110735
|
+
/**
|
|
110736
|
+
* Serve MCP over stdio, both protocol eras: the SDK entry picks the era from the
|
|
110737
|
+
* client's opening exchange and pins one server from `build` per connection (a
|
|
110738
|
+
* `server/discover` probe that falls back to `initialize` builds one more, then
|
|
110739
|
+
* drops it).
|
|
110740
|
+
*/
|
|
110741
|
+
const startStdioTransport = (build, logger = silentLogger, options = {}) => {
|
|
110742
|
+
const requestStateCodec = createRequestStateCodec({});
|
|
110743
|
+
const handle = serveStdio(() => {
|
|
110744
|
+
let server;
|
|
110745
|
+
server = build({
|
|
110746
|
+
requestStateCodec,
|
|
110747
|
+
notifyResourceUpdated: (uri) => {
|
|
110748
|
+
server?.server.sendResourceUpdated({ uri }).catch(() => void 0);
|
|
110749
|
+
}
|
|
110750
|
+
});
|
|
110751
|
+
return server;
|
|
110752
|
+
}, {
|
|
110753
|
+
transport: options.transport ?? new StdioServerTransport(void 0, void 0, { maxBufferSize: 10485760 }),
|
|
110754
|
+
onerror: (error) => logger.warn("stdio_transport_error", { error: error.message })
|
|
110755
|
+
});
|
|
108304
110756
|
logger.info("stdio_transport_ready");
|
|
110757
|
+
return handle;
|
|
108305
110758
|
};
|
|
108306
110759
|
//#endregion
|
|
108307
110760
|
//#region src/dev/reload.ts
|
|
@@ -108353,19 +110806,19 @@ const loadFreshTools = async (ctx) => {
|
|
|
108353
110806
|
* only matters after an edit); `reload()` disposes the current generation and
|
|
108354
110807
|
* registers a fresh one.
|
|
108355
110808
|
*/
|
|
108356
|
-
const createReloadableServer = (config, getToken, logger = silentLogger, onUnauthorized, loadTools = loadFreshTools) => {
|
|
108357
|
-
const server = createServerShell(config,
|
|
108358
|
-
const
|
|
110809
|
+
const createReloadableServer = (config, getToken, logger = silentLogger, onUnauthorized, loadTools = loadFreshTools, requestStateCodec) => {
|
|
110810
|
+
const server = createServerShell(config, requestStateCodec);
|
|
110811
|
+
const caches = createServerCaches(config, getToken, onUnauthorized);
|
|
108359
110812
|
const ctx = {
|
|
108360
110813
|
subdomain: config.subdomain,
|
|
108361
110814
|
brandIds: config.brandIds,
|
|
108362
|
-
resolveBrandSubdomain,
|
|
110815
|
+
resolveBrandSubdomain: caches.resolveBrandSubdomain,
|
|
108363
110816
|
getToken
|
|
108364
110817
|
};
|
|
108365
110818
|
const params = {
|
|
108366
110819
|
config,
|
|
108367
110820
|
getToken,
|
|
108368
|
-
|
|
110821
|
+
caches,
|
|
108369
110822
|
onUnauthorized,
|
|
108370
110823
|
logger
|
|
108371
110824
|
};
|
|
@@ -108402,7 +110855,7 @@ const createReloadableServer = (config, getToken, logger = silentLogger, onUnaut
|
|
|
108402
110855
|
const registerReloadTool = (server, reload, logger = silentLogger) => {
|
|
108403
110856
|
server.registerTool("reload_tools", {
|
|
108404
110857
|
title: "Reload tools from source (dev)",
|
|
108405
|
-
description: "Dev only. Re-imports the Zendesk tool modules from source and re-registers them on this live session, so tool code you just edited takes effect without restarting the server or reconnecting the client. Call it once at the end of an edit cycle, before testing your changes; the refreshed tool list is announced via tools/list_changed. Only the tool modules (tickets, search, help_center, users) are reloaded — edits to shared infrastructure (HTTP client, shared definitions, server wiring) still require a full restart. Takes no arguments and makes no Zendesk API calls.",
|
|
110858
|
+
description: "Dev only. Re-imports the Zendesk tool modules from source and re-registers them on this live session, so tool code you just edited takes effect without restarting the server or reconnecting the client. Call it once at the end of an edit cycle, before testing your changes; the refreshed tool list is announced via tools/list_changed (a client on the 2026-07-28 revision receives it only on an open subscriptions/listen stream; otherwise call tools/list again). Only the tool modules (tickets, search, help_center, users) are reloaded — edits to shared infrastructure (HTTP client, shared definitions, server wiring) still require a full restart. Takes no arguments and makes no Zendesk API calls.",
|
|
108406
110859
|
inputSchema: object({}).strict(),
|
|
108407
110860
|
annotations: {
|
|
108408
110861
|
readOnlyHint: false,
|
|
@@ -108416,7 +110869,7 @@ const registerReloadTool = (server, reload, logger = silentLogger) => {
|
|
|
108416
110869
|
logger.info("tools_reloaded", { count });
|
|
108417
110870
|
return { content: [{
|
|
108418
110871
|
type: "text",
|
|
108419
|
-
text: `Reloaded ${count} tool(s) from source. The updated tool list is now live (tools/list_changed sent).`
|
|
110872
|
+
text: `Reloaded ${count} tool(s) from source. The updated tool list is now live (tools/list_changed sent; if your client did not receive it, call tools/list again).`
|
|
108420
110873
|
}] };
|
|
108421
110874
|
} catch (err) {
|
|
108422
110875
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -108434,21 +110887,23 @@ const registerReloadTool = (server, reload, logger = silentLogger) => {
|
|
|
108434
110887
|
/**
|
|
108435
110888
|
* Start the stdio server in dev mode: the normal toolset plus a persistent
|
|
108436
110889
|
* `reload_tools` tool that hot-reloads edited tool code on demand. stdio only —
|
|
108437
|
-
* HTTP builds a per
|
|
110890
|
+
* HTTP builds a server per session or per request, so there is no long-lived
|
|
108438
110891
|
* server to hot-swap.
|
|
108439
110892
|
*
|
|
108440
|
-
* Returns the
|
|
110893
|
+
* Returns the transport handle so the caller can close it on shutdown: dev mode
|
|
108441
110894
|
* runs over the same stdio transport as normal mode and must exit the same way
|
|
108442
110895
|
* when the client disconnects.
|
|
108443
110896
|
*/
|
|
108444
110897
|
/* v8 ignore start -- runtime bootstrap: binds the reload tool to a real stdio
|
|
108445
110898
|
transport; the reload machinery it wires up is covered by dev-reload.test.ts */
|
|
108446
|
-
const startDevServer =
|
|
108447
|
-
const
|
|
108448
|
-
|
|
108449
|
-
|
|
110899
|
+
const startDevServer = (config, getToken, logger = silentLogger, onUnauthorized) => {
|
|
110900
|
+
const handle = startStdioTransport(({ requestStateCodec }) => {
|
|
110901
|
+
const { server, reload } = createReloadableServer(config, getToken, logger, onUnauthorized, loadFreshTools, requestStateCodec);
|
|
110902
|
+
registerReloadTool(server, reload, logger);
|
|
110903
|
+
return server;
|
|
110904
|
+
}, logger);
|
|
108450
110905
|
logger.info("dev_mode_enabled");
|
|
108451
|
-
return
|
|
110906
|
+
return handle;
|
|
108452
110907
|
};
|
|
108453
110908
|
/* v8 ignore stop */
|
|
108454
110909
|
//#endregion
|
|
@@ -108600,7 +111055,7 @@ const installShutdown = (options) => {
|
|
|
108600
111055
|
//#endregion
|
|
108601
111056
|
//#region src/index.ts
|
|
108602
111057
|
const buildStdioTokenStore = async (config, logger) => {
|
|
108603
|
-
const { createTokenStore } = await import("./token-store-
|
|
111058
|
+
const { createTokenStore } = await import("./token-store-DyO12sbo.js");
|
|
108604
111059
|
return createTokenStore({
|
|
108605
111060
|
subdomain: config.subdomain,
|
|
108606
111061
|
oauthClientId: config.oauthClientId,
|
|
@@ -108608,11 +111063,15 @@ const buildStdioTokenStore = async (config, logger) => {
|
|
|
108608
111063
|
readOnly: config.readOnly
|
|
108609
111064
|
}, logger);
|
|
108610
111065
|
};
|
|
108611
|
-
const connectStdio =
|
|
111066
|
+
const connectStdio = (config, tokenStore, logger) => {
|
|
108612
111067
|
if (config.dev) return startDevServer(config, tokenStore.getToken, logger, tokenStore.invalidate);
|
|
108613
|
-
|
|
108614
|
-
|
|
108615
|
-
|
|
111068
|
+
return startStdioTransport((extras) => defaultServerFactory({
|
|
111069
|
+
config,
|
|
111070
|
+
getToken: tokenStore.getToken,
|
|
111071
|
+
logger,
|
|
111072
|
+
onUnauthorized: tokenStore.invalidate,
|
|
111073
|
+
...extras
|
|
111074
|
+
}), logger);
|
|
108616
111075
|
};
|
|
108617
111076
|
const main = async () => {
|
|
108618
111077
|
const config = loadConfig();
|
|
@@ -108629,19 +111088,19 @@ const main = async () => {
|
|
|
108629
111088
|
const logger = createLogger(config.logLevel);
|
|
108630
111089
|
if (config.transport === "stdio") {
|
|
108631
111090
|
const tokenStore = await buildStdioTokenStore(config, logger);
|
|
108632
|
-
const
|
|
111091
|
+
const stdio = connectStdio(config, tokenStore, logger);
|
|
108633
111092
|
installShutdown({
|
|
108634
111093
|
watchStdin: true,
|
|
108635
111094
|
logger,
|
|
108636
111095
|
cleanup: async () => {
|
|
108637
|
-
await
|
|
111096
|
+
await stdio.close();
|
|
108638
111097
|
tokenStore.dispose();
|
|
108639
111098
|
}
|
|
108640
111099
|
});
|
|
108641
111100
|
return;
|
|
108642
111101
|
}
|
|
108643
111102
|
if (config.dev) logger.warn("dev_mode_ignored_http");
|
|
108644
|
-
const { startHttpTransport } = await import("./http-
|
|
111103
|
+
const { startHttpTransport } = await import("./http-DU1dSrPe.js");
|
|
108645
111104
|
const http = await startHttpTransport(config, logger);
|
|
108646
111105
|
installShutdown({
|
|
108647
111106
|
watchStdin: false,
|
|
@@ -108655,4 +111114,4 @@ main().catch((error) => {
|
|
|
108655
111114
|
process.exit(1);
|
|
108656
111115
|
});
|
|
108657
111116
|
//#endregion
|
|
108658
|
-
export {
|
|
111117
|
+
export { __exportAll$1 as C, __commonJSMin$1 as S, __toESM$1 as T, OAuthErrorCode as _, silentLogger as a, errnoCode as b, deadlineSignal as c, DEFAULT_MAX_REQUEST_BODY_SIZE as d, WebStandardStreamableHTTPServerTransport as f, OAuthError as g, verifyBearerToken as h, readPackageInfo as i, DEFAULT_CALLBACK_PORT as l, isLegacyRequest as m, defaultServerFactory as n, zendeskGet as o, createMcpHandler as p, createRequestStateCodec as r, REQUEST_TIMEOUT_MS as s, createServerCaches as t, getOAuthUrls as u, STARTUP_DOCS as v, __require as w, readEnv as x, createStartupError as y };
|