@fruggr/zendesk-mcp-server 3.3.0 → 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 +8 -8
- package/dist/{http-GIKJdf05.js → http-DU1dSrPe.js} +382 -42
- package/dist/index.js +3233 -598
- package/dist/sbom.cdx.json +22 -22
- package/dist/{token-persistence-BeetFqPF.js → token-persistence-r0v9RO3q.js} +3 -3
- package/dist/{token-store-Aq7gP39o.js → token-store-DyO12sbo.js} +2 -2
- package/package.json +8 -8
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
|
}
|
|
@@ -7546,6 +7584,92 @@ const isStartupError = (err) => err instanceof Error && err.name === "StartupErr
|
|
|
7546
7584
|
/** The errno code of a failed filesystem call, for the message. */
|
|
7547
7585
|
const errnoCode = (err) => err?.code ?? "unknown error";
|
|
7548
7586
|
//#endregion
|
|
7587
|
+
//#region src/utils/validation.ts
|
|
7588
|
+
const formatPath = (path) => path.map(String).join(".");
|
|
7589
|
+
const formatIssue$1 = (issue) => issue.path.length > 0 ? `${formatPath(issue.path)}: ${issue.message}` : issue.message;
|
|
7590
|
+
/**
|
|
7591
|
+
* `path: message` per issue, joined by `; ` since zod messages carry commas.
|
|
7592
|
+
* Ours rather than the SDK's (not exported): one wording for every mode and for
|
|
7593
|
+
* startup config errors, stable across SDK bumps.
|
|
7594
|
+
*/
|
|
7595
|
+
const formatIssues = (issues) => issues.map(formatIssue$1).join("; ");
|
|
7596
|
+
const unwrap$1 = (node) => node._zod.def.innerType ? unwrap$1(node._zod.def.innerType) : node;
|
|
7597
|
+
/**
|
|
7598
|
+
* Keys declared by the object at `path`, or undefined when no plain object sits
|
|
7599
|
+
* there (a record, say, whose keys zod checks against an enum).
|
|
7600
|
+
*/
|
|
7601
|
+
const keysAt = (node, path) => {
|
|
7602
|
+
const { def } = unwrap$1(node)._zod;
|
|
7603
|
+
if (path.length === 0) return def.shape && Object.keys(def.shape);
|
|
7604
|
+
const [head, ...rest] = path;
|
|
7605
|
+
const child = typeof head === "number" ? def.element : def.shape?.[String(head)];
|
|
7606
|
+
return child && keysAt(child, rest);
|
|
7607
|
+
};
|
|
7608
|
+
const keyList = (keys) => [...keys].sort().join(", ") || "(none)";
|
|
7609
|
+
/** One sentence per object holding unknown keys, the top level first. */
|
|
7610
|
+
const unknownKeysMessage = (schema, issues, topLevelKeys) => [...issues].sort((a, b) => a.path.length - b.path.length).map(({ path, keys }) => {
|
|
7611
|
+
if (path.length === 0) return `Unknown parameter(s): ${keys.join(", ")}. Valid parameters: ${topLevelKeys}.`;
|
|
7612
|
+
const parent = formatPath(path);
|
|
7613
|
+
const unknown = `Unknown parameter(s): ${keys.map((key) => `${parent}.${key}`).join(", ")}.`;
|
|
7614
|
+
const valid = keysAt(schema, path);
|
|
7615
|
+
return valid ? `${unknown} Valid parameters for ${parent}: ${keyList(valid)}.` : unknown;
|
|
7616
|
+
}).join(" ");
|
|
7617
|
+
/**
|
|
7618
|
+
* Build a strict validator for a tool's input schema, computing the strict
|
|
7619
|
+
* schema and the valid-key list once at construction rather than per call.
|
|
7620
|
+
*
|
|
7621
|
+
* Zod objects default to `strip`, which silently drops unknown keys. That hid
|
|
7622
|
+
* #100: a caller passing `per_page` to list_tickets (whose parameter is
|
|
7623
|
+
* `page_size`) had the key dropped, so a large unpaginated page came back. The
|
|
7624
|
+
* validator rejects unknown keys and names both the offending ones and the
|
|
7625
|
+
* valid parameters, so a misremembered name fails loudly. Any other failure is
|
|
7626
|
+
* one `path: message` per issue (#206). When both occur only the unknown keys
|
|
7627
|
+
* are reported: a guessed name is the one error a caller cannot spot by
|
|
7628
|
+
* re-reading its own arguments.
|
|
7629
|
+
*/
|
|
7630
|
+
const createStrictValidator = (schema, strict = schema.strict()) => {
|
|
7631
|
+
const topLevelKeys = keyList(Object.keys(schema.shape));
|
|
7632
|
+
return (params) => {
|
|
7633
|
+
const result = strict.safeParse(params);
|
|
7634
|
+
if (result.success) return {
|
|
7635
|
+
success: true,
|
|
7636
|
+
data: result.data
|
|
7637
|
+
};
|
|
7638
|
+
const unknownKeyIssues = result.error.issues.filter((issue) => issue.code === "unrecognized_keys");
|
|
7639
|
+
return {
|
|
7640
|
+
success: false,
|
|
7641
|
+
message: unknownKeyIssues.length > 0 ? unknownKeysMessage(schema, unknownKeyIssues, topLevelKeys) : `Invalid parameter(s): ${formatIssues(result.error.issues)}`
|
|
7642
|
+
};
|
|
7643
|
+
};
|
|
7644
|
+
};
|
|
7645
|
+
/** The proxy dispatch path (namespace/single modes): throws the validator's message. */
|
|
7646
|
+
const createStrictParamsParser = (schema) => {
|
|
7647
|
+
const validate = createStrictValidator(schema);
|
|
7648
|
+
return (params) => {
|
|
7649
|
+
const result = validate(params);
|
|
7650
|
+
if (!result.success) throw new Error(result.message);
|
|
7651
|
+
return result.data;
|
|
7652
|
+
};
|
|
7653
|
+
};
|
|
7654
|
+
/**
|
|
7655
|
+
* The same validator as a Standard Schema, for `all` mode: the SDK calls this
|
|
7656
|
+
* `validate` and prints our pathless issue after its own prefix, so all three
|
|
7657
|
+
* modes word an error alike. The JSON Schema is zod's own, untouched.
|
|
7658
|
+
*/
|
|
7659
|
+
const toStrictStandardSchema = (schema) => {
|
|
7660
|
+
const strict = schema.strict();
|
|
7661
|
+
const validate = createStrictValidator(schema, strict);
|
|
7662
|
+
return { "~standard": {
|
|
7663
|
+
version: 1,
|
|
7664
|
+
vendor: "zendesk-mcp-server",
|
|
7665
|
+
validate: (value) => {
|
|
7666
|
+
const result = validate(value);
|
|
7667
|
+
return result.success ? { value: result.data } : { issues: [{ message: result.message }] };
|
|
7668
|
+
},
|
|
7669
|
+
jsonSchema: strict["~standard"].jsonSchema
|
|
7670
|
+
} };
|
|
7671
|
+
};
|
|
7672
|
+
//#endregion
|
|
7549
7673
|
//#region src/config.ts
|
|
7550
7674
|
const ToolMode = _enum([
|
|
7551
7675
|
"single",
|
|
@@ -7790,7 +7914,7 @@ const parseStrict = (args) => {
|
|
|
7790
7914
|
const validated = (input) => {
|
|
7791
7915
|
const result = ConfigSchema.safeParse(input);
|
|
7792
7916
|
if (result.success) return result.data;
|
|
7793
|
-
throw createStartupError(`${result.error.issues
|
|
7917
|
+
throw createStartupError(`${formatIssues(result.error.issues)}.`, STARTUP_DOCS.cliReference, result.error);
|
|
7794
7918
|
};
|
|
7795
7919
|
const parseCliArgs = (args) => {
|
|
7796
7920
|
const { values, positionals } = parseStrict(args);
|
|
@@ -7847,8 +7971,8 @@ const loadConfig = (argv = process.argv.slice(2)) => {
|
|
|
7847
7971
|
readOnly: cli.readOnly ?? false,
|
|
7848
7972
|
namespaces,
|
|
7849
7973
|
tools: cli.tools,
|
|
7850
|
-
topology: cli.topology
|
|
7851
|
-
promotedArticles: cli.promotedArticles
|
|
7974
|
+
topology: cli.topology,
|
|
7975
|
+
promotedArticles: cli.promotedArticles,
|
|
7852
7976
|
hcResourceScheme,
|
|
7853
7977
|
dev: cli.dev ?? false,
|
|
7854
7978
|
printTools: cli.printTools ?? false,
|
|
@@ -7858,389 +7982,7 @@ const loadConfig = (argv = process.argv.slice(2)) => {
|
|
|
7858
7982
|
});
|
|
7859
7983
|
};
|
|
7860
7984
|
//#endregion
|
|
7861
|
-
//#region
|
|
7862
|
-
const positiveIntEnv = (name, fallback) => {
|
|
7863
|
-
const raw = readEnv(name).value;
|
|
7864
|
-
if (raw === void 0 || raw.trim() === "") return fallback;
|
|
7865
|
-
const parsed = Number(raw);
|
|
7866
|
-
return Number.isSafeInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
7867
|
-
};
|
|
7868
|
-
const CHARACTER_LIMIT = positiveIntEnv("RESPONSE_CHARACTER_LIMIT", 25e3);
|
|
7869
|
-
const ARTICLE_RESOURCES_SCAN_MAX_PAGES = positiveIntEnv("ARTICLE_RESOURCES_SCAN_MAX_PAGES", 20);
|
|
7870
|
-
const DEFAULT_CALLBACK_PORT = 27439;
|
|
7871
|
-
const MAX_ATTACHMENT_BYTES = positiveIntEnv("ATTACHMENT_MAX_BYTES", 5242880);
|
|
7872
|
-
const MAX_EMBEDDED_IMAGE_COUNT = positiveIntEnv("EMBEDDED_IMAGES_MAX", 10);
|
|
7873
|
-
const STDIO_MAX_MESSAGE_BYTES = 10485760;
|
|
7874
|
-
const MESSAGE_CONTENT_BUDGET_BYTES = 10420224;
|
|
7875
|
-
const MAX_RESPONSE_BYTES = Math.min(positiveIntEnv("RESPONSE_MAX_BYTES", MESSAGE_CONTENT_BUDGET_BYTES), MESSAGE_CONTENT_BUDGET_BYTES);
|
|
7876
|
-
const MAX_BASE64_INPUT_CHARS = MESSAGE_CONTENT_BUDGET_BYTES;
|
|
7877
|
-
const MAX_BASE64_INPUT_MB = Number.parseFloat((MAX_BASE64_INPUT_CHARS / 4 * 3 / 1048576).toFixed(2));
|
|
7878
|
-
const MAX_COMMENT_PAGES = positiveIntEnv("COMMENT_MAX_PAGES", 10);
|
|
7879
|
-
const TICKET_FIELD_SCAN_MAX_PAGES = positiveIntEnv("TICKET_FIELD_SCAN_MAX_PAGES", 10);
|
|
7880
|
-
const REORDER_CONFIRM_THRESHOLD = positiveIntEnv("REORDER_CONFIRM_THRESHOLD", 20);
|
|
7881
|
-
const getBaseUrl = (subdomain) => `https://${subdomain}.zendesk.com/api/v2`;
|
|
7882
|
-
const getHelpCenterBaseUrl = (subdomain) => `https://${subdomain}.zendesk.com/api/v2/help_center`;
|
|
7883
|
-
const getOAuthUrls = (subdomain) => ({
|
|
7884
|
-
authorizeUrl: `https://${subdomain}.zendesk.com/oauth/authorizations/new`,
|
|
7885
|
-
tokenUrl: `https://${subdomain}.zendesk.com/oauth/tokens`
|
|
7886
|
-
});
|
|
7887
|
-
//#endregion
|
|
7888
|
-
//#region src/client/retry.ts
|
|
7889
|
-
const MAX_ATTEMPTS = 3;
|
|
7890
|
-
const BASE_DELAY_MS = 250;
|
|
7891
|
-
const MAX_DELAY_MS = 4e3;
|
|
7892
|
-
const REQUEST_TIMEOUT_MS = 3e4;
|
|
7893
|
-
const TRANSFER_TIMEOUT_MS = 12e4;
|
|
7894
|
-
/**
|
|
7895
|
-
* A deadline for one attempt. The abort surfaces as a `TimeoutError` whose `code`
|
|
7896
|
-
* is numeric, not a syscall string, so it classifies as `unknown`: a GET is
|
|
7897
|
-
* retried, a write is not — the request may have arrived before the deadline.
|
|
7898
|
-
*/
|
|
7899
|
-
const deadlineSignal = (timeoutMs) => AbortSignal.timeout(timeoutMs);
|
|
7900
|
-
const MAX_RETRY_AFTER_MS = 5e3;
|
|
7901
|
-
const POLICIES = {
|
|
7902
|
-
GET: {
|
|
7903
|
-
network: "any",
|
|
7904
|
-
serverErrors: true
|
|
7905
|
-
},
|
|
7906
|
-
DELETE: {
|
|
7907
|
-
network: "pre-send",
|
|
7908
|
-
serverErrors: false
|
|
7909
|
-
},
|
|
7910
|
-
POST: {
|
|
7911
|
-
network: "pre-send",
|
|
7912
|
-
serverErrors: false
|
|
7913
|
-
},
|
|
7914
|
-
PUT: {
|
|
7915
|
-
network: "pre-send",
|
|
7916
|
-
serverErrors: false
|
|
7917
|
-
}
|
|
7918
|
-
};
|
|
7919
|
-
const DELAY_SECONDS = /^\d+$/;
|
|
7920
|
-
const HTTP_DATE_START = /^[A-Za-z]/;
|
|
7921
|
-
const NON_ASCII = /[^ -~]/g;
|
|
7922
|
-
const TOKEN_SEGMENT = /\/token\/[^/]+/;
|
|
7923
|
-
const URL_IN_TEXT = /[a-z][a-z0-9+.-]*:\/\/\S+/gi;
|
|
7924
|
-
const PRE_SEND_CODES = /* @__PURE__ */ new Set([
|
|
7925
|
-
"ENOTFOUND",
|
|
7926
|
-
"EAI_AGAIN",
|
|
7927
|
-
"ECONNREFUSED",
|
|
7928
|
-
"UND_ERR_CONNECT_TIMEOUT"
|
|
7929
|
-
]);
|
|
7930
|
-
const defaultRetryDeps = {
|
|
7931
|
-
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
7932
|
-
random: Math.random
|
|
7933
|
-
};
|
|
7934
|
-
/** Non-ASCII bytes break `node:http` headers, so client error text stays ASCII. */
|
|
7935
|
-
const toAscii = (text) => text.replace(NON_ASCII, "?");
|
|
7936
|
-
/**
|
|
7937
|
-
* Identifies the request without leaking a credential: uploads carry an upload
|
|
7938
|
-
* token in the query string, and an attachment `content_url` carries a download
|
|
7939
|
-
* token as a path segment (`/attachments/token/<token>/`).
|
|
7940
|
-
*/
|
|
7941
|
-
const describeTarget = (url) => {
|
|
7942
|
-
const { origin, pathname } = new URL(url);
|
|
7943
|
-
return `${origin}${pathname.replace(TOKEN_SEGMENT, "/token/[redacted]")}`;
|
|
7944
|
-
};
|
|
7945
|
-
/** First `code` in the cause chain, inspecting 5 levels so a cycle cannot hang. */
|
|
7946
|
-
const errorCode = (err) => {
|
|
7947
|
-
let current = err;
|
|
7948
|
-
for (let depth = 0; depth < 5 && current !== null && typeof current === "object"; depth += 1) {
|
|
7949
|
-
const { code, cause } = current;
|
|
7950
|
-
if (typeof code === "string") return code;
|
|
7951
|
-
current = cause;
|
|
7952
|
-
}
|
|
7953
|
-
};
|
|
7954
|
-
const classifyNetworkError = (err) => PRE_SEND_CODES.has(errorCode(err)) ? "pre-send" : "unknown";
|
|
7955
|
-
const UNINFORMATIVE_NAMES = /* @__PURE__ */ new Set(["Error", "TypeError"]);
|
|
7956
|
-
/**
|
|
7957
|
-
* The client refuses to replay a write that may have landed — but the caller is
|
|
7958
|
-
* an LLM whose reflex on a failed tool call is to try again, which would undo
|
|
7959
|
-
* that. So a failed write says whether it may have taken effect. ASCII only,
|
|
7960
|
-
* same rule as the rest of the message.
|
|
7961
|
-
*/
|
|
7962
|
-
const MAY_HAVE_APPLIED_NOTE = "The write may already have been applied. Check the current state before retrying, or you may duplicate it.";
|
|
7963
|
-
const NEVER_SENT_NOTE = "The request never reached Zendesk, so nothing was applied. Retrying is safe.";
|
|
7964
|
-
const REFUSED_NOTE = "Zendesk refused the request, so nothing was applied. Retrying is safe.";
|
|
7965
|
-
/** Only a write can be duplicated by a replay, so only a write carries a note. */
|
|
7966
|
-
const writeNote = (method, note) => method === "GET" ? "" : ` ${note}`;
|
|
7967
|
-
/**
|
|
7968
|
-
* A cause message can quote the URL it failed on — Node's `fetch` refuses a URL
|
|
7969
|
-
* carrying credentials by printing the whole thing, query string included — which
|
|
7970
|
-
* would smuggle back exactly what `describeTarget` drops. Same treatment for both,
|
|
7971
|
-
* so one function owns what a URL may look like in our output.
|
|
7972
|
-
*/
|
|
7973
|
-
const redactUrls = (text) => text.replace(URL_IN_TEXT, (found) => {
|
|
7974
|
-
try {
|
|
7975
|
-
return new URL(found).origin === "null" ? "[url]" : describeTarget(found);
|
|
7976
|
-
} catch {
|
|
7977
|
-
return "[url]";
|
|
7978
|
-
}
|
|
7979
|
-
});
|
|
7980
|
-
const failureDetail = (cause) => {
|
|
7981
|
-
if (!(cause instanceof Error)) return redactUrls(String(cause));
|
|
7982
|
-
const message = redactUrls(cause.message);
|
|
7983
|
-
const code = errorCode(cause);
|
|
7984
|
-
if (code !== void 0) return `${code}: ${message}`;
|
|
7985
|
-
return UNINFORMATIVE_NAMES.has(cause.name) ? message : `${cause.name}: ${message}`;
|
|
7986
|
-
};
|
|
7987
|
-
const networkErrorMessage = (method, target, attempts, cause) => {
|
|
7988
|
-
const base = `Network error on ${method} ${target} after ${attempts === 1 ? "1 attempt" : `${attempts} attempts`}: ${failureDetail(cause)}`;
|
|
7989
|
-
const note = classifyNetworkError(cause) === "pre-send" ? NEVER_SENT_NOTE : MAY_HAVE_APPLIED_NOTE;
|
|
7990
|
-
return toAscii(method === "GET" ? base : `${base}.${writeNote(method, note)}`);
|
|
7991
|
-
};
|
|
7992
|
-
const createZendeskNetworkError = (method, target, attempts, cause) => Object.assign(new Error(networkErrorMessage(method, target, attempts, cause), { cause }), {
|
|
7993
|
-
name: "ZendeskNetworkError",
|
|
7994
|
-
method,
|
|
7995
|
-
target,
|
|
7996
|
-
attempts
|
|
7997
|
-
});
|
|
7998
|
-
/** Exponential backoff with equal jitter: half the window fixed, half random. */
|
|
7999
|
-
const computeBackoffMs = (attempt, random) => {
|
|
8000
|
-
const window = Math.min(BASE_DELAY_MS * 2 ** (attempt - 1), MAX_DELAY_MS);
|
|
8001
|
-
return Math.round(window / 2 + window * random() / 2);
|
|
8002
|
-
};
|
|
8003
|
-
/** `Retry-After` in ms — delay-seconds or HTTP-date. */
|
|
8004
|
-
const parseRetryAfter = (header, now = Date.now()) => {
|
|
8005
|
-
if (header === null) return void 0;
|
|
8006
|
-
const value = header.trim();
|
|
8007
|
-
if (DELAY_SECONDS.test(value)) return Number(value) * 1e3;
|
|
8008
|
-
if (!HTTP_DATE_START.test(value)) return void 0;
|
|
8009
|
-
const date = Date.parse(value);
|
|
8010
|
-
if (Number.isNaN(date)) return void 0;
|
|
8011
|
-
return Math.max(0, date - now);
|
|
8012
|
-
};
|
|
8013
|
-
/**
|
|
8014
|
-
* How long to wait before replaying this response, or undefined to accept it.
|
|
8015
|
-
* `Retry-After` wins over backoff wherever it appears — Zendesk sends it on a 503
|
|
8016
|
-
* during maintenance as well as on a 429 — and a value past the cap means the
|
|
8017
|
-
* response is surfaced rather than parking the call for that long.
|
|
8018
|
-
*/
|
|
8019
|
-
const retryDelayFor = (response, policy, attempt, random) => {
|
|
8020
|
-
if (!(response.status === 429 || response.status >= 500 && policy.serverErrors)) return void 0;
|
|
8021
|
-
const retryAfter = parseRetryAfter(response.headers.get("retry-after"));
|
|
8022
|
-
if (retryAfter === void 0) return computeBackoffMs(attempt, random);
|
|
8023
|
-
return retryAfter > MAX_RETRY_AFTER_MS ? void 0 : retryAfter;
|
|
8024
|
-
};
|
|
8025
|
-
/**
|
|
8026
|
-
* Runs `attempt` until it succeeds, hits a terminal outcome, or spends the
|
|
8027
|
-
* attempt budget. Returns the last response for the caller to inspect (a
|
|
8028
|
-
* non-ok status is still the caller's to turn into a `ZendeskApiError`), and
|
|
8029
|
-
* throws `ZendeskNetworkError` when no response was ever obtained.
|
|
8030
|
-
*/
|
|
8031
|
-
const fetchWithRetry = async (attempt, method, target, deps = defaultRetryDeps) => {
|
|
8032
|
-
const policy = POLICIES[method];
|
|
8033
|
-
for (let tries = 1;; tries += 1) {
|
|
8034
|
-
const last = tries >= MAX_ATTEMPTS;
|
|
8035
|
-
let response;
|
|
8036
|
-
try {
|
|
8037
|
-
response = await attempt();
|
|
8038
|
-
} catch (err) {
|
|
8039
|
-
const replayable = policy.network === "any" || classifyNetworkError(err) === policy.network;
|
|
8040
|
-
if (last || !replayable) throw createZendeskNetworkError(method, target, tries, err);
|
|
8041
|
-
await deps.sleep(computeBackoffMs(tries, deps.random));
|
|
8042
|
-
continue;
|
|
8043
|
-
}
|
|
8044
|
-
if (last) return response;
|
|
8045
|
-
const delayMs = retryDelayFor(response, policy, tries, deps.random);
|
|
8046
|
-
if (delayMs === void 0) return response;
|
|
8047
|
-
await response.text().catch(() => void 0);
|
|
8048
|
-
await deps.sleep(delayMs);
|
|
8049
|
-
}
|
|
8050
|
-
};
|
|
8051
|
-
//#endregion
|
|
8052
|
-
//#region src/client/zendesk-api.ts
|
|
8053
|
-
var ZendeskApiError = class ZendeskApiError extends Error {
|
|
8054
|
-
status;
|
|
8055
|
-
statusText;
|
|
8056
|
-
body;
|
|
8057
|
-
method;
|
|
8058
|
-
constructor(status, statusText, body, method) {
|
|
8059
|
-
super(ZendeskApiError.buildMessage(status, statusText, body, method));
|
|
8060
|
-
this.status = status;
|
|
8061
|
-
this.statusText = statusText;
|
|
8062
|
-
this.body = body;
|
|
8063
|
-
this.method = method;
|
|
8064
|
-
this.name = "ZendeskApiError";
|
|
8065
|
-
}
|
|
8066
|
-
static buildMessage(status, statusText, body, method) {
|
|
8067
|
-
switch (status) {
|
|
8068
|
-
case 401: return "Authentication failed. Your Zendesk token may be expired or invalid. Re-authenticate to get a new token.";
|
|
8069
|
-
case 403: return "Permission denied. Your Zendesk account does not have access to this resource.";
|
|
8070
|
-
case 404: return `Resource not found. Please verify the ID is correct. (${statusText})`;
|
|
8071
|
-
case 422: return `Validation error: ${body}`;
|
|
8072
|
-
case 429: return `Rate limit exceeded. Please wait before making more requests.${writeNote(method, REFUSED_NOTE)}`;
|
|
8073
|
-
default: {
|
|
8074
|
-
const message = `Zendesk API error ${status}: ${statusText}. ${body}`;
|
|
8075
|
-
return status >= 500 ? `${message}${writeNote(method, MAY_HAVE_APPLIED_NOTE)}` : message;
|
|
8076
|
-
}
|
|
8077
|
-
}
|
|
8078
|
-
}
|
|
8079
|
-
};
|
|
8080
|
-
const buildAuthHeader = (token) => `Bearer ${token}`;
|
|
8081
|
-
const buildUrl = (base, path, params) => {
|
|
8082
|
-
const url = new URL(`${base}${path}`);
|
|
8083
|
-
if (params) for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
|
|
8084
|
-
return url.toString();
|
|
8085
|
-
};
|
|
8086
|
-
const performFetch = (method, url, init, timeoutMs = REQUEST_TIMEOUT_MS) => fetchWithRetry(() => fetch(url, {
|
|
8087
|
-
...init,
|
|
8088
|
-
method,
|
|
8089
|
-
signal: deadlineSignal(timeoutMs)
|
|
8090
|
-
}), method, describeTarget(url));
|
|
8091
|
-
const executeRequest = async (url, token, options = {}) => {
|
|
8092
|
-
const { method = "GET", body } = options;
|
|
8093
|
-
const headers = {
|
|
8094
|
-
Authorization: buildAuthHeader(token),
|
|
8095
|
-
Accept: "application/json"
|
|
8096
|
-
};
|
|
8097
|
-
if (body) headers["Content-Type"] = "application/json";
|
|
8098
|
-
const init = { headers };
|
|
8099
|
-
if (body) init.body = JSON.stringify(body);
|
|
8100
|
-
const response = await performFetch(method, url, init);
|
|
8101
|
-
if (!response.ok) {
|
|
8102
|
-
const responseBody = await response.text();
|
|
8103
|
-
throw new ZendeskApiError(response.status, response.statusText, responseBody, method);
|
|
8104
|
-
}
|
|
8105
|
-
if (response.status === 204) return {};
|
|
8106
|
-
return response.json();
|
|
8107
|
-
};
|
|
8108
|
-
const zendeskGet = (subdomain, token, path, params) => {
|
|
8109
|
-
const url = buildUrl(getBaseUrl(subdomain), path, params);
|
|
8110
|
-
return executeRequest(url, token);
|
|
8111
|
-
};
|
|
8112
|
-
const zendeskPost = (subdomain, token, path, body) => {
|
|
8113
|
-
const url = buildUrl(getBaseUrl(subdomain), path);
|
|
8114
|
-
return executeRequest(url, token, {
|
|
8115
|
-
method: "POST",
|
|
8116
|
-
body
|
|
8117
|
-
});
|
|
8118
|
-
};
|
|
8119
|
-
const zendeskPut = (subdomain, token, path, body) => {
|
|
8120
|
-
const url = buildUrl(getBaseUrl(subdomain), path);
|
|
8121
|
-
return executeRequest(url, token, {
|
|
8122
|
-
method: "PUT",
|
|
8123
|
-
body
|
|
8124
|
-
});
|
|
8125
|
-
};
|
|
8126
|
-
const helpCenterGet = (subdomain, token, path, params) => {
|
|
8127
|
-
const url = buildUrl(getHelpCenterBaseUrl(subdomain), path, params);
|
|
8128
|
-
return executeRequest(url, token);
|
|
8129
|
-
};
|
|
8130
|
-
const helpCenterPost = (subdomain, token, path, body) => {
|
|
8131
|
-
const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
|
|
8132
|
-
return executeRequest(url, token, {
|
|
8133
|
-
method: "POST",
|
|
8134
|
-
body
|
|
8135
|
-
});
|
|
8136
|
-
};
|
|
8137
|
-
const helpCenterPut = (subdomain, token, path, body) => {
|
|
8138
|
-
const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
|
|
8139
|
-
return executeRequest(url, token, {
|
|
8140
|
-
method: "PUT",
|
|
8141
|
-
body
|
|
8142
|
-
});
|
|
8143
|
-
};
|
|
8144
|
-
const helpCenterDelete = (subdomain, token, path) => {
|
|
8145
|
-
const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
|
|
8146
|
-
return executeRequest(url, token, { method: "DELETE" });
|
|
8147
|
-
};
|
|
8148
|
-
const fetchZendeskBinary = async (subdomain, token, contentUrl) => {
|
|
8149
|
-
const expectedHost = `${subdomain}.zendesk.com`;
|
|
8150
|
-
const headers = {};
|
|
8151
|
-
if (new URL(contentUrl).hostname === expectedHost) headers["Authorization"] = buildAuthHeader(token);
|
|
8152
|
-
const response = await performFetch("GET", contentUrl, { headers }, TRANSFER_TIMEOUT_MS);
|
|
8153
|
-
if (!response.ok) {
|
|
8154
|
-
const body = await response.text();
|
|
8155
|
-
throw new ZendeskApiError(response.status, response.statusText, body, "GET");
|
|
8156
|
-
}
|
|
8157
|
-
const contentType = response.headers.get("content-type") ?? "application/octet-stream";
|
|
8158
|
-
const arrayBuffer = await response.arrayBuffer();
|
|
8159
|
-
return {
|
|
8160
|
-
data: Buffer.from(arrayBuffer),
|
|
8161
|
-
contentType
|
|
8162
|
-
};
|
|
8163
|
-
};
|
|
8164
|
-
const zendeskUpload = async (subdomain, token, filename, data, contentType, uploadToken) => {
|
|
8165
|
-
const params = { filename };
|
|
8166
|
-
if (uploadToken) params["token"] = uploadToken;
|
|
8167
|
-
const url = buildUrl(getBaseUrl(subdomain), "/uploads", params);
|
|
8168
|
-
const response = await performFetch("POST", url, {
|
|
8169
|
-
headers: {
|
|
8170
|
-
Authorization: buildAuthHeader(token),
|
|
8171
|
-
"Content-Type": contentType
|
|
8172
|
-
},
|
|
8173
|
-
body: data
|
|
8174
|
-
}, TRANSFER_TIMEOUT_MS);
|
|
8175
|
-
if (!response.ok) {
|
|
8176
|
-
const responseBody = await response.text();
|
|
8177
|
-
throw new ZendeskApiError(response.status, response.statusText, responseBody, "POST");
|
|
8178
|
-
}
|
|
8179
|
-
return response.json();
|
|
8180
|
-
};
|
|
8181
|
-
const helpCenterUpload = async (subdomain, token, path, formData) => {
|
|
8182
|
-
const url = buildUrl(getHelpCenterBaseUrl(subdomain), path);
|
|
8183
|
-
const response = await performFetch("POST", url, {
|
|
8184
|
-
headers: { Authorization: buildAuthHeader(token) },
|
|
8185
|
-
body: formData
|
|
8186
|
-
}, TRANSFER_TIMEOUT_MS);
|
|
8187
|
-
if (!response.ok) {
|
|
8188
|
-
const responseBody = await response.text();
|
|
8189
|
-
throw new ZendeskApiError(response.status, response.statusText, responseBody, "POST");
|
|
8190
|
-
}
|
|
8191
|
-
return response.json();
|
|
8192
|
-
};
|
|
8193
|
-
//#endregion
|
|
8194
|
-
//#region src/client/brands.ts
|
|
8195
|
-
const BRAND_SUBDOMAIN = /^[a-z0-9][a-z0-9-]*$/;
|
|
8196
|
-
/**
|
|
8197
|
-
* Fetch every brand of the account (cursor-paginated). `page[size]` is sent on
|
|
8198
|
-
* the FIRST request too: without it Zendesk falls back to OFFSET pagination,
|
|
8199
|
-
* which answers without `meta.after_cursor`, and the loop below would stop
|
|
8200
|
-
* after page 1 — a brand past the first 100 would resolve as Unknown.
|
|
8201
|
-
*/
|
|
8202
|
-
const fetchAllBrands = async (subdomain, token) => {
|
|
8203
|
-
const brands = [];
|
|
8204
|
-
let cursor;
|
|
8205
|
-
do {
|
|
8206
|
-
const response = await zendeskGet(subdomain, token, "/brands", {
|
|
8207
|
-
"page[size]": String(100),
|
|
8208
|
-
...cursor ? { "page[after]": cursor } : {}
|
|
8209
|
-
});
|
|
8210
|
-
brands.push(...response.brands ?? []);
|
|
8211
|
-
const meta = response.meta;
|
|
8212
|
-
if (meta?.has_more) {
|
|
8213
|
-
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.");
|
|
8214
|
-
cursor = meta.after_cursor;
|
|
8215
|
-
} else cursor = void 0;
|
|
8216
|
-
} while (cursor);
|
|
8217
|
-
return brands;
|
|
8218
|
-
};
|
|
8219
|
-
const createBrandSubdomainResolver = (subdomain, getToken) => {
|
|
8220
|
-
let cached;
|
|
8221
|
-
const brands = () => {
|
|
8222
|
-
if (!cached || Date.now() - cached.at >= 3e5) {
|
|
8223
|
-
const promise = (async () => fetchAllBrands(subdomain, await getToken()))();
|
|
8224
|
-
cached = {
|
|
8225
|
-
at: Date.now(),
|
|
8226
|
-
promise
|
|
8227
|
-
};
|
|
8228
|
-
promise.catch(() => {
|
|
8229
|
-
cached = void 0;
|
|
8230
|
-
});
|
|
8231
|
-
}
|
|
8232
|
-
return cached.promise;
|
|
8233
|
-
};
|
|
8234
|
-
return async (idOrSubdomain) => {
|
|
8235
|
-
const all = await brands();
|
|
8236
|
-
const brand = all.find((b) => String(b.id) === idOrSubdomain) ?? all.find((b) => b.subdomain.toLowerCase() === idOrSubdomain.toLowerCase());
|
|
8237
|
-
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.`);
|
|
8238
|
-
if (!BRAND_SUBDOMAIN.test(brand.subdomain)) throw new Error(`Brand ${brand.id} has an unexpected subdomain; refusing to send a request to it.`);
|
|
8239
|
-
return brand.subdomain;
|
|
8240
|
-
};
|
|
8241
|
-
};
|
|
8242
|
-
//#endregion
|
|
8243
|
-
//#region node_modules/.pnpm/@modelcontextprotocol+server@2.2.0/node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
|
|
7985
|
+
//#region node_modules/.pnpm/@modelcontextprotocol+server@2.3.1/node_modules/@modelcontextprotocol/server/dist/chunk-Br0eD_fh.mjs
|
|
8244
7986
|
var __create = Object.create;
|
|
8245
7987
|
var __defProp = Object.defineProperty;
|
|
8246
7988
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -8272,7 +8014,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
8272
8014
|
enumerable: true
|
|
8273
8015
|
}) : target, mod));
|
|
8274
8016
|
//#endregion
|
|
8275
|
-
//#region node_modules/.pnpm/@modelcontextprotocol+server@2.
|
|
8017
|
+
//#region node_modules/.pnpm/@modelcontextprotocol+server@2.3.1/node_modules/@modelcontextprotocol/server/dist/dialects-DoSzNhcb.mjs
|
|
8276
8018
|
/**
|
|
8277
8019
|
* Canonical `$schema` URIs per supported dialect (http + https variants, trailing-`#` stripped).
|
|
8278
8020
|
*/
|
|
@@ -9935,7 +9677,7 @@ const OAuthTokenRevocationRequestSchema = object({
|
|
|
9935
9677
|
token_type_hint: string$2().optional()
|
|
9936
9678
|
}).strip();
|
|
9937
9679
|
//#endregion
|
|
9938
|
-
//#region node_modules/.pnpm/@modelcontextprotocol+server@2.
|
|
9680
|
+
//#region node_modules/.pnpm/@modelcontextprotocol+server@2.3.1/node_modules/@modelcontextprotocol/server/dist/src-Cqbh3MYc.mjs
|
|
9939
9681
|
/**
|
|
9940
9682
|
* Cross-bundle `instanceof` support for the SDK error classes.
|
|
9941
9683
|
*
|
|
@@ -10347,6 +10089,26 @@ var SdkHttpError = class extends SdkError {
|
|
|
10347
10089
|
return this.data.statusText;
|
|
10348
10090
|
}
|
|
10349
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
|
+
}
|
|
10350
10112
|
function isPlainObject$7(value) {
|
|
10351
10113
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
10352
10114
|
}
|
|
@@ -10429,6 +10191,13 @@ function missingClientCapabilities(required, declared) {
|
|
|
10429
10191
|
* are ISO dates, so lexicographic comparison orders them chronologically.
|
|
10430
10192
|
*/
|
|
10431
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];
|
|
10432
10201
|
/** Whether the given protocol revision belongs to the modern (2026-07-28+) era. */
|
|
10433
10202
|
function isModernProtocolVersion(version) {
|
|
10434
10203
|
return version >= FIRST_MODERN_PROTOCOL_VERSION;
|
|
@@ -12990,7 +12759,7 @@ function build() {
|
|
|
12990
12759
|
const subscriptionsListenParamsShape = { notifications: SubscriptionFilterSchema$1 };
|
|
12991
12760
|
const SubscriptionsListenRequestSchema$1 = wireRequest("subscriptions/listen", subscriptionsListenParamsShape);
|
|
12992
12761
|
/**
|
|
12993
|
-
* Anchor
|
|
12762
|
+
* Anchor SubscriptionsListenResultMetaObject — required subscriptionId stamp on
|
|
12994
12763
|
* the graceful-close result. Extends `ResultMetaObject` since spec PR
|
|
12995
12764
|
* #3002 (composed, so the serverInfo key and its leniency stay single-sourced).
|
|
12996
12765
|
*/
|
|
@@ -13263,7 +13032,8 @@ function build() {
|
|
|
13263
13032
|
ListResourceTemplatesResultResponseSchema: wireResultResponse(ListResourceTemplatesResultSchema$1),
|
|
13264
13033
|
ReadResourceResultResponseSchema: wireResultResponse(union([ReadResourceResultSchema$1, InputRequiredResultSchema])),
|
|
13265
13034
|
CompleteResultResponseSchema: wireResultResponse(CompleteResultSchema$1),
|
|
13266
|
-
DiscoverResultResponseSchema: wireResultResponse(DiscoverResultSchema$1)
|
|
13035
|
+
DiscoverResultResponseSchema: wireResultResponse(DiscoverResultSchema$1),
|
|
13036
|
+
SubscriptionsListenResultResponseSchema: wireResultResponse(SubscriptionsListenResultSchema$1)
|
|
13267
13037
|
};
|
|
13268
13038
|
}
|
|
13269
13039
|
let memo;
|
|
@@ -14007,7 +13777,71 @@ function isSpecRequestMethod(method) {
|
|
|
14007
13777
|
function isSpecNotificationMethod(method) {
|
|
14008
13778
|
return ALL_CODECS.some((codec) => codec.hasNotificationMethod(method));
|
|
14009
13779
|
}
|
|
13780
|
+
/** The names SEP-2663 re-defines for the Tasks extension on the era that removed them from core. */
|
|
13781
|
+
function isExtensionReusedRequestMethod(method) {
|
|
13782
|
+
return method === "tasks/get" || method === "tasks/cancel";
|
|
13783
|
+
}
|
|
14010
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
|
+
}
|
|
14011
13845
|
var schemas_exports = /* @__PURE__ */ __exportAll({
|
|
14012
13846
|
AnnotationsSchema: () => AnnotationsSchema,
|
|
14013
13847
|
AudioContentSchema: () => AudioContentSchema,
|
|
@@ -14212,6 +14046,8 @@ function assertCompleteRequestPrompt(request) {
|
|
|
14212
14046
|
function assertCompleteRequestResourceTemplate(request) {
|
|
14213
14047
|
if (request.params.ref.type !== "ref/resource") throw new TypeError(`Expected CompleteRequestResourceTemplate, but got ${request.params.ref.type}`);
|
|
14214
14048
|
}
|
|
14049
|
+
/** The fixed prefix every custom-parameter header carries. */
|
|
14050
|
+
const MCP_PARAM_HEADER_PREFIX = "Mcp-Param-";
|
|
14215
14051
|
/** The schema-extension property name a tool's `inputSchema` carries. */
|
|
14216
14052
|
const X_MCP_HEADER_KEY = "x-mcp-header";
|
|
14217
14053
|
/**
|
|
@@ -14334,8 +14170,566 @@ const OBJECT_VALUED_SUBSCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
|
|
|
14334
14170
|
function pathName(path) {
|
|
14335
14171
|
return path.length === 0 ? "<root>" : path.join(".");
|
|
14336
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;
|
|
14337
14363
|
ProtocolErrorCode.InvalidRequest, ProtocolErrorCode.UnsupportedProtocolVersion, ProtocolErrorCode.InvalidParams, ProtocolErrorCode.MethodNotFound, ProtocolErrorCode.InvalidParams, ProtocolErrorCode.MissingRequiredClientCapability;
|
|
14338
|
-
|
|
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
|
+
}
|
|
14339
14733
|
/**
|
|
14340
14734
|
* Internal Zod schema utilities for protocol handling.
|
|
14341
14735
|
* These are used internally by the SDK for protocol message validation.
|
|
@@ -15028,6 +15422,17 @@ function withRequestStateValue(ctx, value) {
|
|
|
15028
15422
|
}
|
|
15029
15423
|
};
|
|
15030
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
|
+
}
|
|
15031
15436
|
/**
|
|
15032
15437
|
* Implements MCP protocol framing on top of a pluggable transport, including
|
|
15033
15438
|
* features like request/response linking, notifications, and progress.
|
|
@@ -15039,6 +15444,8 @@ var Protocol = class {
|
|
|
15039
15444
|
_transport;
|
|
15040
15445
|
_requestMessageId = 0;
|
|
15041
15446
|
_requestHandlers = /* @__PURE__ */ new Map();
|
|
15447
|
+
/** Methods registered with an explicit schema; the era gate in `_onrequest` reads it for the Tasks extension names. */
|
|
15448
|
+
_customSchemaRequestMethods = /* @__PURE__ */ new Set();
|
|
15042
15449
|
_requestHandlerAbortControllers = /* @__PURE__ */ new Map();
|
|
15043
15450
|
_notificationHandlers = /* @__PURE__ */ new Map();
|
|
15044
15451
|
_responseHandlers = /* @__PURE__ */ new Map();
|
|
@@ -15052,7 +15459,11 @@ var Protocol = class {
|
|
|
15052
15459
|
* (`Client.connect`, `Server._oninitialize`).
|
|
15053
15460
|
*/
|
|
15054
15461
|
_negotiatedProtocolVersion;
|
|
15055
|
-
static {
|
|
15462
|
+
static {
|
|
15463
|
+
writeNegotiatedProtocolVersion = (instance, version) => {
|
|
15464
|
+
instance._negotiatedProtocolVersion = version;
|
|
15465
|
+
};
|
|
15466
|
+
}
|
|
15056
15467
|
_supportedProtocolVersions;
|
|
15057
15468
|
/**
|
|
15058
15469
|
* Callback for when the connection is closed for any reason.
|
|
@@ -15316,7 +15727,7 @@ var Protocol = class {
|
|
|
15316
15727
|
return;
|
|
15317
15728
|
}
|
|
15318
15729
|
}
|
|
15319
|
-
if (isSpecRequestMethod(request.method) && !codec.hasRequestMethod(request.method)) {
|
|
15730
|
+
if (isSpecRequestMethod(request.method) && !codec.hasRequestMethod(request.method) && !(isExtensionReusedRequestMethod(request.method) && this._customSchemaRequestMethods.has(request.method))) {
|
|
15320
15731
|
sendErrorResponse(ProtocolErrorCode.MethodNotFound, "Method not found");
|
|
15321
15732
|
return;
|
|
15322
15733
|
}
|
|
@@ -15449,8 +15860,11 @@ var Protocol = class {
|
|
|
15449
15860
|
}
|
|
15450
15861
|
request(request, schemaOrOptions, maybeOptions) {
|
|
15451
15862
|
const codec = this._resolveOutboundCodec(request.method);
|
|
15863
|
+
if (isStandardSchema(schemaOrOptions)) {
|
|
15864
|
+
if (!isExtensionReusedRequestMethod(request.method)) this._assertOutboundRequestInEra(codec, request.method);
|
|
15865
|
+
return this._requestWithSchemaViaCodec(codec, request, schemaOrOptions, maybeOptions);
|
|
15866
|
+
}
|
|
15452
15867
|
this._assertOutboundRequestInEra(codec, request.method);
|
|
15453
|
-
if (isStandardSchema(schemaOrOptions)) return this._requestWithSchemaViaCodec(codec, request, schemaOrOptions, maybeOptions);
|
|
15454
15868
|
const validate = codecResultValidator(codec, request.method);
|
|
15455
15869
|
if (validate === void 0) throw new TypeError(`'${request.method}' is not a spec method; pass a result schema as the second argument to request().`);
|
|
15456
15870
|
return this._requestWithSchemaViaCodec(codec, request, validate, schemaOrOptions);
|
|
@@ -15694,6 +16108,8 @@ var Protocol = class {
|
|
|
15694
16108
|
return maybeHandler(parsed.data, ctx);
|
|
15695
16109
|
};
|
|
15696
16110
|
else throw new TypeError("setRequestHandler: handler is required");
|
|
16111
|
+
if (typeof schemasOrHandler === "function") this._customSchemaRequestMethods.delete(method);
|
|
16112
|
+
else this._customSchemaRequestMethods.add(method);
|
|
15697
16113
|
this._requestHandlers.set(method, this._wrapHandler(method, stored));
|
|
15698
16114
|
}
|
|
15699
16115
|
/**
|
|
@@ -15722,6 +16138,7 @@ var Protocol = class {
|
|
|
15722
16138
|
*/
|
|
15723
16139
|
removeRequestHandler(method) {
|
|
15724
16140
|
this._requestHandlers.delete(method);
|
|
16141
|
+
this._customSchemaRequestMethods.delete(method);
|
|
15725
16142
|
}
|
|
15726
16143
|
/**
|
|
15727
16144
|
* Asserts that a request handler has not already been set for the given method, in preparation for a new one being automatically installed.
|
|
@@ -15773,7 +16190,7 @@ function mergeCapabilities(base, additional) {
|
|
|
15773
16190
|
}
|
|
15774
16191
|
return result;
|
|
15775
16192
|
}
|
|
15776
|
-
function isPlainObject$
|
|
16193
|
+
function isPlainObject$8(value) {
|
|
15777
16194
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
15778
16195
|
}
|
|
15779
16196
|
/**
|
|
@@ -15787,12 +16204,12 @@ function isPlainObject$2(value) {
|
|
|
15787
16204
|
function partitionInputResponses(inputResponses) {
|
|
15788
16205
|
const accepted = {};
|
|
15789
16206
|
const droppedKeys = [];
|
|
15790
|
-
if (!isPlainObject$
|
|
16207
|
+
if (!isPlainObject$8(inputResponses)) return {
|
|
15791
16208
|
accepted,
|
|
15792
16209
|
droppedKeys
|
|
15793
16210
|
};
|
|
15794
16211
|
for (const [key, entry] of Object.entries(inputResponses)) {
|
|
15795
|
-
if (!isPlainObject$
|
|
16212
|
+
if (!isPlainObject$8(entry) || "method" in entry || "result" in entry) {
|
|
15796
16213
|
droppedKeys.push(key);
|
|
15797
16214
|
continue;
|
|
15798
16215
|
}
|
|
@@ -16303,7 +16720,7 @@ function normalizeRawShapeSchema(schema) {
|
|
|
16303
16720
|
return schema;
|
|
16304
16721
|
}
|
|
16305
16722
|
//#endregion
|
|
16306
|
-
//#region node_modules/.pnpm/@modelcontextprotocol+server@2.
|
|
16723
|
+
//#region node_modules/.pnpm/@modelcontextprotocol+server@2.3.1/node_modules/@modelcontextprotocol/server/dist/ajvProvider-CEoC__sr.mjs
|
|
16307
16724
|
var require_code$1 = /* @__PURE__ */ __commonJSMin(((exports) => {
|
|
16308
16725
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16309
16726
|
exports.regexpCode = exports.getEsmExportName = exports.getProperty = exports.safeStringify = exports.stringify = exports.strConcat = exports.addCodeArg = exports.str = exports._ = exports.nil = exports._Code = exports.Name = exports.IDENTIFIER = exports._CodeOrName = void 0;
|
|
@@ -23438,7 +23855,7 @@ var AjvJsonSchemaValidator = class {
|
|
|
23438
23855
|
};
|
|
23439
23856
|
import_ajv.Ajv;
|
|
23440
23857
|
//#endregion
|
|
23441
|
-
//#region node_modules/.pnpm/@modelcontextprotocol+server@2.
|
|
23858
|
+
//#region node_modules/.pnpm/@modelcontextprotocol+server@2.3.1/node_modules/@modelcontextprotocol/server/dist/mcp-DIH4cS6P.mjs
|
|
23442
23859
|
const COMPLETABLE_SYMBOL = Symbol.for("mcp.completable");
|
|
23443
23860
|
/**
|
|
23444
23861
|
* Checks if a schema is completable (has completion metadata).
|
|
@@ -23460,6 +23877,418 @@ function armSseKeepAlive(intervalMs, onTick) {
|
|
|
23460
23877
|
timer.unref?.();
|
|
23461
23878
|
return timer;
|
|
23462
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
|
+
}
|
|
24283
|
+
function comparableResource(value) {
|
|
24284
|
+
const text = String(value);
|
|
24285
|
+
const hash = text.indexOf("#");
|
|
24286
|
+
return (hash === -1 ? text : text.slice(0, hash)).replace(/\/$/, "");
|
|
24287
|
+
}
|
|
24288
|
+
function sameResource(reported, expected) {
|
|
24289
|
+
if (!reported) return false;
|
|
24290
|
+
return comparableResource(reported) === comparableResource(expected);
|
|
24291
|
+
}
|
|
23463
24292
|
function headerQuotedValue(value) {
|
|
23464
24293
|
return value.replaceAll(/[\\"]/g, String.raw`\$&`).replaceAll(/[^\u0020-\u007E]/g, " ");
|
|
23465
24294
|
}
|
|
@@ -23479,6 +24308,38 @@ function buildWwwAuthenticateHeader(errorCode, description, requiredScopes, reso
|
|
|
23479
24308
|
return header;
|
|
23480
24309
|
}
|
|
23481
24310
|
/**
|
|
24311
|
+
* Validate a raw `Authorization` header value as a Bearer token and return
|
|
24312
|
+
* the verified {@link AuthInfo}.
|
|
24313
|
+
*
|
|
24314
|
+
* The runtime-neutral core of Bearer authentication: it parses the header,
|
|
24315
|
+
* runs the verifier, compares the token's resource with `expectedResource`,
|
|
24316
|
+
* enforces `requiredScopes`, and rejects tokens without an
|
|
24317
|
+
* expiration or past it. On any failure it throws an {@link OAuthError} —
|
|
24318
|
+
* pass that to {@link bearerAuthChallengeResponse} for the matching HTTP
|
|
24319
|
+
* answer, or use {@link requireBearerAuth} to get both steps as one call.
|
|
24320
|
+
*
|
|
24321
|
+
* Framework adapters build on this: `requireBearerAuth` from
|
|
24322
|
+
* `@modelcontextprotocol/express` feeds it `req.headers.authorization`.
|
|
24323
|
+
*/
|
|
24324
|
+
async function verifyBearerToken(authorizationHeader, options) {
|
|
24325
|
+
const { verifier, requiredScopes = [], expectedResource } = options;
|
|
24326
|
+
if (!authorizationHeader) throw new OAuthError(OAuthErrorCode.InvalidToken, "Missing Authorization header");
|
|
24327
|
+
const [type, token] = authorizationHeader.split(" ");
|
|
24328
|
+
if (type?.toLowerCase() !== "bearer" || !token) throw new OAuthError(OAuthErrorCode.InvalidToken, "Invalid Authorization header format, expected 'Bearer TOKEN'");
|
|
24329
|
+
const authInfo = await verifier.verifyAccessToken(token);
|
|
24330
|
+
if (expectedResource !== void 0 && !sameResource(authInfo.resource, expectedResource)) throw new OAuthError(OAuthErrorCode.InvalidToken, "Token was not issued for this resource");
|
|
24331
|
+
if (requiredScopes.length > 0) {
|
|
24332
|
+
if (!requiredScopes.every((scope) => authInfo.scopes.includes(scope))) throw new OAuthError(OAuthErrorCode.InsufficientScope, "Insufficient scope");
|
|
24333
|
+
}
|
|
24334
|
+
if (typeof authInfo.expiresAt !== "number" || Number.isNaN(authInfo.expiresAt)) throw new OAuthError(OAuthErrorCode.InvalidToken, "Token has no expiration time");
|
|
24335
|
+
else if (authInfo.expiresAt < Date.now() / 1e3) throw new OAuthError(OAuthErrorCode.InvalidToken, "Token has expired");
|
|
24336
|
+
if (options.resourceMetadataUrl !== void 0 && authInfo.resourceMetadataUrl === void 0) return {
|
|
24337
|
+
...authInfo,
|
|
24338
|
+
resourceMetadataUrl: options.resourceMetadataUrl
|
|
24339
|
+
};
|
|
24340
|
+
return authInfo;
|
|
24341
|
+
}
|
|
24342
|
+
/**
|
|
23482
24343
|
* Build the HTTP answer for a Bearer authentication failure.
|
|
23483
24344
|
*
|
|
23484
24345
|
* Maps an {@link OAuthError} to its status — `401` for `invalid_token` and
|
|
@@ -23737,6 +24598,41 @@ const INPUT_REQUIRED_CAPABLE_METHODS = /* @__PURE__ */ new Set([
|
|
|
23737
24598
|
"prompts/get",
|
|
23738
24599
|
"resources/read"
|
|
23739
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
|
+
}
|
|
23740
24636
|
/**
|
|
23741
24637
|
* An MCP server on top of a pluggable transport.
|
|
23742
24638
|
*
|
|
@@ -23747,7 +24643,18 @@ const INPUT_REQUIRED_CAPABLE_METHODS = /* @__PURE__ */ new Set([
|
|
|
23747
24643
|
var Server = class extends Protocol {
|
|
23748
24644
|
_clientCapabilities;
|
|
23749
24645
|
_clientVersion;
|
|
23750
|
-
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
|
+
}
|
|
23751
24658
|
_capabilities;
|
|
23752
24659
|
_instructions;
|
|
23753
24660
|
_jsonSchemaValidator;
|
|
@@ -23791,6 +24698,11 @@ var Server = class extends Protocol {
|
|
|
23791
24698
|
if (modernProtocolVersions(this._supportedProtocolVersions).length > 0) this.setRequestHandler("server/discover", () => this._ondiscover());
|
|
23792
24699
|
if (this._capabilities.logging) this._registerLoggingHandler();
|
|
23793
24700
|
}
|
|
24701
|
+
/** Attaches to the given transport; rejects while this instance is connected to another one. */
|
|
24702
|
+
async connect(transport) {
|
|
24703
|
+
if (this.transport !== void 0) throw new SdkError(SdkErrorCode.AlreadyConnected, "Use a separate Server instance per connection: this instance is already connected to a transport. To connect it to a new transport, call close() first.");
|
|
24704
|
+
await super.connect(transport);
|
|
24705
|
+
}
|
|
23794
24706
|
/**
|
|
23795
24707
|
* Registers the built-in `logging/setLevel` request handler.
|
|
23796
24708
|
*
|
|
@@ -24336,6 +25248,30 @@ var Server = class extends Protocol {
|
|
|
24336
25248
|
function discoverAdvertisedCapabilities(capabilities) {
|
|
24337
25249
|
return { ...capabilities };
|
|
24338
25250
|
}
|
|
25251
|
+
function toolInputElementCount(value, max) {
|
|
25252
|
+
let count = 0;
|
|
25253
|
+
const stack = [value];
|
|
25254
|
+
while (stack.length > 0) {
|
|
25255
|
+
const node = stack.pop();
|
|
25256
|
+
if (node === null || typeof node !== "object") continue;
|
|
25257
|
+
if (Array.isArray(node)) for (const child of node) {
|
|
25258
|
+
if (++count > max) return count;
|
|
25259
|
+
if (child !== null && typeof child === "object") stack.push(child);
|
|
25260
|
+
}
|
|
25261
|
+
else for (const key in node) {
|
|
25262
|
+
if (!Object.prototype.hasOwnProperty.call(node, key)) continue;
|
|
25263
|
+
if (++count > max) return count;
|
|
25264
|
+
const child = node[key];
|
|
25265
|
+
if (child !== null && typeof child === "object") stack.push(child);
|
|
25266
|
+
}
|
|
25267
|
+
}
|
|
25268
|
+
return count;
|
|
25269
|
+
}
|
|
25270
|
+
function resolveMaxToolInputElements(value) {
|
|
25271
|
+
if (value === void 0 || value === Infinity) return void 0;
|
|
25272
|
+
if (typeof value !== "number" || Number.isNaN(value) || value < 1) throw new RangeError(`maxToolInputElements must be a number of at least 1, or Infinity, got ${String(value)}`);
|
|
25273
|
+
return value;
|
|
25274
|
+
}
|
|
24339
25275
|
/**
|
|
24340
25276
|
* High-level MCP server that provides a simpler API for working with resources, tools, and prompts.
|
|
24341
25277
|
* For advanced usage (like sending notifications or setting custom request handlers), use the underlying
|
|
@@ -24354,16 +25290,12 @@ var McpServer = class {
|
|
|
24354
25290
|
* The underlying {@linkcode Server} instance, useful for advanced operations like sending notifications.
|
|
24355
25291
|
*/
|
|
24356
25292
|
server;
|
|
25293
|
+
_maxToolInputElements;
|
|
24357
25294
|
_registeredResources = {};
|
|
24358
25295
|
_registeredResourceTemplates = {};
|
|
24359
25296
|
_registeredTools = {};
|
|
24360
25297
|
_registeredPrompts = {};
|
|
24361
|
-
/**
|
|
24362
|
-
* Per-tool JSON-converted `inputSchema`, memoized so the SEP-2243
|
|
24363
|
-
* registration-time scan and the pre-dispatch validation step share one
|
|
24364
|
-
* conversion instead of paying it twice per request under the
|
|
24365
|
-
* per-request-factory `createMcpHandler` model.
|
|
24366
|
-
*/
|
|
25298
|
+
/** Per-tool JSON-converted `inputSchema`, filled on first use by `toolInputSchemaJson()`. */
|
|
24367
25299
|
_toolInputSchemaJson = {};
|
|
24368
25300
|
/**
|
|
24369
25301
|
* The JSON-serialized `inputSchema` of a registered tool, or `undefined`
|
|
@@ -24388,6 +25320,7 @@ var McpServer = class {
|
|
|
24388
25320
|
}
|
|
24389
25321
|
constructor(serverInfo, options) {
|
|
24390
25322
|
this.server = new Server(serverInfo, options);
|
|
25323
|
+
this._maxToolInputElements = resolveMaxToolInputElements(options?.maxToolInputElements);
|
|
24391
25324
|
if (options?.capabilities?.tools) this.setToolRequestHandlers();
|
|
24392
25325
|
if (options?.capabilities?.resources) this.setResourceRequestHandlers();
|
|
24393
25326
|
if (options?.capabilities?.prompts) this.setPromptRequestHandlers();
|
|
@@ -24459,7 +25392,7 @@ var McpServer = class {
|
|
|
24459
25392
|
name,
|
|
24460
25393
|
title: tool.title,
|
|
24461
25394
|
description: tool.description,
|
|
24462
|
-
inputSchema: tool.inputSchema ?
|
|
25395
|
+
inputSchema: tool.inputSchema ? convertListedInputSchema(name, tool.inputSchema) : EMPTY_OBJECT_JSON_SCHEMA,
|
|
24463
25396
|
annotations: tool.annotations,
|
|
24464
25397
|
icons: tool.icons,
|
|
24465
25398
|
execution: tool.execution,
|
|
@@ -24504,6 +25437,7 @@ var McpServer = class {
|
|
|
24504
25437
|
* Validates tool input arguments against the tool's input schema.
|
|
24505
25438
|
*/
|
|
24506
25439
|
async validateToolInput(tool, args, toolName) {
|
|
25440
|
+
if (this._maxToolInputElements !== void 0 && toolInputElementCount(args, this._maxToolInputElements) > this._maxToolInputElements) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid arguments for tool ${toolName}: arguments contain more than the maximum of ${this._maxToolInputElements} elements`);
|
|
24507
25441
|
if (!tool.inputSchema) return;
|
|
24508
25442
|
const parseResult = await validateStandardSchema(tool.inputSchema, args ?? {});
|
|
24509
25443
|
if (!parseResult.success) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Input validation error: Invalid arguments for tool ${toolName}: ${parseResult.error}`);
|
|
@@ -24777,19 +25711,19 @@ var McpServer = class {
|
|
|
24777
25711
|
}
|
|
24778
25712
|
_createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, icons, execution, scopeChallenge, _meta, handler) {
|
|
24779
25713
|
validateAndWarnToolName(name);
|
|
24780
|
-
if (inputSchema !== void 0) try {
|
|
24781
|
-
const json = standardSchemaToJsonSchema(inputSchema, "input");
|
|
24782
|
-
this._toolInputSchemaJson[name] = json;
|
|
24783
|
-
const scan = scanXMcpHeaderDeclarations(json);
|
|
24784
|
-
if (!scan.valid) console.warn(`[mcp-sdk] tool '${name}' carries an invalid x-mcp-header declaration and will be excluded by conforming Streamable HTTP clients: ${scan.reason}`);
|
|
24785
|
-
} catch {}
|
|
24786
25714
|
let currentHandler = handler;
|
|
25715
|
+
let outputSchemaJson;
|
|
24787
25716
|
const registeredTool = {
|
|
24788
25717
|
title,
|
|
24789
25718
|
description,
|
|
24790
25719
|
inputSchema,
|
|
24791
25720
|
outputSchema,
|
|
24792
|
-
outputSchemaJson
|
|
25721
|
+
get outputSchemaJson() {
|
|
25722
|
+
return outputSchemaJson ??= convertOutputSchemaJson(registeredTool.outputSchema);
|
|
25723
|
+
},
|
|
25724
|
+
set outputSchemaJson(value) {
|
|
25725
|
+
outputSchemaJson = value;
|
|
25726
|
+
},
|
|
24793
25727
|
annotations,
|
|
24794
25728
|
icons,
|
|
24795
25729
|
execution,
|
|
@@ -24950,6 +25884,13 @@ const EMPTY_OBJECT_JSON_SCHEMA = {
|
|
|
24950
25884
|
type: "object",
|
|
24951
25885
|
properties: {}
|
|
24952
25886
|
};
|
|
25887
|
+
/** Converts a tool's `inputSchema` for `tools/list` and warns on an invalid SEP-2243 `x-mcp-header` declaration. */
|
|
25888
|
+
function convertListedInputSchema(name, inputSchema) {
|
|
25889
|
+
const json = standardSchemaToJsonSchema(inputSchema, "input");
|
|
25890
|
+
const scan = scanXMcpHeaderDeclarations(json);
|
|
25891
|
+
if (!scan.valid) console.warn(`[mcp-sdk] tool '${name}' carries an invalid x-mcp-header declaration and will be excluded by conforming Streamable HTTP clients: ${scan.reason}`);
|
|
25892
|
+
return json;
|
|
25893
|
+
}
|
|
24953
25894
|
/**
|
|
24954
25895
|
* Convert a registered `outputSchema` to JSON Schema, memoised on {@link RegisteredTool.outputSchemaJson}
|
|
24955
25896
|
* so `tools/call` passes the SAME advertised schema to the wire codec's `projectCallToolResult` that
|
|
@@ -24972,7 +25913,7 @@ function createPromptHandler(name, argsSchema, callback) {
|
|
|
24972
25913
|
if (argsSchema) {
|
|
24973
25914
|
const typedCallback = callback;
|
|
24974
25915
|
return async (args, ctx) => {
|
|
24975
|
-
const parseResult = await validateStandardSchema(argsSchema, args);
|
|
25916
|
+
const parseResult = await validateStandardSchema(argsSchema, args ?? {});
|
|
24976
25917
|
if (!parseResult.success) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Invalid arguments for prompt ${name}: ${parseResult.error}`);
|
|
24977
25918
|
return typedCallback(parseResult.data, ctx);
|
|
24978
25919
|
};
|
|
@@ -25009,7 +25950,246 @@ function unwrapOptionalSchema(schema) {
|
|
|
25009
25950
|
return schema.def?.innerType ?? schema;
|
|
25010
25951
|
}
|
|
25011
25952
|
//#endregion
|
|
25012
|
-
//#region node_modules/.pnpm/@modelcontextprotocol+server@2.
|
|
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
|
+
}
|
|
25013
26193
|
/** Default upper bound, in bytes, on a request body read by the HTTP entry points (4 MiB). */
|
|
25014
26194
|
const DEFAULT_MAX_REQUEST_BODY_SIZE = 4194304;
|
|
25015
26195
|
/** Upper bound on the number of messages accepted in one JSON-RPC batch array. */
|
|
@@ -25098,6 +26278,10 @@ async function readRequestBody(request, maxBytes = DEFAULT_MAX_REQUEST_BODY_SIZE
|
|
|
25098
26278
|
* @example Hono.js
|
|
25099
26279
|
* ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_hono"
|
|
25100
26280
|
* app.all('/mcp', async c => {
|
|
26281
|
+
* // Stateless example: create a server and a transport per request.
|
|
26282
|
+
* const server = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
26283
|
+
* const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
26284
|
+
* await server.connect(transport);
|
|
25101
26285
|
* return transport.handleRequest(c.req.raw);
|
|
25102
26286
|
* });
|
|
25103
26287
|
* ```
|
|
@@ -25106,6 +26290,10 @@ async function readRequestBody(request, maxBytes = DEFAULT_MAX_REQUEST_BODY_SIZE
|
|
|
25106
26290
|
* ```ts source="./streamableHttp.examples.ts#WebStandardStreamableHTTPServerTransport_workers"
|
|
25107
26291
|
* const worker = {
|
|
25108
26292
|
* async fetch(request: Request): Promise<Response> {
|
|
26293
|
+
* // Stateless example: create a server and a transport per request.
|
|
26294
|
+
* const server = new McpServer({ name: 'my-server', version: '1.0.0' });
|
|
26295
|
+
* const transport = new WebStandardStreamableHTTPServerTransport({ sessionIdGenerator: undefined });
|
|
26296
|
+
* await server.connect(transport);
|
|
25109
26297
|
* return transport.handleRequest(request);
|
|
25110
26298
|
* }
|
|
25111
26299
|
* };
|
|
@@ -25114,6 +26302,7 @@ async function readRequestBody(request, maxBytes = DEFAULT_MAX_REQUEST_BODY_SIZE
|
|
|
25114
26302
|
var WebStandardStreamableHTTPServerTransport = class {
|
|
25115
26303
|
sessionIdGenerator;
|
|
25116
26304
|
_started = false;
|
|
26305
|
+
_hasHandledRequest = false;
|
|
25117
26306
|
_closed = false;
|
|
25118
26307
|
_streamMapping = /* @__PURE__ */ new Map();
|
|
25119
26308
|
_requestToStreamMapping = /* @__PURE__ */ new Map();
|
|
@@ -25235,6 +26424,8 @@ var WebStandardStreamableHTTPServerTransport = class {
|
|
|
25235
26424
|
*/
|
|
25236
26425
|
async handleRequest(req, options) {
|
|
25237
26426
|
if (this._closed) return this.createJsonErrorResponse(404, -32001, "Session not found");
|
|
26427
|
+
if (!this.sessionIdGenerator && this._hasHandledRequest) throw new Error("Stateless transport cannot be reused across requests. Create a new transport per request.");
|
|
26428
|
+
this._hasHandledRequest = true;
|
|
25238
26429
|
const validationError = this.validateRequestHeaders(req);
|
|
25239
26430
|
if (validationError) return validationError;
|
|
25240
26431
|
switch (req.method) {
|
|
@@ -25749,6 +26940,995 @@ var WebStandardStreamableHTTPServerTransport = class {
|
|
|
25749
26940
|
}
|
|
25750
26941
|
}
|
|
25751
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
|
+
};
|
|
25752
27932
|
//#endregion
|
|
25753
27933
|
//#region node_modules/.pnpm/cheerio@1.2.0/node_modules/cheerio/dist/esm/options.js
|
|
25754
27934
|
const defaultOpts$2 = { _useHtmlParser2: false };
|
|
@@ -83080,7 +85260,7 @@ function escapeStringRegexp(string) {
|
|
|
83080
85260
|
return string.replace(/[|\\{}()[\]^$+*?.]/g, "\\$&").replace(/-/g, "\\x2d");
|
|
83081
85261
|
}
|
|
83082
85262
|
//#endregion
|
|
83083
|
-
//#region node_modules/.pnpm/mdast-util-find-and-replace@3.0.
|
|
85263
|
+
//#region node_modules/.pnpm/mdast-util-find-and-replace@3.0.3/node_modules/mdast-util-find-and-replace/lib/index.js
|
|
83084
85264
|
/**
|
|
83085
85265
|
* @import {Nodes, Parents, PhrasingContent, Root, Text} from 'mdast'
|
|
83086
85266
|
* @import {BuildVisitor, Test, VisitorResult} from 'unist-util-visit-parents'
|
|
@@ -83157,7 +85337,11 @@ function findAndReplace(tree, list, options) {
|
|
|
83157
85337
|
const pairs = toPairs(list);
|
|
83158
85338
|
let pairIndex = -1;
|
|
83159
85339
|
while (++pairIndex < pairs.length) visitParents(tree, "text", visitor);
|
|
83160
|
-
/**
|
|
85340
|
+
/**
|
|
85341
|
+
* Visit a text node, and handle it if it is not in an ignored parent.
|
|
85342
|
+
*
|
|
85343
|
+
* @type {BuildVisitor<Root, 'text'>}
|
|
85344
|
+
*/
|
|
83161
85345
|
function visitor(node, parents) {
|
|
83162
85346
|
let index = -1;
|
|
83163
85347
|
/** @type {Parents | undefined} */
|
|
@@ -83175,11 +85359,11 @@ function findAndReplace(tree, list, options) {
|
|
|
83175
85359
|
* Handle a text node which is not in an ignored parent.
|
|
83176
85360
|
*
|
|
83177
85361
|
* @param {Text} node
|
|
83178
|
-
* Text node.
|
|
85362
|
+
* Text node to search in.
|
|
83179
85363
|
* @param {Array<Parents>} parents
|
|
83180
|
-
*
|
|
85364
|
+
* Ancestors of `node`.
|
|
83181
85365
|
* @returns {VisitorResult}
|
|
83182
|
-
*
|
|
85366
|
+
* Where to continue visiting.
|
|
83183
85367
|
*/
|
|
83184
85368
|
function handler(node, parents) {
|
|
83185
85369
|
const parent = parents[parents.length - 1];
|
|
@@ -83207,12 +85391,12 @@ function findAndReplace(tree, list, options) {
|
|
|
83207
85391
|
} : void 0;
|
|
83208
85392
|
if (value === false) find.lastIndex = position + 1;
|
|
83209
85393
|
else {
|
|
83210
|
-
if (start !== position)
|
|
85394
|
+
if (start !== position) add({
|
|
83211
85395
|
type: "text",
|
|
83212
85396
|
value: node.value.slice(start, position)
|
|
83213
85397
|
});
|
|
83214
|
-
if (Array.isArray(value))
|
|
83215
|
-
else if (value)
|
|
85398
|
+
if (Array.isArray(value)) for (const child of value) add(child);
|
|
85399
|
+
else if (value) add(value);
|
|
83216
85400
|
start = position + match[0].length;
|
|
83217
85401
|
change = true;
|
|
83218
85402
|
}
|
|
@@ -83220,13 +85404,26 @@ function findAndReplace(tree, list, options) {
|
|
|
83220
85404
|
match = find.exec(node.value);
|
|
83221
85405
|
}
|
|
83222
85406
|
if (change) {
|
|
83223
|
-
if (start < node.value.length)
|
|
85407
|
+
if (start < node.value.length) add({
|
|
83224
85408
|
type: "text",
|
|
83225
85409
|
value: node.value.slice(start)
|
|
83226
85410
|
});
|
|
83227
85411
|
parent.children.splice(index, 1, ...nodes);
|
|
83228
85412
|
} else nodes = [node];
|
|
83229
85413
|
return index + nodes.length;
|
|
85414
|
+
/**
|
|
85415
|
+
* Add a node.
|
|
85416
|
+
*
|
|
85417
|
+
* @param {PhrasingContent} child
|
|
85418
|
+
* Node to add.
|
|
85419
|
+
* @returns {undefined}
|
|
85420
|
+
* Nothing.
|
|
85421
|
+
*/
|
|
85422
|
+
function add(child) {
|
|
85423
|
+
const previous = nodes[nodes.length - 1];
|
|
85424
|
+
if (previous && previous.type === "text" && child.type === "text") previous.value += child.value;
|
|
85425
|
+
else nodes.push(child);
|
|
85426
|
+
}
|
|
83230
85427
|
}
|
|
83231
85428
|
}
|
|
83232
85429
|
/**
|
|
@@ -83238,9 +85435,9 @@ function findAndReplace(tree, list, options) {
|
|
|
83238
85435
|
* Clean pairs.
|
|
83239
85436
|
*/
|
|
83240
85437
|
function toPairs(tupleOrList) {
|
|
85438
|
+
if (!Array.isArray(tupleOrList)) throw new TypeError("Expected find and replace tuple or list of tuples");
|
|
83241
85439
|
/** @type {Pairs} */
|
|
83242
85440
|
const result = [];
|
|
83243
|
-
if (!Array.isArray(tupleOrList)) throw new TypeError("Expected find and replace tuple or list of tuples");
|
|
83244
85441
|
/** @type {FindAndReplaceList} */
|
|
83245
85442
|
const list = !tupleOrList[0] || Array.isArray(tupleOrList[0]) ? tupleOrList : [tupleOrList];
|
|
83246
85443
|
let index = -1;
|
|
@@ -83254,9 +85451,9 @@ function toPairs(tupleOrList) {
|
|
|
83254
85451
|
* Turn a find into an expression.
|
|
83255
85452
|
*
|
|
83256
85453
|
* @param {Find} find
|
|
83257
|
-
*
|
|
85454
|
+
* Pattern to find.
|
|
83258
85455
|
* @returns {RegExp}
|
|
83259
|
-
*
|
|
85456
|
+
* Global expression.
|
|
83260
85457
|
*/
|
|
83261
85458
|
function toExpression(find) {
|
|
83262
85459
|
return typeof find === "string" ? new RegExp(escapeStringRegexp(find), "g") : find;
|
|
@@ -83265,9 +85462,9 @@ function toExpression(find) {
|
|
|
83265
85462
|
* Turn a replace into a function.
|
|
83266
85463
|
*
|
|
83267
85464
|
* @param {Replace} replace
|
|
83268
|
-
*
|
|
85465
|
+
* Thing to replace with.
|
|
83269
85466
|
* @returns {ReplaceFunction}
|
|
83270
|
-
* Function.
|
|
85467
|
+
* Function that returns that thing.
|
|
83271
85468
|
*/
|
|
83272
85469
|
function toFunction(replace) {
|
|
83273
85470
|
return typeof replace === "function" ? replace : function() {
|
|
@@ -103508,7 +105705,6 @@ const countWords = (text) => {
|
|
|
103508
105705
|
return trimmed.split(WHITESPACE_RUN).length;
|
|
103509
105706
|
};
|
|
103510
105707
|
const textOf = (html) => {
|
|
103511
|
-
if (!html) return "";
|
|
103512
105708
|
return load(`<div>${html}</div>`, null, false)("div").first().text();
|
|
103513
105709
|
};
|
|
103514
105710
|
const parseSections = (html) => {
|
|
@@ -103624,14 +105820,8 @@ const htmlToMdProcessor = unified().use(rehypeParse, { fragment: true }).use(reh
|
|
|
103624
105820
|
emphasis: "_"
|
|
103625
105821
|
});
|
|
103626
105822
|
const mdToHtmlProcessor = unified().use(remarkParse).use(remarkGfm).use(remarkRehype, { allowDangerousHtml: true }).use(rehypeRaw).use(rehypeStringify);
|
|
103627
|
-
const htmlToMarkdown = (html) =>
|
|
103628
|
-
|
|
103629
|
-
return String(htmlToMdProcessor.processSync(html));
|
|
103630
|
-
};
|
|
103631
|
-
const markdownToHtml = (markdown) => {
|
|
103632
|
-
if (!markdown) return "";
|
|
103633
|
-
return String(mdToHtmlProcessor.processSync(markdown));
|
|
103634
|
-
};
|
|
105823
|
+
const htmlToMarkdown = (html) => String(htmlToMdProcessor.processSync(html));
|
|
105824
|
+
const markdownToHtml = (markdown) => String(mdToHtmlProcessor.processSync(markdown));
|
|
103635
105825
|
//#endregion
|
|
103636
105826
|
//#region src/utils/formatting.ts
|
|
103637
105827
|
const truncateIfNeeded = (text, advice = "Use pagination or filters to reduce results.") => {
|
|
@@ -105892,7 +108082,7 @@ const attachmentSchema = object({
|
|
|
105892
108082
|
error: (issue) => `Attachment too large: ${issue.input.length} base64 characters, limit ${MAX_BASE64_INPUT_CHARS}. Downscale the file, split the upload, or link to it instead of uploading.`
|
|
105893
108083
|
}).base64().describe(`File content encoded as base64. At most ${MAX_BASE64_INPUT_CHARS} characters (about ${MAX_BASE64_INPUT_MB} MB of file), and the attachments of one call must stay under that total; the HTTP transport additionally caps request bodies at 4 MB.`),
|
|
105894
108084
|
content_type: string$2().min(1).default("application/octet-stream").describe("MIME type, e.g. \"text/plain\", \"image/png\", \"application/pdf\".")
|
|
105895
|
-
});
|
|
108085
|
+
}).strict();
|
|
105896
108086
|
/**
|
|
105897
108087
|
* The optional `attachments` array parameter, capped as a whole.
|
|
105898
108088
|
*
|
|
@@ -106222,7 +108412,7 @@ const createRequestTools = (ctx) => {
|
|
|
106222
108412
|
custom_fields: array(object({
|
|
106223
108413
|
id: number$2().int(),
|
|
106224
108414
|
value: unknown$3()
|
|
106225
|
-
})).optional().describe("Answers to the form's own questions, as { id, value } pairs. Take both the ids and the accepted values from get_request_form; an id this form does not carry, or a value a dropdown does not offer, is refused here because Zendesk would drop it without an error. Option values are the tag strings, never numbers."),
|
|
108415
|
+
}).strict()).optional().describe("Answers to the form's own questions, as { id, value } pairs. Take both the ids and the accepted values from get_request_form; an id this form does not carry, or a value a dropdown does not offer, is refused here because Zendesk would drop it without an error. Option values are the tag strings, never numbers."),
|
|
106226
108416
|
attachments: attachmentsParam("Files to attach to the request, such as a screenshot or a log, with their content base64-encoded.")
|
|
106227
108417
|
}),
|
|
106228
108418
|
annotations: {
|
|
@@ -107012,7 +109202,7 @@ const createTicketTools = (ctx) => {
|
|
|
107012
109202
|
custom_fields: array(object({
|
|
107013
109203
|
id: number$2().int(),
|
|
107014
109204
|
value: unknown$3()
|
|
107015
|
-
})).optional().describe("Custom field values as { id, value } pairs (field ids come from your Zendesk admin settings). Call list_ticket_fields first to discover the numeric field ids and, for dropdown/multiselect fields, the exact option values Zendesk accepts.")
|
|
109205
|
+
}).strict()).optional().describe("Custom field values as { id, value } pairs (field ids come from your Zendesk admin settings). Call list_ticket_fields first to discover the numeric field ids and, for dropdown/multiselect fields, the exact option values Zendesk accepts.")
|
|
107016
109206
|
}),
|
|
107017
109207
|
annotations: {
|
|
107018
109208
|
readOnlyHint: false,
|
|
@@ -107069,7 +109259,7 @@ const createTicketTools = (ctx) => {
|
|
|
107069
109259
|
custom_fields: array(object({
|
|
107070
109260
|
id: number$2().int(),
|
|
107071
109261
|
value: unknown$3()
|
|
107072
|
-
})).optional().describe("Custom field values as { id, value } pairs (field ids come from your Zendesk admin settings). Call list_ticket_fields first to discover the numeric field ids and, for dropdown/multiselect fields, the exact option values Zendesk accepts."),
|
|
109262
|
+
}).strict()).optional().describe("Custom field values as { id, value } pairs (field ids come from your Zendesk admin settings). Call list_ticket_fields first to discover the numeric field ids and, for dropdown/multiselect fields, the exact option values Zendesk accepts."),
|
|
107073
109263
|
followers: object({
|
|
107074
109264
|
add: subscriberIdList("User ids to start following the ticket. Omit to only remove."),
|
|
107075
109265
|
remove: subscriberIdList("User ids to stop following the ticket. Omit to only add.")
|
|
@@ -107606,12 +109796,6 @@ const SEVERITY = {
|
|
|
107606
109796
|
warn: 2,
|
|
107607
109797
|
error: 3
|
|
107608
109798
|
};
|
|
107609
|
-
const MCP_LEVEL = {
|
|
107610
|
-
debug: "debug",
|
|
107611
|
-
info: "info",
|
|
107612
|
-
warn: "warning",
|
|
107613
|
-
error: "error"
|
|
107614
|
-
};
|
|
107615
109799
|
const REDACTED_KEYS = /* @__PURE__ */ new Set([
|
|
107616
109800
|
"token",
|
|
107617
109801
|
"accesstoken",
|
|
@@ -107660,37 +109844,21 @@ const silentLogger = {
|
|
|
107660
109844
|
debug: noop,
|
|
107661
109845
|
info: noop,
|
|
107662
109846
|
warn: noop,
|
|
107663
|
-
error: noop
|
|
107664
|
-
attachServer: noop
|
|
109847
|
+
error: noop
|
|
107665
109848
|
};
|
|
107666
109849
|
const createLogger = (level) => {
|
|
107667
109850
|
const min = SEVERITY[level];
|
|
107668
|
-
let server;
|
|
107669
109851
|
const emit = (lvl, event, fields) => {
|
|
107670
109852
|
if (SEVERITY[lvl] < min) return;
|
|
107671
|
-
const safe = sanitise(fields);
|
|
107672
109853
|
try {
|
|
107673
|
-
console.error(formatLine(lvl, event,
|
|
107674
|
-
} catch {}
|
|
107675
|
-
if (server) try {
|
|
107676
|
-
server.sendLoggingMessage({
|
|
107677
|
-
level: MCP_LEVEL[lvl],
|
|
107678
|
-
logger: "zendesk-mcp-server",
|
|
107679
|
-
data: {
|
|
107680
|
-
...safe,
|
|
107681
|
-
event
|
|
107682
|
-
}
|
|
107683
|
-
}).catch(noop);
|
|
109854
|
+
console.error(formatLine(lvl, event, sanitise(fields)));
|
|
107684
109855
|
} catch {}
|
|
107685
109856
|
};
|
|
107686
109857
|
return {
|
|
107687
109858
|
debug: (event, fields) => emit("debug", event, fields),
|
|
107688
109859
|
info: (event, fields) => emit("info", event, fields),
|
|
107689
109860
|
warn: (event, fields) => emit("warn", event, fields),
|
|
107690
|
-
error: (event, fields) => emit("error", event, fields)
|
|
107691
|
-
attachServer: (s) => {
|
|
107692
|
-
server = s;
|
|
107693
|
-
}
|
|
109861
|
+
error: (event, fields) => emit("error", event, fields)
|
|
107694
109862
|
};
|
|
107695
109863
|
};
|
|
107696
109864
|
//#endregion
|
|
@@ -107713,16 +109881,13 @@ const readPackageInfo = () => {
|
|
|
107713
109881
|
let dir = dirname(fileURLToPath(import.meta.url));
|
|
107714
109882
|
for (let depth = 0; depth < 8; depth++) {
|
|
107715
109883
|
try {
|
|
107716
|
-
const
|
|
107717
|
-
if (
|
|
107718
|
-
|
|
107719
|
-
|
|
107720
|
-
|
|
107721
|
-
|
|
107722
|
-
|
|
107723
|
-
};
|
|
107724
|
-
return cached;
|
|
107725
|
-
}
|
|
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;
|
|
107726
109891
|
}
|
|
107727
109892
|
} catch {}
|
|
107728
109893
|
const parent = dirname(dir);
|
|
@@ -107733,31 +109898,18 @@ const readPackageInfo = () => {
|
|
|
107733
109898
|
return cached;
|
|
107734
109899
|
};
|
|
107735
109900
|
//#endregion
|
|
107736
|
-
//#region src/utils/
|
|
109901
|
+
//#region src/utils/request-state.ts
|
|
107737
109902
|
/**
|
|
107738
|
-
*
|
|
107739
|
-
*
|
|
107740
|
-
*
|
|
107741
|
-
*
|
|
107742
|
-
*
|
|
107743
|
-
* `page_size`) had the key dropped, so a large unpaginated page came back. The
|
|
107744
|
-
* parser rejects unknown keys and names both the offending ones and the valid
|
|
107745
|
-
* parameters, so a misremembered name fails loudly.
|
|
107746
|
-
*
|
|
107747
|
-
* Used on the proxy dispatch path (namespace/single modes); in `all` mode the
|
|
107748
|
-
* SDK validates against the strict schema we register.
|
|
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.
|
|
107749
109908
|
*/
|
|
107750
|
-
const
|
|
107751
|
-
|
|
107752
|
-
|
|
107753
|
-
|
|
107754
|
-
const result = strict.safeParse(params);
|
|
107755
|
-
if (result.success) return result.data;
|
|
107756
|
-
const unknownKeys = result.error.issues.flatMap((issue) => issue.code === "unrecognized_keys" ? issue.keys : []);
|
|
107757
|
-
if (unknownKeys.length > 0) throw new Error(`Unknown parameter(s): ${unknownKeys.join(", ")}. Valid parameters: ${validKeys || "(none)"}.`);
|
|
107758
|
-
throw result.error;
|
|
107759
|
-
};
|
|
107760
|
-
};
|
|
109909
|
+
const createRequestStateCodec = ({ key = randomBytes(32), principal = () => "" }) => createRequestStateCodec$1({
|
|
109910
|
+
key,
|
|
109911
|
+
bind: (ctx) => `${ctx.mcpReq.method}\0${principal()}`
|
|
109912
|
+
});
|
|
107761
109913
|
//#endregion
|
|
107762
109914
|
//#region src/server.ts
|
|
107763
109915
|
/**
|
|
@@ -107821,24 +109973,29 @@ const registerProxyTool = (server, toolName, title, tools, readOnlyMode, onUnaut
|
|
|
107821
109973
|
}, async (args) => dispatch(args));
|
|
107822
109974
|
};
|
|
107823
109975
|
/**
|
|
107824
|
-
* Build the bare `McpServer` — identity,
|
|
107825
|
-
*
|
|
107826
|
-
*
|
|
107827
|
-
*
|
|
107828
|
-
* 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`.
|
|
107829
109980
|
*/
|
|
107830
|
-
const createServerShell = (config,
|
|
109981
|
+
const createServerShell = (config, requestStateCodec = createRequestStateCodec({})) => {
|
|
107831
109982
|
const pkg = readPackageInfo();
|
|
107832
109983
|
const instructions = buildInstructions(config);
|
|
107833
|
-
|
|
109984
|
+
return new McpServer({
|
|
107834
109985
|
name: pkg.name,
|
|
107835
109986
|
version: pkg.version
|
|
107836
109987
|
}, {
|
|
107837
|
-
|
|
107838
|
-
|
|
109988
|
+
...instructions ? { instructions } : {},
|
|
109989
|
+
requestState: { verify: (state, ctx) => requestStateCodec.verify(state, ctx) }
|
|
107839
109990
|
});
|
|
107840
|
-
|
|
107841
|
-
|
|
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
|
+
};
|
|
107842
109999
|
};
|
|
107843
110000
|
/**
|
|
107844
110001
|
* Lazily resolve the Help Center subdomain the topology resource pins to: the
|
|
@@ -107875,8 +110032,8 @@ const resourceBrandResolver = (config, resolveBrandSubdomain) => {
|
|
|
107875
110032
|
* without dropping the transport. `tools` is passed in (not built here) so the
|
|
107876
110033
|
* reload path can hand over freshly re-imported definitions.
|
|
107877
110034
|
*/
|
|
107878
|
-
const registerToolset = (server, { config, getToken,
|
|
107879
|
-
const
|
|
110035
|
+
const registerToolset = (server, { config, getToken, caches, onUnauthorized, logger = silentLogger }, tools) => {
|
|
110036
|
+
const { topology, articles } = caches ?? createServerCaches(config, getToken, onUnauthorized);
|
|
107880
110037
|
const registered = [];
|
|
107881
110038
|
const dispose = () => {
|
|
107882
110039
|
for (const handle of registered) handle.remove();
|
|
@@ -107892,7 +110049,7 @@ const registerToolset = (server, { config, getToken, resolveBrandSubdomain, onUn
|
|
|
107892
110049
|
for (const tool of filteredTools) registered.push(server.registerTool(tool.name, {
|
|
107893
110050
|
title: tool.title,
|
|
107894
110051
|
description: tool.description,
|
|
107895
|
-
inputSchema: tool.inputSchema
|
|
110052
|
+
inputSchema: toStrictStandardSchema(tool.inputSchema),
|
|
107896
110053
|
annotations: tool.annotations
|
|
107897
110054
|
}, async (params) => runHandler(tool, params, onUnauthorized)));
|
|
107898
110055
|
break;
|
|
@@ -107912,20 +110069,16 @@ const registerToolset = (server, { config, getToken, resolveBrandSubdomain, onUn
|
|
|
107912
110069
|
throw new Error(`Unsupported tool mode: ${String(unhandled)}`);
|
|
107913
110070
|
}
|
|
107914
110071
|
}
|
|
107915
|
-
if (
|
|
107916
|
-
|
|
107917
|
-
|
|
107918
|
-
|
|
107919
|
-
|
|
107920
|
-
|
|
107921
|
-
|
|
107922
|
-
|
|
107923
|
-
|
|
107924
|
-
|
|
107925
|
-
}] })));
|
|
107926
|
-
}
|
|
107927
|
-
if (articleResourceEnabled(config)) {
|
|
107928
|
-
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) {
|
|
107929
110082
|
const listPromotedEnabled = promotedArticlesEnabled(config);
|
|
107930
110083
|
const template = new ResourceTemplate(articleResourceUriTemplate(config), { list: async () => {
|
|
107931
110084
|
if (!listPromotedEnabled) return { resources: [] };
|
|
@@ -107968,7 +110121,7 @@ const registerToolset = (server, { config, getToken, resolveBrandSubdomain, onUn
|
|
|
107968
110121
|
dispose();
|
|
107969
110122
|
throw err;
|
|
107970
110123
|
}
|
|
107971
|
-
logger.
|
|
110124
|
+
logger.debug("tools_registered", {
|
|
107972
110125
|
count: filteredTools.length,
|
|
107973
110126
|
mode: config.mode
|
|
107974
110127
|
});
|
|
@@ -107977,26 +110130,29 @@ const registerToolset = (server, { config, getToken, resolveBrandSubdomain, onUn
|
|
|
107977
110130
|
dispose
|
|
107978
110131
|
};
|
|
107979
110132
|
};
|
|
107980
|
-
const createMcpServer = (config, getToken, logger = silentLogger, onUnauthorized) => {
|
|
107981
|
-
const server = createServerShell(config,
|
|
107982
|
-
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);
|
|
107983
110135
|
const tools = createAllTools({
|
|
107984
110136
|
subdomain: config.subdomain,
|
|
107985
110137
|
brandIds: config.brandIds,
|
|
107986
|
-
resolveBrandSubdomain,
|
|
110138
|
+
resolveBrandSubdomain: caches.resolveBrandSubdomain,
|
|
107987
110139
|
getToken
|
|
107988
110140
|
});
|
|
107989
110141
|
registerToolset(server, {
|
|
107990
110142
|
config,
|
|
107991
110143
|
getToken,
|
|
107992
|
-
|
|
110144
|
+
caches,
|
|
107993
110145
|
onUnauthorized,
|
|
107994
110146
|
logger
|
|
107995
110147
|
}, tools);
|
|
107996
110148
|
return server;
|
|
107997
110149
|
};
|
|
110150
|
+
const defaultServerFactory = ({ config, getToken, logger, onUnauthorized, caches, requestStateCodec }) => createMcpServer(config, getToken, logger, onUnauthorized, {
|
|
110151
|
+
...caches ? { caches } : {},
|
|
110152
|
+
requestStateCodec
|
|
110153
|
+
});
|
|
107998
110154
|
//#endregion
|
|
107999
|
-
//#region node_modules/.pnpm/@modelcontextprotocol+server@2.
|
|
110155
|
+
//#region node_modules/.pnpm/@modelcontextprotocol+server@2.3.1/node_modules/@modelcontextprotocol/server/dist/stdio.mjs
|
|
108000
110156
|
/**
|
|
108001
110157
|
* Marks a stdout `'error'` listener that a closed transport left attached purely to
|
|
108002
110158
|
* swallow late write failures (see {@link StdioServerTransport.close}). A fresh
|
|
@@ -108120,12 +110276,485 @@ var StdioServerTransport = class {
|
|
|
108120
110276
|
});
|
|
108121
110277
|
}
|
|
108122
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
|
+
}
|
|
108123
110733
|
//#endregion
|
|
108124
110734
|
//#region src/transports/stdio.ts
|
|
108125
|
-
|
|
108126
|
-
|
|
108127
|
-
|
|
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
|
+
});
|
|
108128
110756
|
logger.info("stdio_transport_ready");
|
|
110757
|
+
return handle;
|
|
108129
110758
|
};
|
|
108130
110759
|
//#endregion
|
|
108131
110760
|
//#region src/dev/reload.ts
|
|
@@ -108177,19 +110806,19 @@ const loadFreshTools = async (ctx) => {
|
|
|
108177
110806
|
* only matters after an edit); `reload()` disposes the current generation and
|
|
108178
110807
|
* registers a fresh one.
|
|
108179
110808
|
*/
|
|
108180
|
-
const createReloadableServer = (config, getToken, logger = silentLogger, onUnauthorized, loadTools = loadFreshTools) => {
|
|
108181
|
-
const server = createServerShell(config,
|
|
108182
|
-
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);
|
|
108183
110812
|
const ctx = {
|
|
108184
110813
|
subdomain: config.subdomain,
|
|
108185
110814
|
brandIds: config.brandIds,
|
|
108186
|
-
resolveBrandSubdomain,
|
|
110815
|
+
resolveBrandSubdomain: caches.resolveBrandSubdomain,
|
|
108187
110816
|
getToken
|
|
108188
110817
|
};
|
|
108189
110818
|
const params = {
|
|
108190
110819
|
config,
|
|
108191
110820
|
getToken,
|
|
108192
|
-
|
|
110821
|
+
caches,
|
|
108193
110822
|
onUnauthorized,
|
|
108194
110823
|
logger
|
|
108195
110824
|
};
|
|
@@ -108226,7 +110855,7 @@ const createReloadableServer = (config, getToken, logger = silentLogger, onUnaut
|
|
|
108226
110855
|
const registerReloadTool = (server, reload, logger = silentLogger) => {
|
|
108227
110856
|
server.registerTool("reload_tools", {
|
|
108228
110857
|
title: "Reload tools from source (dev)",
|
|
108229
|
-
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.",
|
|
108230
110859
|
inputSchema: object({}).strict(),
|
|
108231
110860
|
annotations: {
|
|
108232
110861
|
readOnlyHint: false,
|
|
@@ -108240,7 +110869,7 @@ const registerReloadTool = (server, reload, logger = silentLogger) => {
|
|
|
108240
110869
|
logger.info("tools_reloaded", { count });
|
|
108241
110870
|
return { content: [{
|
|
108242
110871
|
type: "text",
|
|
108243
|
-
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).`
|
|
108244
110873
|
}] };
|
|
108245
110874
|
} catch (err) {
|
|
108246
110875
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -108258,21 +110887,23 @@ const registerReloadTool = (server, reload, logger = silentLogger) => {
|
|
|
108258
110887
|
/**
|
|
108259
110888
|
* Start the stdio server in dev mode: the normal toolset plus a persistent
|
|
108260
110889
|
* `reload_tools` tool that hot-reloads edited tool code on demand. stdio only —
|
|
108261
|
-
* HTTP builds a per
|
|
110890
|
+
* HTTP builds a server per session or per request, so there is no long-lived
|
|
108262
110891
|
* server to hot-swap.
|
|
108263
110892
|
*
|
|
108264
|
-
* Returns the
|
|
110893
|
+
* Returns the transport handle so the caller can close it on shutdown: dev mode
|
|
108265
110894
|
* runs over the same stdio transport as normal mode and must exit the same way
|
|
108266
110895
|
* when the client disconnects.
|
|
108267
110896
|
*/
|
|
108268
110897
|
/* v8 ignore start -- runtime bootstrap: binds the reload tool to a real stdio
|
|
108269
110898
|
transport; the reload machinery it wires up is covered by dev-reload.test.ts */
|
|
108270
|
-
const startDevServer =
|
|
108271
|
-
const
|
|
108272
|
-
|
|
108273
|
-
|
|
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);
|
|
108274
110905
|
logger.info("dev_mode_enabled");
|
|
108275
|
-
return
|
|
110906
|
+
return handle;
|
|
108276
110907
|
};
|
|
108277
110908
|
/* v8 ignore stop */
|
|
108278
110909
|
//#endregion
|
|
@@ -108424,7 +111055,7 @@ const installShutdown = (options) => {
|
|
|
108424
111055
|
//#endregion
|
|
108425
111056
|
//#region src/index.ts
|
|
108426
111057
|
const buildStdioTokenStore = async (config, logger) => {
|
|
108427
|
-
const { createTokenStore } = await import("./token-store-
|
|
111058
|
+
const { createTokenStore } = await import("./token-store-DyO12sbo.js");
|
|
108428
111059
|
return createTokenStore({
|
|
108429
111060
|
subdomain: config.subdomain,
|
|
108430
111061
|
oauthClientId: config.oauthClientId,
|
|
@@ -108432,11 +111063,15 @@ const buildStdioTokenStore = async (config, logger) => {
|
|
|
108432
111063
|
readOnly: config.readOnly
|
|
108433
111064
|
}, logger);
|
|
108434
111065
|
};
|
|
108435
|
-
const connectStdio =
|
|
111066
|
+
const connectStdio = (config, tokenStore, logger) => {
|
|
108436
111067
|
if (config.dev) return startDevServer(config, tokenStore.getToken, logger, tokenStore.invalidate);
|
|
108437
|
-
|
|
108438
|
-
|
|
108439
|
-
|
|
111068
|
+
return startStdioTransport((extras) => defaultServerFactory({
|
|
111069
|
+
config,
|
|
111070
|
+
getToken: tokenStore.getToken,
|
|
111071
|
+
logger,
|
|
111072
|
+
onUnauthorized: tokenStore.invalidate,
|
|
111073
|
+
...extras
|
|
111074
|
+
}), logger);
|
|
108440
111075
|
};
|
|
108441
111076
|
const main = async () => {
|
|
108442
111077
|
const config = loadConfig();
|
|
@@ -108453,19 +111088,19 @@ const main = async () => {
|
|
|
108453
111088
|
const logger = createLogger(config.logLevel);
|
|
108454
111089
|
if (config.transport === "stdio") {
|
|
108455
111090
|
const tokenStore = await buildStdioTokenStore(config, logger);
|
|
108456
|
-
const
|
|
111091
|
+
const stdio = connectStdio(config, tokenStore, logger);
|
|
108457
111092
|
installShutdown({
|
|
108458
111093
|
watchStdin: true,
|
|
108459
111094
|
logger,
|
|
108460
111095
|
cleanup: async () => {
|
|
108461
|
-
await
|
|
111096
|
+
await stdio.close();
|
|
108462
111097
|
tokenStore.dispose();
|
|
108463
111098
|
}
|
|
108464
111099
|
});
|
|
108465
111100
|
return;
|
|
108466
111101
|
}
|
|
108467
111102
|
if (config.dev) logger.warn("dev_mode_ignored_http");
|
|
108468
|
-
const { startHttpTransport } = await import("./http-
|
|
111103
|
+
const { startHttpTransport } = await import("./http-DU1dSrPe.js");
|
|
108469
111104
|
const http = await startHttpTransport(config, logger);
|
|
108470
111105
|
installShutdown({
|
|
108471
111106
|
watchStdin: false,
|
|
@@ -108479,4 +111114,4 @@ main().catch((error) => {
|
|
|
108479
111114
|
process.exit(1);
|
|
108480
111115
|
});
|
|
108481
111116
|
//#endregion
|
|
108482
|
-
export { __toESM$1 as _,
|
|
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 };
|