infinitecampus-mcp 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -7,7 +7,7 @@
7
7
  },
8
8
  "metadata": {
9
9
  "description": "MCP server for Infinite Campus (Campus Parent) — grades, attendance, assignments, messages, documents",
10
- "version": "3.0.0"
10
+ "version": "3.1.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "Infinite Campus",
16
16
  "source": "./",
17
17
  "description": "Infinite Campus (Campus Parent) MCP server for Claude — grades, attendance, assignments, messages, and documents via natural language",
18
- "version": "3.0.0",
18
+ "version": "3.1.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "infinitecampus-mcp",
3
3
  "displayName": "Infinite Campus",
4
- "version": "3.0.0",
4
+ "version": "3.1.0",
5
5
  "description": "Infinite Campus (Campus Parent) MCP server for Claude — grades, attendance, assignments, messages, and documents",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/bundle.js CHANGED
@@ -28157,6 +28157,7 @@ function missingClientCapabilities(required2, declared) {
28157
28157
  return Object.keys(missing).length > 0 ? missing : void 0;
28158
28158
  }
28159
28159
  var FIRST_MODERN_PROTOCOL_VERSION = "2026-07-28";
28160
+ var SUPPORTED_MODERN_PROTOCOL_VERSIONS = [FIRST_MODERN_PROTOCOL_VERSION];
28160
28161
  function isModernProtocolVersion(version2) {
28161
28162
  return version2 >= FIRST_MODERN_PROTOCOL_VERSION;
28162
28163
  }
@@ -30715,6 +30716,25 @@ function isSpecNotificationMethod(method) {
30715
30716
  return ALL_CODECS.some((codec2) => codec2.hasNotificationMethod(method));
30716
30717
  }
30717
30718
  var ALL_CODECS = [rev2025Codec, rev2026Codec];
30719
+ function isPlainObject$3(value) {
30720
+ return value !== null && typeof value === "object" && !Array.isArray(value);
30721
+ }
30722
+ function requestMetaOf(params) {
30723
+ if (!isPlainObject$3(params)) return void 0;
30724
+ const meta3 = params["_meta"];
30725
+ return isPlainObject$3(meta3) ? meta3 : void 0;
30726
+ }
30727
+ function hasEnvelopeClaim(params) {
30728
+ const meta3 = requestMetaOf(params);
30729
+ return meta3 !== void 0 && PROTOCOL_VERSION_META_KEY in meta3;
30730
+ }
30731
+ function envelopeClaimVersion(params) {
30732
+ const value = requestMetaOf(params)?.[PROTOCOL_VERSION_META_KEY];
30733
+ return typeof value === "string" ? value : void 0;
30734
+ }
30735
+ function validateEnvelopeMeta(meta3) {
30736
+ return codecForVersion(MODERN_WIRE_REVISION).validateEnvelopeMeta(meta3);
30737
+ }
30718
30738
  var schemas_exports = /* @__PURE__ */ __exportAll({
30719
30739
  AnnotationsSchema: () => AnnotationsSchema,
30720
30740
  AudioContentSchema: () => AudioContentSchema,
@@ -31065,6 +31085,45 @@ var LADDER_ERROR_HTTP_STATUS = {
31065
31085
  [ProtocolErrorCode.MissingRequiredClientCapability]: 400,
31066
31086
  [HEADER_MISMATCH_ERROR_CODE]: 400
31067
31087
  };
31088
+ function rejection(rung, cell, httpStatus, error62, settled) {
31089
+ return {
31090
+ kind: "reject",
31091
+ rung,
31092
+ cell,
31093
+ httpStatus,
31094
+ code: error62.code,
31095
+ message: error62.message,
31096
+ ...error62.data !== void 0 && { data: error62.data },
31097
+ settled
31098
+ };
31099
+ }
31100
+ function carriesValidModernEnvelopeClaim(params) {
31101
+ if (!hasEnvelopeClaim(params)) return false;
31102
+ const claimedVersion = envelopeClaimVersion(params);
31103
+ if (claimedVersion === void 0 || !isModernProtocolVersion(claimedVersion)) return false;
31104
+ const meta3 = requestMetaOf(params);
31105
+ return meta3 !== void 0 && validateEnvelopeMeta(meta3).length === 0;
31106
+ }
31107
+ function modernOnlyStrictRejection(route, supportedVersions) {
31108
+ switch (route.reason) {
31109
+ case "http-method":
31110
+ return rejection("http-method", "modern-only-method-not-allowed", 405, new ProtocolError(-32e3, "Method not allowed."), true);
31111
+ case "batch":
31112
+ return rejection("jsonrpc-shape", "modern-only-batch-not-supported", 400, new ProtocolError(ProtocolErrorCode.InvalidRequest, "Bad Request: JSON-RPC batches are not supported by this endpoint"), true);
31113
+ case "response":
31114
+ return rejection("jsonrpc-shape", "modern-only-response-post", 400, new ProtocolError(ProtocolErrorCode.InvalidRequest, "Bad Request: JSON-RPC responses cannot be posted to this endpoint"), true);
31115
+ case "notification":
31116
+ return;
31117
+ case "initialize":
31118
+ case "no-claim": {
31119
+ const requested = route.requestedVersion;
31120
+ return rejection("era-classification", "modern-only-missing-envelope", 400, requested === void 0 ? new ProtocolError(ProtocolErrorCode.UnsupportedProtocolVersion, "Unsupported protocol version: the request did not name a protocol version", { supported: [...supportedVersions] }) : new UnsupportedProtocolVersionError({
31121
+ supported: [...supportedVersions],
31122
+ requested
31123
+ }), true);
31124
+ }
31125
+ }
31126
+ }
31068
31127
  function parseSchema(schema, data) {
31069
31128
  return z2.safeParse(schema, data);
31070
31129
  }
@@ -31626,6 +31685,9 @@ function withRequestStateValue(ctx, value) {
31626
31685
  };
31627
31686
  }
31628
31687
  var writeNegotiatedProtocolVersion;
31688
+ function setNegotiatedProtocolVersion(instance, version2) {
31689
+ writeNegotiatedProtocolVersion(instance, version2);
31690
+ }
31629
31691
  var Protocol = class {
31630
31692
  _transport;
31631
31693
  _requestMessageId = 0;
@@ -39569,6 +39631,190 @@ function getCompleter(schema) {
39569
39631
  return schema[COMPLETABLE_SYMBOL]?.complete;
39570
39632
  }
39571
39633
  var MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
39634
+ function listenFilterAccepts(filter, event) {
39635
+ switch (event.kind) {
39636
+ case "tools_list_changed":
39637
+ return filter.toolsListChanged === true;
39638
+ case "prompts_list_changed":
39639
+ return filter.promptsListChanged === true;
39640
+ case "resources_list_changed":
39641
+ return filter.resourcesListChanged === true;
39642
+ case "resource_updated":
39643
+ return filter.resourceSubscriptions !== void 0 && filter.resourceSubscriptions.includes(event.uri);
39644
+ }
39645
+ }
39646
+ function honoredSubset(requested, capabilities) {
39647
+ const honored = {};
39648
+ const allow = (bit) => capabilities === void 0 || bit === true;
39649
+ if (requested.toolsListChanged === true && allow(capabilities?.tools?.listChanged)) honored.toolsListChanged = true;
39650
+ if (requested.promptsListChanged === true && allow(capabilities?.prompts?.listChanged)) honored.promptsListChanged = true;
39651
+ if (requested.resourcesListChanged === true && allow(capabilities?.resources?.listChanged)) honored.resourcesListChanged = true;
39652
+ if (requested.resourceSubscriptions !== void 0 && requested.resourceSubscriptions.length > 0 && allow(capabilities?.resources?.subscribe)) honored.resourceSubscriptions = [...requested.resourceSubscriptions];
39653
+ return honored;
39654
+ }
39655
+ var DEFAULT_MAX_SUBSCRIPTIONS = 1024;
39656
+ function stampSubscriptionId(notification, subscriptionId) {
39657
+ return {
39658
+ method: notification.method,
39659
+ params: {
39660
+ ...notification.params,
39661
+ _meta: {
39662
+ ...notification.params?._meta,
39663
+ [SUBSCRIPTION_ID_META_KEY]: subscriptionId
39664
+ }
39665
+ }
39666
+ };
39667
+ }
39668
+ function parseListenFilter(message) {
39669
+ const outcome = codecForVersion(MODERN_WIRE_REVISION).validateRequest("subscriptions/listen", message);
39670
+ return outcome.ok ? outcome.value.params?.notifications : void 0;
39671
+ }
39672
+ var CHANGE_NOTIFICATION_METHODS = /* @__PURE__ */ new Set([
39673
+ "notifications/tools/list_changed",
39674
+ "notifications/prompts/list_changed",
39675
+ "notifications/resources/list_changed",
39676
+ "notifications/resources/updated"
39677
+ ]);
39678
+ var StdioListenRouter = class {
39679
+ /** Active subscriptions, keyed by the listen request's JSON-RPC id verbatim. */
39680
+ _subs = /* @__PURE__ */ new Map();
39681
+ /**
39682
+ * The serving instance's declared capabilities. Filled in by the entry
39683
+ * once the modern instance is constructed (the router is created before
39684
+ * the instance exists), so the acknowledged filter is narrowed against
39685
+ * what the server can actually deliver.
39686
+ */
39687
+ _serverCapabilities;
39688
+ /**
39689
+ * The serving instance's identity, stamped onto the graceful-close
39690
+ * results' `_meta` (the spec's `SubscriptionsListenResultMeta` extends
39691
+ * `ResultMetaObject`). Handed over together with the capabilities.
39692
+ */
39693
+ _serverInfo;
39694
+ constructor(_maxSubscriptions = DEFAULT_MAX_SUBSCRIPTIONS, serverCapabilities, serverInfo) {
39695
+ this._maxSubscriptions = _maxSubscriptions;
39696
+ this._serverCapabilities = serverCapabilities;
39697
+ this._serverInfo = serverInfo;
39698
+ }
39699
+ /**
39700
+ * Record the serving instance's declared capabilities and identity once
39701
+ * it has been constructed. Called by `serveStdio`'s connect path;
39702
+ * subsequent `serve()` calls narrow the honored filter against the
39703
+ * capabilities, and `teardownAll()` stamps the identity.
39704
+ */
39705
+ setServerCapabilities(capabilities, serverInfo) {
39706
+ this._serverCapabilities = capabilities;
39707
+ if (serverInfo !== void 0) this._serverInfo = serverInfo;
39708
+ }
39709
+ /** Whether `id` is an active listen subscription on this connection. */
39710
+ has(id) {
39711
+ return this._subs.has(id);
39712
+ }
39713
+ /**
39714
+ * Serve one inbound `subscriptions/listen` request: registers the
39715
+ * subscription and returns the stamped acknowledged notification (or, on
39716
+ * capacity / params rejection, the in-band JSON-RPC error response).
39717
+ *
39718
+ * @throws when called before {@linkcode setServerCapabilities} (or the
39719
+ * constructor) has supplied the serving instance's capabilities. Honoring a
39720
+ * filter without knowing the server's advertised capabilities would fail
39721
+ * open (deliver unadvertised types); the entry guarantees capabilities are
39722
+ * set before any listen request is routed here.
39723
+ */
39724
+ serve(message) {
39725
+ if (this._serverCapabilities === void 0) throw new Error("StdioListenRouter.serve() called before setServerCapabilities(); refusing to honor a filter without capabilities");
39726
+ if (this._subs.size >= this._maxSubscriptions) return {
39727
+ jsonrpc: "2.0",
39728
+ id: message.id,
39729
+ error: {
39730
+ code: -32603,
39731
+ message: "Subscription limit reached"
39732
+ }
39733
+ };
39734
+ const filter = parseListenFilter(message);
39735
+ if (filter === void 0) return {
39736
+ jsonrpc: "2.0",
39737
+ id: message.id,
39738
+ error: {
39739
+ code: -32602,
39740
+ message: "Invalid params: 'notifications' is required and must be a valid SubscriptionFilter"
39741
+ }
39742
+ };
39743
+ const honored = honoredSubset(filter, this._serverCapabilities);
39744
+ this._subs.set(message.id, honored);
39745
+ return stampSubscriptionId({
39746
+ method: "notifications/subscriptions/acknowledged",
39747
+ params: { notifications: honored }
39748
+ }, message.id);
39749
+ }
39750
+ /**
39751
+ * Tear down one subscription (inbound `notifications/cancelled`). Returns
39752
+ * `true` when a subscription was removed. After this call NOTHING further
39753
+ * is delivered for that subscription id (the post-cancel hardening).
39754
+ */
39755
+ cancel(id) {
39756
+ return this._subs.delete(id);
39757
+ }
39758
+ /**
39759
+ * Route an outbound notification through the active subscriptions.
39760
+ *
39761
+ * - For a subscription-gated change notification, returns one stamped copy
39762
+ * per subscription that opted in to it (an empty array means it is
39763
+ * dropped — the modern era never delivers an un-requested change type).
39764
+ * - For any other outbound message, returns `'passthrough'` (the entry
39765
+ * forwards it as-is).
39766
+ */
39767
+ routeOutbound(message) {
39768
+ if (!CHANGE_NOTIFICATION_METHODS.has(message.method)) return "passthrough";
39769
+ const uriParam = message.params?.["uri"];
39770
+ const uri = typeof uriParam === "string" ? uriParam : void 0;
39771
+ const event = notificationToServerEvent(message.method, uri);
39772
+ const out = [];
39773
+ for (const [subscriptionId, filter] of this._subs) if (listenFilterAccepts(filter, event)) out.push(stampSubscriptionId({
39774
+ method: message.method,
39775
+ params: message.params ?? {}
39776
+ }, subscriptionId));
39777
+ return out;
39778
+ }
39779
+ /**
39780
+ * Server-side graceful teardown of every active subscription: returns the
39781
+ * empty `subscriptions/listen` JSON-RPC result for each subscription id —
39782
+ * the spec's graceful-close signal, `_meta` carrying the subscription id
39783
+ * and the serving instance's identity — for the entry to emit before
39784
+ * closing the wire. Clears the set so nothing further is delivered.
39785
+ */
39786
+ teardownAll() {
39787
+ const out = [];
39788
+ for (const id of this._subs.keys()) out.push({
39789
+ jsonrpc: "2.0",
39790
+ id,
39791
+ result: {
39792
+ resultType: "complete",
39793
+ _meta: {
39794
+ [SUBSCRIPTION_ID_META_KEY]: id,
39795
+ ...this._serverInfo !== void 0 && { [SERVER_INFO_META_KEY]: this._serverInfo }
39796
+ }
39797
+ }
39798
+ });
39799
+ this._subs.clear();
39800
+ return out;
39801
+ }
39802
+ };
39803
+ function notificationToServerEvent(method, uri) {
39804
+ switch (method) {
39805
+ case "notifications/tools/list_changed":
39806
+ return { kind: "tools_list_changed" };
39807
+ case "notifications/prompts/list_changed":
39808
+ return { kind: "prompts_list_changed" };
39809
+ case "notifications/resources/list_changed":
39810
+ return { kind: "resources_list_changed" };
39811
+ default:
39812
+ return {
39813
+ kind: "resource_updated",
39814
+ uri: uri ?? ""
39815
+ };
39816
+ }
39817
+ }
39572
39818
  var DEFAULT_LEGACY_SHIM_MAX_ROUNDS = 8;
39573
39819
  var DEFAULT_LEGACY_SHIM_ROUND_TIMEOUT_MS = 6e5;
39574
39820
  function resolveLegacyShimOptions(options) {
@@ -39706,6 +39952,12 @@ var INPUT_REQUIRED_CAPABLE_METHODS = /* @__PURE__ */ new Set([
39706
39952
  var writeClientIdentity;
39707
39953
  var installDiscoverHandler;
39708
39954
  var readServerIdentity;
39955
+ function installModernOnlyHandlers(server, servedModernVersions) {
39956
+ installDiscoverHandler(server, servedModernVersions);
39957
+ }
39958
+ function serverIdentityOf(server) {
39959
+ return readServerIdentity(server);
39960
+ }
39709
39961
  var Server = class extends Protocol {
39710
39962
  _clientCapabilities;
39711
39963
  _clientVersion;
@@ -40980,6 +41232,400 @@ var StdioServerTransport = class {
40980
41232
  });
40981
41233
  }
40982
41234
  };
41235
+ var DISCARD_ANSWER_TIMEOUT_MS = 3e3;
41236
+ var StdioConnectionChannel = class {
41237
+ onclose;
41238
+ onerror;
41239
+ onmessage;
41240
+ _closed = false;
41241
+ /** Request ids the entry delivered to the instance that the instance has not yet answered. */
41242
+ _pendingRequests = /* @__PURE__ */ new Set();
41243
+ _drainWaiters = [];
41244
+ constructor(_wire, _onInstanceClose, _outboundIntercept) {
41245
+ this._wire = _wire;
41246
+ this._onInstanceClose = _onInstanceClose;
41247
+ this._outboundIntercept = _outboundIntercept;
41248
+ }
41249
+ async start() {
41250
+ }
41251
+ async send(message, options) {
41252
+ if (isJSONRPCResultResponse(message) || isJSONRPCErrorResponse(message)) {
41253
+ const { id } = message;
41254
+ if (id !== void 0) this._settle(id);
41255
+ }
41256
+ if (this._closed) return;
41257
+ if (this._outboundIntercept?.(message) === "handled") return;
41258
+ return this._wire.send(message, options);
41259
+ }
41260
+ setProtocolVersion = (version2) => {
41261
+ this._wire.setProtocolVersion?.(version2);
41262
+ };
41263
+ /** Forwards one inbound message to the connected instance. */
41264
+ deliver(message, extra) {
41265
+ if (this._closed) return;
41266
+ if (isJSONRPCRequest(message)) this._pendingRequests.add(message.id);
41267
+ else if (isJSONRPCNotification(message) && message.method === "notifications/cancelled") {
41268
+ const cancelledId = message.params?.requestId;
41269
+ if (cancelledId !== void 0) this._settle(cancelledId);
41270
+ }
41271
+ this.onmessage?.(message, extra);
41272
+ }
41273
+ /**
41274
+ * Resolves once every request delivered to the instance has been answered
41275
+ * through {@linkcode send}, settled by a delivered cancellation, or the
41276
+ * channel has been closed and nothing further can be answered. The wait is
41277
+ * bounded by `timeoutMs` as a backstop so no edge can hold the caller
41278
+ * indefinitely; resolves `false` only when the bound elapsed with requests
41279
+ * still unanswered. Used by the probe-discard path so a probe request the
41280
+ * entry accepted is never silently dropped.
41281
+ */
41282
+ async whenRequestsAnswered(timeoutMs) {
41283
+ if (this._closed || this._pendingRequests.size === 0) return true;
41284
+ return await new Promise((resolve2) => {
41285
+ const waiter = () => {
41286
+ clearTimeout(timer);
41287
+ resolve2(true);
41288
+ };
41289
+ const timer = setTimeout(() => {
41290
+ this._drainWaiters = this._drainWaiters.filter((pending) => pending !== waiter);
41291
+ resolve2(false);
41292
+ }, timeoutMs);
41293
+ this._drainWaiters.push(waiter);
41294
+ });
41295
+ }
41296
+ async close() {
41297
+ if (this._closed) return;
41298
+ this._closed = true;
41299
+ this._pendingRequests.clear();
41300
+ this._releaseDrainWaiters();
41301
+ try {
41302
+ this._onInstanceClose();
41303
+ } finally {
41304
+ this.onclose?.();
41305
+ }
41306
+ }
41307
+ _settle(id) {
41308
+ this._pendingRequests.delete(id);
41309
+ if (this._pendingRequests.size === 0) this._releaseDrainWaiters();
41310
+ }
41311
+ _releaseDrainWaiters() {
41312
+ const waiters = this._drainWaiters;
41313
+ this._drainWaiters = [];
41314
+ for (const waiter of waiters) waiter();
41315
+ }
41316
+ };
41317
+ function classifyOpeningMessage(message) {
41318
+ const params = message.params;
41319
+ if (message.method === "initialize" && !carriesValidModernEnvelopeClaim(params)) {
41320
+ const requestedVersion = params !== null && typeof params === "object" && typeof params.protocolVersion === "string" ? params.protocolVersion : void 0;
41321
+ return {
41322
+ kind: "legacy",
41323
+ reason: "initialize",
41324
+ ...requestedVersion !== void 0 && { requestedVersion }
41325
+ };
41326
+ }
41327
+ if (!hasEnvelopeClaim(params)) return {
41328
+ kind: "legacy",
41329
+ reason: "no-claim"
41330
+ };
41331
+ const meta3 = requestMetaOf(params);
41332
+ const firstIssue = (meta3 === void 0 ? [] : validateEnvelopeMeta(meta3))[0];
41333
+ if (firstIssue !== void 0) return {
41334
+ kind: "invalid-envelope",
41335
+ issue: firstIssue
41336
+ };
41337
+ const claimedVersion = envelopeClaimVersion(params);
41338
+ if (claimedVersion === void 0 || !SUPPORTED_MODERN_PROTOCOL_VERSIONS.includes(claimedVersion)) return {
41339
+ kind: "unsupported-revision",
41340
+ requested: claimedVersion ?? "unknown"
41341
+ };
41342
+ return {
41343
+ kind: "modern",
41344
+ revision: claimedVersion,
41345
+ classification: {
41346
+ era: "modern",
41347
+ revision: claimedVersion
41348
+ }
41349
+ };
41350
+ }
41351
+ function serveStdio(factory, options = {}) {
41352
+ const legacyMode = options.legacy ?? "serve";
41353
+ const wire = options.transport ?? new StdioServerTransport();
41354
+ let state = { phase: "opening" };
41355
+ let discarding;
41356
+ let closing = false;
41357
+ const isTornDown = () => closing || state.phase === "closed";
41358
+ const reportError = (error62) => {
41359
+ try {
41360
+ options.onerror?.(error62);
41361
+ } catch {
41362
+ }
41363
+ };
41364
+ const writeErrorResponse = (id, code, message, data) => wire.send({
41365
+ jsonrpc: "2.0",
41366
+ id,
41367
+ error: {
41368
+ code,
41369
+ message,
41370
+ ...data !== void 0 && { data }
41371
+ }
41372
+ }).catch((error62) => reportError(toError(error62)));
41373
+ const listenRouter = new StdioListenRouter(options.maxSubscriptions ?? DEFAULT_MAX_SUBSCRIPTIONS);
41374
+ const modernOutboundIntercept = (message) => {
41375
+ if (!isJSONRPCNotification(message)) return void 0;
41376
+ const routed = listenRouter.routeOutbound(message);
41377
+ if (routed === "passthrough") return void 0;
41378
+ for (const stamped of routed) wire.send({
41379
+ jsonrpc: "2.0",
41380
+ ...stamped
41381
+ }).catch((error62) => reportError(toError(error62)));
41382
+ return "handled";
41383
+ };
41384
+ const tryServeListen = async (message) => {
41385
+ if (isJSONRPCRequest(message) && message.method === "subscriptions/listen") {
41386
+ const meta3 = requestMetaOf(message.params);
41387
+ const issue2 = hasEnvelopeClaim(message.params) ? (meta3 === void 0 ? [] : validateEnvelopeMeta(meta3))[0] : {
41388
+ key: "_meta",
41389
+ problem: "the per-request envelope is required on protocol revision 2026-07-28"
41390
+ };
41391
+ const claimedVersion = envelopeClaimVersion(message.params);
41392
+ let reply;
41393
+ if (issue2 !== void 0) reply = {
41394
+ jsonrpc: "2.0",
41395
+ id: message.id,
41396
+ error: {
41397
+ code: -32602,
41398
+ message: `Invalid _meta envelope: ${issue2.key}: ${issue2.problem}`
41399
+ }
41400
+ };
41401
+ else if (claimedVersion === void 0 || !SUPPORTED_MODERN_PROTOCOL_VERSIONS.includes(claimedVersion)) {
41402
+ const error62 = new UnsupportedProtocolVersionError({
41403
+ supported: [...SUPPORTED_MODERN_PROTOCOL_VERSIONS],
41404
+ requested: claimedVersion ?? "unknown"
41405
+ });
41406
+ reply = {
41407
+ jsonrpc: "2.0",
41408
+ id: message.id,
41409
+ error: {
41410
+ code: error62.code,
41411
+ message: error62.message,
41412
+ data: error62.data
41413
+ }
41414
+ };
41415
+ } else reply = listenRouter.serve(message);
41416
+ await wire.send("error" in reply ? reply : {
41417
+ jsonrpc: "2.0",
41418
+ method: reply.method,
41419
+ params: reply.params
41420
+ }).catch((error62) => reportError(toError(error62)));
41421
+ return true;
41422
+ }
41423
+ if (isJSONRPCNotification(message) && message.method === "notifications/cancelled") {
41424
+ const cancelledId = message.params?.requestId;
41425
+ if (cancelledId !== void 0 && listenRouter.cancel(cancelledId)) return true;
41426
+ }
41427
+ return false;
41428
+ };
41429
+ const answerLegacyRejection = (request, reason, requestedVersion) => {
41430
+ const rejection2 = modernOnlyStrictRejection({
41431
+ kind: "legacy",
41432
+ reason,
41433
+ ...requestedVersion !== void 0 && { requestedVersion }
41434
+ }, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
41435
+ if (rejection2 === void 0) return Promise.resolve();
41436
+ reportError(/* @__PURE__ */ new Error(`Rejected 2025-era request on a modern-only stdio connection (${rejection2.cell}): ${rejection2.message}`));
41437
+ return writeErrorResponse(request.id, rejection2.code, rejection2.message, rejection2.data);
41438
+ };
41439
+ const onInstanceClosed = (channel) => {
41440
+ if (closing || channel === discarding) return;
41441
+ closeAll();
41442
+ };
41443
+ const connectInstance = async (era, revision) => {
41444
+ const product = await factory({ era });
41445
+ const server = product instanceof McpServer ? product.server : product;
41446
+ if (era === "modern") {
41447
+ setNegotiatedProtocolVersion(server, revision);
41448
+ installModernOnlyHandlers(server, SUPPORTED_MODERN_PROTOCOL_VERSIONS);
41449
+ listenRouter.setServerCapabilities(server.getCapabilities(), serverIdentityOf(server));
41450
+ }
41451
+ const channel = new StdioConnectionChannel(wire, () => onInstanceClosed(channel), era === "modern" ? modernOutboundIntercept : void 0);
41452
+ await product.connect(channel);
41453
+ return {
41454
+ product,
41455
+ channel
41456
+ };
41457
+ };
41458
+ const disposeLateInstance = (instance) => instance.product.close().catch((error62) => reportError(toError(error62)));
41459
+ const discardProbeInstance = async (instance) => {
41460
+ discarding = instance.channel;
41461
+ try {
41462
+ if (!await instance.channel.whenRequestsAnswered(DISCARD_ANSWER_TIMEOUT_MS)) reportError(/* @__PURE__ */ new Error(`Discarded the probe instance with requests still unanswered after ${DISCARD_ANSWER_TIMEOUT_MS}ms; continuing with the fallback`));
41463
+ await instance.product.close();
41464
+ } catch (error62) {
41465
+ reportError(toError(error62));
41466
+ } finally {
41467
+ discarding = void 0;
41468
+ }
41469
+ };
41470
+ const processMessage = async (message) => {
41471
+ if (state.phase === "closed") return;
41472
+ if (state.phase === "pinned") {
41473
+ if (state.era === "modern" && isJSONRPCRequest(message) && message.method === "initialize" && !carriesValidModernEnvelopeClaim(message.params)) {
41474
+ await answerLegacyRejection(message, "initialize", message.params !== null && typeof message.params === "object" && typeof message.params.protocolVersion === "string" ? message.params.protocolVersion : void 0);
41475
+ return;
41476
+ }
41477
+ if (state.era === "modern" && await tryServeListen(message)) return;
41478
+ state.instance.channel.deliver(message);
41479
+ return;
41480
+ }
41481
+ if (!isJSONRPCRequest(message) && !isJSONRPCNotification(message)) {
41482
+ reportError(/* @__PURE__ */ new Error("Discarded a JSON-RPC response received before the connection negotiated an era"));
41483
+ return;
41484
+ }
41485
+ const opening = classifyOpeningMessage(message);
41486
+ switch (opening.kind) {
41487
+ case "invalid-envelope": {
41488
+ const detail = `Invalid _meta envelope for protocol revision 2026-07-28: ${opening.issue.key}: ${opening.issue.problem}`;
41489
+ if (isJSONRPCRequest(message)) await writeErrorResponse(message.id, ProtocolErrorCode.InvalidParams, detail, { envelope: opening.issue });
41490
+ else reportError(/* @__PURE__ */ new Error(`Discarded a notification with a malformed envelope: ${detail}`));
41491
+ return;
41492
+ }
41493
+ case "unsupported-revision":
41494
+ if (isJSONRPCRequest(message)) {
41495
+ const error62 = new UnsupportedProtocolVersionError({
41496
+ supported: [...SUPPORTED_MODERN_PROTOCOL_VERSIONS],
41497
+ requested: opening.requested
41498
+ });
41499
+ reportError(error62);
41500
+ await writeErrorResponse(message.id, error62.code, error62.message, error62.data);
41501
+ } else reportError(/* @__PURE__ */ new Error(`Discarded a notification claiming unsupported protocol revision ${opening.requested}`));
41502
+ return;
41503
+ case "modern":
41504
+ if (isJSONRPCRequest(message) && message.method === "server/discover") {
41505
+ if (state.phase === "probe") {
41506
+ state.instance.channel.deliver(message, { classification: opening.classification });
41507
+ return;
41508
+ }
41509
+ const instance = await connectInstance("modern", opening.revision);
41510
+ if (isTornDown()) {
41511
+ await disposeLateInstance(instance);
41512
+ return;
41513
+ }
41514
+ state = {
41515
+ phase: "probe",
41516
+ instance
41517
+ };
41518
+ instance.channel.deliver(message, { classification: opening.classification });
41519
+ return;
41520
+ }
41521
+ if (state.phase === "probe") {
41522
+ if (isJSONRPCNotification(message)) {
41523
+ state.instance.channel.deliver(message, { classification: opening.classification });
41524
+ return;
41525
+ }
41526
+ state = {
41527
+ phase: "pinned",
41528
+ era: "modern",
41529
+ instance: state.instance
41530
+ };
41531
+ } else {
41532
+ const instance = await connectInstance("modern", opening.revision);
41533
+ if (isTornDown()) {
41534
+ await disposeLateInstance(instance);
41535
+ return;
41536
+ }
41537
+ state = {
41538
+ phase: "pinned",
41539
+ era: "modern",
41540
+ instance
41541
+ };
41542
+ }
41543
+ if (await tryServeListen(message)) return;
41544
+ state.instance.channel.deliver(message, { classification: opening.classification });
41545
+ return;
41546
+ case "legacy": {
41547
+ if (legacyMode === "reject") {
41548
+ if (isJSONRPCRequest(message)) await answerLegacyRejection(message, opening.reason, opening.requestedVersion);
41549
+ return;
41550
+ }
41551
+ if (state.phase === "probe") {
41552
+ await discardProbeInstance(state.instance);
41553
+ if (isTornDown()) return;
41554
+ state = { phase: "opening" };
41555
+ }
41556
+ const instance = await connectInstance("legacy");
41557
+ if (isTornDown()) {
41558
+ await disposeLateInstance(instance);
41559
+ return;
41560
+ }
41561
+ state = {
41562
+ phase: "pinned",
41563
+ era: "legacy",
41564
+ instance
41565
+ };
41566
+ state.instance.channel.deliver(message);
41567
+ return;
41568
+ }
41569
+ }
41570
+ };
41571
+ const queue = [];
41572
+ let pumping = false;
41573
+ const pump = async () => {
41574
+ if (pumping) return;
41575
+ pumping = true;
41576
+ try {
41577
+ while (queue.length > 0) {
41578
+ const message = queue.shift();
41579
+ try {
41580
+ await processMessage(message);
41581
+ } catch (error62) {
41582
+ if (isJSONRPCRequest(message)) await writeErrorResponse(message.id, ProtocolErrorCode.InternalError, "Internal server error");
41583
+ reportError(toError(error62));
41584
+ }
41585
+ }
41586
+ } finally {
41587
+ pumping = false;
41588
+ }
41589
+ };
41590
+ const closeAll = async () => {
41591
+ if (closing || state.phase === "closed") return;
41592
+ closing = true;
41593
+ const current = state;
41594
+ state = { phase: "closed" };
41595
+ for (const result of listenRouter.teardownAll()) await wire.send(result).catch((error62) => reportError(toError(error62)));
41596
+ if (current.phase === "probe" || current.phase === "pinned") await current.instance.product.close().catch((error62) => reportError(toError(error62)));
41597
+ await wire.close().catch((error62) => reportError(toError(error62)));
41598
+ };
41599
+ wire.onmessage = (message) => {
41600
+ queue.push(message);
41601
+ pump();
41602
+ };
41603
+ wire.onerror = (error62) => {
41604
+ reportError(error62);
41605
+ if (state.phase === "probe" || state.phase === "pinned") state.instance.channel.onerror?.(error62);
41606
+ };
41607
+ wire.onclose = () => {
41608
+ if (closing || state.phase === "closed") return;
41609
+ closing = true;
41610
+ const current = state;
41611
+ state = { phase: "closed" };
41612
+ if (current.phase === "probe" || current.phase === "pinned") current.instance.product.close().catch((error62) => reportError(toError(error62)));
41613
+ };
41614
+ const started = wire.start().catch((error62) => {
41615
+ reportError(toError(error62));
41616
+ throw error62;
41617
+ });
41618
+ started.catch(() => {
41619
+ });
41620
+ return { close: async () => {
41621
+ await started.catch(() => {
41622
+ });
41623
+ await closeAll();
41624
+ } };
41625
+ }
41626
+ function toError(value) {
41627
+ return value instanceof Error ? value : new Error(String(value));
41628
+ }
40983
41629
 
40984
41630
  // node_modules/@chrischall/mcp-utils/dist/errors/index.js
40985
41631
  var DEFAULT_ERROR_MESSAGE_MAX = 500;
@@ -60823,7 +61469,7 @@ async function createMcpServer(opts) {
60823
61469
  }
60824
61470
  return server;
60825
61471
  }
60826
- function withGracefulShutdown(server, opts = {}) {
61472
+ function withGracefulShutdown(target, opts = {}) {
60827
61473
  const shouldExit = opts.exit ?? true;
60828
61474
  let shuttingDown = false;
60829
61475
  const handler = (signal) => {
@@ -60834,7 +61480,7 @@ function withGracefulShutdown(server, opts = {}) {
60834
61480
  try {
60835
61481
  if (opts.onSignal)
60836
61482
  await opts.onSignal(signal);
60837
- await server.close();
61483
+ await target.close();
60838
61484
  } catch (err) {
60839
61485
  console.error(`[mcp-utils] error during graceful shutdown on ${signal}: ${err instanceof Error ? err.message : String(err)}`);
60840
61486
  } finally {
@@ -60846,16 +61492,26 @@ function withGracefulShutdown(server, opts = {}) {
60846
61492
  process.on("SIGINT", () => handler("SIGINT"));
60847
61493
  process.on("SIGTERM", () => handler("SIGTERM"));
60848
61494
  }
60849
- async function runMcp(opts) {
60850
- const server = await createMcpServer(opts);
61495
+ function runMcp(opts) {
61496
+ if (opts.banner !== void 0) {
61497
+ console.error(opts.banner);
61498
+ }
61499
+ const spec = opts.transport ?? "stdio";
61500
+ const handle = serveStdio(() => createMcpServer({ ...opts, banner: void 0 }), {
61501
+ legacy: opts.legacy ?? "serve",
61502
+ onerror: opts.onerror ?? ((error62) => {
61503
+ console.error(`[mcp-utils] stdio server error: ${redactSecrets(error62.message)}`);
61504
+ }),
61505
+ // `'stdio'` means "let the entry make its own StdioServerTransport over
61506
+ // this process"; anything else is the caller's transport, which the entry
61507
+ // then owns (it starts it, pumps it, and closes it with the handle).
61508
+ ...spec === "stdio" ? {} : { transport: spec }
61509
+ });
60851
61510
  const shutdown = opts.shutdown ?? true;
60852
61511
  if (shutdown !== false) {
60853
- withGracefulShutdown(server, shutdown === true ? {} : shutdown);
61512
+ withGracefulShutdown(handle, shutdown === true ? {} : shutdown);
60854
61513
  }
60855
- const spec = opts.transport ?? "stdio";
60856
- const transport = spec === "stdio" ? new StdioServerTransport() : spec;
60857
- await server.connect(transport);
60858
- return server;
61514
+ return handle;
60859
61515
  }
60860
61516
 
60861
61517
  // node_modules/@chrischall/mcp-utils/dist/config/index.js
@@ -66207,7 +66863,7 @@ function loadAccount(env = process.env) {
66207
66863
  // package.json
66208
66864
  var package_default = {
66209
66865
  name: "infinitecampus-mcp",
66210
- version: "3.0.0",
66866
+ version: "3.1.0",
66211
66867
  mcpName: "io.github.chrischall/infinitecampus-mcp",
66212
66868
  description: "Infinite Campus (Campus Parent) MCP server \u2014 multi-district read + message/document write",
66213
66869
  author: "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -66252,7 +66908,7 @@ var package_default = {
66252
66908
  typecheck: "tsc -p tsconfig.json --noEmit"
66253
66909
  },
66254
66910
  dependencies: {
66255
- "@chrischall/mcp-utils": "^0.28.0",
66911
+ "@chrischall/mcp-utils": "^1.0.0",
66256
66912
  "@fetchproxy/bootstrap": "^3.0.1",
66257
66913
  "@fetchproxy/server": "^3.0.1",
66258
66914
  "@modelcontextprotocol/server": "^2.0.0",
@@ -68226,7 +68882,7 @@ try {
68226
68882
  }
68227
68883
  var COMMON = {
68228
68884
  name: "infinitecampus",
68229
- version: "3.0.0"
68885
+ version: "3.1.0"
68230
68886
  // x-release-please-version
68231
68887
  };
68232
68888
  var healthState = { account, source, configError, client: null };
package/dist/index.js CHANGED
@@ -56,7 +56,7 @@ catch (e) {
56
56
  // avoids — see `unconfiguredClient`.
57
57
  const COMMON = {
58
58
  name: 'infinitecampus',
59
- version: '3.0.0', // x-release-please-version
59
+ version: '3.1.0', // x-release-please-version
60
60
  };
61
61
  // Shared with the healthcheck so it can report the SAME state the tools see,
62
62
  // including on the unconfigured path where there are no tools at all.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "infinitecampus-mcp",
3
- "version": "3.0.0",
3
+ "version": "3.1.0",
4
4
  "mcpName": "io.github.chrischall/infinitecampus-mcp",
5
5
  "description": "Infinite Campus (Campus Parent) MCP server — multi-district read + message/document write",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -45,7 +45,7 @@
45
45
  "typecheck": "tsc -p tsconfig.json --noEmit"
46
46
  },
47
47
  "dependencies": {
48
- "@chrischall/mcp-utils": "^0.28.0",
48
+ "@chrischall/mcp-utils": "^1.0.0",
49
49
  "@fetchproxy/bootstrap": "^3.0.1",
50
50
  "@fetchproxy/server": "^3.0.1",
51
51
  "@modelcontextprotocol/server": "^2.0.0",
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/chrischall/infinitecampus-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "3.0.0",
9
+ "version": "3.1.0",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "infinitecampus-mcp",
14
- "version": "3.0.0",
14
+ "version": "3.1.0",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  },