infinitecampus-mcp 2.3.1 → 2.3.3

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";
@@ -34560,7 +34791,9 @@ var KNOWN_CAPABILITIES = /* @__PURE__ */ new Set([
34560
34791
  "read_local_storage",
34561
34792
  "read_session_storage",
34562
34793
  "capture_request_header",
34563
- "read_indexed_db"
34794
+ "capture_redirect",
34795
+ "read_indexed_db",
34796
+ "download"
34564
34797
  ]);
34565
34798
 
34566
34799
  // node_modules/@fetchproxy/protocol/dist/mcp-id.js
@@ -34707,6 +34940,16 @@ function assertScopeKeyArray(value, label) {
34707
34940
  seen.add(k);
34708
34941
  }
34709
34942
  }
34943
+ var CAPTURE_PATH_RE = /^\/[A-Za-z0-9._~%\-/]*\*?$/;
34944
+ function hostMatchesAnyDomain(host, domains) {
34945
+ const h = host.toLowerCase();
34946
+ for (const d of domains) {
34947
+ const dom = d.toLowerCase();
34948
+ if (h === dom || h.endsWith("." + dom))
34949
+ return true;
34950
+ }
34951
+ return false;
34952
+ }
34710
34953
  function assertCaptureHeadersArray(value, label) {
34711
34954
  if (!Array.isArray(value)) {
34712
34955
  throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
@@ -34715,14 +34958,25 @@ function assertCaptureHeadersArray(value, label) {
34715
34958
  for (let i = 0; i < value.length; i++) {
34716
34959
  const entry = value[i];
34717
34960
  assertObject(entry, `${label}[${i}]`);
34718
- if (entry.urlPattern === void 0) {
34719
- throw new ProtocolError(`${label}[${i}].urlPattern: missing`);
34961
+ if (entry.host === void 0) {
34962
+ throw new ProtocolError(`${label}[${i}].host: missing`);
34720
34963
  }
34721
34964
  if (entry.headerName === void 0) {
34722
34965
  throw new ProtocolError(`${label}[${i}].headerName: missing`);
34723
34966
  }
34724
- if (typeof entry.urlPattern !== "string") {
34725
- throw new ProtocolError(`${label}[${i}].urlPattern: expected string, got ${typeof entry.urlPattern}`);
34967
+ if (typeof entry.host !== "string") {
34968
+ throw new ProtocolError(`${label}[${i}].host: expected string, got ${typeof entry.host}`);
34969
+ }
34970
+ if (!HOSTNAME_RE.test(entry.host)) {
34971
+ throw new ProtocolError(`${label}[${i}].host: invalid hostname ${JSON.stringify(entry.host)}`);
34972
+ }
34973
+ if (entry.path !== void 0) {
34974
+ if (typeof entry.path !== "string") {
34975
+ throw new ProtocolError(`${label}[${i}].path: expected string, got ${typeof entry.path}`);
34976
+ }
34977
+ if (!CAPTURE_PATH_RE.test(entry.path)) {
34978
+ throw new ProtocolError(`${label}[${i}].path: must start with '/' ${JSON.stringify(entry.path)}`);
34979
+ }
34726
34980
  }
34727
34981
  if (typeof entry.headerName !== "string") {
34728
34982
  throw new ProtocolError(`${label}[${i}].headerName: expected string, got ${typeof entry.headerName}`);
@@ -34730,19 +34984,29 @@ function assertCaptureHeadersArray(value, label) {
34730
34984
  if (!HEADER_NAME_RE.test(entry.headerName)) {
34731
34985
  throw new ProtocolError(`${label}[${i}].headerName: invalid name ${JSON.stringify(entry.headerName)}`);
34732
34986
  }
34733
- assertCaptureUrlPattern(entry.urlPattern, `${label}[${i}].urlPattern`);
34734
- const key = `${entry.urlPattern}\0${entry.headerName}`;
34987
+ const normalizedPath = entry.path ?? "/*";
34988
+ const key = `${entry.host}\0${normalizedPath}\0${entry.headerName}`;
34735
34989
  if (seen.has(key)) {
34736
- throw new ProtocolError(`${label}: duplicate ${JSON.stringify({ urlPattern: entry.urlPattern, headerName: entry.headerName })}`);
34990
+ throw new ProtocolError(`${label}: duplicate ${JSON.stringify({ host: entry.host, path: normalizedPath, headerName: entry.headerName })}`);
34737
34991
  }
34738
34992
  seen.add(key);
34739
34993
  for (const k of Object.keys(entry)) {
34740
- if (k !== "urlPattern" && k !== "headerName") {
34994
+ if (k !== "host" && k !== "path" && k !== "headerName") {
34741
34995
  throw new ProtocolError(`${label}[${i}]: unexpected field ${JSON.stringify(k)}`);
34742
34996
  }
34743
34997
  }
34744
34998
  }
34745
34999
  }
35000
+ function validateCaptureHeaderDecls(value, domains, label = "captureHeaders") {
35001
+ assertCaptureHeadersArray(value, label);
35002
+ const arr = value;
35003
+ for (let i = 0; i < arr.length; i++) {
35004
+ const host = arr[i].host;
35005
+ if (!hostMatchesAnyDomain(host, domains)) {
35006
+ throw new ProtocolError(`${label}[${i}].host: ${JSON.stringify(host)} is not a declared domain or subdomain of [${domains.join(", ")}]`);
35007
+ }
35008
+ }
35009
+ }
34746
35010
  function assertStoragePointersArray(value, label, declaredKeys) {
34747
35011
  if (!Array.isArray(value)) {
34748
35012
  throw new ProtocolError(`${label}: expected array, got ${typeof value}`);
@@ -34830,23 +35094,6 @@ function assertIndexedDbScopesArray(value, label) {
34830
35094
  }
34831
35095
  }
34832
35096
  }
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
35097
  function validateFrame(raw) {
34851
35098
  assertObject(raw, "frame");
34852
35099
  const t = raw.type;
@@ -34911,7 +35158,7 @@ function validateHello(raw) {
34911
35158
  assertScopeKeyArray(raw.sessionStorageKeys, "hello.sessionStorageKeys");
34912
35159
  }
34913
35160
  if (raw.captureHeaders !== void 0) {
34914
- assertCaptureHeadersArray(raw.captureHeaders, "hello.captureHeaders");
35161
+ validateCaptureHeaderDecls(raw.captureHeaders, raw.domains, "hello.captureHeaders");
34915
35162
  }
34916
35163
  if (raw.indexedDbScopes !== void 0) {
34917
35164
  assertIndexedDbScopesArray(raw.indexedDbScopes, "hello.indexedDbScopes");
@@ -35079,24 +35326,58 @@ function validateInnerRequest(raw) {
35079
35326
  }
35080
35327
  if (raw.op === "capture_request_header") {
35081
35328
  assertObject(raw.init, "inner.init");
35082
- if (raw.init.urlPattern === void 0) {
35083
- throw new ProtocolError("inner.init.urlPattern: missing");
35329
+ if (raw.init.host === void 0) {
35330
+ throw new ProtocolError("inner.init.host: missing");
35084
35331
  }
35085
35332
  if (raw.init.headerName === void 0) {
35086
35333
  throw new ProtocolError("inner.init.headerName: missing");
35087
35334
  }
35088
- assertString(raw.init.urlPattern, "inner.init.urlPattern");
35335
+ assertString(raw.init.host, "inner.init.host");
35336
+ if (!HOSTNAME_RE.test(raw.init.host)) {
35337
+ throw new ProtocolError(`inner.init.host: invalid hostname ${JSON.stringify(raw.init.host)}`);
35338
+ }
35339
+ if (raw.init.path !== void 0) {
35340
+ assertString(raw.init.path, "inner.init.path");
35341
+ if (!CAPTURE_PATH_RE.test(raw.init.path)) {
35342
+ throw new ProtocolError(`inner.init.path: must start with '/' ${JSON.stringify(raw.init.path)}`);
35343
+ }
35344
+ }
35089
35345
  assertString(raw.init.headerName, "inner.init.headerName");
35090
35346
  if (raw.init.timeoutMs !== void 0) {
35091
35347
  assertPositiveInt(raw.init.timeoutMs, "inner.init.timeoutMs");
35092
35348
  }
35093
35349
  for (const k of Object.keys(raw.init)) {
35094
- if (k !== "urlPattern" && k !== "headerName" && k !== "timeoutMs") {
35350
+ if (k !== "host" && k !== "path" && k !== "headerName" && k !== "timeoutMs") {
35095
35351
  throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on capture_request_header`);
35096
35352
  }
35097
35353
  }
35098
35354
  return raw;
35099
35355
  }
35356
+ if (raw.op === "capture_redirect") {
35357
+ assertObject(raw.init, "inner.init");
35358
+ if (raw.init.host === void 0) {
35359
+ throw new ProtocolError("inner.init.host: missing");
35360
+ }
35361
+ assertString(raw.init.host, "inner.init.host");
35362
+ if (!HOSTNAME_RE.test(raw.init.host)) {
35363
+ throw new ProtocolError(`inner.init.host: invalid hostname ${JSON.stringify(raw.init.host)}`);
35364
+ }
35365
+ if (raw.init.path !== void 0) {
35366
+ assertString(raw.init.path, "inner.init.path");
35367
+ if (!CAPTURE_PATH_RE.test(raw.init.path)) {
35368
+ throw new ProtocolError(`inner.init.path: must start with '/' ${JSON.stringify(raw.init.path)}`);
35369
+ }
35370
+ }
35371
+ if (raw.init.timeoutMs !== void 0) {
35372
+ assertPositiveInt(raw.init.timeoutMs, "inner.init.timeoutMs");
35373
+ }
35374
+ for (const k of Object.keys(raw.init)) {
35375
+ if (k !== "host" && k !== "path" && k !== "timeoutMs") {
35376
+ throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on capture_redirect`);
35377
+ }
35378
+ }
35379
+ return raw;
35380
+ }
35100
35381
  if (raw.op === "read_indexed_db") {
35101
35382
  assertObject(raw.init, "inner.init");
35102
35383
  if (raw.init.origin === void 0)
@@ -35122,7 +35403,32 @@ function validateInnerRequest(raw) {
35122
35403
  }
35123
35404
  return raw;
35124
35405
  }
35125
- throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "read_indexed_db"; got ${JSON.stringify(raw.op)}`);
35406
+ if (raw.op === "download") {
35407
+ assertObject(raw.init, "inner.init");
35408
+ if (raw.init.url === void 0) {
35409
+ throw new ProtocolError("inner.init.url: missing");
35410
+ }
35411
+ assertHttpUrl(raw.init.url, "inner.init.url");
35412
+ if (raw.init.filename !== void 0) {
35413
+ assertString(raw.init.filename, "inner.init.filename");
35414
+ if (/^([/\\]|[A-Za-z]:)/.test(raw.init.filename)) {
35415
+ throw new ProtocolError(`inner.init.filename: must be relative ${JSON.stringify(raw.init.filename)}`);
35416
+ }
35417
+ if (raw.init.filename.split(/[/\\]/).includes("..")) {
35418
+ throw new ProtocolError(`inner.init.filename: must not contain '..' ${JSON.stringify(raw.init.filename)}`);
35419
+ }
35420
+ }
35421
+ if (raw.init.timeoutMs !== void 0) {
35422
+ assertPositiveInt(raw.init.timeoutMs, "inner.init.timeoutMs");
35423
+ }
35424
+ for (const k of Object.keys(raw.init)) {
35425
+ if (k !== "url" && k !== "filename" && k !== "timeoutMs") {
35426
+ throw new ProtocolError(`inner.init: unexpected field ${JSON.stringify(k)} on download`);
35427
+ }
35428
+ }
35429
+ return raw;
35430
+ }
35431
+ throw new ProtocolError(`inner.op: must be one of "fetch", "read_cookies", "read_local_storage", "read_session_storage", "capture_request_header", "capture_redirect", "read_indexed_db", "download"; got ${JSON.stringify(raw.op)}`);
35126
35432
  }
35127
35433
  function assertNonEmptyKeyArray(value, label) {
35128
35434
  if (!Array.isArray(value)) {
@@ -35193,6 +35499,13 @@ function validateInnerResponse(raw) {
35193
35499
  assertString(raw.value, "inner.value");
35194
35500
  return raw;
35195
35501
  }
35502
+ if (op === "capture_redirect") {
35503
+ if (raw.value === void 0) {
35504
+ throw new ProtocolError("inner.value: missing on capture_redirect response");
35505
+ }
35506
+ assertString(raw.value, "inner.value");
35507
+ return raw;
35508
+ }
35196
35509
  if (op === "read_indexed_db") {
35197
35510
  if (raw.values === void 0) {
35198
35511
  throw new ProtocolError("inner.values: missing on read_indexed_db response");
@@ -35200,6 +35513,25 @@ function validateInnerResponse(raw) {
35200
35513
  assertObject(raw.values, "inner.values");
35201
35514
  return raw;
35202
35515
  }
35516
+ if (op === "download") {
35517
+ assertObject(raw.value, "inner.value");
35518
+ assertString(raw.value.path, "inner.value.path");
35519
+ if (typeof raw.value.bytes !== "number" || !Number.isInteger(raw.value.bytes) || raw.value.bytes < 0) {
35520
+ throw new ProtocolError("inner.value.bytes: expected non-negative integer");
35521
+ }
35522
+ if (raw.value.mime !== void 0) {
35523
+ assertString(raw.value.mime, "inner.value.mime");
35524
+ }
35525
+ if (raw.value.finalUrl !== void 0) {
35526
+ assertString(raw.value.finalUrl, "inner.value.finalUrl");
35527
+ }
35528
+ for (const k of Object.keys(raw.value)) {
35529
+ if (k !== "path" && k !== "bytes" && k !== "mime" && k !== "finalUrl") {
35530
+ throw new ProtocolError(`inner.value: unexpected field ${JSON.stringify(k)} on download response`);
35531
+ }
35532
+ }
35533
+ return raw;
35534
+ }
35203
35535
  throw new ProtocolError(`inner.op: unknown success-response op ${JSON.stringify(raw.op)}`);
35204
35536
  }
35205
35537
  if (raw.ok === false) {
@@ -35407,13 +35739,13 @@ async function openEncryptedFrame(sessionKey, frame) {
35407
35739
  // node_modules/@fetchproxy/server/dist/election.js
35408
35740
  import { createServer, Server as HttpServer } from "node:http";
35409
35741
  async function electRole(opts) {
35410
- const server2 = createServer();
35742
+ const server = createServer();
35411
35743
  return new Promise((resolve, reject) => {
35412
35744
  const onError = (e) => {
35413
- server2.removeListener("listening", onListening);
35745
+ server.removeListener("listening", onListening);
35414
35746
  if (e.code === "EADDRINUSE") {
35415
35747
  try {
35416
- server2.close();
35748
+ server.close();
35417
35749
  } catch {
35418
35750
  }
35419
35751
  resolve({ role: "peer" });
@@ -35422,12 +35754,12 @@ async function electRole(opts) {
35422
35754
  }
35423
35755
  };
35424
35756
  const onListening = () => {
35425
- server2.removeListener("error", onError);
35426
- resolve({ role: "host", server: server2 });
35757
+ server.removeListener("error", onError);
35758
+ resolve({ role: "host", server });
35427
35759
  };
35428
- server2.once("error", onError);
35429
- server2.once("listening", onListening);
35430
- server2.listen(opts.port, opts.host);
35760
+ server.once("error", onError);
35761
+ server.once("listening", onListening);
35762
+ server.listen(opts.port, opts.host);
35431
35763
  });
35432
35764
  }
35433
35765
 
@@ -35471,7 +35803,8 @@ async function buildServerHello(opts) {
35471
35803
  }
35472
35804
  if (opts.captureHeaders && opts.captureHeaders.length > 0) {
35473
35805
  hello.captureHeaders = opts.captureHeaders.map((d) => ({
35474
- urlPattern: d.urlPattern,
35806
+ host: d.host,
35807
+ ...d.path !== void 0 ? { path: d.path } : {},
35475
35808
  headerName: d.headerName
35476
35809
  }));
35477
35810
  }
@@ -35709,7 +36042,9 @@ async function startHost(opts) {
35709
36042
  } catch {
35710
36043
  }
35711
36044
  }
35712
- wss.close(() => resolve());
36045
+ wss.close(() => {
36046
+ opts.httpServer.close(() => resolve());
36047
+ });
35713
36048
  }),
35714
36049
  sendOwnInner: async (inner) => {
35715
36050
  const session = await ownSessionReady;
@@ -35759,6 +36094,7 @@ async function startPeer(opts) {
35759
36094
  const innerListeners = [];
35760
36095
  const renegotiateListeners = [];
35761
36096
  const pendingPairListeners = [];
36097
+ const closeListeners = [];
35762
36098
  let session = null;
35763
36099
  let pendingPairCode = null;
35764
36100
  let resolveFirstReady;
@@ -35808,6 +36144,7 @@ async function startPeer(opts) {
35808
36144
  ws.on("message", onMessage);
35809
36145
  ws.once("close", () => {
35810
36146
  rejectFirstReady(new Error("peer WS closed before ready"));
36147
+ closeListeners.forEach((cb) => cb());
35811
36148
  });
35812
36149
  sessionPromise.catch(() => {
35813
36150
  });
@@ -35830,6 +36167,9 @@ async function startPeer(opts) {
35830
36167
  pendingPairListeners.push(cb);
35831
36168
  },
35832
36169
  pendingPairCode: () => pendingPairCode,
36170
+ onClose: (cb) => {
36171
+ closeListeners.push(cb);
36172
+ },
35833
36173
  close: () => ws.close()
35834
36174
  };
35835
36175
  return handle;
@@ -35912,6 +36252,19 @@ function classifyFetchError(error51) {
35912
36252
  return "other";
35913
36253
  }
35914
36254
 
36255
+ // node_modules/@fetchproxy/server/dist/classify-bridge-error.js
36256
+ function classifyBridgeError(err) {
36257
+ if (err instanceof FetchproxyTimeoutError)
36258
+ return "timeout";
36259
+ if (err instanceof FetchproxyBridgeDownError)
36260
+ return "bridge_down";
36261
+ if (err instanceof FetchproxyHttpError)
36262
+ return "http";
36263
+ if (err instanceof FetchproxyProtocolError)
36264
+ return "protocol";
36265
+ return "other";
36266
+ }
36267
+
35915
36268
  // node_modules/@fetchproxy/server/dist/ws-server.js
35916
36269
  var FetchproxyProtocolError = class extends Error {
35917
36270
  constructor(message) {
@@ -35941,7 +36294,7 @@ var FetchproxyBridgeDownError = class extends FetchproxyProtocolError {
35941
36294
  const retryAttempted = args.retryAttempted ?? false;
35942
36295
  const op = args.op ?? "fetch";
35943
36296
  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.`;
36297
+ 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
36298
  super(`fetchproxy bridge down during ${op}${args.url ? ` (${args.url})` : ""}. ${hint}`);
35946
36299
  this.name = "FetchproxyBridgeDownError";
35947
36300
  this.originalError = args.originalError;
@@ -35963,6 +36316,14 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
35963
36316
  port;
35964
36317
  /** 0.8.0+: actual elapsed milliseconds when the timer won the race. */
35965
36318
  elapsedMs;
36319
+ /**
36320
+ * 0.11.0+ (#90/#91): true when the server's lazy-revive retry path
36321
+ * fired for this timeout (a cold-start `timeout` symptom followed by
36322
+ * a warm-and-retry that also timed out). False when the retry was
36323
+ * disabled (`bridgeReviveDelayMs` unset/0) so the timeout surfaced on
36324
+ * the first attempt.
36325
+ */
36326
+ retryAttempted;
35966
36327
  constructor(args) {
35967
36328
  super(`fetchproxy: ${args.url} did not respond within ${args.timeoutMs}ms`);
35968
36329
  this.name = "FetchproxyTimeoutError";
@@ -35971,6 +36332,7 @@ var FetchproxyTimeoutError = class extends FetchproxyProtocolError {
35971
36332
  this.role = args.role ?? null;
35972
36333
  this.port = args.port ?? 0;
35973
36334
  this.elapsedMs = args.elapsedMs ?? args.timeoutMs;
36335
+ this.retryAttempted = args.retryAttempted ?? false;
35974
36336
  }
35975
36337
  };
35976
36338
  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 +36371,12 @@ var FetchproxyServer = class {
36009
36371
  opts;
36010
36372
  hostHandle = null;
36011
36373
  peerHandle = null;
36374
+ // 0.13.0+: true from the start of `close()` until the next `doConnect()`.
36375
+ // Distinguishes an intentional shutdown (whose WS close we must ignore)
36376
+ // from the host process dying (which strands a peer and must trigger
36377
+ // re-election). The WS `close` event fires asynchronously, so this stays
36378
+ // latched across `close()` rather than being reset in its tail.
36379
+ closing = false;
36012
36380
  nextRequestId = 1;
36013
36381
  // 0.8.0+: process-wide freshness counters surfaced via bridgeHealth().
36014
36382
  // Replaces the local copies every downstream MCP was rolling on top
@@ -36034,8 +36402,12 @@ var FetchproxyServer = class {
36034
36402
  pendingStorage = /* @__PURE__ */ new Map();
36035
36403
  // 0.3.0+: capture-header awaiters resolve a single string.
36036
36404
  pendingCapture = /* @__PURE__ */ new Map();
36405
+ // capture_redirect awaiters resolve the captured redirect URL string.
36406
+ pendingRedirect = /* @__PURE__ */ new Map();
36037
36407
  // 0.4.0+: read_indexed_db awaiters resolve a JSON-typed values map.
36038
36408
  pendingIdb = /* @__PURE__ */ new Map();
36409
+ // download awaiters resolve the saved-file metadata (path + size + mime).
36410
+ pendingDownload = /* @__PURE__ */ new Map();
36039
36411
  mcpId = null;
36040
36412
  identity = null;
36041
36413
  // 0.5.3+: in-flight role-election / handle-start promise. Set the
@@ -36044,6 +36416,25 @@ var FetchproxyServer = class {
36044
36416
  // for "we're connecting right now" so two parallel first-calls don't
36045
36417
  // race the port bind.
36046
36418
  connectingPromise = null;
36419
+ // 0.8.1+ (#67): server-initiated keep-alive ping. Active when
36420
+ // `keepAliveIntervalMs` is set AND we've seen recent activity
36421
+ // (fetch/capture success or failure, or markActive()) within
36422
+ // `keepAliveMaxIdleMs`. The interval handle is created lazily on
36423
+ // first activity and torn down on close() / extension disconnect.
36424
+ keepAliveTimer = null;
36425
+ lastActiveAt = null;
36426
+ // 0.10.0+ (#73): observability counters surfaced via
36427
+ // bridgeHealth().keepAlive / .swEviction. Monotonic across the process
36428
+ // lifetime so a downstream healthcheck tool can verify the keep-alive
36429
+ // is actually preventing SW eviction. `lastPingAt` and `totalPings`
36430
+ // are stamped from `startKeepaliveIfIdle`'s tick. `lazyRevive*` and
36431
+ // `lastEvictionDetectedAt` are stamped from the lazy-revive code path
36432
+ // in fetch() / captureRequestHeader().
36433
+ lastPingAt = null;
36434
+ totalPings = 0;
36435
+ lazyReviveAttempts = 0;
36436
+ lazyReviveSuccesses = 0;
36437
+ lastEvictionDetectedAt = null;
36047
36438
  constructor(opts) {
36048
36439
  if (!Array.isArray(opts.domains) || opts.domains.length === 0) {
36049
36440
  throw new Error("FetchproxyServer: opts.domains must be a non-empty array of hostnames");
@@ -36062,6 +36453,14 @@ var FetchproxyServer = class {
36062
36453
  }
36063
36454
  capabilities = [...opts.capabilities];
36064
36455
  }
36456
+ if (opts.captureHeaders !== void 0) {
36457
+ try {
36458
+ validateCaptureHeaderDecls(opts.captureHeaders, opts.domains);
36459
+ } catch (err) {
36460
+ const message = err instanceof Error ? err.message : String(err);
36461
+ throw new Error("FetchproxyServer: invalid captureHeaders \u2014 " + message);
36462
+ }
36463
+ }
36065
36464
  this.opts = {
36066
36465
  port: opts.port ?? 37149,
36067
36466
  host: opts.host ?? "127.0.0.1",
@@ -36073,7 +36472,8 @@ var FetchproxyServer = class {
36073
36472
  localStorageKeys: [...opts.localStorageKeys ?? []],
36074
36473
  sessionStorageKeys: [...opts.sessionStorageKeys ?? []],
36075
36474
  captureHeaders: (opts.captureHeaders ?? []).map((d) => ({
36076
- urlPattern: d.urlPattern,
36475
+ host: d.host,
36476
+ ...d.path !== void 0 ? { path: d.path } : {},
36077
36477
  headerName: d.headerName
36078
36478
  })),
36079
36479
  indexedDbScopes: (opts.indexedDbScopes ?? []).map((d) => ({
@@ -36096,6 +36496,22 @@ var FetchproxyServer = class {
36096
36496
  // the legacy hang-forever / fail-once-on-SW-eviction behavior.
36097
36497
  fetchTimeoutMs: opts.fetchTimeoutMs ?? 3e4,
36098
36498
  bridgeReviveDelayMs: opts.bridgeReviveDelayMs ?? 2e3,
36499
+ // 0.10.0+ (#72): keep-alive defaults to 25s — round-3 #71 cohort
36500
+ // wave showed every Pattern A consumer was opting into this same
36501
+ // value. Pass `0` to disable; the existing `<= 0` guards in
36502
+ // `startKeepaliveIfIdle` / `noteActivityForKeepalive` honour that.
36503
+ //
36504
+ // #90 (P1-1): tightened to 20s. 25s left only ~5s of slack under
36505
+ // Chrome's ~30s SW-eviction window — slack that timer drift, a
36506
+ // busy host event loop (CPU-bound response parsing between calls),
36507
+ // and the ping's own round-trip latency routinely ate, so the SW
36508
+ // evicted before the next ping landed and the next call cold-
36509
+ // started. 20s restores real margin. (The extension
36510
+ // `chrome.alarms` backstop is clamped by Chrome to a 30s minimum
36511
+ // period, firing *at* the edge — it can't rescue a sub-30s race;
36512
+ // the server ping is the real defense.)
36513
+ keepAliveIntervalMs: opts.keepAliveIntervalMs ?? 2e4,
36514
+ keepAliveMaxIdleMs: opts.keepAliveMaxIdleMs ?? 5 * 60 * 1e3,
36099
36515
  identityDir: opts.identityDir,
36100
36516
  onPairCode: opts.onPairCode
36101
36517
  };
@@ -36175,6 +36591,7 @@ var FetchproxyServer = class {
36175
36591
  async doConnect() {
36176
36592
  const identity = this.identity;
36177
36593
  const mcpId = this.mcpId;
36594
+ this.closing = false;
36178
36595
  const el = await electRole({ host: this.opts.host, port: this.opts.port });
36179
36596
  if (el.role === "host") {
36180
36597
  this.role = "host";
@@ -36196,7 +36613,10 @@ var FetchproxyServer = class {
36196
36613
  onPairCode: this.opts.onPairCode
36197
36614
  });
36198
36615
  this.hostHandle.onOwnInner((inner) => this.onInner(inner));
36199
- this.hostHandle.onExtensionDisconnect(() => this.rejectAllPending());
36616
+ this.hostHandle.onExtensionDisconnect(() => {
36617
+ this.stopKeepalive();
36618
+ this.rejectAllPending();
36619
+ });
36200
36620
  this.hostHandle.onPendingPair((code) => {
36201
36621
  this.rejectAllPending(this.pairingErrorMessage(code));
36202
36622
  });
@@ -36220,7 +36640,10 @@ var FetchproxyServer = class {
36220
36640
  sessionStoragePointers: this.opts.sessionStoragePointers
36221
36641
  });
36222
36642
  this.peerHandle.onInner((inner) => this.onInner(inner));
36223
- this.peerHandle.onRenegotiate(() => this.rejectAllPending());
36643
+ this.peerHandle.onRenegotiate(() => {
36644
+ this.stopKeepalive();
36645
+ this.rejectAllPending();
36646
+ });
36224
36647
  this.peerHandle.onPendingPair((code) => {
36225
36648
  this.rejectAllPending(this.pairingErrorMessage(code));
36226
36649
  });
@@ -36228,6 +36651,14 @@ var FetchproxyServer = class {
36228
36651
  const cb = this.opts.onPairCode;
36229
36652
  this.peerHandle.onPendingPair((code) => cb(code));
36230
36653
  }
36654
+ this.peerHandle.onClose(() => {
36655
+ if (this.closing || this.peerHandle === null)
36656
+ return;
36657
+ this.stopKeepalive();
36658
+ this.rejectAllPending();
36659
+ this.peerHandle = null;
36660
+ this.role = null;
36661
+ });
36231
36662
  }
36232
36663
  }
36233
36664
  pairingErrorMessage(code) {
@@ -36261,13 +36692,18 @@ var FetchproxyServer = class {
36261
36692
  }
36262
36693
  const first = await this._fetchOnceWithTimeout(init);
36263
36694
  const reviveMs = this.opts.bridgeReviveDelayMs;
36264
- let final = first;
36265
- if (!first.ok && first.kind === "content_script_unreachable" && reviveMs !== void 0 && reviveMs > 0) {
36695
+ const isColdStartSymptom = !first.ok && (first.kind === "content_script_unreachable" || first.kind === "timeout");
36696
+ if (isColdStartSymptom) {
36697
+ this.lastEvictionDetectedAt = Date.now();
36698
+ }
36699
+ if (isColdStartSymptom && reviveMs !== void 0 && reviveMs > 0) {
36700
+ this.lazyReviveAttempts += 1;
36266
36701
  await new Promise((r) => setTimeout(r, reviveMs));
36267
36702
  const second = await this._fetchOnceWithTimeout(init);
36268
- if (second.ok)
36703
+ if (second.ok) {
36704
+ this.lazyReviveSuccesses += 1;
36269
36705
  this.recordSuccess();
36270
- else
36706
+ } else
36271
36707
  this.recordFailure(`${second.kind ?? "other"}: ${second.error}`);
36272
36708
  return { ...second, retryAttempted: true };
36273
36709
  }
@@ -36289,6 +36725,9 @@ var FetchproxyServer = class {
36289
36725
  * call (addresses #23 ask 4).
36290
36726
  */
36291
36727
  bridgeHealth() {
36728
+ const intervalMs = this.opts.keepAliveIntervalMs;
36729
+ const maxIdleMs = this.opts.keepAliveMaxIdleMs;
36730
+ const idleSinceMs = this.lastActiveAt === null ? null : Date.now() - this.lastActiveAt;
36292
36731
  return {
36293
36732
  role: this.role,
36294
36733
  port: this.opts.port,
@@ -36299,17 +36738,85 @@ var FetchproxyServer = class {
36299
36738
  lastFailureAt: this.lastFailureAt,
36300
36739
  lastFailureReason: this.lastFailureReason,
36301
36740
  consecutiveFailures: this.consecutiveFailures,
36302
- lastExtensionMessageAt: this.lastExtensionMessageAt
36741
+ lastExtensionMessageAt: this.lastExtensionMessageAt,
36742
+ keepAlive: {
36743
+ enabled: intervalMs > 0,
36744
+ intervalMs,
36745
+ maxIdleMs,
36746
+ lastPingAt: this.lastPingAt,
36747
+ totalPings: this.totalPings,
36748
+ idleSinceMs
36749
+ },
36750
+ swEviction: {
36751
+ lazyReviveAttempts: this.lazyReviveAttempts,
36752
+ lazyReviveSuccesses: this.lazyReviveSuccesses,
36753
+ lastEvictionDetectedAt: this.lastEvictionDetectedAt
36754
+ }
36303
36755
  };
36304
36756
  }
36305
36757
  recordSuccess() {
36306
36758
  this.lastSuccessAt = Date.now();
36307
36759
  this.consecutiveFailures = 0;
36760
+ this.noteActivityForKeepalive();
36308
36761
  }
36309
36762
  recordFailure(reason) {
36310
36763
  this.lastFailureAt = Date.now();
36311
36764
  this.lastFailureReason = reason;
36312
36765
  this.consecutiveFailures += 1;
36766
+ this.noteActivityForKeepalive();
36767
+ }
36768
+ /**
36769
+ * 0.8.1+ (#67): caller-side hint that work is happening or about to
36770
+ * happen — bumps the keep-alive idle gate so the server keeps pinging
36771
+ * the extension. Useful for MCPs that do a chain of side-effectful
36772
+ * work between bridge calls and don't want the SW to evict in the
36773
+ * gap (e.g. server-side parsing of a previous response that takes
36774
+ * tens of seconds). No-op when `keepAliveIntervalMs` is `0`.
36775
+ */
36776
+ markActive() {
36777
+ this.noteActivityForKeepalive();
36778
+ }
36779
+ noteActivityForKeepalive() {
36780
+ const intervalMs = this.opts.keepAliveIntervalMs;
36781
+ if (intervalMs <= 0)
36782
+ return;
36783
+ this.lastActiveAt = Date.now();
36784
+ this.startKeepaliveIfIdle();
36785
+ }
36786
+ startKeepaliveIfIdle() {
36787
+ if (this.keepAliveTimer !== null)
36788
+ return;
36789
+ const intervalMs = this.opts.keepAliveIntervalMs;
36790
+ if (intervalMs <= 0)
36791
+ return;
36792
+ this.keepAliveTimer = setInterval(() => {
36793
+ const now = Date.now();
36794
+ if (this.lastActiveAt === null || now - this.lastActiveAt > this.opts.keepAliveMaxIdleMs) {
36795
+ this.stopKeepalive();
36796
+ return;
36797
+ }
36798
+ this.totalPings += 1;
36799
+ this.lastPingAt = now;
36800
+ void this.sendKeepalivePing();
36801
+ }, intervalMs);
36802
+ }
36803
+ async sendKeepalivePing() {
36804
+ try {
36805
+ const inner = { type: "ping" };
36806
+ if (this.hostHandle) {
36807
+ await this.hostHandle.sendOwnInner(inner);
36808
+ } else if (this.peerHandle) {
36809
+ await this.peerHandle.sendInner(inner);
36810
+ }
36811
+ } catch (e) {
36812
+ console.error("[fetchproxy] keepalive ping send failed:", e);
36813
+ }
36814
+ }
36815
+ stopKeepalive() {
36816
+ if (this.keepAliveTimer !== null) {
36817
+ clearInterval(this.keepAliveTimer);
36818
+ this.keepAliveTimer = null;
36819
+ }
36313
36820
  }
36314
36821
  /**
36315
36822
  * Single bridge round-trip, wrapped by `fetchTimeoutMs` when set.
@@ -36368,7 +36875,8 @@ var FetchproxyServer = class {
36368
36875
  timeoutMs: this.opts.fetchTimeoutMs ?? 0,
36369
36876
  role: this.role,
36370
36877
  port: this.opts.port,
36371
- elapsedMs: result.elapsedMs
36878
+ elapsedMs: result.elapsedMs,
36879
+ retryAttempted
36372
36880
  });
36373
36881
  }
36374
36882
  if (result.kind === "content_script_unreachable") {
@@ -36499,6 +37007,108 @@ var FetchproxyServer = class {
36499
37007
  const response = await this.get(path, this.applyJsonDefaults(opts));
36500
37008
  return response.body;
36501
37009
  }
37010
+ /**
37011
+ * 0.11.0+: method-generic JSON convenience helper. Generalizes the
37012
+ * `fetchJson<T>(path, { method, headers, body })` that
37013
+ * zillow/redfin/compass/homes hand-rolled char-for-char in their
37014
+ * `src/client.ts`:
37015
+ *
37016
+ * - sets `Accept: application/json`;
37017
+ * - adds `Content-Type: application/json` only for a non-GET request
37018
+ * that carries a `body` (and only if the caller didn't set one);
37019
+ * - `JSON.stringify`s the body (GET / no-body sends nothing);
37020
+ * - treats a `204` or an empty body as `data: null` (no parse);
37021
+ * - otherwise `JSON.parse`s the body.
37022
+ *
37023
+ * Scope is serialization + header defaults + 204-handling +
37024
+ * JSON.parse ONLY. It deliberately does NOT assert on the HTTP status
37025
+ * or look for a sign-in interstitial — those guards differ per site
37026
+ * (Zillow's `captcha-delivery`, Redfin's AWS-WAF challenge, …), so it
37027
+ * returns BOTH the parsed `data` and the raw `FetchResult` and leaves
37028
+ * the consumer to run its own `throwIfNotOk` / `throwIfSignInPage`
37029
+ * over `result`.
37030
+ *
37031
+ * Bridge-level failures (no signed-in tab, SW down, timeout) still
37032
+ * throw the typed errors via `request()`, exactly like the verb
37033
+ * helpers — only successful round-trips (any HTTP status) return.
37034
+ */
37035
+ async requestJson(method, path, opts = {}) {
37036
+ const isGet = method.toUpperCase() === "GET";
37037
+ const sendBody = !isGet && opts.body !== void 0;
37038
+ const headers = {
37039
+ Accept: "application/json",
37040
+ ...sendBody && !this.hasContentType(opts.headers ?? {}) ? { "Content-Type": "application/json" } : {},
37041
+ ...opts.headers ?? {}
37042
+ };
37043
+ const response = await this.request(method, path, {
37044
+ headers,
37045
+ body: sendBody ? JSON.stringify(opts.body) : void 0,
37046
+ ...opts.subdomain !== void 0 ? { subdomain: opts.subdomain } : {},
37047
+ ...opts.domain !== void 0 ? { domain: opts.domain } : {}
37048
+ });
37049
+ const result = {
37050
+ ok: true,
37051
+ status: response.status,
37052
+ url: response.url,
37053
+ body: response.body
37054
+ };
37055
+ if (response.status === 204 || response.body === "") {
37056
+ return { data: null, result };
37057
+ }
37058
+ let data;
37059
+ try {
37060
+ data = JSON.parse(response.body);
37061
+ } catch (e) {
37062
+ throw new Error(`fetchproxy ${method} ${path} \u2014 response was not JSON: ${e instanceof Error ? e.message : String(e)}`);
37063
+ }
37064
+ return { data, result };
37065
+ }
37066
+ /**
37067
+ * 0.11.0+: run a single healthcheck probe through `fetchFn`, measure
37068
+ * the elapsed round-trip, classify any thrown error, and project the
37069
+ * post-probe `bridgeHealth()` into a snake-cased `bridge` sub-object.
37070
+ *
37071
+ * This is the transport half of the probe loop zillow/redfin/homes
37072
+ * had duplicated verbatim in `src/tools/healthcheck.ts`. The MCP
37073
+ * supplies its own probe call (`(path) => client.fetchHtml(path)`)
37074
+ * and probe path (e.g. `'/robots.txt'`); the tool registration and
37075
+ * the site-specific plain-English hint text STAY in the consumer.
37076
+ *
37077
+ * `bridgeHealth()` is read AFTER the probe so its freshness counters
37078
+ * (`lastSuccessAt` / `consecutiveFailures` / …) reflect this very
37079
+ * round-trip rather than a stale pre-probe snapshot.
37080
+ */
37081
+ async runProbe(fetchFn, probePath) {
37082
+ const start = Date.now();
37083
+ let ok = false;
37084
+ let error51;
37085
+ try {
37086
+ await fetchFn(probePath);
37087
+ ok = true;
37088
+ } catch (e) {
37089
+ error51 = {
37090
+ kind: classifyBridgeError(e),
37091
+ message: e instanceof Error ? e.message : String(e)
37092
+ };
37093
+ }
37094
+ const elapsed_ms = Date.now() - start;
37095
+ const health = this.bridgeHealth();
37096
+ return {
37097
+ ok,
37098
+ elapsed_ms,
37099
+ bridge: {
37100
+ role: health.role,
37101
+ port: health.port,
37102
+ server_version: health.serverVersion,
37103
+ fetch_timeout_ms: health.fetchTimeoutMs,
37104
+ last_success_at: health.lastSuccessAt,
37105
+ last_failure_at: health.lastFailureAt,
37106
+ last_failure_reason: health.lastFailureReason,
37107
+ consecutive_failures: health.consecutiveFailures
37108
+ },
37109
+ ...error51 ? { error: error51 } : {}
37110
+ };
37111
+ }
36502
37112
  /**
36503
37113
  * Snapshot the user's non-HttpOnly cookies for the chosen domain.
36504
37114
  *
@@ -36623,12 +37233,12 @@ var FetchproxyServer = class {
36623
37233
  /**
36624
37234
  * 0.3.0+: snapshot the next outgoing request's named header. Single-
36625
37235
  * 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).
37236
+ * filtered on `https://${host}${path ?? '/*'}`, captures the named
37237
+ * header on the first match, removes itself, and resolves with the
37238
+ * value. Times out after `timeoutMs` (default 30s on the extension).
36629
37239
  *
36630
- * `(urlPattern, headerName)` must exactly match a declared entry in
36631
- * `FetchproxyServerOpts.captureHeaders`.
37240
+ * `(host, path?, headerName)` must match a declared entry in
37241
+ * `FetchproxyServerOpts.captureHeaders` (omitted path ≡ `/*`).
36632
37242
  */
36633
37243
  async captureRequestHeader(opts) {
36634
37244
  if (!this.opts.capabilities.includes("capture_request_header")) {
@@ -36637,24 +37247,25 @@ var FetchproxyServer = class {
36637
37247
  await this.ensureConnected();
36638
37248
  this.throwIfPendingPair();
36639
37249
  const decls = this.opts.captureHeaders;
37250
+ const normPath = (p) => p ?? "/*";
36640
37251
  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);
37252
+ if (opts?.host !== void 0 && opts?.headerName !== void 0) {
37253
+ const found = decls.find((d) => d.host === opts.host && normPath(d.path) === normPath(opts.path) && d.headerName === opts.headerName);
36643
37254
  if (!found) {
36644
- throw new Error(`FetchproxyServer.captureRequestHeader: (urlPattern=${JSON.stringify(opts.urlPattern)}, headerName=${JSON.stringify(opts.headerName)}) not declared in captureHeaders`);
37255
+ 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
37256
  }
36646
37257
  resolved = found;
36647
- } else if (opts?.urlPattern === void 0 && opts?.headerName === void 0) {
37258
+ } else if (opts?.host === void 0 && opts?.headerName === void 0) {
36648
37259
  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");
37260
+ 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
37261
  }
36651
37262
  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`);
37263
+ const list = decls.map((d) => `${JSON.stringify(d.host)}${JSON.stringify(normPath(d.path))}/${JSON.stringify(d.headerName)}`).join(", ");
37264
+ throw new Error(`FetchproxyServer.captureRequestHeader: multiple captureHeaders declared (${decls.length}: ${list}); pass {host, headerName} to disambiguate`);
36654
37265
  }
36655
37266
  resolved = decls[0];
36656
37267
  } else {
36657
- throw new Error("FetchproxyServer.captureRequestHeader: pass both urlPattern AND headerName, or neither (which defaults to the single declared entry)");
37268
+ throw new Error("FetchproxyServer.captureRequestHeader: pass both host AND headerName, or neither (which defaults to the single declared entry)");
36658
37269
  }
36659
37270
  const callOpts = { ...resolved, ...opts?.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {} };
36660
37271
  try {
@@ -36667,11 +37278,14 @@ var FetchproxyServer = class {
36667
37278
  this.recordFailure(`capture_request_header: ${err.message ?? String(err)}`);
36668
37279
  throw err;
36669
37280
  }
37281
+ this.lastEvictionDetectedAt = Date.now();
36670
37282
  const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
36671
37283
  if (reviveMs > 0) {
37284
+ this.lazyReviveAttempts += 1;
36672
37285
  await new Promise((r) => setTimeout(r, reviveMs));
36673
37286
  try {
36674
37287
  const result = await this._captureRequestHeaderOnce(callOpts);
37288
+ this.lazyReviveSuccesses += 1;
36675
37289
  this.recordSuccess();
36676
37290
  return result;
36677
37291
  } catch (retryErr) {
@@ -36685,7 +37299,7 @@ var FetchproxyServer = class {
36685
37299
  originalError: retryErr.message,
36686
37300
  retryAttempted: true,
36687
37301
  op: "capture_request_header",
36688
- url: resolved.urlPattern,
37302
+ url: `https://${resolved.host}${resolved.path ?? "/*"}`,
36689
37303
  role: this.role,
36690
37304
  port: this.opts.port
36691
37305
  });
@@ -36696,7 +37310,7 @@ var FetchproxyServer = class {
36696
37310
  originalError: err.message,
36697
37311
  retryAttempted: false,
36698
37312
  op: "capture_request_header",
36699
- url: resolved.urlPattern,
37313
+ url: `https://${resolved.host}${resolved.path ?? "/*"}`,
36700
37314
  role: this.role,
36701
37315
  port: this.opts.port
36702
37316
  });
@@ -36709,7 +37323,8 @@ var FetchproxyServer = class {
36709
37323
  id,
36710
37324
  op: "capture_request_header",
36711
37325
  init: {
36712
- urlPattern: opts.urlPattern,
37326
+ host: opts.host,
37327
+ ...opts.path !== void 0 ? { path: opts.path } : {},
36713
37328
  headerName: opts.headerName,
36714
37329
  ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
36715
37330
  }
@@ -36724,6 +37339,181 @@ var FetchproxyServer = class {
36724
37339
  }
36725
37340
  return pending;
36726
37341
  }
37342
+ /**
37343
+ * Snapshot the redirect target URL of the next request the browser
37344
+ * makes to `(host, path?)`. Single-shot: the extension registers a
37345
+ * one-time `chrome.webRequest.onBeforeRedirect` listener filtered on
37346
+ * `https://${host}${path ?? '/*'}`, captures `details.redirectUrl` on
37347
+ * the first match, removes itself, and resolves with the URL. Times out
37348
+ * after `timeoutMs` (default 30s on the extension).
37349
+ *
37350
+ * Use case: a Cloudflare-walled endpoint that 302-redirects cross-origin
37351
+ * to a presigned URL — a page-level fetch sees only an opaque redirect,
37352
+ * but `onBeforeRedirect` exposes the target. Capture is limited to the
37353
+ * MCP's own declared `domains`; no per-entry declared scope is required.
37354
+ */
37355
+ async captureRedirect(opts) {
37356
+ if (!this.opts.capabilities.includes("capture_redirect")) {
37357
+ throw new Error('FetchproxyServer.captureRedirect(): MCP did not declare "capture_redirect" in capabilities');
37358
+ }
37359
+ await this.ensureConnected();
37360
+ this.throwIfPendingPair();
37361
+ try {
37362
+ const result = await this._captureRedirectOnce(opts);
37363
+ this.recordSuccess();
37364
+ return result;
37365
+ } catch (err) {
37366
+ const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
37367
+ if (!swDown) {
37368
+ this.recordFailure(`capture_redirect: ${err.message ?? String(err)}`);
37369
+ throw err;
37370
+ }
37371
+ this.lastEvictionDetectedAt = Date.now();
37372
+ const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
37373
+ if (reviveMs > 0) {
37374
+ this.lazyReviveAttempts += 1;
37375
+ await new Promise((r) => setTimeout(r, reviveMs));
37376
+ try {
37377
+ const result = await this._captureRedirectOnce(opts);
37378
+ this.lazyReviveSuccesses += 1;
37379
+ this.recordSuccess();
37380
+ return result;
37381
+ } catch (retryErr) {
37382
+ const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
37383
+ if (!stillDown) {
37384
+ this.recordFailure(`capture_redirect: ${retryErr.message ?? String(retryErr)}`);
37385
+ throw retryErr;
37386
+ }
37387
+ this.recordFailure(`capture_redirect bridge-down: ${retryErr.message}`);
37388
+ throw new FetchproxyBridgeDownError({
37389
+ originalError: retryErr.message,
37390
+ retryAttempted: true,
37391
+ op: "capture_redirect",
37392
+ url: `https://${opts.host}${opts.path ?? "/*"}`,
37393
+ role: this.role,
37394
+ port: this.opts.port
37395
+ });
37396
+ }
37397
+ }
37398
+ this.recordFailure(`capture_redirect bridge-down: ${err.message}`);
37399
+ throw new FetchproxyBridgeDownError({
37400
+ originalError: err.message,
37401
+ retryAttempted: false,
37402
+ op: "capture_redirect",
37403
+ url: `https://${opts.host}${opts.path ?? "/*"}`,
37404
+ role: this.role,
37405
+ port: this.opts.port
37406
+ });
37407
+ }
37408
+ }
37409
+ async _captureRedirectOnce(opts) {
37410
+ const id = this.nextRequestId++;
37411
+ const inner = {
37412
+ type: "request",
37413
+ id,
37414
+ op: "capture_redirect",
37415
+ init: {
37416
+ host: opts.host,
37417
+ ...opts.path !== void 0 ? { path: opts.path } : {},
37418
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
37419
+ }
37420
+ };
37421
+ const pending = new Promise((resolve, reject) => {
37422
+ this.pendingRedirect.set(id, { resolve, reject });
37423
+ });
37424
+ if (this.hostHandle) {
37425
+ await this.hostHandle.sendOwnInner(inner);
37426
+ } else if (this.peerHandle) {
37427
+ await this.peerHandle.sendInner(inner);
37428
+ }
37429
+ return pending;
37430
+ }
37431
+ /**
37432
+ * Download `url` through the BROWSER's own network stack via
37433
+ * `chrome.downloads` (real cookies + TLS/JA3 fingerprint). Unlike a
37434
+ * page-level `fetch()` (cors mode), this clears a Cloudflare bot-challenge
37435
+ * on the endpoint and follows the cross-origin redirect to the final file.
37436
+ * Resolves the saved local file path + size; the bridge is loopback-only /
37437
+ * single-host, so the MCP reads the bytes from the same disk. Requires
37438
+ * `'download'` in capabilities and the `url` host to be a declared `domain`.
37439
+ */
37440
+ async download(opts) {
37441
+ if (!this.opts.capabilities.includes("download")) {
37442
+ throw new Error('FetchproxyServer.download(): MCP did not declare "download" in capabilities');
37443
+ }
37444
+ assertUrlInDomains("download url", opts.url, this.opts.domains);
37445
+ await this.ensureConnected();
37446
+ this.throwIfPendingPair();
37447
+ try {
37448
+ const result = await this._downloadOnce(opts);
37449
+ this.recordSuccess();
37450
+ return result;
37451
+ } catch (err) {
37452
+ const swDown = err instanceof FetchproxyProtocolError && classifyFetchError(err.message) === "content_script_unreachable";
37453
+ if (!swDown) {
37454
+ this.recordFailure(`download: ${err.message ?? String(err)}`);
37455
+ throw err;
37456
+ }
37457
+ this.lastEvictionDetectedAt = Date.now();
37458
+ const reviveMs = this.opts.bridgeReviveDelayMs ?? 0;
37459
+ if (reviveMs > 0) {
37460
+ this.lazyReviveAttempts += 1;
37461
+ await new Promise((r) => setTimeout(r, reviveMs));
37462
+ try {
37463
+ const result = await this._downloadOnce(opts);
37464
+ this.lazyReviveSuccesses += 1;
37465
+ this.recordSuccess();
37466
+ return result;
37467
+ } catch (retryErr) {
37468
+ const stillDown = retryErr instanceof FetchproxyProtocolError && classifyFetchError(retryErr.message) === "content_script_unreachable";
37469
+ if (!stillDown) {
37470
+ this.recordFailure(`download: ${retryErr.message ?? String(retryErr)}`);
37471
+ throw retryErr;
37472
+ }
37473
+ this.recordFailure(`download bridge-down: ${retryErr.message}`);
37474
+ throw new FetchproxyBridgeDownError({
37475
+ originalError: retryErr.message,
37476
+ retryAttempted: true,
37477
+ op: "download",
37478
+ url: opts.url,
37479
+ role: this.role,
37480
+ port: this.opts.port
37481
+ });
37482
+ }
37483
+ }
37484
+ this.recordFailure(`download bridge-down: ${err.message}`);
37485
+ throw new FetchproxyBridgeDownError({
37486
+ originalError: err.message,
37487
+ retryAttempted: false,
37488
+ op: "download",
37489
+ url: opts.url,
37490
+ role: this.role,
37491
+ port: this.opts.port
37492
+ });
37493
+ }
37494
+ }
37495
+ async _downloadOnce(opts) {
37496
+ const id = this.nextRequestId++;
37497
+ const inner = {
37498
+ type: "request",
37499
+ id,
37500
+ op: "download",
37501
+ init: {
37502
+ url: opts.url,
37503
+ ...opts.filename !== void 0 ? { filename: opts.filename } : {},
37504
+ ...opts.timeoutMs !== void 0 ? { timeoutMs: opts.timeoutMs } : {}
37505
+ }
37506
+ };
37507
+ const pending = new Promise((resolve, reject) => {
37508
+ this.pendingDownload.set(id, { resolve, reject });
37509
+ });
37510
+ if (this.hostHandle) {
37511
+ await this.hostHandle.sendOwnInner(inner);
37512
+ } else if (this.peerHandle) {
37513
+ await this.peerHandle.sendInner(inner);
37514
+ }
37515
+ return pending;
37516
+ }
36727
37517
  /**
36728
37518
  * 0.4.0+: read declared IndexedDB keys from the user's signed-in
36729
37519
  * tab. Requires `'read_indexed_db'` in capabilities AND the
@@ -36871,6 +37661,20 @@ var FetchproxyServer = class {
36871
37661
  }
36872
37662
  return;
36873
37663
  }
37664
+ const redirectCb = this.pendingRedirect.get(inner.id);
37665
+ if (redirectCb) {
37666
+ this.pendingRedirect.delete(inner.id);
37667
+ if (inner.ok) {
37668
+ if (inner.op === "capture_redirect" && typeof inner.value === "string") {
37669
+ redirectCb.resolve(inner.value);
37670
+ } else {
37671
+ redirectCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on capture_redirect awaiter`));
37672
+ }
37673
+ } else {
37674
+ redirectCb.reject(new FetchproxyProtocolError(inner.error));
37675
+ }
37676
+ return;
37677
+ }
36874
37678
  const idbCb = this.pendingIdb.get(inner.id);
36875
37679
  if (idbCb) {
36876
37680
  this.pendingIdb.delete(inner.id);
@@ -36885,6 +37689,20 @@ var FetchproxyServer = class {
36885
37689
  }
36886
37690
  return;
36887
37691
  }
37692
+ const downloadCb = this.pendingDownload.get(inner.id);
37693
+ if (downloadCb) {
37694
+ this.pendingDownload.delete(inner.id);
37695
+ if (inner.ok) {
37696
+ if (inner.op === "download" && inner.value && typeof inner.value === "object") {
37697
+ downloadCb.resolve({ ...inner.value });
37698
+ } else {
37699
+ downloadCb.reject(new FetchproxyProtocolError(`unexpected ${String(inner.op)} response on download awaiter`));
37700
+ }
37701
+ } else {
37702
+ downloadCb.reject(new FetchproxyProtocolError(inner.error));
37703
+ }
37704
+ return;
37705
+ }
36888
37706
  const cookiesCb = this.pendingReadCookies.get(inner.id);
36889
37707
  if (cookiesCb) {
36890
37708
  this.pendingReadCookies.delete(inner.id);
@@ -36927,9 +37745,15 @@ var FetchproxyServer = class {
36927
37745
  for (const { reject } of this.pendingCapture.values())
36928
37746
  reject(err);
36929
37747
  this.pendingCapture.clear();
37748
+ for (const { reject } of this.pendingRedirect.values())
37749
+ reject(err);
37750
+ this.pendingRedirect.clear();
36930
37751
  for (const { reject } of this.pendingIdb.values())
36931
37752
  reject(err);
36932
37753
  this.pendingIdb.clear();
37754
+ for (const { reject } of this.pendingDownload.values())
37755
+ reject(err);
37756
+ this.pendingDownload.clear();
36933
37757
  }
36934
37758
  /**
36935
37759
  * 0.5.2+: read the current pair-pending pair code from whichever handle
@@ -36964,6 +37788,8 @@ var FetchproxyServer = class {
36964
37788
  * twice in a row.
36965
37789
  */
36966
37790
  async close() {
37791
+ this.closing = true;
37792
+ this.stopKeepalive();
36967
37793
  this.rejectAllPending();
36968
37794
  if (this.connectingPromise) {
36969
37795
  await this.connectingPromise.catch(() => void 0);
@@ -36979,19 +37805,6 @@ var FetchproxyServer = class {
36979
37805
  }
36980
37806
  };
36981
37807
 
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
37808
  // node_modules/@fetchproxy/bootstrap/dist/index.js
36996
37809
  var defaultFactory = (opts) => new FetchproxyServer(opts);
36997
37810
  async function bootstrap(opts) {
@@ -37023,7 +37836,7 @@ async function bootstrap(opts) {
37023
37836
  const sessionStorageKeys = new Set(opts.declare.sessionStorage);
37024
37837
  for (const p of sessionStoragePointers)
37025
37838
  sessionStorageKeys.add(p.storageKey);
37026
- const server2 = factory({
37839
+ const server = factory({
37027
37840
  serverName: opts.serverName,
37028
37841
  version: opts.version,
37029
37842
  domains: [...opts.domains],
@@ -37058,10 +37871,10 @@ async function bootstrap(opts) {
37058
37871
  if (opts.storageSubdomain !== void 0)
37059
37872
  storageDomainOpts.subdomain = opts.storageSubdomain;
37060
37873
  try {
37061
- await server2.listen();
37874
+ await server.listen();
37062
37875
  const cookies = {};
37063
37876
  if (opts.declare.cookies.length > 0) {
37064
- const joined = await server2.readCookies({
37877
+ const joined = await server.readCookies({
37065
37878
  keys: opts.declare.cookies,
37066
37879
  ...storageDomainOpts
37067
37880
  });
@@ -37081,7 +37894,7 @@ async function bootstrap(opts) {
37081
37894
  for (const p of localStoragePointers) {
37082
37895
  pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
37083
37896
  }
37084
- localStorage = await server2.readLocalStorage({
37897
+ localStorage = await server.readLocalStorage({
37085
37898
  keys: allKeys,
37086
37899
  ...storageDomainOpts,
37087
37900
  ...localStoragePointers.length > 0 ? { pointers } : {}
@@ -37094,7 +37907,7 @@ async function bootstrap(opts) {
37094
37907
  for (const p of sessionStoragePointers) {
37095
37908
  pointers[p.outputKey] = { storageKey: p.storageKey, jsonPointer: p.jsonPointer };
37096
37909
  }
37097
- sessionStorage = await server2.readSessionStorage({
37910
+ sessionStorage = await server.readSessionStorage({
37098
37911
  keys: allKeys,
37099
37912
  ...storageDomainOpts,
37100
37913
  ...sessionStoragePointers.length > 0 ? { pointers } : {}
@@ -37103,21 +37916,17 @@ async function bootstrap(opts) {
37103
37916
  const capturedHeaders = {};
37104
37917
  for (const h of opts.declare.captureHeaders) {
37105
37918
  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
- }
37919
+ opts.onWaiting(`waiting for next request to ${h.host} to capture ${h.headerName} \u2014 interact with the page in your browser`);
37112
37920
  }
37113
- capturedHeaders[h.headerName] = await server2.captureRequestHeader({
37114
- urlPattern: h.urlPattern,
37921
+ capturedHeaders[h.headerName] = await server.captureRequestHeader({
37922
+ host: h.host,
37923
+ ...h.path !== void 0 ? { path: h.path } : {},
37115
37924
  headerName: h.headerName
37116
37925
  });
37117
37926
  }
37118
37927
  const indexedDbBucket = {};
37119
37928
  for (const d of indexedDb) {
37120
- const values = await server2.readIndexedDb({
37929
+ const values = await server.readIndexedDb({
37121
37930
  database: d.database,
37122
37931
  store: d.store,
37123
37932
  keys: [...d.keys],
@@ -37134,7 +37943,7 @@ async function bootstrap(opts) {
37134
37943
  };
37135
37944
  } finally {
37136
37945
  try {
37137
- await server2.close();
37946
+ await server.close();
37138
37947
  } catch {
37139
37948
  }
37140
37949
  }
@@ -37152,13 +37961,7 @@ var BootstrapDisabledError = class extends Error {
37152
37961
 
37153
37962
  // src/config.ts
37154
37963
  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;
37964
+ return readEnvVar(key, { env });
37162
37965
  }
37163
37966
  function loadAccount(env = process.env) {
37164
37967
  const baseUrl = readVar(env, "IC_BASE_URL");
@@ -37197,7 +38000,7 @@ function loadAccount(env = process.env) {
37197
38000
  // package.json
37198
38001
  var package_default = {
37199
38002
  name: "infinitecampus-mcp",
37200
- version: "2.3.1",
38003
+ version: "2.3.3",
37201
38004
  mcpName: "io.github.chrischall/infinitecampus-mcp",
37202
38005
  description: "Infinite Campus (Campus Parent) MCP server \u2014 multi-district read + message/document write",
37203
38006
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -37239,10 +38042,12 @@ var package_default = {
37239
38042
  "test:watch": "vitest"
37240
38043
  },
37241
38044
  dependencies: {
37242
- "@fetchproxy/bootstrap": "^0.8.0",
37243
- "@fetchproxy/server": "^0.8.0",
38045
+ "@chrischall/mcp-utils": "^0.5.0",
38046
+ "@fetchproxy/bootstrap": "^1.0.0",
38047
+ "@fetchproxy/server": "^1.0.0",
37244
38048
  "@modelcontextprotocol/sdk": "^1.29.0",
37245
38049
  dotenv: "^17.4.2",
38050
+ "node-html-parser": "^7.1.0",
37246
38051
  zod: "^4.4.3"
37247
38052
  },
37248
38053
  devDependencies: {
@@ -37255,19 +38060,8 @@ var package_default = {
37255
38060
  };
37256
38061
 
37257
38062
  // 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
38063
  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());
38064
+ return parseBoolEnv("IC_DISABLE_FETCHPROXY", { default: false });
37271
38065
  }
37272
38066
  async function resolveAuth() {
37273
38067
  const account2 = loadAccount();
@@ -37438,6 +38232,20 @@ var ICClient = class {
37438
38232
  }
37439
38233
  }
37440
38234
  async login(account2) {
38235
+ const primaryName = this.linkedTo.get(account2.name);
38236
+ if (primaryName) {
38237
+ this.sessions.delete(account2.name);
38238
+ const primaryAccount = this.accounts.get(primaryName);
38239
+ await this.ensureSession(primaryAccount);
38240
+ if (!this.sessions.has(account2.name)) {
38241
+ throw new AuthFailedError(
38242
+ account2.name,
38243
+ "linked-district session could not be re-established via the primary district",
38244
+ { credentialHint: false }
38245
+ );
38246
+ }
38247
+ return;
38248
+ }
37441
38249
  if (!account2.username || !account2.password) {
37442
38250
  throw new AuthFailedError(
37443
38251
  account2.name,
@@ -37445,8 +38253,17 @@ var ICClient = class {
37445
38253
  );
37446
38254
  }
37447
38255
  const postRes = await fetch(
37448
- `${account2.baseUrl}/campus/verify.jsp?nonBrowser=true&username=${encodeURIComponent(account2.username)}&password=${encodeURIComponent(account2.password)}&appName=${encodeURIComponent(account2.district)}&portalLoginPage=parents`,
37449
- { method: "POST" }
38256
+ `${account2.baseUrl}/campus/verify.jsp?nonBrowser=true`,
38257
+ {
38258
+ method: "POST",
38259
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
38260
+ body: new URLSearchParams({
38261
+ username: account2.username,
38262
+ password: account2.password,
38263
+ appName: account2.district,
38264
+ portalLoginPage: "parents"
38265
+ }).toString()
38266
+ }
37450
38267
  );
37451
38268
  if (postRes.status >= 500) throw new PortalUnreachableError(account2.name, postRes.status);
37452
38269
  const body = await postRes.text();
@@ -37469,9 +38286,7 @@ var ICClient = class {
37469
38286
  session.cookie = cookies.cookieHeader;
37470
38287
  session.xsrfToken = cookies.xsrfToken;
37471
38288
  session.loggedInAt = Date.now();
37472
- if (!this.linkedTo.has(account2.name)) {
37473
- await this.discoverLinkedDistricts(account2);
37474
- }
38289
+ await this.discoverLinkedDistricts(account2);
37475
38290
  }
37476
38291
  async discoverLinkedDistricts(account2) {
37477
38292
  try {
@@ -37635,22 +38450,9 @@ var ICClient = class {
37635
38450
  };
37636
38451
  function parseSetCookies(headers) {
37637
38452
  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 };
38453
+ const entries = raw.length > 0 ? raw : splitFallback(headers.get("set-cookie"));
38454
+ const { cookies, cookieHeader } = parseCookieJar(entries);
38455
+ return { cookieHeader, xsrfToken: cookies["XSRF-TOKEN"] ?? "" };
37654
38456
  }
37655
38457
  function splitFallback(header) {
37656
38458
  if (!header) return [];
@@ -37667,11 +38469,16 @@ var UnknownDistrictError = class extends Error {
37667
38469
  available;
37668
38470
  };
37669
38471
  var AuthFailedError = class extends Error {
37670
- constructor(district, reason) {
38472
+ /**
38473
+ * @param opts.credentialHint When `false`, the message omits the
38474
+ * "Check IC_USERNAME and IC_PASSWORD" suffix — used for failures where the
38475
+ * credentials are known-good (e.g. a linked-district CUPS/SSO re-discovery
38476
+ * failure) and pointing the user at their creds would be misleading.
38477
+ */
38478
+ constructor(district, reason, opts) {
37671
38479
  const detail = reason ? ` (${reason})` : "";
37672
- super(
37673
- `Login failed for district '${district}'${detail}. Check IC_USERNAME and IC_PASSWORD; if those are correct, the account may be locked or the portal may be down.`
37674
- );
38480
+ const remedy = opts?.credentialHint === false ? "Sign in again at the IC portal in your browser, then restart the MCP." : "Check IC_USERNAME and IC_PASSWORD; if those are correct, the account may be locked or the portal may be down.";
38481
+ super(`Login failed for district '${district}'${detail}. ${remedy}`);
37675
38482
  this.district = district;
37676
38483
  this.reason = reason;
37677
38484
  this.name = "AuthFailedError";
@@ -37724,7 +38531,7 @@ var FileExistsError = class extends Error {
37724
38531
 
37725
38532
  // src/tools/_shared.ts
37726
38533
  function textContent(data) {
37727
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
38534
+ return textResult(data);
37728
38535
  }
37729
38536
  function is404(e) {
37730
38537
  return e instanceof Error && e.message.startsWith("IC 404 ");
@@ -37757,8 +38564,8 @@ function toArray(value) {
37757
38564
  }
37758
38565
 
37759
38566
  // src/tools/districts.ts
37760
- function registerDistrictTools(server2, client) {
37761
- server2.registerTool("ic_list_districts", {
38567
+ function registerDistrictTools(server, client) {
38568
+ server.registerTool("ic_list_districts", {
37762
38569
  description: "List Infinite Campus districts configured for this MCP server. Returns names + base URLs (no credentials).",
37763
38570
  annotations: { readOnlyHint: true }
37764
38571
  }, async () => {
@@ -37772,8 +38579,8 @@ function registerDistrictTools(server2, client) {
37772
38579
  var argsSchema = external_exports.object({
37773
38580
  district: external_exports.string().describe("District name from ic_list_districts")
37774
38581
  });
37775
- function registerStudentTools(server2, client) {
37776
- server2.registerTool("ic_list_students", {
38582
+ function registerStudentTools(server, client) {
38583
+ server.registerTool("ic_list_students", {
37777
38584
  description: "List students enrolled under the parent account for a given district.",
37778
38585
  annotations: { readOnlyHint: true },
37779
38586
  inputSchema: argsSchema.shape
@@ -37791,8 +38598,8 @@ var argsSchema2 = external_exports.object({
37791
38598
  date: external_exports.string().describe("YYYY-MM-DD; defaults to today").optional(),
37792
38599
  termFilter: external_exports.string().describe("Term name or ID; optional").optional()
37793
38600
  });
37794
- function registerScheduleTools(server2, client) {
37795
- server2.registerTool("ic_get_schedule", {
38601
+ function registerScheduleTools(server, client) {
38602
+ server.registerTool("ic_get_schedule", {
37796
38603
  description: "Get a student's class schedule for a given date (default: today).",
37797
38604
  annotations: { readOnlyHint: true },
37798
38605
  inputSchema: argsSchema2.shape
@@ -37813,8 +38620,8 @@ var argsSchema3 = external_exports.object({
37813
38620
  until: external_exports.string().describe("YYYY-MM-DD; filters dueDate <= until (client-side)").optional(),
37814
38621
  missingOnly: external_exports.boolean().describe("Only return assignments flagged missing by the teacher").optional()
37815
38622
  });
37816
- function registerAssignmentTools(server2, client) {
37817
- server2.registerTool("ic_list_assignments", {
38623
+ function registerAssignmentTools(server, client) {
38624
+ server.registerTool("ic_list_assignments", {
37818
38625
  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
38626
  annotations: { readOnlyHint: true },
37820
38627
  inputSchema: argsSchema3.shape
@@ -37848,8 +38655,8 @@ var argsSchema4 = external_exports.object({
37848
38655
  studentId: external_exports.string(),
37849
38656
  termId: external_exports.string().optional()
37850
38657
  });
37851
- function registerGradeTools(server2, client) {
37852
- server2.registerTool("ic_list_grades", {
38658
+ function registerGradeTools(server, client) {
38659
+ server.registerTool("ic_list_grades", {
37853
38660
  description: "List a student's term grades and in-progress course grades.",
37854
38661
  annotations: { readOnlyHint: true },
37855
38662
  inputSchema: argsSchema4.shape
@@ -37892,8 +38699,8 @@ function processList(list, since, until) {
37892
38699
  if (list === void 0) return list;
37893
38700
  return toArray(list).filter((e) => inRange(e.date, since, until)).map(trimEntry);
37894
38701
  }
37895
- function registerAttendanceTools(server2, client) {
37896
- server2.registerTool("ic_list_attendance", {
38702
+ function registerAttendanceTools(server, client) {
38703
+ server.registerTool("ic_list_attendance", {
37897
38704
  description: "List a student's absences and tardies (per-course summary grouped by term). Auto-resolves enrollmentID from the student record.",
37898
38705
  annotations: { readOnlyHint: true },
37899
38706
  inputSchema: argsSchema5.shape
@@ -37941,8 +38748,8 @@ var argsSchema6 = external_exports.object({
37941
38748
  since: external_exports.string().optional(),
37942
38749
  until: external_exports.string().optional()
37943
38750
  });
37944
- function registerBehaviorTools(server2, client) {
37945
- server2.registerTool("ic_list_behavior", {
38751
+ function registerBehaviorTools(server, client) {
38752
+ server.registerTool("ic_list_behavior", {
37946
38753
  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
38754
  annotations: { readOnlyHint: true },
37948
38755
  inputSchema: argsSchema6.shape
@@ -37973,8 +38780,8 @@ var argsSchema7 = external_exports.object({
37973
38780
  until: external_exports.string().optional()
37974
38781
  });
37975
38782
  var EMPTY_FOOD = { balance: null, transactions: [] };
37976
- function registerFoodServiceTools(server2, client) {
37977
- server2.registerTool("ic_list_food_service", {
38783
+ function registerFoodServiceTools(server, client) {
38784
+ server.registerTool("ic_list_food_service", {
37978
38785
  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
38786
  annotations: { readOnlyHint: true },
37980
38787
  inputSchema: argsSchema7.shape
@@ -38005,6 +38812,29 @@ function registerFoodServiceTools(server2, client) {
38005
38812
  });
38006
38813
  }
38007
38814
 
38815
+ // node_modules/@chrischall/mcp-utils/dist/html/index.js
38816
+ var NAMED_ENTITIES = {
38817
+ nbsp: " ",
38818
+ amp: "&",
38819
+ lt: "<",
38820
+ gt: ">",
38821
+ quot: '"',
38822
+ apos: "'"
38823
+ };
38824
+ function extractPlainTextFromHtml(html) {
38825
+ if (!html)
38826
+ return "";
38827
+ let text = html.replace(/<script[\s\S]*?<\/script>/gi, " ").replace(/<style[\s\S]*?<\/style>/gi, " ");
38828
+ text = text.replace(/<[^>]+>/g, " ");
38829
+ text = text.replace(/&#(\d+);/g, (_, d) => String.fromCodePoint(Number(d)));
38830
+ text = text.replace(/&#x([0-9a-fA-F]+);/g, (_, h) => String.fromCodePoint(parseInt(h, 16)));
38831
+ text = text.replace(/&([a-zA-Z]+);/g, (whole, name) => {
38832
+ const decoded = NAMED_ENTITIES[name.toLowerCase()];
38833
+ return decoded ?? whole;
38834
+ });
38835
+ return text.replace(/\s+/g, " ").trim();
38836
+ }
38837
+
38008
38838
  // src/tools/messages.ts
38009
38839
  var listArgs = external_exports.object({
38010
38840
  district: external_exports.string(),
@@ -38051,10 +38881,7 @@ function parseMessageHtml(html, url2) {
38051
38881
  const titleMatch = html.match(/<title>([^<]*)<\/title>/i);
38052
38882
  let subject = titleMatch ? titleMatch[1].trim() : "";
38053
38883
  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();
38884
+ const text = extractPlainTextFromHtml(html);
38058
38885
  const dateMatch = text.match(/Date:\s*(\d{1,2}\/\d{1,2}\/\d{2,4})/);
38059
38886
  const date5 = dateMatch ? dateMatch[1] : null;
38060
38887
  let body = text;
@@ -38064,8 +38891,8 @@ function parseMessageHtml(html, url2) {
38064
38891
  }
38065
38892
  return { subject, date: date5, body, url: url2 };
38066
38893
  }
38067
- function registerMessageTools(server2, client) {
38068
- server2.registerTool("ic_list_messages", {
38894
+ function registerMessageTools(server, client) {
38895
+ server.registerTool("ic_list_messages", {
38069
38896
  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
38897
  annotations: { readOnlyHint: true },
38071
38898
  inputSchema: listArgs.shape
@@ -38103,7 +38930,7 @@ function registerMessageTools(server2, client) {
38103
38930
  const [notifications, inbox, announcements] = await Promise.all([prismPromise, inboxPromise, noticePromise]);
38104
38931
  return textContent({ notifications, inbox, announcements });
38105
38932
  });
38106
- server2.registerTool("ic_get_message", {
38933
+ server.registerTool("ic_get_message", {
38107
38934
  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
38935
  annotations: { readOnlyHint: true },
38109
38936
  inputSchema: getArgs.shape
@@ -38127,8 +38954,8 @@ var downloadArgs = external_exports.object({
38127
38954
  destinationPath: external_exports.string().describe("Absolute path where the PDF should be written"),
38128
38955
  overwrite: external_exports.boolean().optional()
38129
38956
  });
38130
- function registerDocumentTools(server2, client) {
38131
- server2.registerTool("ic_list_documents", {
38957
+ function registerDocumentTools(server, client) {
38958
+ server.registerTool("ic_list_documents", {
38132
38959
  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
38960
  annotations: { readOnlyHint: true },
38134
38961
  inputSchema: listArgs2.shape
@@ -38158,7 +38985,7 @@ function registerDocumentTools(server2, client) {
38158
38985
  throw e;
38159
38986
  }
38160
38987
  });
38161
- server2.registerTool("ic_download_document", {
38988
+ server.registerTool("ic_download_document", {
38162
38989
  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
38990
  annotations: { destructiveHint: true },
38164
38991
  inputSchema: downloadArgs.shape
@@ -38185,8 +39012,8 @@ var argsSchema8 = external_exports.object({
38185
39012
  since: external_exports.string().describe("YYYY-MM-DD; include only days on or after this date").optional(),
38186
39013
  until: external_exports.string().describe("YYYY-MM-DD; include only days on or before this date").optional()
38187
39014
  });
38188
- function registerCalendarTools(server2, client) {
38189
- server2.registerTool("ic_list_school_days", {
39015
+ function registerCalendarTools(server, client) {
39016
+ server.registerTool("ic_list_school_days", {
38190
39017
  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
39018
  annotations: { readOnlyHint: true },
38192
39019
  inputSchema: argsSchema8.shape
@@ -38281,8 +39108,8 @@ function trimEvent(e) {
38281
39108
  }
38282
39109
  return out;
38283
39110
  }
38284
- function registerAttendanceEventsTools(server2, client) {
38285
- server2.registerTool("ic_list_attendance_events", {
39111
+ function registerAttendanceEventsTools(server, client) {
39112
+ server.registerTool("ic_list_attendance_events", {
38286
39113
  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
39114
  annotations: { readOnlyHint: true },
38288
39115
  inputSchema: argsSchema9.shape
@@ -38353,8 +39180,8 @@ function defaultSinceDate(now) {
38353
39180
  d.setUTCDate(d.getUTCDate() - 14);
38354
39181
  return d.toISOString().substring(0, 10);
38355
39182
  }
38356
- function registerRecentGradesTools(server2, client) {
38357
- server2.registerTool("ic_list_recent_grades", {
39183
+ function registerRecentGradesTools(server, client) {
39184
+ server.registerTool("ic_list_recent_grades", {
38358
39185
  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
39186
  annotations: { readOnlyHint: true },
38360
39187
  inputSchema: argsSchema10.shape
@@ -38404,8 +39231,8 @@ function trimRecord(raw) {
38404
39231
  }
38405
39232
  return out;
38406
39233
  }
38407
- function registerTeacherTools(server2, client) {
38408
- server2.registerTool("ic_list_teachers", {
39234
+ function registerTeacherTools(server, client) {
39235
+ server.registerTool("ic_list_teachers", {
38409
39236
  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
39237
  annotations: { readOnlyHint: true },
38411
39238
  inputSchema: argsSchema11.shape
@@ -38438,8 +39265,8 @@ var argsSchema12 = external_exports.object({
38438
39265
  district: external_exports.string(),
38439
39266
  studentId: external_exports.string().describe("Student personID from ic_list_students")
38440
39267
  });
38441
- function registerAssessmentTools(server2, client) {
38442
- server2.registerTool("ic_list_assessments", {
39268
+ function registerAssessmentTools(server, client) {
39269
+ server.registerTool("ic_list_assessments", {
38443
39270
  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
39271
  annotations: { readOnlyHint: true },
38445
39272
  inputSchema: argsSchema12.shape
@@ -38489,8 +39316,8 @@ var argsSchema13 = external_exports.object({
38489
39316
  district: external_exports.string(),
38490
39317
  studentId: external_exports.string().describe("Student personID from ic_list_students")
38491
39318
  });
38492
- function registerFeeTools(server2, client) {
38493
- server2.registerTool("ic_list_fees", {
39319
+ function registerFeeTools(server, client) {
39320
+ server.registerTool("ic_list_fees", {
38494
39321
  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
39322
  annotations: { readOnlyHint: true },
38496
39323
  inputSchema: argsSchema13.shape
@@ -38533,8 +39360,8 @@ var argsSchema14 = external_exports.object({
38533
39360
  district: external_exports.string(),
38534
39361
  studentId: external_exports.string().describe("Student personID from ic_list_students")
38535
39362
  });
38536
- function registerFeaturesTools(server2, client) {
38537
- server2.registerTool("ic_get_features", {
39363
+ function registerFeaturesTools(server, client) {
39364
+ server.registerTool("ic_get_features", {
38538
39365
  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
39366
  annotations: { readOnlyHint: true },
38540
39367
  inputSchema: argsSchema14.shape
@@ -38557,12 +39384,11 @@ function registerFeaturesTools(server2, client) {
38557
39384
  }
38558
39385
 
38559
39386
  // 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
- }
39387
+ await loadDotenvSafely({
39388
+ path: join2(dirname2(fileURLToPath(import.meta.url)), "..", ".env"),
39389
+ override: false
39390
+ });
39391
+ var AI_NOTICE = "[infinitecampus-mcp] Developed and maintained by AI (Claude). Use at your own discretion.";
38566
39392
  var account = null;
38567
39393
  var preloaded;
38568
39394
  var source;
@@ -38575,32 +39401,39 @@ try {
38575
39401
  } catch (e) {
38576
39402
  configError = e;
38577
39403
  }
38578
- var server = new McpServer({ name: "infinitecampus", version: "2.3.1" });
39404
+ var COMMON = {
39405
+ name: "infinitecampus",
39406
+ version: "2.3.3"
39407
+ // x-release-please-version
39408
+ };
38579
39409
  if (account) {
38580
39410
  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);
39411
+ const tools = [
39412
+ registerDistrictTools,
39413
+ registerStudentTools,
39414
+ registerScheduleTools,
39415
+ registerAssignmentTools,
39416
+ registerGradeTools,
39417
+ registerAttendanceTools,
39418
+ registerBehaviorTools,
39419
+ registerFoodServiceTools,
39420
+ registerMessageTools,
39421
+ registerDocumentTools,
39422
+ registerCalendarTools,
39423
+ registerAttendanceEventsTools,
39424
+ registerRecentGradesTools,
39425
+ registerTeacherTools,
39426
+ registerAssessmentTools,
39427
+ registerFeeTools,
39428
+ registerFeaturesTools
39429
+ ];
38598
39430
  const suffix = source === "fetchproxy" ? " [via fetchproxy]" : "";
38599
- console.error(`[infinitecampus-mcp] District: ${account.name} (${account.baseUrl})${suffix}`);
39431
+ const districtLine = `[infinitecampus-mcp] District: ${account.name} (${account.baseUrl})${suffix}`;
39432
+ await runMcp({ ...COMMON, deps: client, tools, banner: `${districtLine}
39433
+ ${AI_NOTICE}` });
38600
39434
  } 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.");
39435
+ const notConfigured = `[infinitecampus-mcp] Not configured: ${configError?.message ?? "unknown error"}
39436
+ [infinitecampus-mcp] Server is running with no tools registered. Set the required env vars and reinstall.`;
39437
+ await runMcp({ ...COMMON, tools: [], banner: `${notConfigured}
39438
+ ${AI_NOTICE}` });
38603
39439
  }
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);