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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/auth.js +6 -18
- package/dist/bundle.js +1123 -290
- package/dist/client.js +71 -46
- package/dist/config.js +5 -11
- package/dist/index.js +45 -36
- package/dist/tools/_shared.js +8 -2
- package/dist/tools/messages.js +7 -17
- package/package.json +5 -3
- package/server.json +2 -2
package/dist/client.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { writeFile, stat } from 'fs/promises';
|
|
2
2
|
import { dirname } from 'path';
|
|
3
|
+
import { parseCookieJar } from '@chrischall/mcp-utils';
|
|
3
4
|
const SESSION_TTL_MS = 5 * 60 * 60 * 1000; // 5h, slightly under IC's typical 6h
|
|
4
5
|
export class ICClient {
|
|
5
6
|
accounts = new Map();
|
|
@@ -43,10 +44,9 @@ export class ICClient {
|
|
|
43
44
|
await this.ensureSession(this.accounts.get(this.primaryName));
|
|
44
45
|
// fetchproxy mode skips login() and therefore the discovery call inside
|
|
45
46
|
// it. Run discovery directly the first time someone asks for it. The
|
|
46
|
-
// primary is never in linkedTo (it's the root of the linkedTo map)
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
// district during a TTL refresh.
|
|
47
|
+
// primary is never in linkedTo (it's the root of the linkedTo map). When
|
|
48
|
+
// login() is called for an already-linked district during a TTL refresh,
|
|
49
|
+
// it re-auths through the primary instead of running discovery itself.
|
|
50
50
|
if (this.fetchproxyMode && !this.fetchproxyDiscoveryRan) {
|
|
51
51
|
this.fetchproxyDiscoveryRan = true;
|
|
52
52
|
await this.discoverLinkedDistricts(this.accounts.get(this.primaryName));
|
|
@@ -118,18 +118,50 @@ export class ICClient {
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
async login(account) {
|
|
121
|
+
// Linked districts have no real credentials of their own — they hold
|
|
122
|
+
// synthetic '(linked)' placeholders and are authenticated by the primary's
|
|
123
|
+
// CUPS SSO switch, not by their own verify.jsp. When a linked-district
|
|
124
|
+
// session expires by TTL (vs. a 401, which doRequest already routes through
|
|
125
|
+
// the primary), ensureSession lands here. Re-login the primary, which
|
|
126
|
+
// re-runs discoverLinkedDistricts and re-establishes this district's
|
|
127
|
+
// session — POSTing the placeholder creds to verify.jsp would instead yield
|
|
128
|
+
// a misleading password-error. See the comment block at the top of the file.
|
|
129
|
+
const primaryName = this.linkedTo.get(account.name);
|
|
130
|
+
if (primaryName) {
|
|
131
|
+
// Drop the stale linked session so re-discovery installs a fresh one.
|
|
132
|
+
this.sessions.delete(account.name);
|
|
133
|
+
const primaryAccount = this.accounts.get(primaryName);
|
|
134
|
+
await this.ensureSession(primaryAccount);
|
|
135
|
+
// ensureSession(primary) re-ran login() → discoverLinkedDistricts(),
|
|
136
|
+
// which re-creates the linked session. If discovery failed to restore it,
|
|
137
|
+
// surface a clear error rather than leaving callers with a null session.
|
|
138
|
+
if (!this.sessions.has(account.name)) {
|
|
139
|
+
throw new AuthFailedError(account.name, 'linked-district session could not be re-established via the primary district', { credentialHint: false });
|
|
140
|
+
}
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
121
143
|
// fetchproxy mode: empty primary creds, can't post to verify.jsp.
|
|
122
144
|
// The user must re-sign-in in the browser (and the next process start
|
|
123
|
-
// will pick up the fresh cookies).
|
|
124
|
-
// creds too; their re-auth flows through the primary via CUPS, not
|
|
125
|
-
// verify.jsp directly.
|
|
145
|
+
// will pick up the fresh cookies).
|
|
126
146
|
if (!account.username || !account.password) {
|
|
127
147
|
throw new AuthFailedError(account.name, 'session expired and no IC_USERNAME/IC_PASSWORD set — ' +
|
|
128
148
|
'sign back into your IC portal in the browser and restart the MCP');
|
|
129
149
|
}
|
|
130
150
|
// ic_parent_api's pattern: single POST to verify.jsp, let the response
|
|
131
151
|
// set cookies. No pre-login GET needed (unlike OFW's Spring Security).
|
|
132
|
-
|
|
152
|
+
// Credentials go in the urlencoded form body, NOT the URL query string —
|
|
153
|
+
// query-string creds land in proxy/LB/server access logs even over HTTPS.
|
|
154
|
+
// Mirrors the CUPS switch POST body construction against the same host.
|
|
155
|
+
const postRes = await fetch(`${account.baseUrl}/campus/verify.jsp?nonBrowser=true`, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
158
|
+
body: new URLSearchParams({
|
|
159
|
+
username: account.username,
|
|
160
|
+
password: account.password,
|
|
161
|
+
appName: account.district,
|
|
162
|
+
portalLoginPage: 'parents',
|
|
163
|
+
}).toString(),
|
|
164
|
+
});
|
|
133
165
|
if (postRes.status >= 500)
|
|
134
166
|
throw new PortalUnreachableError(account.name, postRes.status);
|
|
135
167
|
// IC's verify.jsp returns 200 with an <AUTHENTICATION>X</AUTHENTICATION>
|
|
@@ -159,10 +191,11 @@ export class ICClient {
|
|
|
159
191
|
session.cookie = cookies.cookieHeader;
|
|
160
192
|
session.xsrfToken = cookies.xsrfToken;
|
|
161
193
|
session.loggedInAt = Date.now();
|
|
162
|
-
// Discover linked districts (CUPS SSO) — non-blocking, errors logged not
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
194
|
+
// Discover linked districts (CUPS SSO) — non-blocking, errors logged not
|
|
195
|
+
// thrown. By construction we only reach here for the primary (or fetchproxy
|
|
196
|
+
// primary): linked districts return early above and re-auth via the primary,
|
|
197
|
+
// so no `linkedTo` guard is needed.
|
|
198
|
+
await this.discoverLinkedDistricts(account);
|
|
166
199
|
}
|
|
167
200
|
async discoverLinkedDistricts(account) {
|
|
168
201
|
try {
|
|
@@ -342,39 +375,23 @@ export class ICClient {
|
|
|
342
375
|
}
|
|
343
376
|
}
|
|
344
377
|
/**
|
|
345
|
-
* Parse Set-Cookie headers into a deduplicated
|
|
346
|
-
*
|
|
347
|
-
* IC's login response sets ~20 cookies including deletion markers (Max-Age=0).
|
|
348
|
-
* Sending both `appName=` (delete) and `appName=springfield` (set) causes IC to
|
|
349
|
-
* reject requests with "conflicting app name values". This parser:
|
|
350
|
-
* - Filters out cookies with Max-Age=0 (deletion markers)
|
|
351
|
-
* - Deduplicates by name (last value wins)
|
|
352
|
-
* - Extracts XSRF-TOKEN separately for the X-XSRF-TOKEN request header
|
|
378
|
+
* Parse Set-Cookie headers into a deduplicated Cookie header + XSRF token via
|
|
379
|
+
* `parseCookieJar` (see inline comment for the splitter/jar details).
|
|
353
380
|
*/
|
|
354
381
|
function parseSetCookies(headers) {
|
|
382
|
+
// Prefer the structured getSetCookie() split (one cookie per array entry,
|
|
383
|
+
// what real Node fetch Responses provide). Only when it's unavailable do we
|
|
384
|
+
// fall back to naively comma-splitting the joined header — kept identical to
|
|
385
|
+
// the original so the edge-case behavior (commas inside attribute lists) is
|
|
386
|
+
// preserved rather than inheriting parseCookieJar's smarter-but-different
|
|
387
|
+
// splitter. parseCookieJar then does the jar logic: drop deletion markers
|
|
388
|
+
// (Max-Age=0 / expired Expires) and empty values, dedupe by name. That's
|
|
389
|
+
// load-bearing so the synthesized Cookie header never carries both the
|
|
390
|
+
// `appName=` deletion and the real value (IC rejects "conflicting app name").
|
|
355
391
|
const raw = headers.getSetCookie?.() ?? [];
|
|
356
|
-
const
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
for (const entry of headerStrings) {
|
|
360
|
-
// Check for Max-Age=0 → this is a cookie deletion, skip it
|
|
361
|
-
if (/Max-Age=0/i.test(entry))
|
|
362
|
-
continue;
|
|
363
|
-
const nameValue = entry.split(';')[0].trim();
|
|
364
|
-
const eqIdx = nameValue.indexOf('=');
|
|
365
|
-
if (eqIdx < 1)
|
|
366
|
-
continue;
|
|
367
|
-
const name = nameValue.substring(0, eqIdx);
|
|
368
|
-
const value = nameValue.substring(eqIdx + 1);
|
|
369
|
-
// Skip cookies with empty values (clearing instructions)
|
|
370
|
-
if (!value)
|
|
371
|
-
continue;
|
|
372
|
-
jar.set(name, value);
|
|
373
|
-
if (name === 'XSRF-TOKEN')
|
|
374
|
-
xsrfToken = value;
|
|
375
|
-
}
|
|
376
|
-
const cookieHeader = [...jar.entries()].map(([k, v]) => `${k}=${v}`).join('; ');
|
|
377
|
-
return { cookieHeader, xsrfToken };
|
|
392
|
+
const entries = raw.length > 0 ? raw : splitFallback(headers.get('set-cookie'));
|
|
393
|
+
const { cookies, cookieHeader } = parseCookieJar(entries);
|
|
394
|
+
return { cookieHeader, xsrfToken: cookies['XSRF-TOKEN'] ?? '' };
|
|
378
395
|
}
|
|
379
396
|
function splitFallback(header) {
|
|
380
397
|
if (!header)
|
|
@@ -394,11 +411,19 @@ export class UnknownDistrictError extends Error {
|
|
|
394
411
|
export class AuthFailedError extends Error {
|
|
395
412
|
district;
|
|
396
413
|
reason;
|
|
397
|
-
|
|
414
|
+
/**
|
|
415
|
+
* @param opts.credentialHint When `false`, the message omits the
|
|
416
|
+
* "Check IC_USERNAME and IC_PASSWORD" suffix — used for failures where the
|
|
417
|
+
* credentials are known-good (e.g. a linked-district CUPS/SSO re-discovery
|
|
418
|
+
* failure) and pointing the user at their creds would be misleading.
|
|
419
|
+
*/
|
|
420
|
+
constructor(district, reason, opts) {
|
|
398
421
|
const detail = reason ? ` (${reason})` : '';
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
422
|
+
const remedy = opts?.credentialHint === false
|
|
423
|
+
? 'Sign in again at the IC portal in your browser, then restart the MCP.'
|
|
424
|
+
: 'Check IC_USERNAME and IC_PASSWORD; ' +
|
|
425
|
+
'if those are correct, the account may be locked or the portal may be down.';
|
|
426
|
+
super(`Login failed for district '${district}'${detail}. ${remedy}`);
|
|
402
427
|
this.district = district;
|
|
403
428
|
this.reason = reason;
|
|
404
429
|
this.name = 'AuthFailedError';
|
package/dist/config.js
CHANGED
|
@@ -1,20 +1,14 @@
|
|
|
1
|
+
import { readEnvVar } from '@chrischall/mcp-utils';
|
|
1
2
|
/**
|
|
2
3
|
* Read an env var, trim whitespace, and treat as unset if blank or if the value
|
|
3
4
|
* looks like an unsubstituted shell placeholder (e.g. `${FOO}`) — defends
|
|
4
5
|
* against MCP hosts that pass .mcp.json env blocks through unexpanded.
|
|
6
|
+
*
|
|
7
|
+
* Thin adapter over mcp-utils' `readEnvVar` (same defensive semantics), keeping
|
|
8
|
+
* the local `(env, key)` arg order so the call sites below stay unchanged.
|
|
5
9
|
*/
|
|
6
10
|
function readVar(env, key) {
|
|
7
|
-
|
|
8
|
-
if (typeof raw !== 'string')
|
|
9
|
-
return undefined;
|
|
10
|
-
const trimmed = raw.trim();
|
|
11
|
-
if (trimmed.length === 0)
|
|
12
|
-
return undefined;
|
|
13
|
-
if (trimmed === 'undefined' || trimmed === 'null')
|
|
14
|
-
return undefined;
|
|
15
|
-
if (/^\$\{[^}]*\}$/.test(trimmed))
|
|
16
|
-
return undefined;
|
|
17
|
-
return trimmed;
|
|
11
|
+
return readEnvVar(key, { env });
|
|
18
12
|
}
|
|
19
13
|
/**
|
|
20
14
|
* Load the Account from env vars. IC_BASE_URL + IC_DISTRICT are ALWAYS
|
package/dist/index.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { dirname, join } from 'path';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
15
|
-
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { loadDotenvSafely, runMcp } from '@chrischall/mcp-utils';
|
|
5
|
+
// quiet load of the local .env (no-throw, silent when the file is absent —
|
|
6
|
+
// e.g. inside an mcpb bundle). Path is resolved next to dist/ so the same
|
|
7
|
+
// `..`/.env that the previous inline dotenv call used still applies.
|
|
8
|
+
// override:false keeps real host-provided env winning. stdout stays clean
|
|
9
|
+
// for JSON-RPC (loadDotenvSafely forces dotenv's quiet:true).
|
|
10
|
+
await loadDotenvSafely({
|
|
11
|
+
path: join(dirname(fileURLToPath(import.meta.url)), '..', '.env'),
|
|
12
|
+
override: false,
|
|
13
|
+
});
|
|
16
14
|
import { resolveAuth } from './auth.js';
|
|
17
15
|
import { ICClient } from './client.js';
|
|
18
16
|
import { registerDistrictTools } from './tools/districts.js';
|
|
@@ -32,6 +30,7 @@ import { registerTeacherTools } from './tools/teachers.js';
|
|
|
32
30
|
import { registerAssessmentTools } from './tools/assessments.js';
|
|
33
31
|
import { registerFeeTools } from './tools/fees.js';
|
|
34
32
|
import { registerFeaturesTools } from './tools/features.js';
|
|
33
|
+
const AI_NOTICE = '[infinitecampus-mcp] Developed and maintained by AI (Claude). Use at your own discretion.';
|
|
35
34
|
// Defer config errors so the server can still start cleanly when env vars
|
|
36
35
|
// aren't set (e.g. during the host's install-time smoke test, before the
|
|
37
36
|
// user has filled in user_config OR the user hasn't yet signed into their
|
|
@@ -50,33 +49,43 @@ try {
|
|
|
50
49
|
catch (e) {
|
|
51
50
|
configError = e;
|
|
52
51
|
}
|
|
53
|
-
|
|
52
|
+
// Hand off to mcp-utils' runMcp for the connect + stdio transport + graceful
|
|
53
|
+
// shutdown. We keep the deferred-config-error pattern by deciding the registrar
|
|
54
|
+
// list and banner up here: when configured we wire every
|
|
55
|
+
// register<Domain>Tools(server, client) call; when not, we register no tools so
|
|
56
|
+
// the host's install-time tools/list still succeeds and the user gets an
|
|
57
|
+
// actionable stderr message (banner) instead of a crash loop.
|
|
58
|
+
const COMMON = {
|
|
59
|
+
name: 'infinitecampus',
|
|
60
|
+
version: '2.3.3', // x-release-please-version
|
|
61
|
+
};
|
|
54
62
|
if (account) {
|
|
55
63
|
const client = new ICClient(account, { preloaded });
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
const tools = [
|
|
65
|
+
registerDistrictTools,
|
|
66
|
+
registerStudentTools,
|
|
67
|
+
registerScheduleTools,
|
|
68
|
+
registerAssignmentTools,
|
|
69
|
+
registerGradeTools,
|
|
70
|
+
registerAttendanceTools,
|
|
71
|
+
registerBehaviorTools,
|
|
72
|
+
registerFoodServiceTools,
|
|
73
|
+
registerMessageTools,
|
|
74
|
+
registerDocumentTools,
|
|
75
|
+
registerCalendarTools,
|
|
76
|
+
registerAttendanceEventsTools,
|
|
77
|
+
registerRecentGradesTools,
|
|
78
|
+
registerTeacherTools,
|
|
79
|
+
registerAssessmentTools,
|
|
80
|
+
registerFeeTools,
|
|
81
|
+
registerFeaturesTools,
|
|
82
|
+
];
|
|
73
83
|
const suffix = source === 'fetchproxy' ? ' [via fetchproxy]' : '';
|
|
74
|
-
|
|
84
|
+
const districtLine = `[infinitecampus-mcp] District: ${account.name} (${account.baseUrl})${suffix}`;
|
|
85
|
+
await runMcp({ ...COMMON, deps: client, tools, banner: `${districtLine}\n${AI_NOTICE}` });
|
|
75
86
|
}
|
|
76
87
|
else {
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
const notConfigured = `[infinitecampus-mcp] Not configured: ${configError?.message ?? 'unknown error'}\n` +
|
|
89
|
+
'[infinitecampus-mcp] Server is running with no tools registered. Set the required env vars and reinstall.';
|
|
90
|
+
await runMcp({ ...COMMON, tools: [], banner: `${notConfigured}\n${AI_NOTICE}` });
|
|
79
91
|
}
|
|
80
|
-
console.error('[infinitecampus-mcp] Developed and maintained by AI (Claude). Use at your own discretion.');
|
|
81
|
-
const transport = new StdioServerTransport();
|
|
82
|
-
await server.connect(transport);
|
package/dist/tools/_shared.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
import { textResult } from '@chrischall/mcp-utils';
|
|
2
|
+
/**
|
|
3
|
+
* Wrap a value as an MCP text content block — the standard tool return shape.
|
|
4
|
+
* Thin re-export of mcp-utils' `textResult` so the whole fleet shares one
|
|
5
|
+
* pretty-printed-JSON formatter; the local name is kept to avoid churning
|
|
6
|
+
* every tool call site.
|
|
7
|
+
*/
|
|
2
8
|
export function textContent(data) {
|
|
3
|
-
return
|
|
9
|
+
return textResult(data);
|
|
4
10
|
}
|
|
5
11
|
/** Detect "IC 404 ..." errors thrown by ICClient.request for endpoints that don't exist. */
|
|
6
12
|
export function is404(e) {
|
package/dist/tools/messages.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { extractPlainTextFromHtml } from '@chrischall/mcp-utils/html';
|
|
2
3
|
import { textContent, toArray } from './_shared.js';
|
|
3
4
|
const listArgs = z.object({
|
|
4
5
|
district: z.string(),
|
|
@@ -44,29 +45,18 @@ export function normalizeMessageUrl(input) {
|
|
|
44
45
|
* The HTML has:
|
|
45
46
|
* <title>Message -- <subject></title>
|
|
46
47
|
* body with "Message: <subject>", "Date: MM/DD/YYYY", then the message body.
|
|
47
|
-
* Dependency-free:
|
|
48
|
+
* Dependency-free: title via regex, body via mcp-utils'
|
|
49
|
+
* `extractPlainTextFromHtml` (script/style strip + tag strip + entity decode +
|
|
50
|
+
* whitespace collapse — the shared fleet helper this extractor was hoisted from).
|
|
48
51
|
*/
|
|
49
52
|
export function parseMessageHtml(html, url) {
|
|
50
53
|
// Extract <title>
|
|
51
54
|
const titleMatch = html.match(/<title>([^<]*)<\/title>/i);
|
|
52
55
|
let subject = titleMatch ? titleMatch[1].trim() : '';
|
|
53
56
|
subject = subject.replace(/^Message\s*--\s*/i, '');
|
|
54
|
-
// Strip
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
.replace(/<style[\s\S]*?<\/style>/gi, ' ');
|
|
58
|
-
// Strip remaining tags
|
|
59
|
-
text = text.replace(/<[^>]+>/g, ' ');
|
|
60
|
-
// Decode a few common entities
|
|
61
|
-
text = text
|
|
62
|
-
.replace(/ /g, ' ')
|
|
63
|
-
.replace(/&/g, '&')
|
|
64
|
-
.replace(/</g, '<')
|
|
65
|
-
.replace(/>/g, '>')
|
|
66
|
-
.replace(/"/g, '"')
|
|
67
|
-
.replace(/'/g, "'");
|
|
68
|
-
// Collapse whitespace
|
|
69
|
-
text = text.replace(/\s+/g, ' ').trim();
|
|
57
|
+
// Strip the body down to plain text (script/style content removed, tags
|
|
58
|
+
// dropped, entities decoded, whitespace collapsed).
|
|
59
|
+
const text = extractPlainTextFromHtml(html);
|
|
70
60
|
// Extract "Date: MM/DD/YYYY" if present
|
|
71
61
|
const dateMatch = text.match(/Date:\s*(\d{1,2}\/\d{1,2}\/\d{2,4})/);
|
|
72
62
|
const date = dateMatch ? dateMatch[1] : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "infinitecampus-mcp",
|
|
3
|
-
"version": "2.3.
|
|
3
|
+
"version": "2.3.3",
|
|
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>",
|
|
@@ -42,10 +42,12 @@
|
|
|
42
42
|
"test:watch": "vitest"
|
|
43
43
|
},
|
|
44
44
|
"dependencies": {
|
|
45
|
-
"@
|
|
46
|
-
"@fetchproxy/
|
|
45
|
+
"@chrischall/mcp-utils": "^0.5.0",
|
|
46
|
+
"@fetchproxy/bootstrap": "^1.0.0",
|
|
47
|
+
"@fetchproxy/server": "^1.0.0",
|
|
47
48
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
48
49
|
"dotenv": "^17.4.2",
|
|
50
|
+
"node-html-parser": "^7.1.0",
|
|
49
51
|
"zod": "^4.4.3"
|
|
50
52
|
},
|
|
51
53
|
"devDependencies": {
|
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": "2.3.
|
|
9
|
+
"version": "2.3.3",
|
|
10
10
|
"packages": [
|
|
11
11
|
{
|
|
12
12
|
"registryType": "npm",
|
|
13
13
|
"identifier": "infinitecampus-mcp",
|
|
14
|
-
"version": "2.3.
|
|
14
|
+
"version": "2.3.3",
|
|
15
15
|
"transport": {
|
|
16
16
|
"type": "stdio"
|
|
17
17
|
},
|