gogcli-mcp-classroom 2.19.2 → 2.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +134 -30
- package/manifest.json +1 -1
- package/package.json +3 -3
- package/server.json +2 -2
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "Extended Google Classroom for Claude via gogcli — auth + full Classroom support (courses, rosters, coursework, submissions, announcements, topics, invitations)",
|
|
10
|
-
"version": "2.
|
|
10
|
+
"version": "2.21.0"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "gogcli (Classroom)",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Extended Google Classroom for Claude via gogcli — auth + full Classroom support (courses, rosters, coursework, submissions, announcements, topics, invitations)",
|
|
18
|
-
"version": "2.
|
|
18
|
+
"version": "2.21.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp-classroom",
|
|
3
3
|
"displayName": "gogcli (Classroom)",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.21.0",
|
|
5
5
|
"description": "Extended Google Classroom for Claude via gogcli — auth + full Classroom support (courses, rosters, coursework, submissions, announcements, topics, invitations)",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Chris Hall",
|
package/dist/index.js
CHANGED
|
@@ -4264,8 +4264,8 @@ var require_core = __commonJS({
|
|
|
4264
4264
|
return this;
|
|
4265
4265
|
}
|
|
4266
4266
|
case "object": {
|
|
4267
|
-
const
|
|
4268
|
-
this._cache.delete(
|
|
4267
|
+
const cacheKey2 = schemaKeyRef;
|
|
4268
|
+
this._cache.delete(cacheKey2);
|
|
4269
4269
|
let id = schemaKeyRef[this.opts.schemaId];
|
|
4270
4270
|
if (id) {
|
|
4271
4271
|
id = (0, resolve_1.normalizeId)(id);
|
|
@@ -23068,17 +23068,33 @@ function normalizeObjectSchema(schema) {
|
|
|
23068
23068
|
}
|
|
23069
23069
|
return void 0;
|
|
23070
23070
|
}
|
|
23071
|
+
function getDotPath(path) {
|
|
23072
|
+
if (path.length === 0) {
|
|
23073
|
+
return "object root";
|
|
23074
|
+
}
|
|
23075
|
+
return path.reduce((acc, seg, index) => {
|
|
23076
|
+
if (index === 0) {
|
|
23077
|
+
return String(seg);
|
|
23078
|
+
}
|
|
23079
|
+
if (typeof seg === "number") {
|
|
23080
|
+
return `${acc}[${seg}]`;
|
|
23081
|
+
}
|
|
23082
|
+
return `${acc}.${seg}`;
|
|
23083
|
+
}, "");
|
|
23084
|
+
}
|
|
23071
23085
|
function getParseErrorMessage(error51) {
|
|
23072
23086
|
if (error51 && typeof error51 === "object") {
|
|
23087
|
+
if ("issues" in error51 && Array.isArray(error51.issues) && error51.issues.length > 0) {
|
|
23088
|
+
return error51.issues.map((i) => {
|
|
23089
|
+
if (!i.path?.length) {
|
|
23090
|
+
return i.message;
|
|
23091
|
+
}
|
|
23092
|
+
return `${i.message} at ${getDotPath(i.path)}`;
|
|
23093
|
+
}).join("\n");
|
|
23094
|
+
}
|
|
23073
23095
|
if ("message" in error51 && typeof error51.message === "string") {
|
|
23074
23096
|
return error51.message;
|
|
23075
23097
|
}
|
|
23076
|
-
if ("issues" in error51 && Array.isArray(error51.issues) && error51.issues.length > 0) {
|
|
23077
|
-
const firstIssue = error51.issues[0];
|
|
23078
|
-
if (firstIssue && typeof firstIssue === "object" && "message" in firstIssue) {
|
|
23079
|
-
return String(firstIssue.message);
|
|
23080
|
-
}
|
|
23081
|
-
}
|
|
23082
23098
|
try {
|
|
23083
23099
|
return JSON.stringify(error51);
|
|
23084
23100
|
} catch {
|
|
@@ -29693,16 +29709,7 @@ var Server = class extends Protocol {
|
|
|
29693
29709
|
if (!methodSchema) {
|
|
29694
29710
|
throw new Error("Schema is missing a method literal");
|
|
29695
29711
|
}
|
|
29696
|
-
|
|
29697
|
-
if (isZ4Schema(methodSchema)) {
|
|
29698
|
-
const v4Schema = methodSchema;
|
|
29699
|
-
const v4Def = v4Schema._zod?.def;
|
|
29700
|
-
methodValue = v4Def?.value ?? v4Schema.value;
|
|
29701
|
-
} else {
|
|
29702
|
-
const v3Schema = methodSchema;
|
|
29703
|
-
const legacyDef = v3Schema._def;
|
|
29704
|
-
methodValue = legacyDef?.value ?? v3Schema.value;
|
|
29705
|
-
}
|
|
29712
|
+
const methodValue = getLiteralValue(methodSchema);
|
|
29706
29713
|
if (typeof methodValue !== "string") {
|
|
29707
29714
|
throw new Error("Schema method literal must be a string");
|
|
29708
29715
|
}
|
|
@@ -30890,8 +30897,17 @@ var EMPTY_COMPLETION_RESULT = {
|
|
|
30890
30897
|
import process3 from "node:process";
|
|
30891
30898
|
|
|
30892
30899
|
// ../../node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
30900
|
+
var STDIO_DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
30893
30901
|
var ReadBuffer = class {
|
|
30902
|
+
constructor(options) {
|
|
30903
|
+
this._maxBufferSize = options?.maxBufferSize ?? STDIO_DEFAULT_MAX_BUFFER_SIZE;
|
|
30904
|
+
}
|
|
30894
30905
|
append(chunk) {
|
|
30906
|
+
const newSize = (this._buffer?.length ?? 0) + chunk.length;
|
|
30907
|
+
if (newSize > this._maxBufferSize) {
|
|
30908
|
+
this.clear();
|
|
30909
|
+
throw new Error(`ReadBuffer exceeded maximum size of ${this._maxBufferSize} bytes`);
|
|
30910
|
+
}
|
|
30895
30911
|
this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
|
|
30896
30912
|
}
|
|
30897
30913
|
readMessage() {
|
|
@@ -30919,18 +30935,24 @@ function serializeMessage(message) {
|
|
|
30919
30935
|
|
|
30920
30936
|
// ../../node_modules/@modelcontextprotocol/sdk/dist/esm/server/stdio.js
|
|
30921
30937
|
var StdioServerTransport = class {
|
|
30922
|
-
constructor(_stdin = process3.stdin, _stdout = process3.stdout) {
|
|
30938
|
+
constructor(_stdin = process3.stdin, _stdout = process3.stdout, options) {
|
|
30923
30939
|
this._stdin = _stdin;
|
|
30924
30940
|
this._stdout = _stdout;
|
|
30925
|
-
this._readBuffer = new ReadBuffer();
|
|
30926
30941
|
this._started = false;
|
|
30927
30942
|
this._ondata = (chunk) => {
|
|
30928
|
-
|
|
30929
|
-
|
|
30943
|
+
try {
|
|
30944
|
+
this._readBuffer.append(chunk);
|
|
30945
|
+
this.processReadBuffer();
|
|
30946
|
+
} catch (error51) {
|
|
30947
|
+
this.onerror?.(error51);
|
|
30948
|
+
this.close().catch(() => {
|
|
30949
|
+
});
|
|
30950
|
+
}
|
|
30930
30951
|
};
|
|
30931
30952
|
this._onerror = (error51) => {
|
|
30932
30953
|
this.onerror?.(error51);
|
|
30933
30954
|
};
|
|
30955
|
+
this._readBuffer = new ReadBuffer({ maxBufferSize: options?.maxBufferSize });
|
|
30934
30956
|
}
|
|
30935
30957
|
/**
|
|
30936
30958
|
* Starts listening for messages on stdin.
|
|
@@ -31550,7 +31572,9 @@ function registerRunTool(server, options) {
|
|
|
31550
31572
|
function errorText(err) {
|
|
31551
31573
|
return err instanceof Error ? `Error: ${err.message}` : String(err);
|
|
31552
31574
|
}
|
|
31553
|
-
var
|
|
31575
|
+
var DEFINITE_AUTH_PATTERN = /\b(401|unauthorized|invalid_grant)\b/i;
|
|
31576
|
+
var STALE_TOKEN_PATTERN = /\b(?:access[ _-]?)?token\b[^.;\n]{0,40}\b(?:has\s+)?(?:been\s+)?(?:expired|revoked)\b|\b(?:expired|revoked)\s+(?:access[ _-]?)?token\b/i;
|
|
31577
|
+
var AUTH_ERROR_PATTERN = new RegExp(`${DEFINITE_AUTH_PATTERN.source}|${STALE_TOKEN_PATTERN.source}`, "i");
|
|
31554
31578
|
var INVALID_GRANT_PATTERN = /invalid_grant|token has been expired or revoked/i;
|
|
31555
31579
|
var TRANSIENT_ERROR_PATTERN = /\b429\b|\b5\d\d\b|\bquota\b|rateLimit|\bDEADLINE_EXCEEDED\b/i;
|
|
31556
31580
|
var GRID_LIMIT_ERROR_PATTERN = /exceeds grid limits/i;
|
|
@@ -31571,8 +31595,8 @@ function formatAccountList(raw) {
|
|
|
31571
31595
|
async function diagnose(err) {
|
|
31572
31596
|
const errText = errorText(err);
|
|
31573
31597
|
const isInvalidGrant = INVALID_GRANT_PATTERN.test(errText);
|
|
31574
|
-
const
|
|
31575
|
-
const
|
|
31598
|
+
const isTransientError = !DEFINITE_AUTH_PATTERN.test(errText) && TRANSIENT_ERROR_PATTERN.test(errText);
|
|
31599
|
+
const isAuthError = !isTransientError && AUTH_ERROR_PATTERN.test(errText);
|
|
31576
31600
|
const isGridLimitError = GRID_LIMIT_ERROR_PATTERN.test(errText);
|
|
31577
31601
|
const hint = isInvalidGrant ? INVALID_GRANT_HINT : isAuthError ? AUTH_HINT : isTransientError ? TRANSIENT_HINT : isGridLimitError ? GRID_LIMIT_HINT : "";
|
|
31578
31602
|
try {
|
|
@@ -31639,7 +31663,7 @@ function formatAuthHealth(raw, now) {
|
|
|
31639
31663
|
function registerAuthToolsWith(server, defaultServices) {
|
|
31640
31664
|
const servicesDescribe = `Services to authorize: "all" or comma-separated list (e.g. "sheets,gmail,calendar"). Default: "${defaultServices}". Prefer the narrowest set you need \u2014 requesting a service whose Google API is not enabled on the OAuth client's project makes Google reject the WHOLE request with invalid_scope.`;
|
|
31641
31665
|
server.registerTool("gog_auth_list", {
|
|
31642
|
-
description: "List
|
|
31666
|
+
description: "List the Google accounts stored in gogcli, with their scopes. This reads local configuration only \u2014 it does not contact Google and does NOT tell you whether an account still works: a signed-out account whose refresh token expired or was revoked is listed here exactly like a healthy one, scopes and all. Use gog_auth_health to check whether an account can actually authenticate.",
|
|
31643
31667
|
annotations: { readOnlyHint: true },
|
|
31644
31668
|
inputSchema: {}
|
|
31645
31669
|
}, async () => {
|
|
@@ -32138,16 +32162,94 @@ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
|
|
|
32138
32162
|
);
|
|
32139
32163
|
|
|
32140
32164
|
// ../gogcli-mcp/src/server.ts
|
|
32141
|
-
var VERSION = true ? "2.
|
|
32165
|
+
var VERSION = true ? "2.21.0" : "0.0.0";
|
|
32166
|
+
|
|
32167
|
+
// ../gogcli-mcp/src/google-token.ts
|
|
32168
|
+
var TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
|
|
32169
|
+
var EXPIRY_MARGIN_MS = 12e4;
|
|
32170
|
+
var cache = /* @__PURE__ */ new Map();
|
|
32171
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
32172
|
+
async function cacheKey(refreshToken, clientId) {
|
|
32173
|
+
const data = new TextEncoder().encode(`${clientId}\0${refreshToken}`);
|
|
32174
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
32175
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
32176
|
+
}
|
|
32177
|
+
function makeAccessTokenSource(env) {
|
|
32178
|
+
const direct = readEnvVar("GOG_ACCESS_TOKEN", { env });
|
|
32179
|
+
if (direct) return async () => direct;
|
|
32180
|
+
const refreshToken = readEnvVar("GOG_REFRESH_TOKEN", { env });
|
|
32181
|
+
if (!refreshToken) return void 0;
|
|
32182
|
+
const clientId = readEnvVar("GOG_CLIENT_ID", { env });
|
|
32183
|
+
const clientSecret = readEnvVar("GOG_CLIENT_SECRET", { env });
|
|
32184
|
+
if (!clientId || !clientSecret) {
|
|
32185
|
+
const missing = [!clientId && "GOG_CLIENT_ID", !clientSecret && "GOG_CLIENT_SECRET"].filter(Boolean).join(" and ");
|
|
32186
|
+
return async () => {
|
|
32187
|
+
throw new Error(
|
|
32188
|
+
`GOG_REFRESH_TOKEN is set but ${missing} is not, so no access token can be minted. Set the OAuth client alongside the refresh token, or unset GOG_REFRESH_TOKEN to use the backend\u2019s own identity.`
|
|
32189
|
+
);
|
|
32190
|
+
};
|
|
32191
|
+
}
|
|
32192
|
+
return async () => {
|
|
32193
|
+
const key = await cacheKey(refreshToken, clientId);
|
|
32194
|
+
const hit = cache.get(key);
|
|
32195
|
+
if (hit && hit.expiresAt - EXPIRY_MARGIN_MS > Date.now()) return hit.accessToken;
|
|
32196
|
+
let pending = inFlight.get(key);
|
|
32197
|
+
if (!pending) {
|
|
32198
|
+
pending = exchange(refreshToken, clientId, clientSecret).then((minted2) => {
|
|
32199
|
+
cache.set(key, minted2);
|
|
32200
|
+
return minted2;
|
|
32201
|
+
}).finally(() => inFlight.delete(key));
|
|
32202
|
+
inFlight.set(key, pending);
|
|
32203
|
+
}
|
|
32204
|
+
const minted = await pending;
|
|
32205
|
+
return minted.accessToken;
|
|
32206
|
+
};
|
|
32207
|
+
}
|
|
32208
|
+
async function exchange(refreshToken, clientId, clientSecret) {
|
|
32209
|
+
let res;
|
|
32210
|
+
try {
|
|
32211
|
+
res = await fetch(TOKEN_ENDPOINT, {
|
|
32212
|
+
method: "POST",
|
|
32213
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
32214
|
+
body: new URLSearchParams({
|
|
32215
|
+
grant_type: "refresh_token",
|
|
32216
|
+
refresh_token: refreshToken,
|
|
32217
|
+
client_id: clientId,
|
|
32218
|
+
client_secret: clientSecret
|
|
32219
|
+
}).toString()
|
|
32220
|
+
});
|
|
32221
|
+
} catch (err) {
|
|
32222
|
+
throw new Error(
|
|
32223
|
+
`the Google token exchange could not be reached: ${err instanceof Error ? err.message : String(err)}`
|
|
32224
|
+
);
|
|
32225
|
+
}
|
|
32226
|
+
const body = await res.json().catch(() => ({}));
|
|
32227
|
+
if (!res.ok) {
|
|
32228
|
+
if (body.error === "invalid_grant") {
|
|
32229
|
+
throw new Error(
|
|
32230
|
+
'the stored refresh token has expired or been revoked, so this account must be re-authorized (commonly the 7-day limit on OAuth consent screens still in "Testing" mode). Re-enrol with gog_auth_add_url + gog_auth_add_complete and store the new refresh token.'
|
|
32231
|
+
);
|
|
32232
|
+
}
|
|
32233
|
+
throw new Error(
|
|
32234
|
+
`the access token could not be refreshed (HTTP ${res.status}${body.error ? `, ${body.error}` : ""})`
|
|
32235
|
+
);
|
|
32236
|
+
}
|
|
32237
|
+
if (!body.access_token) {
|
|
32238
|
+
throw new Error("the access token could not be refreshed: Google returned no access_token");
|
|
32239
|
+
}
|
|
32240
|
+
const expiresInMs = (body.expires_in ?? 3600) * 1e3;
|
|
32241
|
+
return { accessToken: body.access_token, expiresAt: Date.now() + expiresInMs };
|
|
32242
|
+
}
|
|
32142
32243
|
|
|
32143
32244
|
// ../gogcli-mcp/src/connector-runtime.ts
|
|
32144
32245
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
32145
32246
|
var DEADLINE_GRACE_MS = 5e3;
|
|
32146
32247
|
var RUNNER_GOG_FAILED = 422;
|
|
32147
32248
|
var RUNNER_DRAINING = 503;
|
|
32148
|
-
function makeFlyExecutor(endpoint, key) {
|
|
32249
|
+
function makeFlyExecutor(endpoint, key, readAccessToken) {
|
|
32149
32250
|
return async (args, opts) => {
|
|
32150
32251
|
const deadlineMs = (opts?.timeout ?? DEFAULT_TIMEOUT_MS) + DEADLINE_GRACE_MS;
|
|
32252
|
+
const accessToken = await readAccessToken?.();
|
|
32151
32253
|
let res;
|
|
32152
32254
|
try {
|
|
32153
32255
|
res = await fetch(endpoint + "/run", {
|
|
@@ -32156,7 +32258,7 @@ function makeFlyExecutor(endpoint, key) {
|
|
|
32156
32258
|
Authorization: "Bearer " + key,
|
|
32157
32259
|
"Content-Type": "application/json"
|
|
32158
32260
|
},
|
|
32159
|
-
body: JSON.stringify({ args }),
|
|
32261
|
+
body: JSON.stringify(accessToken ? { args, accessToken } : { args }),
|
|
32160
32262
|
signal: AbortSignal.timeout(deadlineMs)
|
|
32161
32263
|
});
|
|
32162
32264
|
} catch (err) {
|
|
@@ -32197,7 +32299,9 @@ function useRemoteGogRunner(env = process.env) {
|
|
|
32197
32299
|
const endpoint = readEnvVar("GOG_RUNNER_URL", { env });
|
|
32198
32300
|
const key = readEnvVar("GOG_RUNNER_KEY", { env });
|
|
32199
32301
|
if (!endpoint || !key) return false;
|
|
32200
|
-
setDefaultGogExecutor(
|
|
32302
|
+
setDefaultGogExecutor(
|
|
32303
|
+
makeFlyExecutor(endpoint.replace(/\/+$/, ""), key, makeAccessTokenSource(env))
|
|
32304
|
+
);
|
|
32201
32305
|
return true;
|
|
32202
32306
|
}
|
|
32203
32307
|
|
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp-classroom",
|
|
5
5
|
"display_name": "gogcli (Classroom)",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.21.0",
|
|
7
7
|
"description": "Extended Google Classroom for Claude via gogcli — auth + full Classroom support (courses, rosters, coursework, submissions, announcements, topics, invitations)",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp-classroom",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.21.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/gogcli-mcp-classroom",
|
|
5
5
|
"description": "Extended Google Classroom MCP server via gogcli — auth + full Classroom support",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"test:coverage": "vitest run --coverage"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@chrischall/mcp-utils": "^0.
|
|
28
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
27
|
+
"@chrischall/mcp-utils": "^0.14.0",
|
|
28
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
29
29
|
"zod": "^4.4.3"
|
|
30
30
|
},
|
|
31
31
|
"license": "MIT",
|
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"source": "github",
|
|
8
8
|
"subfolder": "packages/gogcli-mcp-classroom"
|
|
9
9
|
},
|
|
10
|
-
"version": "2.
|
|
10
|
+
"version": "2.21.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp-classroom",
|
|
15
|
-
"version": "2.
|
|
15
|
+
"version": "2.21.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|