infinitecampus-mcp 2.3.0 → 2.3.2

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/bundle.js CHANGED
@@ -3112,6 +3112,9 @@ var require_utils = __commonJS({
3112
3112
  "use strict";
3113
3113
  var isUUID = RegExp.prototype.test.bind(/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/iu);
3114
3114
  var isIPv4 = RegExp.prototype.test.bind(/^(?:(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)\.){3}(?:25[0-5]|2[0-4]\d|1\d{2}|[1-9]\d|\d)$/u);
3115
+ var isHexPair = RegExp.prototype.test.bind(/^[\da-f]{2}$/iu);
3116
+ var isUnreserved = RegExp.prototype.test.bind(/^[\da-z\-._~]$/iu);
3117
+ var isPathCharacter = RegExp.prototype.test.bind(/^[\da-z\-._~!$&'()*+,;=:@/]$/iu);
3115
3118
  function stringArrayToHexStripped(input) {
3116
3119
  let acc = "";
3117
3120
  let code = 0;
@@ -3304,27 +3307,77 @@ var require_utils = __commonJS({
3304
3307
  }
3305
3308
  return output.join("");
3306
3309
  }
3307
- function normalizeComponentEncoding(component, esc2) {
3308
- const func = esc2 !== true ? escape : unescape;
3309
- if (component.scheme !== void 0) {
3310
- component.scheme = func(component.scheme);
3311
- }
3312
- if (component.userinfo !== void 0) {
3313
- component.userinfo = func(component.userinfo);
3314
- }
3315
- if (component.host !== void 0) {
3316
- component.host = func(component.host);
3310
+ var HOST_DELIMS = { "@": "%40", "/": "%2F", "?": "%3F", "#": "%23", ":": "%3A" };
3311
+ var HOST_DELIM_RE = /[@/?#:]/g;
3312
+ var HOST_DELIM_NO_COLON_RE = /[@/?#]/g;
3313
+ function reescapeHostDelimiters(host, isIP) {
3314
+ const re = isIP ? HOST_DELIM_NO_COLON_RE : HOST_DELIM_RE;
3315
+ re.lastIndex = 0;
3316
+ return host.replace(re, (ch) => HOST_DELIMS[ch]);
3317
+ }
3318
+ function normalizePercentEncoding(input, decodeUnreserved = false) {
3319
+ if (input.indexOf("%") === -1) {
3320
+ return input;
3317
3321
  }
3318
- if (component.path !== void 0) {
3319
- component.path = func(component.path);
3322
+ let output = "";
3323
+ for (let i = 0; i < input.length; i++) {
3324
+ if (input[i] === "%" && i + 2 < input.length) {
3325
+ const hex3 = input.slice(i + 1, i + 3);
3326
+ if (isHexPair(hex3)) {
3327
+ const normalizedHex = hex3.toUpperCase();
3328
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3329
+ if (decodeUnreserved && isUnreserved(decoded)) {
3330
+ output += decoded;
3331
+ } else {
3332
+ output += "%" + normalizedHex;
3333
+ }
3334
+ i += 2;
3335
+ continue;
3336
+ }
3337
+ }
3338
+ output += input[i];
3320
3339
  }
3321
- if (component.query !== void 0) {
3322
- component.query = func(component.query);
3340
+ return output;
3341
+ }
3342
+ function normalizePathEncoding(input) {
3343
+ let output = "";
3344
+ for (let i = 0; i < input.length; i++) {
3345
+ if (input[i] === "%" && i + 2 < input.length) {
3346
+ const hex3 = input.slice(i + 1, i + 3);
3347
+ if (isHexPair(hex3)) {
3348
+ const normalizedHex = hex3.toUpperCase();
3349
+ const decoded = String.fromCharCode(parseInt(normalizedHex, 16));
3350
+ if (decoded !== "." && isUnreserved(decoded)) {
3351
+ output += decoded;
3352
+ } else {
3353
+ output += "%" + normalizedHex;
3354
+ }
3355
+ i += 2;
3356
+ continue;
3357
+ }
3358
+ }
3359
+ if (isPathCharacter(input[i])) {
3360
+ output += input[i];
3361
+ } else {
3362
+ output += escape(input[i]);
3363
+ }
3323
3364
  }
3324
- if (component.fragment !== void 0) {
3325
- component.fragment = func(component.fragment);
3365
+ return output;
3366
+ }
3367
+ function escapePreservingEscapes(input) {
3368
+ let output = "";
3369
+ for (let i = 0; i < input.length; i++) {
3370
+ if (input[i] === "%" && i + 2 < input.length) {
3371
+ const hex3 = input.slice(i + 1, i + 3);
3372
+ if (isHexPair(hex3)) {
3373
+ output += "%" + hex3.toUpperCase();
3374
+ i += 2;
3375
+ continue;
3376
+ }
3377
+ }
3378
+ output += escape(input[i]);
3326
3379
  }
3327
- return component;
3380
+ return output;
3328
3381
  }
3329
3382
  function recomposeAuthority(component) {
3330
3383
  const uriTokens = [];
@@ -3339,7 +3392,7 @@ var require_utils = __commonJS({
3339
3392
  if (ipV6res.isIPV6 === true) {
3340
3393
  host = `[${ipV6res.escapedHost}]`;
3341
3394
  } else {
3342
- host = component.host;
3395
+ host = reescapeHostDelimiters(host, false);
3343
3396
  }
3344
3397
  }
3345
3398
  uriTokens.push(host);
@@ -3353,7 +3406,10 @@ var require_utils = __commonJS({
3353
3406
  module.exports = {
3354
3407
  nonSimpleDomain,
3355
3408
  recomposeAuthority,
3356
- normalizeComponentEncoding,
3409
+ reescapeHostDelimiters,
3410
+ normalizePercentEncoding,
3411
+ normalizePathEncoding,
3412
+ escapePreservingEscapes,
3357
3413
  removeDotSegments,
3358
3414
  isIPv4,
3359
3415
  isUUID,
@@ -3577,12 +3633,12 @@ var require_schemes = __commonJS({
3577
3633
  var require_fast_uri = __commonJS({
3578
3634
  "node_modules/fast-uri/index.js"(exports, module) {
3579
3635
  "use strict";
3580
- var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizeComponentEncoding, isIPv4, nonSimpleDomain } = require_utils();
3636
+ var { normalizeIPv6, removeDotSegments, recomposeAuthority, normalizePercentEncoding, normalizePathEncoding, escapePreservingEscapes, reescapeHostDelimiters, isIPv4, nonSimpleDomain } = require_utils();
3581
3637
  var { SCHEMES, getSchemeHandler } = require_schemes();
3582
3638
  function normalize(uri, options) {
3583
3639
  if (typeof uri === "string") {
3584
3640
  uri = /** @type {T} */
3585
- serialize(parse3(uri, options), options);
3641
+ normalizeString(uri, options);
3586
3642
  } else if (typeof uri === "object") {
3587
3643
  uri = /** @type {T} */
3588
3644
  parse3(serialize(uri, options), options);
@@ -3649,19 +3705,9 @@ var require_fast_uri = __commonJS({
3649
3705
  return target;
3650
3706
  }
3651
3707
  function equal(uriA, uriB, options) {
3652
- if (typeof uriA === "string") {
3653
- uriA = unescape(uriA);
3654
- uriA = serialize(normalizeComponentEncoding(parse3(uriA, options), true), { ...options, skipEscape: true });
3655
- } else if (typeof uriA === "object") {
3656
- uriA = serialize(normalizeComponentEncoding(uriA, true), { ...options, skipEscape: true });
3657
- }
3658
- if (typeof uriB === "string") {
3659
- uriB = unescape(uriB);
3660
- uriB = serialize(normalizeComponentEncoding(parse3(uriB, options), true), { ...options, skipEscape: true });
3661
- } else if (typeof uriB === "object") {
3662
- uriB = serialize(normalizeComponentEncoding(uriB, true), { ...options, skipEscape: true });
3663
- }
3664
- return uriA.toLowerCase() === uriB.toLowerCase();
3708
+ const normalizedA = normalizeComparableURI(uriA, options);
3709
+ const normalizedB = normalizeComparableURI(uriB, options);
3710
+ return normalizedA !== void 0 && normalizedB !== void 0 && normalizedA.toLowerCase() === normalizedB.toLowerCase();
3665
3711
  }
3666
3712
  function serialize(cmpts, opts) {
3667
3713
  const component = {
@@ -3686,12 +3732,12 @@ var require_fast_uri = __commonJS({
3686
3732
  if (schemeHandler && schemeHandler.serialize) schemeHandler.serialize(component, options);
3687
3733
  if (component.path !== void 0) {
3688
3734
  if (!options.skipEscape) {
3689
- component.path = escape(component.path);
3735
+ component.path = escapePreservingEscapes(component.path);
3690
3736
  if (component.scheme !== void 0) {
3691
3737
  component.path = component.path.split("%3A").join(":");
3692
3738
  }
3693
3739
  } else {
3694
- component.path = unescape(component.path);
3740
+ component.path = normalizePercentEncoding(component.path);
3695
3741
  }
3696
3742
  }
3697
3743
  if (options.reference !== "suffix" && component.scheme) {
@@ -3726,7 +3772,16 @@ var require_fast_uri = __commonJS({
3726
3772
  return uriTokens.join("");
3727
3773
  }
3728
3774
  var URI_PARSE = /^(?:([^#/:?]+):)?(?:\/\/((?:([^#/?@]*)@)?(\[[^#/?\]]+\]|[^#/:?]*)(?::(\d*))?))?([^#?]*)(?:\?([^#]*))?(?:#((?:.|[\n\r])*))?/u;
3729
- function parse3(uri, opts) {
3775
+ function getParseError(parsed, matches) {
3776
+ if (matches[2] !== void 0 && parsed.path && parsed.path[0] !== "/") {
3777
+ return 'URI path must start with "/" when authority is present.';
3778
+ }
3779
+ if (typeof parsed.port === "number" && (parsed.port < 0 || parsed.port > 65535)) {
3780
+ return "URI port is malformed.";
3781
+ }
3782
+ return void 0;
3783
+ }
3784
+ function parseWithStatus(uri, opts) {
3730
3785
  const options = Object.assign({}, opts);
3731
3786
  const parsed = {
3732
3787
  scheme: void 0,
@@ -3737,6 +3792,7 @@ var require_fast_uri = __commonJS({
3737
3792
  query: void 0,
3738
3793
  fragment: void 0
3739
3794
  };
3795
+ let malformedAuthorityOrPort = false;
3740
3796
  let isIP = false;
3741
3797
  if (options.reference === "suffix") {
3742
3798
  if (options.scheme) {
@@ -3757,6 +3813,11 @@ var require_fast_uri = __commonJS({
3757
3813
  if (isNaN(parsed.port)) {
3758
3814
  parsed.port = matches[5];
3759
3815
  }
3816
+ const parseError = getParseError(parsed, matches);
3817
+ if (parseError !== void 0) {
3818
+ parsed.error = parsed.error || parseError;
3819
+ malformedAuthorityOrPort = true;
3820
+ }
3760
3821
  if (parsed.host) {
3761
3822
  const ipv4result = isIPv4(parsed.host);
3762
3823
  if (ipv4result === false) {
@@ -3795,14 +3856,18 @@ var require_fast_uri = __commonJS({
3795
3856
  parsed.scheme = unescape(parsed.scheme);
3796
3857
  }
3797
3858
  if (parsed.host !== void 0) {
3798
- parsed.host = unescape(parsed.host);
3859
+ parsed.host = reescapeHostDelimiters(unescape(parsed.host), isIP);
3799
3860
  }
3800
3861
  }
3801
3862
  if (parsed.path) {
3802
- parsed.path = escape(unescape(parsed.path));
3863
+ parsed.path = normalizePathEncoding(parsed.path);
3803
3864
  }
3804
3865
  if (parsed.fragment) {
3805
- parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
3866
+ try {
3867
+ parsed.fragment = encodeURI(decodeURIComponent(parsed.fragment));
3868
+ } catch {
3869
+ parsed.error = parsed.error || "URI malformed";
3870
+ }
3806
3871
  }
3807
3872
  }
3808
3873
  if (schemeHandler && schemeHandler.parse) {
@@ -3811,7 +3876,29 @@ var require_fast_uri = __commonJS({
3811
3876
  } else {
3812
3877
  parsed.error = parsed.error || "URI can not be parsed.";
3813
3878
  }
3814
- return parsed;
3879
+ return { parsed, malformedAuthorityOrPort };
3880
+ }
3881
+ function parse3(uri, opts) {
3882
+ return parseWithStatus(uri, opts).parsed;
3883
+ }
3884
+ function normalizeString(uri, opts) {
3885
+ return normalizeStringWithStatus(uri, opts).normalized;
3886
+ }
3887
+ function normalizeStringWithStatus(uri, opts) {
3888
+ const { parsed, malformedAuthorityOrPort } = parseWithStatus(uri, opts);
3889
+ return {
3890
+ normalized: malformedAuthorityOrPort ? uri : serialize(parsed, opts),
3891
+ malformedAuthorityOrPort
3892
+ };
3893
+ }
3894
+ function normalizeComparableURI(uri, opts) {
3895
+ if (typeof uri === "string") {
3896
+ const { normalized, malformedAuthorityOrPort } = normalizeStringWithStatus(uri, opts);
3897
+ return malformedAuthorityOrPort ? void 0 : normalized;
3898
+ }
3899
+ if (typeof uri === "object") {
3900
+ return serialize(uri, opts);
3901
+ }
3815
3902
  }
3816
3903
  var fastUri = {
3817
3904
  SCHEMES,
@@ -7308,14 +7395,14 @@ var require_permessage_deflate = __commonJS({
7308
7395
  }
7309
7396
  };
7310
7397
  module.exports = PerMessageDeflate2;
7311
- function deflateOnData(chunk) {
7312
- this[kBuffers].push(chunk);
7313
- this[kTotalLength] += chunk.length;
7398
+ function deflateOnData(chunk2) {
7399
+ this[kBuffers].push(chunk2);
7400
+ this[kTotalLength] += chunk2.length;
7314
7401
  }
7315
- function inflateOnData(chunk) {
7316
- this[kTotalLength] += chunk.length;
7402
+ function inflateOnData(chunk2) {
7403
+ this[kTotalLength] += chunk2.length;
7317
7404
  if (this[kPerMessageDeflate]._maxPayload < 1 || this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload) {
7318
- this[kBuffers].push(chunk);
7405
+ this[kBuffers].push(chunk2);
7319
7406
  return;
7320
7407
  }
7321
7408
  this[kError] = new RangeError("Max payload size exceeded");
@@ -7615,7 +7702,7 @@ var require_receiver = __commonJS({
7615
7702
  * @param {Function} cb Callback
7616
7703
  * @private
7617
7704
  */
7618
- _write(chunk, encoding, cb) {
7705
+ _write(chunk2, encoding, cb) {
7619
7706
  if (this._opcode === 8 && this._state == GET_INFO) return cb();
7620
7707
  if (this._maxBufferedChunks > 0 && this._buffers.length >= this._maxBufferedChunks) {
7621
7708
  cb(
@@ -7629,8 +7716,8 @@ var require_receiver = __commonJS({
7629
7716
  );
7630
7717
  return;
7631
7718
  }
7632
- this._bufferedBytes += chunk.length;
7633
- this._buffers.push(chunk);
7719
+ this._bufferedBytes += chunk2.length;
7720
+ this._buffers.push(chunk2);
7634
7721
  this.startLoop(cb);
7635
7722
  }
7636
7723
  /**
@@ -9904,8 +9991,8 @@ var require_websocket = __commonJS({
9904
9991
  this.removeListener("end", socketOnEnd);
9905
9992
  websocket._readyState = WebSocket2.CLOSING;
9906
9993
  if (!this._readableState.endEmitted && !websocket._closeFrameReceived && !websocket._receiver._writableState.errorEmitted && this._readableState.length !== 0) {
9907
- const chunk = this.read(this._readableState.length);
9908
- websocket._receiver.write(chunk);
9994
+ const chunk2 = this.read(this._readableState.length);
9995
+ websocket._receiver.write(chunk2);
9909
9996
  }
9910
9997
  websocket._receiver.end();
9911
9998
  this[kWebSocket] = void 0;
@@ -9917,8 +10004,8 @@ var require_websocket = __commonJS({
9917
10004
  websocket._receiver.on("finish", receiverOnFinish);
9918
10005
  }
9919
10006
  }
9920
- function socketOnData(chunk) {
9921
- if (!this[kWebSocket]._receiver.write(chunk)) {
10007
+ function socketOnData(chunk2) {
10008
+ if (!this[kWebSocket]._receiver.write(chunk2)) {
9922
10009
  this.pause();
9923
10010
  }
9924
10011
  }
@@ -10021,14 +10108,14 @@ var require_stream = __commonJS({
10021
10108
  duplex._read = function() {
10022
10109
  if (ws.isPaused) ws.resume();
10023
10110
  };
10024
- duplex._write = function(chunk, encoding, callback) {
10111
+ duplex._write = function(chunk2, encoding, callback) {
10025
10112
  if (ws.readyState === ws.CONNECTING) {
10026
10113
  ws.once("open", function open() {
10027
- duplex._write(chunk, encoding, callback);
10114
+ duplex._write(chunk2, encoding, callback);
10028
10115
  });
10029
10116
  return;
10030
10117
  }
10031
- ws.send(chunk, callback);
10118
+ ws.send(chunk2, callback);
10032
10119
  };
10033
10120
  duplex.on("end", duplexOnEnd);
10034
10121
  duplex.on("error", duplexOnError);
@@ -10255,10 +10342,10 @@ var require_websocket_server = __commonJS({
10255
10342
  process.nextTick(emitClose, this);
10256
10343
  }
10257
10344
  } else {
10258
- const server2 = this._server;
10345
+ const server = this._server;
10259
10346
  this._removeListeners();
10260
10347
  this._removeListeners = this._server = null;
10261
- server2.close(() => {
10348
+ server.close(() => {
10262
10349
  emitClose(this);
10263
10350
  });
10264
10351
  }
@@ -10443,17 +10530,17 @@ var require_websocket_server = __commonJS({
10443
10530
  }
10444
10531
  };
10445
10532
  module.exports = WebSocketServer2;
10446
- function addListeners(server2, map2) {
10447
- for (const event of Object.keys(map2)) server2.on(event, map2[event]);
10533
+ function addListeners(server, map2) {
10534
+ for (const event of Object.keys(map2)) server.on(event, map2[event]);
10448
10535
  return function removeListeners() {
10449
10536
  for (const event of Object.keys(map2)) {
10450
- server2.removeListener(event, map2[event]);
10537
+ server.removeListener(event, map2[event]);
10451
10538
  }
10452
10539
  };
10453
10540
  }
10454
- function emitClose(server2) {
10455
- server2._state = CLOSED;
10456
- server2.emit("close");
10541
+ function emitClose(server) {
10542
+ server._state = CLOSED;
10543
+ server.emit("close");
10457
10544
  }
10458
10545
  function socketOnError() {
10459
10546
  this.destroy();
@@ -10472,11 +10559,11 @@ var require_websocket_server = __commonJS({
10472
10559
  ` + Object.keys(headers).map((h) => `${h}: ${headers[h]}`).join("\r\n") + "\r\n\r\n" + message
10473
10560
  );
10474
10561
  }
10475
- function abortHandshakeOrEmitwsClientError(server2, req, socket, code, message, headers) {
10476
- if (server2.listenerCount("wsClientError")) {
10562
+ function abortHandshakeOrEmitwsClientError(server, req, socket, code, message, headers) {
10563
+ if (server.listenerCount("wsClientError")) {
10477
10564
  const err = new Error(message);
10478
10565
  Error.captureStackTrace(err, abortHandshakeOrEmitwsClientError);
10479
- server2.emit("wsClientError", err, socket, req);
10566
+ server.emit("wsClientError", err, socket, req);
10480
10567
  } else {
10481
10568
  abortHandshake(socket, code, message, headers);
10482
10569
  }
@@ -32106,11 +32193,11 @@ var Protocol = class {
32106
32193
  *
32107
32194
  * The Protocol object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
32108
32195
  */
32109
- async connect(transport2) {
32196
+ async connect(transport) {
32110
32197
  if (this._transport) {
32111
32198
  throw new Error("Already connected to a transport. Call close() before connecting to a new transport, or use a separate Protocol instance per connection.");
32112
32199
  }
32113
- this._transport = transport2;
32200
+ this._transport = transport;
32114
32201
  const _onclose = this.transport?.onclose;
32115
32202
  this._transport.onclose = () => {
32116
32203
  _onclose?.();
@@ -33700,8 +33787,8 @@ var McpServer = class {
33700
33787
  *
33701
33788
  * The `server` object assumes ownership of the Transport, replacing any callbacks that have already been set, and expects that it is the only user of the Transport instance going forward.
33702
33789
  */
33703
- async connect(transport2) {
33704
- return await this.server.connect(transport2);
33790
+ async connect(transport) {
33791
+ return await this.server.connect(transport);
33705
33792
  }
33706
33793
  /**
33707
33794
  * Closes the connection.
@@ -34464,8 +34551,8 @@ import process3 from "node:process";
34464
34551
 
34465
34552
  // node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
34466
34553
  var ReadBuffer = class {
34467
- append(chunk) {
34468
- this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
34554
+ append(chunk2) {
34555
+ this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk2]) : chunk2;
34469
34556
  }
34470
34557
  readMessage() {
34471
34558
  if (!this._buffer) {
@@ -34497,8 +34584,8 @@ var StdioServerTransport = class {
34497
34584
  this._stdout = _stdout;
34498
34585
  this._readBuffer = new ReadBuffer();
34499
34586
  this._started = false;
34500
- this._ondata = (chunk) => {
34501
- this._readBuffer.append(chunk);
34587
+ this._ondata = (chunk2) => {
34588
+ this._readBuffer.append(chunk2);
34502
34589
  this.processReadBuffer();
34503
34590
  };
34504
34591
  this._onerror = (error51) => {
@@ -34551,6 +34638,150 @@ var StdioServerTransport = class {
34551
34638
  }
34552
34639
  };
34553
34640
 
34641
+ // node_modules/@chrischall/mcp-utils/dist/server/index.js
34642
+ async function createMcpServer(opts) {
34643
+ const server = new McpServer({ name: opts.name, version: opts.version });
34644
+ if (opts.banner !== void 0) {
34645
+ console.error(opts.banner);
34646
+ }
34647
+ const deps = opts.deps;
34648
+ for (const register of opts.tools) {
34649
+ await register(server, deps);
34650
+ }
34651
+ return server;
34652
+ }
34653
+ function withGracefulShutdown(server, opts = {}) {
34654
+ const shouldExit = opts.exit ?? true;
34655
+ let shuttingDown = false;
34656
+ const handler = (signal) => {
34657
+ if (shuttingDown)
34658
+ return;
34659
+ shuttingDown = true;
34660
+ void (async () => {
34661
+ try {
34662
+ if (opts.onSignal)
34663
+ await opts.onSignal(signal);
34664
+ await server.close();
34665
+ } catch (err) {
34666
+ console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
34667
+ } finally {
34668
+ if (shouldExit)
34669
+ process.exit(0);
34670
+ }
34671
+ })();
34672
+ };
34673
+ process.on("SIGINT", () => handler("SIGINT"));
34674
+ process.on("SIGTERM", () => handler("SIGTERM"));
34675
+ }
34676
+ async function runMcp(opts) {
34677
+ const server = await createMcpServer(opts);
34678
+ const shutdown = opts.shutdown ?? true;
34679
+ if (shutdown !== false) {
34680
+ withGracefulShutdown(server, shutdown === true ? {} : shutdown);
34681
+ }
34682
+ const spec = opts.transport ?? "stdio";
34683
+ const transport = spec === "stdio" ? new StdioServerTransport() : spec;
34684
+ await server.connect(transport);
34685
+ return server;
34686
+ }
34687
+
34688
+ // node_modules/@chrischall/mcp-utils/dist/response/index.js
34689
+ function textResult(data) {
34690
+ return {
34691
+ content: [{ type: "text", text: JSON.stringify(data, null, 2) }]
34692
+ };
34693
+ }
34694
+
34695
+ // node_modules/@chrischall/mcp-utils/dist/config/index.js
34696
+ var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
34697
+ function readEnvVar(key, opts = {}) {
34698
+ const env = opts.env ?? process.env;
34699
+ const raw = env[key];
34700
+ if (typeof raw === "string") {
34701
+ const trimmed = raw.trim();
34702
+ if (trimmed.length > 0 && trimmed !== "undefined" && trimmed !== "null" && !PLACEHOLDER_RE.test(trimmed)) {
34703
+ return trimmed;
34704
+ }
34705
+ }
34706
+ return opts.default;
34707
+ }
34708
+ var TRUE_TOKENS = /* @__PURE__ */ new Set(["1", "true", "yes", "on"]);
34709
+ var FALSE_TOKENS = /* @__PURE__ */ new Set(["0", "false", "no", "off"]);
34710
+ function parseBoolEnv(key, opts = {}) {
34711
+ const fallback = opts.default ?? false;
34712
+ const raw = readEnvVar(key, { env: opts.env });
34713
+ if (raw === void 0)
34714
+ return fallback;
34715
+ const token = raw.toLowerCase();
34716
+ if (TRUE_TOKENS.has(token))
34717
+ return true;
34718
+ if (FALSE_TOKENS.has(token))
34719
+ return false;
34720
+ return fallback;
34721
+ }
34722
+ async function loadDotenvSafely(opts = {}) {
34723
+ try {
34724
+ const mod = await import(
34725
+ /* @vite-ignore */
34726
+ "dotenv"
34727
+ );
34728
+ const result = mod.config({
34729
+ ...opts.path !== void 0 ? { path: opts.path } : {},
34730
+ override: opts.override ?? false,
34731
+ quiet: true
34732
+ });
34733
+ return result.error === void 0;
34734
+ } catch {
34735
+ return false;
34736
+ }
34737
+ }
34738
+
34739
+ // node_modules/@chrischall/mcp-utils/dist/http/index.js
34740
+ var MAX_AGE_ZERO_RE = /(?:^|;)\s*Max-Age\s*=\s*0\s*(?:;|$)/i;
34741
+ var EXPIRES_EPOCH_RE = /(?:^|;)\s*Expires\s*=\s*Thu,\s*01\s*Jan\s*1970/i;
34742
+ function parseCookieJar(setCookieHeaders) {
34743
+ const entries = setCookieHeaders == null ? [] : Array.isArray(setCookieHeaders) ? setCookieHeaders : splitSetCookie(setCookieHeaders);
34744
+ const jar = /* @__PURE__ */ new Map();
34745
+ for (const entry of entries) {
34746
+ if (MAX_AGE_ZERO_RE.test(entry) || EXPIRES_EPOCH_RE.test(entry))
34747
+ continue;
34748
+ const nameValue = (entry.split(";")[0] ?? "").trim();
34749
+ const eqIdx = nameValue.indexOf("=");
34750
+ if (eqIdx < 1)
34751
+ continue;
34752
+ const name = nameValue.slice(0, eqIdx).trim();
34753
+ const value = nameValue.slice(eqIdx + 1).trim();
34754
+ if (!value)
34755
+ continue;
34756
+ jar.set(name, value);
34757
+ }
34758
+ const cookies = {};
34759
+ for (const [k, v] of jar)
34760
+ cookies[k] = v;
34761
+ const cookieHeader = [...jar.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
34762
+ return { cookies, cookieHeader };
34763
+ }
34764
+ function splitSetCookie(header) {
34765
+ return header.split(/,(?=\s*[^;,\s]+\s*=)/).map((s) => s.trim()).filter((s) => s.length > 0);
34766
+ }
34767
+
34768
+ // node_modules/@chrischall/mcp-utils/dist/zod/index.js
34769
+ var PositiveInt = external_exports.number().int().positive();
34770
+ var NonNegInt = external_exports.number().int().nonnegative();
34771
+ var NonEmptyString = external_exports.string().min(1);
34772
+ var IsoDate = external_exports.iso.date();
34773
+ var IsoTime = external_exports.string().regex(/^([01]?\d|2[0-3]):[0-5]\d$/, "must be HH:MM (24h), e.g. 19:30");
34774
+ var schemaOrigin = external_exports.string().optional().describe("Portal origin (e.g. https://<vendor>.example.co) selecting which active session to use. Optional when only one session is active.");
34775
+ var schemaConfirm = external_exports.boolean().optional().describe("Must be true to proceed. Without this, the tool returns a preview.");
34776
+ var paginationSchema = {
34777
+ offset: NonNegInt.default(0).describe("Number of items to skip (0-based)."),
34778
+ limit: external_exports.number().int().min(1).max(200).default(50).describe("Maximum number of items to return (1-200).")
34779
+ };
34780
+ var pageSchema = {
34781
+ page_num: PositiveInt.default(1).describe("1-based page number."),
34782
+ page_size: external_exports.number().int().min(1).max(200).default(50).describe("Number of items per page (1-200).")
34783
+ };
34784
+
34554
34785
  // node_modules/@fetchproxy/protocol/dist/frames.js
34555
34786
  var PROTOCOL_VERSION = 2;
34556
34787
  var HKDF_SESSION_INFO = "fetchproxy/1.0.0/session";
@@ -34707,6 +34938,16 @@ function assertScopeKeyArray(value, label) {
34707
34938
  seen.add(k);
34708
34939
  }
34709
34940
  }
34941
+ var CAPTURE_PATH_RE = /^\/[A-Za-z0-9._~%\-/]*\*?$/;
34942
+ function hostMatchesAnyDomain(host, domains) {
34943
+ const h = host.toLowerCase();
34944
+ for (const d of domains) {
34945
+ const dom = d.toLowerCase();
34946
+ if (h === dom || h.endsWith("." + dom))
34947
+ return true;
34948
+ }
34949
+ return false;
34950
+ }
34710
34951
  function assertCaptureHeadersArray(value, label) {
34711
34952
  if (!Array.isArray(value)) {
34712
34953
  throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
@@ -34715,14 +34956,25 @@ function assertCaptureHeadersArray(value, label) {
34715
34956
  for (let i = 0; i < value.length; i++) {
34716
34957
  const entry = value[i];
34717
34958
  assertObject(entry, `${label}[${i}]`);
34718
- if (entry.urlPattern === void 0) {
34719
- throw new ProtocolError(`${label}[${i}].urlPattern: missing`);
34959
+ if (entry.host === void 0) {
34960
+ throw new ProtocolError(`${label}[${i}].host: missing`);
34720
34961
  }
34721
34962
  if (entry.headerName === void 0) {
34722
34963
  throw new ProtocolError(`${label}[${i}].headerName: missing`);
34723
34964
  }
34724
- if (typeof entry.urlPattern !== "string") {
34725
- throw new ProtocolError(`${label}[${i}].urlPattern: expected string, got ${typeof entry.urlPattern}`);
34965
+ if (typeof entry.host !== "string") {
34966
+ throw new ProtocolError(`${label}[${i}].host: expected string, got ${typeof entry.host}`);
34967
+ }
34968
+ if (!HOSTNAME_RE.test(entry.host)) {
34969
+ throw new ProtocolError(`${label}[${i}].host: invalid hostname ${JSON.stringify(entry.host)}`);
34970
+ }
34971
+ if (entry.path !== void 0) {
34972
+ if (typeof entry.path !== "string") {
34973
+ throw new ProtocolError(`${label}[${i}].path: expected string, got ${typeof entry.path}`);
34974
+ }
34975
+ if (!CAPTURE_PATH_RE.test(entry.path)) {
34976
+ throw new ProtocolError(`${label}[${i}].path: must start with '/' ${JSON.stringify(entry.path)}`);
34977
+ }
34726
34978
  }
34727
34979
  if (typeof entry.headerName !== "string") {
34728
34980
  throw new ProtocolError(`${label}[${i}].headerName: expected string, got ${typeof entry.headerName}`);
@@ -34730,19 +34982,29 @@ function assertCaptureHeadersArray(value, label) {
34730
34982
  if (!HEADER_NAME_RE.test(entry.headerName)) {
34731
34983
  throw new ProtocolError(`${label}[${i}].headerName: invalid name ${JSON.stringify(entry.headerName)}`);
34732
34984
  }
34733
- assertCaptureUrlPattern(entry.urlPattern, `${label}[${i}].urlPattern`);
34734
- const key = `${entry.urlPattern}\0${entry.headerName}`;
34985
+ const normalizedPath = entry.path ?? "/*";
34986
+ const key = `${entry.host}\0${normalizedPath}\0${entry.headerName}`;
34735
34987
  if (seen.has(key)) {
34736
- throw new ProtocolError(`${label}: duplicate ${JSON.stringify({ urlPattern: entry.urlPattern, headerName: entry.headerName })}`);
34988
+ throw new ProtocolError(`${label}: duplicate ${JSON.stringify({ host: entry.host, path: normalizedPath, headerName: entry.headerName })}`);
34737
34989
  }
34738
34990
  seen.add(key);
34739
34991
  for (const k of Object.keys(entry)) {
34740
- if (k !== "urlPattern" && k !== "headerName") {
34992
+ if (k !== "host" && k !== "path" && k !== "headerName") {
34741
34993
  throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
34742
34994
  }
34743
34995
  }
34744
34996
  }
34745
34997
  }
34998
+ function validateCaptureHeaderDecls(value, domains, label = "captureHeaders") {
34999
+ assertCaptureHeadersArray(value, label);
35000
+ const arr = value;
35001
+ for (let i = 0; i < arr.length; i++) {
35002
+ const host = arr[i].host;
35003
+ if (!hostMatchesAnyDomain(host, domains)) {
35004
+ throw new ProtocolError(`${label}[${i}].host: ${JSON.stringify(host)} is not a declared domain or subdomain of [${domains.join(", ")}]`);
35005
+ }
35006
+ }
35007
+ }
34746
35008
  function assertStoragePointersArray(value, label, declaredKeys) {
34747
35009
  if (!Array.isArray(value)) {
34748
35010
  throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
@@ -34830,23 +35092,6 @@ function assertIndexedDbScopesArray(value, label) {
34830
35092
  }
34831
35093
  }
34832
35094
  }
34833
- function assertCaptureUrlPattern(pattern, label) {
34834
- if (!pattern.startsWith("https://")) {
34835
- throw new ProtocolError(`${label}: must start with https:// (got ${JSON.stringify(pattern)})`);
34836
- }
34837
- const afterScheme = pattern.slice("https://".length);
34838
- const slash = afterScheme.indexOf("/");
34839
- const host = slash === -1 ? afterScheme : afterScheme.slice(0, slash);
34840
- if (host.length === 0) {
34841
- throw new ProtocolError(`${label}: missing host (got ${JSON.stringify(pattern)})`);
34842
- }
34843
- if (host.includes("*")) {
34844
- throw new ProtocolError(`${label}: wildcards not permitted in host (got ${JSON.stringify(pattern)})`);
34845
- }
34846
- if (!HOSTNAME_RE.test(host)) {
34847
- throw new ProtocolError(`${label}: invalid host ${JSON.stringify(host)} in ${JSON.stringify(pattern)}`);
34848
- }
34849
- }
34850
35095
  function validateFrame(raw) {
34851
35096
  assertObject(raw, "frame");
34852
35097
  const t = raw.type;
@@ -34911,7 +35156,7 @@ function validateHello(raw) {
34911
35156
  assertScopeKeyArray(raw.sessionStorageKeys, "hello.sessionStorageKeys");
34912
35157
  }
34913
35158
  if (raw.captureHeaders !== void 0) {
34914
- assertCaptureHeadersArray(raw.captureHeaders, "hello.captureHeaders");
35159
+ validateCaptureHeaderDecls(raw.captureHeaders, raw.domains, "hello.captureHeaders");
34915
35160
  }
34916
35161
  if (raw.indexedDbScopes !== void 0) {
34917
35162
  assertIndexedDbScopesArray(raw.indexedDbScopes, "hello.indexedDbScopes");
@@ -35079,19 +35324,28 @@ function validateInnerRequest(raw) {
35079
35324
  }
35080
35325
  if (raw.op === "capture_request_header") {
35081
35326
  assertObject(raw.init, "inner.init");
35082
- if (raw.init.urlPattern === void 0) {
35083
- throw new ProtocolError("inner.init.urlPattern: missing");
35327
+ if (raw.init.host === void 0) {
35328
+ throw new ProtocolError("inner.init.host: missing");
35084
35329
  }
35085
35330
  if (raw.init.headerName === void 0) {
35086
35331
  throw new ProtocolError("inner.init.headerName: missing");
35087
35332
  }
35088
- assertString(raw.init.urlPattern, "inner.init.urlPattern");
35333
+ assertString(raw.init.host, "inner.init.host");
35334
+ if (!HOSTNAME_RE.test(raw.init.host)) {
35335
+ throw new ProtocolError(`inner.init.host: invalid hostname ${JSON.stringify(raw.init.host)}`);
35336
+ }
35337
+ if (raw.init.path !== void 0) {
35338
+ assertString(raw.init.path, "inner.init.path");
35339
+ if (!CAPTURE_PATH_RE.test(raw.init.path)) {
35340
+ throw new ProtocolError(`inner.init.path: must start with '/' ${JSON.stringify(raw.init.path)}`);
35341
+ }
35342
+ }
35089
35343
  assertString(raw.init.headerName, "inner.init.headerName");
35090
35344
  if (raw.init.timeoutMs !== void 0) {
35091
35345
  assertPositiveInt(raw.init.timeoutMs, "inner.init.timeoutMs");
35092
35346
  }
35093
35347
  for (const k of Object.keys(raw.init)) {
35094
- if (k !== "urlPattern" && k !== "headerName" && k !== "timeoutMs") {
35348
+ if (k !== "host" && k !== "path" && k !== "headerName" && k !== "timeoutMs") {
35095
35349
  throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on capture_request_header`);
35096
35350
  }
35097
35351
  }
@@ -35407,13 +35661,13 @@ async function openEncryptedFrame(sessionKey, frame) {
35407
35661
  // node_modules/@fetchproxy/server/dist/election.js
35408
35662
  import { createServer, Server as HttpServer } from "node:http";
35409
35663
  async function electRole(opts) {
35410
- const server2 = createServer();
35664
+ const server = createServer();
35411
35665
  return new Promise((resolve, reject) => {
35412
35666
  const onError = (e) => {
35413
- server2.removeListener("listening", onListening);
35667
+ server.removeListener("listening", onListening);
35414
35668
  if (e.code === "EADDRINUSE") {
35415
35669
  try {
35416
- server2.close();
35670
+ server.close();
35417
35671
  } catch {
35418
35672
  }
35419
35673
  resolve({ role: "peer" });
@@ -35422,12 +35676,12 @@ async function electRole(opts) {
35422
35676
  }
35423
35677
  };
35424
35678
  const onListening = () => {
35425
- server2.removeListener("error", onError);
35426
- resolve({ role: "host", server: server2 });
35679
+ server.removeListener("error", onError);
35680
+ resolve({ role: "host", server });
35427
35681
  };
35428
- server2.once("error", onError);
35429
- server2.once("listening", onListening);
35430
- server2.listen(opts.port, opts.host);
35682
+ server.once("error", onError);
35683
+ server.once("listening", onListening);
35684
+ server.listen(opts.port, opts.host);
35431
35685
  });
35432
35686
  }
35433
35687
 
@@ -35471,7 +35725,8 @@ async function buildServerHello(opts) {
35471
35725
  }
35472
35726
  if (opts.captureHeaders && opts.captureHeaders.length > 0) {
35473
35727
  hello.captureHeaders = opts.captureHeaders.map((d) => ({
35474
- urlPattern: d.urlPattern,
35728
+ host: d.host,
35729
+ ...d.path !== void 0 ? { path: d.path } : {},
35475
35730
  headerName: d.headerName
35476
35731
  }));
35477
35732
  }
@@ -35709,7 +35964,9 @@ async function startHost(opts) {
35709
35964
  } catch {
35710
35965
  }
35711
35966
  }
35712
- wss.close(() => resolve());
35967
+ wss.close(() => {
35968
+ opts.httpServer.close(() => resolve());
35969
+ });
35713
35970
  }),
35714
35971
  sendOwnInner: async (inner) => {
35715
35972
  const session = await ownSessionReady;
@@ -35759,6 +36016,7 @@ async function startPeer(opts) {
35759
36016
  const innerListeners = [];
35760
36017
  const renegotiateListeners = [];
35761
36018
  const pendingPairListeners = [];
36019
+ const closeListeners = [];
35762
36020
  let session = null;
35763
36021
  let pendingPairCode = null;
35764
36022
  let resolveFirstReady;
@@ -35808,6 +36066,7 @@ async function startPeer(opts) {
35808
36066
  ws.on("message", onMessage);
35809
36067
  ws.once("close", () => {
35810
36068
  rejectFirstReady(new Error("peer WS closed before ready"));
36069
+ closeListeners.forEach((cb) => cb());
35811
36070
  });
35812
36071
  sessionPromise.catch(() => {
35813
36072
  });
@@ -35830,6 +36089,9 @@ async function startPeer(opts) {
35830
36089
  pendingPairListeners.push(cb);
35831
36090
  },
35832
36091
  pendingPairCode: () => pendingPairCode,
36092
+ onClose: (cb) => {
36093
+ closeListeners.push(cb);
36094
+ },
35833
36095
  close: () => ws.close()
35834
36096
  };
35835
36097
  return handle;
@@ -35912,6 +36174,19 @@ function classifyFetchError(error51) {
35912
36174
  return "other";
35913
36175
  }
35914
36176
 
36177
+ // node_modules/@fetchproxy/server/dist/classify-bridge-error.js
36178
+ function classifyBridgeError(err) {
36179
+ if (err instanceof FetchproxyTimeoutError)
36180
+ return "timeout";
36181
+ if (err instanceof FetchproxyBridgeDownError)
36182
+ return "bridge_down";
36183
+ if (err instanceof FetchproxyHttpError)
36184
+ return "http";
36185
+ if (err instanceof FetchproxyProtocolError)
36186
+ return "protocol";
36187
+ return "other";
36188
+ }
36189
+
35915
36190
  // node_modules/@fetchproxy/server/dist/ws-server.js
35916
36191
  var FetchproxyProtocolError = class extends Error {
35917
36192
  constructor(message) {
@@ -35941,7 +36216,7 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
35941
36216
  const retryAttempted = args.retryAttempted ?? false;
35942
36217
  const op = args.op ?? "fetch";
35943
36218
  const retryClause = retryAttempted ? `Server already burned a one-shot lazy-revive retry; SW is still down. ` : `Server lazy-revive retry was disabled (bridgeReviveDelayMs unset/0). `;
35944
- const hint = `the fetchproxy extension's service worker is not responding ("${args.originalError}"). Chrome evicts extension service workers after ~30s idle by default. ${retryClause}Wake it by clicking the fetchproxy extension toolbar icon, then retry. If it keeps happening, reload the extension from chrome://extensions.`;
36219
+ const hint = `the fetchproxy extension's service worker is not responding ("${args.originalError}"). Chrome evicts extension service workers after ~30s idle by default. ${retryClause}Make sure a tab for this domain is open, fully loaded, and signed in (the bridge fetches through that tab) \u2014 then retry. If it keeps happening, reload the extension from chrome://extensions and reload the tab.`;
35945
36220
  super(`fetchproxy bridge down during ${op}${args.url ? ` (${args.url})` : ""}. ${hint}`);
35946
36221
  this.name = "FetchproxyBridgeDownError";
35947
36222
  this.originalError = args.originalError;
@@ -35963,6 +36238,14 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
35963
36238
  port;
35964
36239
  /** 0.8.0+: actual elapsed milliseconds when the timer won the race. */
35965
36240
  elapsedMs;
36241
+ /**
36242
+ * 0.11.0+ (#90/#91): true when the server's lazy-revive retry path
36243
+ * fired for this timeout (a cold-start `timeout` symptom followed by
36244
+ * a warm-and-retry that also timed out). False when the retry was
36245
+ * disabled (`bridgeReviveDelayMs` unset/0) so the timeout surfaced on
36246
+ * the first attempt.
36247
+ */
36248
+ retryAttempted;
35966
36249
  constructor(args) {
35967
36250
  super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms`);
35968
36251
  this.name = "FetchproxyTimeoutError";
@@ -35971,6 +36254,7 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
35971
36254
  this.role = args.role ?? null;
35972
36255
  this.port = args.port ?? 0;
35973
36256
  this.elapsedMs = args.elapsedMs ?? args.timeoutMs;
36257
+ this.retryAttempted = args.retryAttempted ?? false;
35974
36258
  }
35975
36259
  };
35976
36260
  var SUBDOMAIN_LABEL_RE = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$/i;
@@ -36009,6 +36293,12 @@ var FetchproxyServer = class {
36009
36293
  opts;
36010
36294
  hostHandle = null;
36011
36295
  peerHandle = null;
36296
+ // 0.13.0+: true from the start of `close()` until the next `doConnect()`.
36297
+ // Distinguishes an intentional shutdown (whose WS close we must ignore)
36298
+ // from the host process dying (which strands a peer and must trigger
36299
+ // re-election). The WS `close` event fires asynchronously, so this stays
36300
+ // latched across `close()` rather than being reset in its tail.
36301
+ closing = false;
36012
36302
  nextRequestId = 1;
36013
36303
  // 0.8.0+: process-wide freshness counters surfaced via bridgeHealth().
36014
36304
  // Replaces the local copies every downstream MCP was rolling on top
@@ -36044,6 +36334,25 @@ var FetchproxyServer = class {
36044
36334
  // for "we're connecting right now" so two parallel first-calls don't
36045
36335
  // race the port bind.
36046
36336
  connectingPromise = null;
36337
+ // 0.8.1+ (#67): server-initiated keep-alive ping. Active when
36338
+ // `keepAliveIntervalMs` is set AND we've seen recent activity
36339
+ // (fetch/capture success or failure, or markActive()) within
36340
+ // `keepAliveMaxIdleMs`. The interval handle is created lazily on
36341
+ // first activity and torn down on close() / extension disconnect.
36342
+ keepAliveTimer = null;
36343
+ lastActiveAt = null;
36344
+ // 0.10.0+ (#73): observability counters surfaced via
36345
+ // bridgeHealth().keepAlive / .swEviction. Monotonic across the process
36346
+ // lifetime so a downstream healthcheck tool can verify the keep-alive
36347
+ // is actually preventing SW eviction. `lastPingAt` and `totalPings`
36348
+ // are stamped from `startKeepaliveIfIdle`'s tick. `lazyRevive*` and
36349
+ // `lastEvictionDetectedAt` are stamped from the lazy-revive code path
36350
+ // in fetch() / captureRequestHeader().
36351
+ lastPingAt = null;
36352
+ totalPings = 0;
36353
+ lazyReviveAttempts = 0;
36354
+ lazyReviveSuccesses = 0;
36355
+ lastEvictionDetectedAt = null;
36047
36356
  constructor(opts) {
36048
36357
  if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
36049
36358
  throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
@@ -36062,6 +36371,14 @@ var FetchproxyServer = class {
36062
36371
  }
36063
36372
  capabilities = [...opts.capabilities];
36064
36373
  }
36374
+ if (opts.captureHeaders !== void 0) {
36375
+ try {
36376
+ validateCaptureHeaderDecls(opts.captureHeaders, opts.domains);
36377
+ } catch (err) {
36378
+ const message = err instanceof Error ? err.message : String(err);
36379
+ throw new Error("FetchproxyServer: invalid captureHeaders \u2014 " + message);
36380
+ }
36381
+ }
36065
36382
  this.opts = {
36066
36383
  port: opts.port ?? 37149,
36067
36384
  host: opts.host ?? "127.0.0.1",
@@ -36073,7 +36390,8 @@ var FetchproxyServer = class {
36073
36390
  localStorageKeys: [...opts.localStorageKeys ?? []],
36074
36391
  sessionStorageKeys: [...opts.sessionStorageKeys ?? []],
36075
36392
  captureHeaders: (opts.captureHeaders ?? []).map((d) => ({
36076
- urlPattern: d.urlPattern,
36393
+ host: d.host,
36394
+ ...d.path !== void 0 ? { path: d.path } : {},
36077
36395
  headerName: d.headerName
36078
36396
  })),
36079
36397
  indexedDbScopes: (opts.indexedDbScopes ?? []).map((d) => ({
@@ -36096,6 +36414,22 @@ var FetchproxyServer = class {
36096
36414
  // the legacy hang-forever / fail-once-on-SW-eviction behavior.
36097
36415
  fetchTimeoutMs: opts.fetchTimeoutMs ?? 3e4,
36098
36416
  bridgeReviveDelayMs: opts.bridgeReviveDelayMs ?? 2e3,
36417
+ // 0.10.0+ (#72): keep-alive defaults to 25s — round-3 #71 cohort
36418
+ // wave showed every Pattern A consumer was opting into this same
36419
+ // value. Pass `0` to disable; the existing `<= 0` guards in
36420
+ // `startKeepaliveIfIdle` / `noteActivityForKeepalive` honour that.
36421
+ //
36422
+ // #90 (P1-1): tightened to 20s. 25s left only ~5s of slack under
36423
+ // Chrome's ~30s SW-eviction window — slack that timer drift, a
36424
+ // busy host event loop (CPU-bound response parsing between calls),
36425
+ // and the ping's own round-trip latency routinely ate, so the SW
36426
+ // evicted before the next ping landed and the next call cold-
36427
+ // started. 20s restores real margin. (The extension
36428
+ // `chrome.alarms` backstop is clamped by Chrome to a 30s minimum
36429
+ // period, firing *at* the edge — it can't rescue a sub-30s race;
36430
+ // the server ping is the real defense.)
36431
+ keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
36432
+ keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
36099
36433
  identityDir: opts.identityDir,
36100
36434
  onPairCode: opts.onPairCode
36101
36435
  };
@@ -36175,6 +36509,7 @@ var FetchproxyServer = class {
36175
36509
  async doConnect() {
36176
36510
  const identity = this.identity;
36177
36511
  const mcpId = this.mcpId;
36512
+ this.closing = false;
36178
36513
  const el = await electRole({ host: this.opts.host, port: this.opts.port });
36179
36514
  if (el.role === "host") {
36180
36515
  this.role = "host";
@@ -36196,7 +36531,10 @@ var FetchproxyServer = class {
36196
36531
  onPairCode: this.opts.onPairCode
36197
36532
  });
36198
36533
  this.hostHandle.onOwnInner((inner) => this.onInner(inner));
36199
- this.hostHandle.onExtensionDisconnect(() => this.rejectAllPending());
36534
+ this.hostHandle.onExtensionDisconnect(() => {
36535
+ this.stopKeepalive();
36536
+ this.rejectAllPending();
36537
+ });
36200
36538
  this.hostHandle.onPendingPair((code) => {
36201
36539
  this.rejectAllPending(this.pairingErrorMessage(code));
36202
36540
  });
@@ -36220,7 +36558,10 @@ var FetchproxyServer = class {
36220
36558
  sessionStoragePointers: this.opts.sessionStoragePointers
36221
36559
  });
36222
36560
  this.peerHandle.onInner((inner) => this.onInner(inner));
36223
- this.peerHandle.onRenegotiate(() => this.rejectAllPending());
36561
+ this.peerHandle.onRenegotiate(() => {
36562
+ this.stopKeepalive();
36563
+ this.rejectAllPending();
36564
+ });
36224
36565
  this.peerHandle.onPendingPair((code) => {
36225
36566
  this.rejectAllPending(this.pairingErrorMessage(code));
36226
36567
  });
@@ -36228,6 +36569,14 @@ var FetchproxyServer = class {
36228
36569
  const cb = this.opts.onPairCode;
36229
36570
  this.peerHandle.onPendingPair((code) => cb(code));
36230
36571
  }
36572
+ this.peerHandle.onClose(() => {
36573
+ if (this.closing || this.peerHandle === null)
36574
+ return;
36575
+ this.stopKeepalive();
36576
+ this.rejectAllPending();
36577
+ this.peerHandle = null;
36578
+ this.role = null;
36579
+ });
36231
36580
  }
36232
36581
  }
36233
36582
  pairingErrorMessage(code) {
@@ -36261,13 +36610,18 @@ var FetchproxyServer = class {
36261
36610
  }
36262
36611
  const first = await this._fetchOnceWithTimeout(init);
36263
36612
  const reviveMs = this.opts.bridgeReviveDelayMs;
36264
- let final = first;
36265
- if (!first.ok && first.kind === "content_script_unreachable" && reviveMs !== void 0 && reviveMs > 0) {
36613
+ const isColdStartSymptom = !first.ok && (first.kind === "content_script_unreachable" || first.kind === "timeout");
36614
+ if (isColdStartSymptom) {
36615
+ this.lastEvictionDetectedAt = Date.now();
36616
+ }
36617
+ if (isColdStartSymptom && reviveMs !== void 0 && reviveMs > 0) {
36618
+ this.lazyReviveAttempts += 1;
36266
36619
  await new Promise((r) => setTimeout(r, reviveMs));
36267
36620
  const second = await this._fetchOnceWithTimeout(init);
36268
- if (second.ok)
36621
+ if (second.ok) {
36622
+ this.lazyReviveSuccesses += 1;
36269
36623
  this.recordSuccess();
36270
- else
36624
+ } else
36271
36625
  this.recordFailure(`${second.kind ?? "other"}: ${second.error}`);
36272
36626
  return { ...second, retryAttempted: true };
36273
36627
  }
@@ -36289,6 +36643,9 @@ var FetchproxyServer = class {
36289
36643
  * call (addresses #23 ask 4).
36290
36644
  */
36291
36645
  bridgeHealth() {
36646
+ const intervalMs = this.opts.keepAliveIntervalMs;
36647
+ const maxIdleMs = this.opts.keepAliveMaxIdleMs;
36648
+ const idleSinceMs = this.lastActiveAt === null ? null : Date.now() - this.lastActiveAt;
36292
36649
  return {
36293
36650
  role: this.role,
36294
36651
  port: this.opts.port,
@@ -36299,17 +36656,85 @@ var FetchproxyServer = class {
36299
36656
  lastFailureAt: this.lastFailureAt,
36300
36657
  lastFailureReason: this.lastFailureReason,
36301
36658
  consecutiveFailures: this.consecutiveFailures,
36302
- lastExtensionMessageAt: this.lastExtensionMessageAt
36659
+ lastExtensionMessageAt: this.lastExtensionMessageAt,
36660
+ keepAlive: {
36661
+ enabled: intervalMs > 0,
36662
+ intervalMs,
36663
+ maxIdleMs,
36664
+ lastPingAt: this.lastPingAt,
36665
+ totalPings: this.totalPings,
36666
+ idleSinceMs
36667
+ },
36668
+ swEviction: {
36669
+ lazyReviveAttempts: this.lazyReviveAttempts,
36670
+ lazyReviveSuccesses: this.lazyReviveSuccesses,
36671
+ lastEvictionDetectedAt: this.lastEvictionDetectedAt
36672
+ }
36303
36673
  };
36304
36674
  }
36305
36675
  recordSuccess() {
36306
36676
  this.lastSuccessAt = Date.now();
36307
36677
  this.consecutiveFailures = 0;
36678
+ this.noteActivityForKeepalive();
36308
36679
  }
36309
36680
  recordFailure(reason) {
36310
36681
  this.lastFailureAt = Date.now();
36311
36682
  this.lastFailureReason = reason;
36312
36683
  this.consecutiveFailures += 1;
36684
+ this.noteActivityForKeepalive();
36685
+ }
36686
+ /**
36687
+ * 0.8.1+ (#67): caller-side hint that work is happening or about to
36688
+ * happen — bumps the keep-alive idle gate so the server keeps pinging
36689
+ * the extension. Useful for MCPs that do a chain of side-effectful
36690
+ * work between bridge calls and don't want the SW to evict in the
36691
+ * gap (e.g. server-side parsing of a previous response that takes
36692
+ * tens of seconds). No-op when `keepAliveIntervalMs` is `0`.
36693
+ */
36694
+ markActive() {
36695
+ this.noteActivityForKeepalive();
36696
+ }
36697
+ noteActivityForKeepalive() {
36698
+ const intervalMs = this.opts.keepAliveIntervalMs;
36699
+ if (intervalMs <= 0)
36700
+ return;
36701
+ this.lastActiveAt = Date.now();
36702
+ this.startKeepaliveIfIdle();
36703
+ }
36704
+ startKeepaliveIfIdle() {
36705
+ if (this.keepAliveTimer !== null)
36706
+ return;
36707
+ const intervalMs = this.opts.keepAliveIntervalMs;
36708
+ if (intervalMs <= 0)
36709
+ return;
36710
+ this.keepAliveTimer = setInterval(() => {
36711
+ const now = Date.now();
36712
+ if (this.lastActiveAt === null || now - this.lastActiveAt > this.opts.keepAliveMaxIdleMs) {
36713
+ this.stopKeepalive();
36714
+ return;
36715
+ }
36716
+ this.totalPings += 1;
36717
+ this.lastPingAt = now;
36718
+ void this.sendKeepalivePing();
36719
+ }, intervalMs);
36720
+ }
36721
+ async sendKeepalivePing() {
36722
+ try {
36723
+ const inner = { type: "ping" };
36724
+ if (this.hostHandle) {
36725
+ await this.hostHandle.sendOwnInner(inner);
36726
+ } else if (this.peerHandle) {
36727
+ await this.peerHandle.sendInner(inner);
36728
+ }
36729
+ } catch (e) {
36730
+ console.error("[fetchproxy] keepalive ping send failed:", e);
36731
+ }
36732
+ }
36733
+ stopKeepalive() {
36734
+ if (this.keepAliveTimer !== null) {
36735
+ clearInterval(this.keepAliveTimer);
36736
+ this.keepAliveTimer = null;
36737
+ }
36313
36738
  }
36314
36739
  /**
36315
36740
  * Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
@@ -36368,7 +36793,8 @@ var FetchproxyServer = class {
36368
36793
  timeoutMs: this.opts.fetchTimeoutMs ?? 0,
36369
36794
  role: this.role,
36370
36795
  port: this.opts.port,
36371
- elapsedMs: result.elapsedMs
36796
+ elapsedMs: result.elapsedMs,
36797
+ retryAttempted
36372
36798
  });
36373
36799
  }
36374
36800
  if (result.kind === "content_script_unreachable") {
@@ -36499,6 +36925,108 @@ var FetchproxyServer = class {
36499
36925
  const response = await this.get(path, this.applyJsonDefaults(opts));
36500
36926
  return response.body;
36501
36927
  }
36928
+ /**
36929
+ * 0.11.0+: method-generic JSON convenience helper. Generalizes the
36930
+ * `fetchJson<T>(path, { method, headers, body })` that
36931
+ * zillow/redfin/compass/homes hand-rolled char-for-char in their
36932
+ * `src/client.ts`:
36933
+ *
36934
+ * - sets `Accept: application/json`;
36935
+ * - adds `Content-Type: application/json` only for a non-GET request
36936
+ * that carries a `body` (and only if the caller didn't set one);
36937
+ * - `JSON.stringify`s the body (GET / no-body sends nothing);
36938
+ * - treats a `204` or an empty body as `data: null` (no parse);
36939
+ * - otherwise `JSON.parse`s the body.
36940
+ *
36941
+ * Scope is serialization + header defaults + 204-handling +
36942
+ * JSON.parse ONLY. It deliberately does NOT assert on the HTTP status
36943
+ * or look for a sign-in interstitial — those guards differ per site
36944
+ * (Zillow's `captcha-delivery`, Redfin's AWS-WAF challenge, …), so it
36945
+ * returns BOTH the parsed `data` and the raw `FetchResult` and leaves
36946
+ * the consumer to run its own `throwIfNotOk` / `throwIfSignInPage`
36947
+ * over `result`.
36948
+ *
36949
+ * Bridge-level failures (no signed-in tab, SW down, timeout) still
36950
+ * throw the typed errors via `request()`, exactly like the verb
36951
+ * helpers — only successful round-trips (any HTTP status) return.
36952
+ */
36953
+ async requestJson(method, path, opts = {}) {
36954
+ const isGet = method.toUpperCase() === "GET";
36955
+ const sendBody = !isGet && opts.body !== void 0;
36956
+ const headers = {
36957
+ Accept: "application/json",
36958
+ ...sendBody && !this.hasContentType(opts.headers ?? {}) ? { "Content-Type": "application/json" } : {},
36959
+ ...opts.headers ?? {}
36960
+ };
36961
+ const response = await this.request(method, path, {
36962
+ headers,
36963
+ body: sendBody ? JSON.stringify(opts.body) : void 0,
36964
+ ...opts.subdomain !== void 0 ? { subdomain: opts.subdomain } : {},
36965
+ ...opts.domain !== void 0 ? { domain: opts.domain } : {}
36966
+ });
36967
+ const result = {
36968
+ ok: true,
36969
+ status: response.status,
36970
+ url: response.url,
36971
+ body: response.body
36972
+ };
36973
+ if (response.status === 204 || response.body === "") {
36974
+ return { data: null, result };
36975
+ }
36976
+ let data;
36977
+ try {
36978
+ data = JSON.parse(response.body);
36979
+ } catch (e) {
36980
+ throw new Error(`fetchproxy ${method} ${path} \u2014 response was not JSON: ${e instanceof Error ? e.message : String(e)}`);
36981
+ }
36982
+ return { data, result };
36983
+ }
36984
+ /**
36985
+ * 0.11.0+: run a single healthcheck probe through `fetchFn`, measure
36986
+ * the elapsed round-trip, classify any thrown error, and project the
36987
+ * post-probe `bridgeHealth()` into a snake-cased `bridge` sub-object.
36988
+ *
36989
+ * This is the transport half of the probe loop zillow/redfin/homes
36990
+ * had duplicated verbatim in `src/tools/healthcheck.ts`. The MCP
36991
+ * supplies its own probe call (`(path) => client.fetchHtml(path)`)
36992
+ * and probe path (e.g. `'/robots.txt'`); the tool registration and
36993
+ * the site-specific plain-English hint text STAY in the consumer.
36994
+ *
36995
+ * `bridgeHealth()` is read AFTER the probe so its freshness counters
36996
+ * (`lastSuccessAt` / `consecutiveFailures` / …) reflect this very
36997
+ * round-trip rather than a stale pre-probe snapshot.
36998
+ */
36999
+ async runProbe(fetchFn, probePath) {
37000
+ const start = Date.now();
37001
+ let ok = false;
37002
+ let error51;
37003
+ try {
37004
+ await fetchFn(probePath);
37005
+ ok = true;
37006
+ } catch (e) {
37007
+ error51 = {
37008
+ kind: classifyBridgeError(e),
37009
+ message: e instanceof Error ? e.message : String(e)
37010
+ };
37011
+ }
37012
+ const elapsed_ms = Date.now() - start;
37013
+ const health = this.bridgeHealth();
37014
+ return {
37015
+ ok,
37016
+ elapsed_ms,
37017
+ bridge: {
37018
+ role: health.role,
37019
+ port: health.port,
37020
+ server_version: health.serverVersion,
37021
+ fetch_timeout_ms: health.fetchTimeoutMs,
37022
+ last_success_at: health.lastSuccessAt,
37023
+ last_failure_at: health.lastFailureAt,
37024
+ last_failure_reason: health.lastFailureReason,
37025
+ consecutive_failures: health.consecutiveFailures
37026
+ },
37027
+ ...error51 ? { error: error51 } : {}
37028
+ };
37029
+ }
36502
37030
  /**
36503
37031
  * Snapshot the user's non-HttpOnly cookies for the chosen domain.
36504
37032
  *
@@ -36623,12 +37151,12 @@ var FetchproxyServer = class {
36623
37151
  /**
36624
37152
  * 0.3.0+: snapshot the next outgoing request's named header. Single-
36625
37153
  * shot: the extension registers a one-time `webRequest` listener
36626
- * filtered on `urlPattern`, captures the named header on the first
36627
- * match, removes itself, and resolves with the value. Times out
36628
- * after `timeoutMs` (default 30s on the extension).
37154
+ * filtered on `https://${host}${path ?? '/*'}`, captures the named
37155
+ * header on the first match, removes itself, and resolves with the
37156
+ * value. Times out after `timeoutMs` (default 30s on the extension).
36629
37157
  *
36630
- * `(urlPattern, headerName)` must exactly match a declared entry in
36631
- * `FetchproxyServerOpts.captureHeaders`.
37158
+ * `(host, path?, headerName)` must match a declared entry in
37159
+ * `FetchproxyServerOpts.captureHeaders` (omitted path ≡ `/*`).
36632
37160
  */
36633
37161
  async captureRequestHeader(opts) {
36634
37162
  if (!this.opts.capabilities.includes("capture_request_header")) {
@@ -36637,24 +37165,25 @@ var FetchproxyServer = class {
36637
37165
  await this.ensureConnected();
36638
37166
  this.throwIfPendingPair();
36639
37167
  const decls = this.opts.captureHeaders;
37168
+ const normPath = (p) => p ?? "/*";
36640
37169
  let resolved;
36641
- if (opts?.urlPattern !== void 0 && opts?.headerName !== void 0) {
36642
- const found = decls.find((d) => d.urlPattern === opts.urlPattern && d.headerName === opts.headerName);
37170
+ if (opts?.host !== void 0 && opts?.headerName !== void 0) {
37171
+ const found = decls.find((d) => d.host === opts.host && normPath(d.path) === normPath(opts.path) && d.headerName === opts.headerName);
36643
37172
  if (!found) {
36644
- throw new Error(`FetchproxyServer.captureRequestHeader: (urlPattern=${JSON.stringify(opts.urlPattern)}, headerName=${JSON.stringify(opts.headerName)}) not declared in captureHeaders`);
37173
+ throw new Error(`FetchproxyServer.captureRequestHeader: (host=${JSON.stringify(opts.host)}, path=${JSON.stringify(normPath(opts.path))}, headerName=${JSON.stringify(opts.headerName)}) not declared in captureHeaders`);
36645
37174
  }
36646
37175
  resolved = found;
36647
- } else if (opts?.urlPattern === void 0 && opts?.headerName === void 0) {
37176
+ } else if (opts?.host === void 0 && opts?.headerName === void 0) {
36648
37177
  if (decls.length === 0) {
36649
- throw new Error("FetchproxyServer.captureRequestHeader: no captureHeaders declared on this server \u2014 declare at least one entry in FetchproxyServerOpts.captureHeaders, or pass {urlPattern, headerName} explicitly");
37178
+ throw new Error("FetchproxyServer.captureRequestHeader: no captureHeaders declared on this server \u2014 declare at least one entry in FetchproxyServerOpts.captureHeaders, or pass {host, headerName} explicitly");
36650
37179
  }
36651
37180
  if (decls.length > 1) {
36652
- const list = decls.map((d) => `${JSON.stringify(d.urlPattern)}/${JSON.stringify(d.headerName)}`).join(", ");
36653
- throw new Error(`FetchproxyServer.captureRequestHeader: multiple captureHeaders declared (${decls.length}: ${list}); pass {urlPattern, headerName} to disambiguate`);
37181
+ const list = decls.map((d) => `${JSON.stringify(d.host)}${JSON.stringify(normPath(d.path))}/${JSON.stringify(d.headerName)}`).join(", ");
37182
+ throw new Error(`FetchproxyServer.captureRequestHeader: multiple captureHeaders declared (${decls.length}: ${list}); pass {host, headerName} to disambiguate`);
36654
37183
  }
36655
37184
  resolved = decls[0];
36656
37185
  } else {
36657
- throw new Error("FetchproxyServer.captureRequestHeader: pass both urlPattern AND headerName, or neither (which defaults to the single declared entry)");
37186
+ throw new Error("FetchproxyServer.captureRequestHeader: pass both host AND headerName, or neither (which defaults to the single declared entry)");
36658
37187
  }
36659
37188
  const callOpts = { ...resolved, ...opts?.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} };
36660
37189
  try {
@@ -36667,11 +37196,14 @@ var FetchproxyServer = class {
36667
37196
  this.recordFailure(`capture_request_header: ${err.message ?? String(err)}`);
36668
37197
  throw err;
36669
37198
  }
37199
+ this.lastEvictionDetectedAt = Date.now();
36670
37200
  const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
36671
37201
  if (reviveMs > 0) {
37202
+ this.lazyReviveAttempts += 1;
36672
37203
  await new Promise((r) => setTimeout(r, reviveMs));
36673
37204
  try {
36674
37205
  const result = await this._captureRequestHeaderOnce(callOpts);
37206
+ this.lazyReviveSuccesses += 1;
36675
37207
  this.recordSuccess();
36676
37208
  return result;
36677
37209
  } catch (retryErr) {
@@ -36685,7 +37217,7 @@ var FetchproxyServer = class {
36685
37217
  originalError: retryErr.message,
36686
37218
  retryAttempted: true,
36687
37219
  op: "capture_request_header",
36688
- url: resolved.urlPattern,
37220
+ url: `https://${resolved.host}${resolved.path ?? "/*"}`,
36689
37221
  role: this.role,
36690
37222
  port: this.opts.port
36691
37223
  });
@@ -36696,7 +37228,7 @@ var FetchproxyServer = class {
36696
37228
  originalError: err.message,
36697
37229
  retryAttempted: false,
36698
37230
  op: "capture_request_header",
36699
- url: resolved.urlPattern,
37231
+ url: `https://${resolved.host}${resolved.path ?? "/*"}`,
36700
37232
  role: this.role,
36701
37233
  port: this.opts.port
36702
37234
  });
@@ -36709,7 +37241,8 @@ var FetchproxyServer = class {
36709
37241
  id,
36710
37242
  op: "capture_request_header",
36711
37243
  init: {
36712
- urlPattern: opts.urlPattern,
37244
+ host: opts.host,
37245
+ ...opts.path !== void 0 ? { path: opts.path } : {},
36713
37246
  headerName: opts.headerName,
36714
37247
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
36715
37248
  }
@@ -36964,6 +37497,8 @@ var FetchproxyServer = class {
36964
37497
  * twice in a row.
36965
37498
  */
36966
37499
  async close() {
37500
+ this.closing = true;
37501
+ this.stopKeepalive();
36967
37502
  this.rejectAllPending();
36968
37503
  if (this.connectingPromise) {
36969
37504
  await this.connectingPromise.catch(() => void 0);
@@ -36979,19 +37514,6 @@ var FetchproxyServer = class {
36979
37514
  }
36980
37515
  };
36981
37516
 
36982
- // node_modules/@fetchproxy/server/dist/classify-bridge-error.js
36983
- function classifyBridgeError(err) {
36984
- if (err instanceof FetchproxyTimeoutError)
36985
- return "timeout";
36986
- if (err instanceof FetchproxyBridgeDownError)
36987
- return "bridge_down";
36988
- if (err instanceof FetchproxyHttpError)
36989
- return "http";
36990
- if (err instanceof FetchproxyProtocolError)
36991
- return "protocol";
36992
- return "other";
36993
- }
36994
-
36995
37517
  // node_modules/@fetchproxy/bootstrap/dist/index.js
36996
37518
  var defaultFactory = (opts) => new FetchproxyServer(opts);
36997
37519
  async function bootstrap(opts) {
@@ -37023,7 +37545,7 @@ async function bootstrap(opts) {
37023
37545
  const sessionStorageKeys = new Set(opts.declare.sessionStorage);
37024
37546
  for (const p of sessionStoragePointers)
37025
37547
  sessionStorageKeys.add(p.storageKey);
37026
- const server2 = factory({
37548
+ const server = factory({
37027
37549
  serverName: opts.serverName,
37028
37550
  version: opts.version,
37029
37551
  domains: [...opts.domains],
@@ -37058,10 +37580,10 @@ async function bootstrap(opts) {
37058
37580
  if (opts.storageSubdomain !== void 0)
37059
37581
  storageDomainOpts.subdomain = opts.storageSubdomain;
37060
37582
  try {
37061
- await server2.listen();
37583
+ await server.listen();
37062
37584
  const cookies = {};
37063
37585
  if (opts.declare.cookies.length > 0) {
37064
- const joined = await server2.readCookies({
37586
+ const joined = await server.readCookies({
37065
37587
  keys: opts.declare.cookies,
37066
37588
  ...storageDomainOpts
37067
37589
  });
@@ -37081,7 +37603,7 @@ async function bootstrap(opts) {
37081
37603
  for (const p of localStoragePointers) {
37082
37604
  pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
37083
37605
  }
37084
- localStorage = await server2.readLocalStorage({
37606
+ localStorage = await server.readLocalStorage({
37085
37607
  keys: allKeys,
37086
37608
  ...storageDomainOpts,
37087
37609
  ...localStoragePointers.length > 0 ? { pointers } : {}
@@ -37094,7 +37616,7 @@ async function bootstrap(opts) {
37094
37616
  for (const p of sessionStoragePointers) {
37095
37617
  pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
37096
37618
  }
37097
- sessionStorage = await server2.readSessionStorage({
37619
+ sessionStorage = await server.readSessionStorage({
37098
37620
  keys: allKeys,
37099
37621
  ...storageDomainOpts,
37100
37622
  ...sessionStoragePointers.length > 0 ? { pointers } : {}
@@ -37103,21 +37625,17 @@ async function bootstrap(opts) {
37103
37625
  const capturedHeaders = {};
37104
37626
  for (const h of opts.declare.captureHeaders) {
37105
37627
  if (opts.onWaiting) {
37106
- try {
37107
- const url2 = new URL(h.urlPattern.replace(/\*+/g, "placeholder"));
37108
- opts.onWaiting(`waiting for next request to ${url2.host} to capture ${h.headerName} \u2014 interact with the page in your browser`);
37109
- } catch {
37110
- opts.onWaiting(`waiting to capture ${h.headerName} \u2014 interact with the page in your browser`);
37111
- }
37628
+ opts.onWaiting(`waiting for next request to ${h.host} to capture ${h.headerName} \u2014 interact with the page in your browser`);
37112
37629
  }
37113
- capturedHeaders[h.headerName] = await server2.captureRequestHeader({
37114
- urlPattern: h.urlPattern,
37630
+ capturedHeaders[h.headerName] = await server.captureRequestHeader({
37631
+ host: h.host,
37632
+ ...h.path !== void 0 ? { path: h.path } : {},
37115
37633
  headerName: h.headerName
37116
37634
  });
37117
37635
  }
37118
37636
  const indexedDbBucket = {};
37119
37637
  for (const d of indexedDb) {
37120
- const values = await server2.readIndexedDb({
37638
+ const values = await server.readIndexedDb({
37121
37639
  database: d.database,
37122
37640
  store: d.store,
37123
37641
  keys: [...d.keys],
@@ -37134,7 +37652,7 @@ async function bootstrap(opts) {
37134
37652
  };
37135
37653
  } finally {
37136
37654
  try {
37137
- await server2.close();
37655
+ await server.close();
37138
37656
  } catch {
37139
37657
  }
37140
37658
  }
@@ -37152,13 +37670,7 @@ var BootstrapDisabledError = class extends Error {
37152
37670
 
37153
37671
  // src/config.ts
37154
37672
  function readVar(env, key) {
37155
- const raw = env[key];
37156
- if (typeof raw !== "string") return void 0;
37157
- const trimmed = raw.trim();
37158
- if (trimmed.length === 0) return void 0;
37159
- if (trimmed === "undefined" || trimmed === "null") return void 0;
37160
- if (/^\$\{[^}]*\}$/.test(trimmed)) return void 0;
37161
- return trimmed;
37673
+ return readEnvVar(key, { env });
37162
37674
  }
37163
37675
  function loadAccount(env = process.env) {
37164
37676
  const baseUrl = readVar(env, "IC_BASE_URL");
@@ -37197,7 +37709,7 @@ function loadAccount(env = process.env) {
37197
37709
  // package.json
37198
37710
  var package_default = {
37199
37711
  name: "infinitecampus-mcp",
37200
- version: "2.3.0",
37712
+ version: "2.3.2",
37201
37713
  mcpName: "io.github.chrischall/infinitecampus-mcp",
37202
37714
  description: "Infinite Campus (Campus Parent) MCP server \u2014 multi-district read + message/document write",
37203
37715
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -37239,10 +37751,12 @@ var package_default = {
37239
37751
  "test:watch": "vitest"
37240
37752
  },
37241
37753
  dependencies: {
37242
- "@fetchproxy/bootstrap": "^0.8.0",
37243
- "@fetchproxy/server": "^0.8.0",
37754
+ "@chrischall/mcp-utils": "^0.5.0",
37755
+ "@fetchproxy/bootstrap": "^1.0.0",
37756
+ "@fetchproxy/server": "^1.0.0",
37244
37757
  "@modelcontextprotocol/sdk": "^1.29.0",
37245
37758
  dotenv: "^17.4.2",
37759
+ "node-html-parser": "^7.1.0",
37246
37760
  zod: "^4.4.3"
37247
37761
  },
37248
37762
  devDependencies: {
@@ -37255,19 +37769,8 @@ var package_default = {
37255
37769
  };
37256
37770
 
37257
37771
  // src/auth.ts
37258
- function readEnv(key) {
37259
- const raw = process.env[key];
37260
- if (typeof raw !== "string") return void 0;
37261
- const trimmed = raw.trim();
37262
- if (trimmed.length === 0) return void 0;
37263
- if (trimmed === "undefined" || trimmed === "null") return void 0;
37264
- if (/^\$\{[^}]*\}$/.test(trimmed)) return void 0;
37265
- return trimmed;
37266
- }
37267
37772
  function fetchproxyDisabled() {
37268
- const raw = readEnv("IC_DISABLE_FETCHPROXY");
37269
- if (raw === void 0) return false;
37270
- return ["1", "true", "yes", "on"].includes(raw.toLowerCase());
37773
+ return parseBoolEnv("IC_DISABLE_FETCHPROXY", { default: false });
37271
37774
  }
37272
37775
  async function resolveAuth() {
37273
37776
  const account2 = loadAccount();
@@ -37635,22 +38138,9 @@ var ICClient = class {
37635
38138
  };
37636
38139
  function parseSetCookies(headers) {
37637
38140
  const raw = headers.getSetCookie?.() ?? [];
37638
- const headerStrings = raw.length > 0 ? raw : splitFallback(headers.get("set-cookie"));
37639
- const jar = /* @__PURE__ */ new Map();
37640
- let xsrfToken = "";
37641
- for (const entry of headerStrings) {
37642
- if (/Max-Age=0/i.test(entry)) continue;
37643
- const nameValue = entry.split(";")[0].trim();
37644
- const eqIdx = nameValue.indexOf("=");
37645
- if (eqIdx < 1) continue;
37646
- const name = nameValue.substring(0, eqIdx);
37647
- const value = nameValue.substring(eqIdx + 1);
37648
- if (!value) continue;
37649
- jar.set(name, value);
37650
- if (name === "XSRF-TOKEN") xsrfToken = value;
37651
- }
37652
- const cookieHeader = [...jar.entries()].map(([k, v]) => `${k}=${v}`).join("; ");
37653
- return { cookieHeader, xsrfToken };
38141
+ const entries = raw.length > 0 ? raw : splitFallback(headers.get("set-cookie"));
38142
+ const { cookies, cookieHeader } = parseCookieJar(entries);
38143
+ return { cookieHeader, xsrfToken: cookies["XSRF-TOKEN"] ?? "" };
37654
38144
  }
37655
38145
  function splitFallback(header) {
37656
38146
  if (!header) return [];
@@ -37724,7 +38214,7 @@ var FileExistsError = class extends Error {
37724
38214
 
37725
38215
  // src/tools/_shared.ts
37726
38216
  function textContent(data) {
37727
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
38217
+ return textResult(data);
37728
38218
  }
37729
38219
  function is404(e) {
37730
38220
  return e instanceof Error && e.message.startsWith("IC 404 ");
@@ -37757,8 +38247,8 @@ function toArray(value) {
37757
38247
  }
37758
38248
 
37759
38249
  // src/tools/districts.ts
37760
- function registerDistrictTools(server2, client) {
37761
- server2.registerTool("ic_list_districts", {
38250
+ function registerDistrictTools(server, client) {
38251
+ server.registerTool("ic_list_districts", {
37762
38252
  description: "List Infinite Campus districts configured for this MCP server. Returns names + base URLs (no credentials).",
37763
38253
  annotations: { readOnlyHint: true }
37764
38254
  }, async () => {
@@ -37772,8 +38262,8 @@ function registerDistrictTools(server2, client) {
37772
38262
  var argsSchema = external_exports.object({
37773
38263
  district: external_exports.string().describe("District name from ic_list_districts")
37774
38264
  });
37775
- function registerStudentTools(server2, client) {
37776
- server2.registerTool("ic_list_students", {
38265
+ function registerStudentTools(server, client) {
38266
+ server.registerTool("ic_list_students", {
37777
38267
  description: "List students enrolled under the parent account for a given district.",
37778
38268
  annotations: { readOnlyHint: true },
37779
38269
  inputSchema: argsSchema.shape
@@ -37791,8 +38281,8 @@ var argsSchema2 = external_exports.object({
37791
38281
  date: external_exports.string().describe("YYYY-MM-DD; defaults to today").optional(),
37792
38282
  termFilter: external_exports.string().describe("Term name or ID; optional").optional()
37793
38283
  });
37794
- function registerScheduleTools(server2, client) {
37795
- server2.registerTool("ic_get_schedule", {
38284
+ function registerScheduleTools(server, client) {
38285
+ server.registerTool("ic_get_schedule", {
37796
38286
  description: "Get a student's class schedule for a given date (default: today).",
37797
38287
  annotations: { readOnlyHint: true },
37798
38288
  inputSchema: argsSchema2.shape
@@ -37813,8 +38303,8 @@ var argsSchema3 = external_exports.object({
37813
38303
  until: external_exports.string().describe("YYYY-MM-DD; filters dueDate <= until (client-side)").optional(),
37814
38304
  missingOnly: external_exports.boolean().describe("Only return assignments flagged missing by the teacher").optional()
37815
38305
  });
37816
- function registerAssignmentTools(server2, client) {
37817
- server2.registerTool("ic_list_assignments", {
38306
+ function registerAssignmentTools(server, client) {
38307
+ server.registerTool("ic_list_assignments", {
37818
38308
  description: "List a student's assignments. The IC endpoint returns the full term history (~hundreds of items); date and missing filters are applied client-side. For a single course, pass courseId (the sectionID from ic_get_schedule).",
37819
38309
  annotations: { readOnlyHint: true },
37820
38310
  inputSchema: argsSchema3.shape
@@ -37848,8 +38338,8 @@ var argsSchema4 = external_exports.object({
37848
38338
  studentId: external_exports.string(),
37849
38339
  termId: external_exports.string().optional()
37850
38340
  });
37851
- function registerGradeTools(server2, client) {
37852
- server2.registerTool("ic_list_grades", {
38341
+ function registerGradeTools(server, client) {
38342
+ server.registerTool("ic_list_grades", {
37853
38343
  description: "List a student's term grades and in-progress course grades.",
37854
38344
  annotations: { readOnlyHint: true },
37855
38345
  inputSchema: argsSchema4.shape
@@ -37892,8 +38382,8 @@ function processList(list, since, until) {
37892
38382
  if (list === void 0) return list;
37893
38383
  return toArray(list).filter((e) => inRange(e.date, since, until)).map(trimEntry);
37894
38384
  }
37895
- function registerAttendanceTools(server2, client) {
37896
- server2.registerTool("ic_list_attendance", {
38385
+ function registerAttendanceTools(server, client) {
38386
+ server.registerTool("ic_list_attendance", {
37897
38387
  description: "List a student's absences and tardies (per-course summary grouped by term). Auto-resolves enrollmentID from the student record.",
37898
38388
  annotations: { readOnlyHint: true },
37899
38389
  inputSchema: argsSchema5.shape
@@ -37941,8 +38431,8 @@ var argsSchema6 = external_exports.object({
37941
38431
  since: external_exports.string().optional(),
37942
38432
  until: external_exports.string().optional()
37943
38433
  });
37944
- function registerBehaviorTools(server2, client) {
37945
- server2.registerTool("ic_list_behavior", {
38434
+ function registerBehaviorTools(server, client) {
38435
+ server.registerTool("ic_list_behavior", {
37946
38436
  description: "List a student's behavior events / referrals. Returns FeatureDisabled if the district has the behavior module turned off (detected via displayOptions or a 404 backstop).",
37947
38437
  annotations: { readOnlyHint: true },
37948
38438
  inputSchema: argsSchema6.shape
@@ -37973,8 +38463,8 @@ var argsSchema7 = external_exports.object({
37973
38463
  until: external_exports.string().optional()
37974
38464
  });
37975
38465
  var EMPTY_FOOD = { balance: null, transactions: [] };
37976
- function registerFoodServiceTools(server2, client) {
37977
- server2.registerTool("ic_list_food_service", {
38466
+ function registerFoodServiceTools(server, client) {
38467
+ server.registerTool("ic_list_food_service", {
37978
38468
  description: "List a student's lunch balance and recent food-service transactions. Returns FeatureDisabled if the district has the module turned off (detected via displayOptions or a 404 backstop).",
37979
38469
  annotations: { readOnlyHint: true },
37980
38470
  inputSchema: argsSchema7.shape
@@ -38005,6 +38495,29 @@ function registerFoodServiceTools(server2, client) {
38005
38495
  });
38006
38496
  }
38007
38497
 
38498
+ // node_modules/@chrischall/mcp-utils/dist/html/index.js
38499
+ var NAMED_ENTITIES = {
38500
+ nbsp: " ",
38501
+ amp: "&",
38502
+ lt: "<",
38503
+ gt: ">",
38504
+ quot: '"',
38505
+ apos: "'"
38506
+ };
38507
+ function extractPlainTextFromHtml(html) {
38508
+ if (!html)
38509
+ return "";
38510
+ let text = html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ");
38511
+ text = text.replace(/<[^>]+>/g, " ");
38512
+ text = text.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)));
38513
+ text = text.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16)));
38514
+ text = text.replace(/&([a-zA-Z]+);/g, (whole, name) => {
38515
+ const decoded = NAMED_ENTITIES[name.toLowerCase()];
38516
+ return decoded ?? whole;
38517
+ });
38518
+ return text.replace(/\s+/g, " ").trim();
38519
+ }
38520
+
38008
38521
  // src/tools/messages.ts
38009
38522
  var listArgs = external_exports.object({
38010
38523
  district: external_exports.string(),
@@ -38051,10 +38564,7 @@ function parseMessageHtml(html, url2) {
38051
38564
  const titleMatch = html.match(/<title>([^<]*)<\/title>/i);
38052
38565
  let subject = titleMatch ? titleMatch[1].trim() : "";
38053
38566
  subject = subject.replace(/^Message\s*--\s*/i, "");
38054
- let text = html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ");
38055
- text = text.replace(/<[^>]+>/g, " ");
38056
- text = text.replace(/&nbsp;/g, " ").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&quot;/g, '"').replace(/&#39;/g, "'");
38057
- text = text.replace(/\s+/g, " ").trim();
38567
+ const text = extractPlainTextFromHtml(html);
38058
38568
  const dateMatch = text.match(/Date:\s*(\d{1,2}\/\d{1,2}\/\d{2,4})/);
38059
38569
  const date5 = dateMatch ? dateMatch[1] : null;
38060
38570
  let body = text;
@@ -38064,8 +38574,8 @@ function parseMessageHtml(html, url2) {
38064
38574
  }
38065
38575
  return { subject, date: date5, body, url: url2 };
38066
38576
  }
38067
- function registerMessageTools(server2, client) {
38068
- server2.registerTool("ic_list_messages", {
38577
+ function registerMessageTools(server, client) {
38578
+ server.registerTool("ic_list_messages", {
38069
38579
  description: "List all parent-visible messages from three IC sources combined: (1) prism notifications (assignment alerts, grade postings, attendance alerts), (2) Messenger 2.0 inbox (teacher messages, district announcements with newMessage/actionRequired flags), and (3) portal userNotice announcements. Each section has its own count and items; if any source errors, that section contains an error field and the others still return normally. The `limit` arg caps the prism notifications only (the high-volume source). Note: listing inbox messages does not mark them as read in normal portal behavior, but some district configurations may update read-tracking; use ic_get_message for the full HTML body.",
38070
38580
  annotations: { readOnlyHint: true },
38071
38581
  inputSchema: listArgs.shape
@@ -38103,7 +38613,7 @@ function registerMessageTools(server2, client) {
38103
38613
  const [notifications, inbox, announcements] = await Promise.all([prismPromise, inboxPromise, noticePromise]);
38104
38614
  return textContent({ notifications, inbox, announcements });
38105
38615
  });
38106
- server2.registerTool("ic_get_message", {
38616
+ server.registerTool("ic_get_message", {
38107
38617
  description: "Fetch the HTML body of an inbox message and return it parsed into { subject, date, body, url }. Takes a `messageUrl` which is the `url` field from an item returned by ic_list_messages' inbox section (e.g. 'portal/messageView.xsl?x=messenger.MessengerEngine-getMessageRecipientView&messageID=...'). Relative and /campus/-prefixed URLs are both accepted. Note: fetching the HTML body may mark the message as read on some district configurations; probe against an empty inbox could not confirm the side effect.",
38108
38618
  annotations: { readOnlyHint: true },
38109
38619
  inputSchema: getArgs.shape
@@ -38127,8 +38637,8 @@ var downloadArgs = external_exports.object({
38127
38637
  destinationPath: external_exports.string().describe("Absolute path where the PDF should be written"),
38128
38638
  overwrite: external_exports.boolean().optional()
38129
38639
  });
38130
- function registerDocumentTools(server2, client) {
38131
- server2.registerTool("ic_list_documents", {
38640
+ function registerDocumentTools(server, client) {
38641
+ server.registerTool("ic_list_documents", {
38132
38642
  description: "List a student's available documents (report cards, transcripts, schedules). Returns metadata only \u2014 use ic_download_document to fetch the file. Returns FeatureDisabled if the district has the module turned off.",
38133
38643
  annotations: { readOnlyHint: true },
38134
38644
  inputSchema: listArgs2.shape
@@ -38158,7 +38668,7 @@ function registerDocumentTools(server2, client) {
38158
38668
  throw e;
38159
38669
  }
38160
38670
  });
38161
- server2.registerTool("ic_download_document", {
38671
+ server.registerTool("ic_download_document", {
38162
38672
  description: "Download a student's document (PDF) to disk. documentId is the url field returned by ic_list_documents. Returns FeatureDisabled if the district has the module turned off.",
38163
38673
  annotations: { destructiveHint: true },
38164
38674
  inputSchema: downloadArgs.shape
@@ -38185,8 +38695,8 @@ var argsSchema8 = external_exports.object({
38185
38695
  since: external_exports.string().describe("YYYY-MM-DD; include only days on or after this date").optional(),
38186
38696
  until: external_exports.string().describe("YYYY-MM-DD; include only days on or before this date").optional()
38187
38697
  });
38188
- function registerCalendarTools(server2, client) {
38189
- server2.registerTool("ic_list_school_days", {
38698
+ function registerCalendarTools(server, client) {
38699
+ server.registerTool("ic_list_school_days", {
38190
38700
  description: "List a student's school days (instructional calendar) grouped by term. Returns one entry per enrollment, with term boundaries (Q1-Q4 start/end dates) and the school days inside each term \u2014 including comments like 'Teacher Workday' or 'Spring Break'. Use since/until to narrow the range.",
38191
38701
  annotations: { readOnlyHint: true },
38192
38702
  inputSchema: argsSchema8.shape
@@ -38281,8 +38791,8 @@ function trimEvent(e) {
38281
38791
  }
38282
38792
  return out;
38283
38793
  }
38284
- function registerAttendanceEventsTools(server2, client) {
38285
- server2.registerTool("ic_list_attendance_events", {
38794
+ function registerAttendanceEventsTools(server, client) {
38795
+ server.registerTool("ic_list_attendance_events", {
38286
38796
  description: "List individual attendance events (absences, tardies, early releases) for a student. Each event has a code, description, excuse reason, and optional human-readable comments. Auto-resolves enrollmentID from the student record. Use since/until to filter by date and excusedOnly to show only excused events.",
38287
38797
  annotations: { readOnlyHint: true },
38288
38798
  inputSchema: argsSchema9.shape
@@ -38353,8 +38863,8 @@ function defaultSinceDate(now) {
38353
38863
  d.setUTCDate(d.getUTCDate() - 14);
38354
38864
  return d.toISOString().substring(0, 10);
38355
38865
  }
38356
- function registerRecentGradesTools(server2, client) {
38357
- server2.registerTool("ic_list_recent_grades", {
38866
+ function registerRecentGradesTools(server, client) {
38867
+ server.registerTool("ic_list_recent_grades", {
38358
38868
  description: "List recently-graded assignments for a student. Server-side filtered by scoreModifiedDate. Pass since=YYYY-MM-DD to set the cutoff; defaults to 14 days ago.",
38359
38869
  annotations: { readOnlyHint: true },
38360
38870
  inputSchema: argsSchema10.shape
@@ -38404,8 +38914,8 @@ function trimRecord(raw) {
38404
38914
  }
38405
38915
  return out;
38406
38916
  }
38407
- function registerTeacherTools(server2, client) {
38408
- server2.registerTool("ic_list_teachers", {
38917
+ function registerTeacherTools(server, client) {
38918
+ server.registerTool("ic_list_teachers", {
38409
38919
  description: "List a student's teachers (per enrolled section) and assigned counselor(s). Combines two endpoints (section/contacts and studentCounselor/byUser). Response field shapes may vary slightly by district \u2014 core fields (firstName, lastName, email) are consistent; additional fields are passed through.",
38410
38920
  annotations: { readOnlyHint: true },
38411
38921
  inputSchema: argsSchema11.shape
@@ -38438,8 +38948,8 @@ var argsSchema12 = external_exports.object({
38438
38948
  district: external_exports.string(),
38439
38949
  studentId: external_exports.string().describe("Student personID from ic_list_students")
38440
38950
  });
38441
- function registerAssessmentTools(server2, client) {
38442
- server2.registerTool("ic_list_assessments", {
38951
+ function registerAssessmentTools(server, client) {
38952
+ server.registerTool("ic_list_assessments", {
38443
38953
  description: "List a student's standardized test scores (state, national, district tests). Auto-resolves calendarID from each of the student's enrollments and returns one entry per enrollment. The shape of individual test records varies by district and test type \u2014 fields are passed through unchanged.",
38444
38954
  annotations: { readOnlyHint: true },
38445
38955
  inputSchema: argsSchema12.shape
@@ -38489,8 +38999,8 @@ var argsSchema13 = external_exports.object({
38489
38999
  district: external_exports.string(),
38490
39000
  studentId: external_exports.string().describe("Student personID from ic_list_students")
38491
39001
  });
38492
- function registerFeeTools(server2, client) {
38493
- server2.registerTool("ic_list_fees", {
39002
+ function registerFeeTools(server, client) {
39003
+ server.registerTool("ic_list_fees", {
38494
39004
  description: "List a student's fee assignments (charges owed) and running balance/surplus. Combines two endpoints: fee assignments and totalSurplus. Returns FeatureDisabled only if both endpoints 404; if only one works, returns that side with warning: 'PartialSuccess' and an issues[] explaining which endpoint failed.",
38495
39005
  annotations: { readOnlyHint: true },
38496
39006
  inputSchema: argsSchema13.shape
@@ -38533,8 +39043,8 @@ var argsSchema14 = external_exports.object({
38533
39043
  district: external_exports.string(),
38534
39044
  studentId: external_exports.string().describe("Student personID from ic_list_students")
38535
39045
  });
38536
- function registerFeaturesTools(server2, client) {
38537
- server2.registerTool("ic_get_features", {
39046
+ function registerFeaturesTools(server, client) {
39047
+ server.registerTool("ic_get_features", {
38538
39048
  description: "List the district's displayOptions feature-flag allow-list for each of a student's enrollments. Each enrollment's `features` object is a map of ~90 flag names (attendance, behavior, assessment, documents, grades, schedule, etc.) to booleans. A `false` value means the district has that feature disabled for this enrollment; `true` or missing means it's available. Used internally by other tools to short-circuit disabled features, but exposed here so the LLM can answer capability questions directly.",
38539
39049
  annotations: { readOnlyHint: true },
38540
39050
  inputSchema: argsSchema14.shape
@@ -38557,12 +39067,11 @@ function registerFeaturesTools(server2, client) {
38557
39067
  }
38558
39068
 
38559
39069
  // src/index.ts
38560
- try {
38561
- const { config: config2 } = await import("dotenv");
38562
- const __dirname = dirname2(fileURLToPath(import.meta.url));
38563
- config2({ path: join2(__dirname, "..", ".env"), override: false, quiet: true });
38564
- } catch {
38565
- }
39070
+ await loadDotenvSafely({
39071
+ path: join2(dirname2(fileURLToPath(import.meta.url)), "..", ".env"),
39072
+ override: false
39073
+ });
39074
+ var AI_NOTICE = "[infinitecampus-mcp] Developed and maintained by AI (Claude). Use at your own discretion.";
38566
39075
  var account = null;
38567
39076
  var preloaded;
38568
39077
  var source;
@@ -38575,32 +39084,39 @@ try {
38575
39084
  } catch (e) {
38576
39085
  configError = e;
38577
39086
  }
38578
- var server = new McpServer({ name: "infinitecampus", version: "2.3.0" });
39087
+ var COMMON = {
39088
+ name: "infinitecampus",
39089
+ version: "2.3.2"
39090
+ // x-release-please-version
39091
+ };
38579
39092
  if (account) {
38580
39093
  const client = new ICClient(account, { preloaded });
38581
- registerDistrictTools(server, client);
38582
- registerStudentTools(server, client);
38583
- registerScheduleTools(server, client);
38584
- registerAssignmentTools(server, client);
38585
- registerGradeTools(server, client);
38586
- registerAttendanceTools(server, client);
38587
- registerBehaviorTools(server, client);
38588
- registerFoodServiceTools(server, client);
38589
- registerMessageTools(server, client);
38590
- registerDocumentTools(server, client);
38591
- registerCalendarTools(server, client);
38592
- registerAttendanceEventsTools(server, client);
38593
- registerRecentGradesTools(server, client);
38594
- registerTeacherTools(server, client);
38595
- registerAssessmentTools(server, client);
38596
- registerFeeTools(server, client);
38597
- registerFeaturesTools(server, client);
39094
+ const tools = [
39095
+ registerDistrictTools,
39096
+ registerStudentTools,
39097
+ registerScheduleTools,
39098
+ registerAssignmentTools,
39099
+ registerGradeTools,
39100
+ registerAttendanceTools,
39101
+ registerBehaviorTools,
39102
+ registerFoodServiceTools,
39103
+ registerMessageTools,
39104
+ registerDocumentTools,
39105
+ registerCalendarTools,
39106
+ registerAttendanceEventsTools,
39107
+ registerRecentGradesTools,
39108
+ registerTeacherTools,
39109
+ registerAssessmentTools,
39110
+ registerFeeTools,
39111
+ registerFeaturesTools
39112
+ ];
38598
39113
  const suffix = source === "fetchproxy" ? " [via fetchproxy]" : "";
38599
- console.error(`[infinitecampus-mcp] District: ${account.name} (${account.baseUrl})${suffix}`);
39114
+ const districtLine = `[infinitecampus-mcp] District: ${account.name} (${account.baseUrl})${suffix}`;
39115
+ await runMcp({ ...COMMON, deps: client, tools, banner: `${districtLine}
39116
+ ${AI_NOTICE}` });
38600
39117
  } else {
38601
- console.error(`[infinitecampus-mcp] Not configured: ${configError?.message ?? "unknown error"}`);
38602
- console.error("[infinitecampus-mcp] Server is running with no tools registered. Set the required env vars and reinstall.");
39118
+ const notConfigured = `[infinitecampus-mcp] Not configured: ${configError?.message ?? "unknown error"}
39119
+ [infinitecampus-mcp] Server is running with no tools registered. Set the required env vars and reinstall.`;
39120
+ await runMcp({ ...COMMON, tools: [], banner: `${notConfigured}
39121
+ ${AI_NOTICE}` });
38603
39122
  }
38604
- console.error("[infinitecampus-mcp] Developed and maintained by AI (Claude). Use at your own discretion.");
38605
- var transport = new StdioServerTransport();
38606
- await server.connect(transport);