gogcli-mcp 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/dist/lib.js +95 -10
- package/manifest.json +1 -1
- package/package.json +3 -3
- package/server.json +2 -2
- package/src/connector-runtime.ts +27 -2
- package/src/google-token.ts +225 -0
- package/src/remote-runner.ts +19 -1
- package/src/tools/auth.ts +6 -1
- package/src/tools/utils.ts +25 -3
- package/src/worker.ts +1 -1
- package/tests/connector-runtime.test.ts +51 -0
- package/tests/google-token.test.ts +304 -0
- package/tests/remote-runner.test.ts +41 -0
- package/tests/sdk-single-copy.test.ts +48 -0
- package/tests/tools/auth.test.ts +21 -0
- package/tests/tools/utils.test.ts +44 -0
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
},
|
|
8
8
|
"metadata": {
|
|
9
9
|
"description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
|
|
10
|
-
"version": "2.
|
|
10
|
+
"version": "2.21.0"
|
|
11
11
|
},
|
|
12
12
|
"plugins": [
|
|
13
13
|
{
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"displayName": "gogcli",
|
|
16
16
|
"source": "./",
|
|
17
17
|
"description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
|
|
18
|
-
"version": "2.
|
|
18
|
+
"version": "2.21.0",
|
|
19
19
|
"author": {
|
|
20
20
|
"name": "Chris Hall"
|
|
21
21
|
},
|
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.
|
|
@@ -31567,7 +31589,9 @@ function registerRunTool(server, options) {
|
|
|
31567
31589
|
function errorText(err) {
|
|
31568
31590
|
return err instanceof Error ? `Error: ${err.message}` : String(err);
|
|
31569
31591
|
}
|
|
31570
|
-
var
|
|
31592
|
+
var DEFINITE_AUTH_PATTERN = /\b(401|unauthorized|invalid_grant)\b/i;
|
|
31593
|
+
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;
|
|
31594
|
+
var AUTH_ERROR_PATTERN = new RegExp(`${DEFINITE_AUTH_PATTERN.source}|${STALE_TOKEN_PATTERN.source}`, "i");
|
|
31571
31595
|
var INVALID_GRANT_PATTERN = /invalid_grant|token has been expired or revoked/i;
|
|
31572
31596
|
var TRANSIENT_ERROR_PATTERN = /\b429\b|\b5\d\d\b|\bquota\b|rateLimit|\bDEADLINE_EXCEEDED\b/i;
|
|
31573
31597
|
var GRID_LIMIT_ERROR_PATTERN = /exceeds grid limits/i;
|
|
@@ -31588,8 +31612,8 @@ function formatAccountList(raw) {
|
|
|
31588
31612
|
async function diagnose(err) {
|
|
31589
31613
|
const errText = errorText(err);
|
|
31590
31614
|
const isInvalidGrant = INVALID_GRANT_PATTERN.test(errText);
|
|
31591
|
-
const
|
|
31592
|
-
const
|
|
31615
|
+
const isTransientError = !DEFINITE_AUTH_PATTERN.test(errText) && TRANSIENT_ERROR_PATTERN.test(errText);
|
|
31616
|
+
const isAuthError = !isTransientError && AUTH_ERROR_PATTERN.test(errText);
|
|
31593
31617
|
const isGridLimitError = GRID_LIMIT_ERROR_PATTERN.test(errText);
|
|
31594
31618
|
const hint = isInvalidGrant ? INVALID_GRANT_HINT : isAuthError ? AUTH_HINT : isTransientError ? TRANSIENT_HINT : isGridLimitError ? GRID_LIMIT_HINT : "";
|
|
31595
31619
|
try {
|
|
@@ -31710,7 +31734,7 @@ function registerApiTools(server) {
|
|
|
31710
31734
|
function registerAuthToolsWith(server, defaultServices) {
|
|
31711
31735
|
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.`;
|
|
31712
31736
|
server.registerTool("gog_auth_list", {
|
|
31713
|
-
description: "List
|
|
31737
|
+
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.",
|
|
31714
31738
|
annotations: { readOnlyHint: true },
|
|
31715
31739
|
inputSchema: {}
|
|
31716
31740
|
}, async () => {
|
|
@@ -33079,7 +33103,7 @@ function registerTasksTools(server) {
|
|
|
33079
33103
|
}
|
|
33080
33104
|
|
|
33081
33105
|
// src/server.ts
|
|
33082
|
-
var VERSION = true ? "2.
|
|
33106
|
+
var VERSION = true ? "2.21.0" : "0.0.0";
|
|
33083
33107
|
var BASE_TOOL_REGISTRARS = [
|
|
33084
33108
|
registerApiTools,
|
|
33085
33109
|
registerAuthTools,
|
|
@@ -33094,14 +33118,92 @@ var BASE_TOOL_REGISTRARS = [
|
|
|
33094
33118
|
registerTasksTools
|
|
33095
33119
|
];
|
|
33096
33120
|
|
|
33121
|
+
// src/google-token.ts
|
|
33122
|
+
var TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
|
|
33123
|
+
var EXPIRY_MARGIN_MS = 12e4;
|
|
33124
|
+
var cache = /* @__PURE__ */ new Map();
|
|
33125
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
33126
|
+
async function cacheKey(refreshToken, clientId) {
|
|
33127
|
+
const data = new TextEncoder().encode(`${clientId}\0${refreshToken}`);
|
|
33128
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
33129
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
33130
|
+
}
|
|
33131
|
+
function makeAccessTokenSource(env) {
|
|
33132
|
+
const direct = readEnvVar("GOG_ACCESS_TOKEN", { env });
|
|
33133
|
+
if (direct) return async () => direct;
|
|
33134
|
+
const refreshToken = readEnvVar("GOG_REFRESH_TOKEN", { env });
|
|
33135
|
+
if (!refreshToken) return void 0;
|
|
33136
|
+
const clientId = readEnvVar("GOG_CLIENT_ID", { env });
|
|
33137
|
+
const clientSecret = readEnvVar("GOG_CLIENT_SECRET", { env });
|
|
33138
|
+
if (!clientId || !clientSecret) {
|
|
33139
|
+
const missing = [!clientId && "GOG_CLIENT_ID", !clientSecret && "GOG_CLIENT_SECRET"].filter(Boolean).join(" and ");
|
|
33140
|
+
return async () => {
|
|
33141
|
+
throw new Error(
|
|
33142
|
+
`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.`
|
|
33143
|
+
);
|
|
33144
|
+
};
|
|
33145
|
+
}
|
|
33146
|
+
return async () => {
|
|
33147
|
+
const key = await cacheKey(refreshToken, clientId);
|
|
33148
|
+
const hit = cache.get(key);
|
|
33149
|
+
if (hit && hit.expiresAt - EXPIRY_MARGIN_MS > Date.now()) return hit.accessToken;
|
|
33150
|
+
let pending = inFlight.get(key);
|
|
33151
|
+
if (!pending) {
|
|
33152
|
+
pending = exchange(refreshToken, clientId, clientSecret).then((minted2) => {
|
|
33153
|
+
cache.set(key, minted2);
|
|
33154
|
+
return minted2;
|
|
33155
|
+
}).finally(() => inFlight.delete(key));
|
|
33156
|
+
inFlight.set(key, pending);
|
|
33157
|
+
}
|
|
33158
|
+
const minted = await pending;
|
|
33159
|
+
return minted.accessToken;
|
|
33160
|
+
};
|
|
33161
|
+
}
|
|
33162
|
+
async function exchange(refreshToken, clientId, clientSecret) {
|
|
33163
|
+
let res;
|
|
33164
|
+
try {
|
|
33165
|
+
res = await fetch(TOKEN_ENDPOINT, {
|
|
33166
|
+
method: "POST",
|
|
33167
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
33168
|
+
body: new URLSearchParams({
|
|
33169
|
+
grant_type: "refresh_token",
|
|
33170
|
+
refresh_token: refreshToken,
|
|
33171
|
+
client_id: clientId,
|
|
33172
|
+
client_secret: clientSecret
|
|
33173
|
+
}).toString()
|
|
33174
|
+
});
|
|
33175
|
+
} catch (err) {
|
|
33176
|
+
throw new Error(
|
|
33177
|
+
`the Google token exchange could not be reached: ${err instanceof Error ? err.message : String(err)}`
|
|
33178
|
+
);
|
|
33179
|
+
}
|
|
33180
|
+
const body = await res.json().catch(() => ({}));
|
|
33181
|
+
if (!res.ok) {
|
|
33182
|
+
if (body.error === "invalid_grant") {
|
|
33183
|
+
throw new Error(
|
|
33184
|
+
'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.'
|
|
33185
|
+
);
|
|
33186
|
+
}
|
|
33187
|
+
throw new Error(
|
|
33188
|
+
`the access token could not be refreshed (HTTP ${res.status}${body.error ? `, ${body.error}` : ""})`
|
|
33189
|
+
);
|
|
33190
|
+
}
|
|
33191
|
+
if (!body.access_token) {
|
|
33192
|
+
throw new Error("the access token could not be refreshed: Google returned no access_token");
|
|
33193
|
+
}
|
|
33194
|
+
const expiresInMs = (body.expires_in ?? 3600) * 1e3;
|
|
33195
|
+
return { accessToken: body.access_token, expiresAt: Date.now() + expiresInMs };
|
|
33196
|
+
}
|
|
33197
|
+
|
|
33097
33198
|
// src/connector-runtime.ts
|
|
33098
33199
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
33099
33200
|
var DEADLINE_GRACE_MS = 5e3;
|
|
33100
33201
|
var RUNNER_GOG_FAILED = 422;
|
|
33101
33202
|
var RUNNER_DRAINING = 503;
|
|
33102
|
-
function makeFlyExecutor(endpoint, key) {
|
|
33203
|
+
function makeFlyExecutor(endpoint, key, readAccessToken) {
|
|
33103
33204
|
return async (args, opts) => {
|
|
33104
33205
|
const deadlineMs = (opts?.timeout ?? DEFAULT_TIMEOUT_MS) + DEADLINE_GRACE_MS;
|
|
33206
|
+
const accessToken = await readAccessToken?.();
|
|
33105
33207
|
let res;
|
|
33106
33208
|
try {
|
|
33107
33209
|
res = await fetch(endpoint + "/run", {
|
|
@@ -33110,7 +33212,7 @@ function makeFlyExecutor(endpoint, key) {
|
|
|
33110
33212
|
Authorization: "Bearer " + key,
|
|
33111
33213
|
"Content-Type": "application/json"
|
|
33112
33214
|
},
|
|
33113
|
-
body: JSON.stringify({ args }),
|
|
33215
|
+
body: JSON.stringify(accessToken ? { args, accessToken } : { args }),
|
|
33114
33216
|
signal: AbortSignal.timeout(deadlineMs)
|
|
33115
33217
|
});
|
|
33116
33218
|
} catch (err) {
|
|
@@ -33151,7 +33253,9 @@ function useRemoteGogRunner(env = process.env) {
|
|
|
33151
33253
|
const endpoint = readEnvVar("GOG_RUNNER_URL", { env });
|
|
33152
33254
|
const key = readEnvVar("GOG_RUNNER_KEY", { env });
|
|
33153
33255
|
if (!endpoint || !key) return false;
|
|
33154
|
-
setDefaultGogExecutor(
|
|
33256
|
+
setDefaultGogExecutor(
|
|
33257
|
+
makeFlyExecutor(endpoint.replace(/\/+$/, ""), key, makeAccessTokenSource(env))
|
|
33258
|
+
);
|
|
33155
33259
|
return true;
|
|
33156
33260
|
}
|
|
33157
33261
|
|
package/dist/lib.js
CHANGED
|
@@ -4263,8 +4263,8 @@ var require_core = __commonJS({
|
|
|
4263
4263
|
return this;
|
|
4264
4264
|
}
|
|
4265
4265
|
case "object": {
|
|
4266
|
-
const
|
|
4267
|
-
this._cache.delete(
|
|
4266
|
+
const cacheKey2 = schemaKeyRef;
|
|
4267
|
+
this._cache.delete(cacheKey2);
|
|
4268
4268
|
let id = schemaKeyRef[this.opts.schemaId];
|
|
4269
4269
|
if (id) {
|
|
4270
4270
|
id = (0, resolve_1.normalizeId)(id);
|
|
@@ -22922,6 +22922,9 @@ var McpZodTypeKind;
|
|
|
22922
22922
|
McpZodTypeKind2["Completable"] = "McpCompletable";
|
|
22923
22923
|
})(McpZodTypeKind || (McpZodTypeKind = {}));
|
|
22924
22924
|
|
|
22925
|
+
// ../../node_modules/@modelcontextprotocol/sdk/dist/esm/shared/stdio.js
|
|
22926
|
+
var STDIO_DEFAULT_MAX_BUFFER_SIZE = 10 * 1024 * 1024;
|
|
22927
|
+
|
|
22925
22928
|
// ../../node_modules/@chrischall/mcp-utils/dist/errors/index.js
|
|
22926
22929
|
var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
22927
22930
|
var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
|
|
@@ -23470,7 +23473,9 @@ function registerRunTool(server, options) {
|
|
|
23470
23473
|
function errorText(err) {
|
|
23471
23474
|
return err instanceof Error ? `Error: ${err.message}` : String(err);
|
|
23472
23475
|
}
|
|
23473
|
-
var
|
|
23476
|
+
var DEFINITE_AUTH_PATTERN = /\b(401|unauthorized|invalid_grant)\b/i;
|
|
23477
|
+
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;
|
|
23478
|
+
var AUTH_ERROR_PATTERN = new RegExp(`${DEFINITE_AUTH_PATTERN.source}|${STALE_TOKEN_PATTERN.source}`, "i");
|
|
23474
23479
|
var INVALID_GRANT_PATTERN = /invalid_grant|token has been expired or revoked/i;
|
|
23475
23480
|
var TRANSIENT_ERROR_PATTERN = /\b429\b|\b5\d\d\b|\bquota\b|rateLimit|\bDEADLINE_EXCEEDED\b/i;
|
|
23476
23481
|
var GRID_LIMIT_ERROR_PATTERN = /exceeds grid limits/i;
|
|
@@ -23491,8 +23496,8 @@ function formatAccountList(raw) {
|
|
|
23491
23496
|
async function diagnose(err) {
|
|
23492
23497
|
const errText = errorText(err);
|
|
23493
23498
|
const isInvalidGrant = INVALID_GRANT_PATTERN.test(errText);
|
|
23494
|
-
const
|
|
23495
|
-
const
|
|
23499
|
+
const isTransientError = !DEFINITE_AUTH_PATTERN.test(errText) && TRANSIENT_ERROR_PATTERN.test(errText);
|
|
23500
|
+
const isAuthError = !isTransientError && AUTH_ERROR_PATTERN.test(errText);
|
|
23496
23501
|
const isGridLimitError = GRID_LIMIT_ERROR_PATTERN.test(errText);
|
|
23497
23502
|
const hint = isInvalidGrant ? INVALID_GRANT_HINT : isAuthError ? AUTH_HINT : isTransientError ? TRANSIENT_HINT : isGridLimitError ? GRID_LIMIT_HINT : "";
|
|
23498
23503
|
try {
|
|
@@ -23613,7 +23618,7 @@ function registerApiTools(server) {
|
|
|
23613
23618
|
function registerAuthToolsWith(server, defaultServices) {
|
|
23614
23619
|
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.`;
|
|
23615
23620
|
server.registerTool("gog_auth_list", {
|
|
23616
|
-
description: "List
|
|
23621
|
+
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.",
|
|
23617
23622
|
annotations: { readOnlyHint: true },
|
|
23618
23623
|
inputSchema: {}
|
|
23619
23624
|
}, async () => {
|
|
@@ -24985,7 +24990,7 @@ function registerTasksTools(server) {
|
|
|
24985
24990
|
}
|
|
24986
24991
|
|
|
24987
24992
|
// src/server.ts
|
|
24988
|
-
var VERSION = true ? "2.
|
|
24993
|
+
var VERSION = true ? "2.21.0" : "0.0.0";
|
|
24989
24994
|
var BASE_TOOL_REGISTRARS = [
|
|
24990
24995
|
registerApiTools,
|
|
24991
24996
|
registerAuthTools,
|
|
@@ -25000,14 +25005,92 @@ var BASE_TOOL_REGISTRARS = [
|
|
|
25000
25005
|
registerTasksTools
|
|
25001
25006
|
];
|
|
25002
25007
|
|
|
25008
|
+
// src/google-token.ts
|
|
25009
|
+
var TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
|
|
25010
|
+
var EXPIRY_MARGIN_MS = 12e4;
|
|
25011
|
+
var cache = /* @__PURE__ */ new Map();
|
|
25012
|
+
var inFlight = /* @__PURE__ */ new Map();
|
|
25013
|
+
async function cacheKey(refreshToken, clientId) {
|
|
25014
|
+
const data = new TextEncoder().encode(`${clientId}\0${refreshToken}`);
|
|
25015
|
+
const digest = await crypto.subtle.digest("SHA-256", data);
|
|
25016
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, "0")).join("");
|
|
25017
|
+
}
|
|
25018
|
+
function makeAccessTokenSource(env) {
|
|
25019
|
+
const direct = readEnvVar("GOG_ACCESS_TOKEN", { env });
|
|
25020
|
+
if (direct) return async () => direct;
|
|
25021
|
+
const refreshToken = readEnvVar("GOG_REFRESH_TOKEN", { env });
|
|
25022
|
+
if (!refreshToken) return void 0;
|
|
25023
|
+
const clientId = readEnvVar("GOG_CLIENT_ID", { env });
|
|
25024
|
+
const clientSecret = readEnvVar("GOG_CLIENT_SECRET", { env });
|
|
25025
|
+
if (!clientId || !clientSecret) {
|
|
25026
|
+
const missing = [!clientId && "GOG_CLIENT_ID", !clientSecret && "GOG_CLIENT_SECRET"].filter(Boolean).join(" and ");
|
|
25027
|
+
return async () => {
|
|
25028
|
+
throw new Error(
|
|
25029
|
+
`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.`
|
|
25030
|
+
);
|
|
25031
|
+
};
|
|
25032
|
+
}
|
|
25033
|
+
return async () => {
|
|
25034
|
+
const key = await cacheKey(refreshToken, clientId);
|
|
25035
|
+
const hit = cache.get(key);
|
|
25036
|
+
if (hit && hit.expiresAt - EXPIRY_MARGIN_MS > Date.now()) return hit.accessToken;
|
|
25037
|
+
let pending = inFlight.get(key);
|
|
25038
|
+
if (!pending) {
|
|
25039
|
+
pending = exchange(refreshToken, clientId, clientSecret).then((minted2) => {
|
|
25040
|
+
cache.set(key, minted2);
|
|
25041
|
+
return minted2;
|
|
25042
|
+
}).finally(() => inFlight.delete(key));
|
|
25043
|
+
inFlight.set(key, pending);
|
|
25044
|
+
}
|
|
25045
|
+
const minted = await pending;
|
|
25046
|
+
return minted.accessToken;
|
|
25047
|
+
};
|
|
25048
|
+
}
|
|
25049
|
+
async function exchange(refreshToken, clientId, clientSecret) {
|
|
25050
|
+
let res;
|
|
25051
|
+
try {
|
|
25052
|
+
res = await fetch(TOKEN_ENDPOINT, {
|
|
25053
|
+
method: "POST",
|
|
25054
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
25055
|
+
body: new URLSearchParams({
|
|
25056
|
+
grant_type: "refresh_token",
|
|
25057
|
+
refresh_token: refreshToken,
|
|
25058
|
+
client_id: clientId,
|
|
25059
|
+
client_secret: clientSecret
|
|
25060
|
+
}).toString()
|
|
25061
|
+
});
|
|
25062
|
+
} catch (err) {
|
|
25063
|
+
throw new Error(
|
|
25064
|
+
`the Google token exchange could not be reached: ${err instanceof Error ? err.message : String(err)}`
|
|
25065
|
+
);
|
|
25066
|
+
}
|
|
25067
|
+
const body = await res.json().catch(() => ({}));
|
|
25068
|
+
if (!res.ok) {
|
|
25069
|
+
if (body.error === "invalid_grant") {
|
|
25070
|
+
throw new Error(
|
|
25071
|
+
'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.'
|
|
25072
|
+
);
|
|
25073
|
+
}
|
|
25074
|
+
throw new Error(
|
|
25075
|
+
`the access token could not be refreshed (HTTP ${res.status}${body.error ? `, ${body.error}` : ""})`
|
|
25076
|
+
);
|
|
25077
|
+
}
|
|
25078
|
+
if (!body.access_token) {
|
|
25079
|
+
throw new Error("the access token could not be refreshed: Google returned no access_token");
|
|
25080
|
+
}
|
|
25081
|
+
const expiresInMs = (body.expires_in ?? 3600) * 1e3;
|
|
25082
|
+
return { accessToken: body.access_token, expiresAt: Date.now() + expiresInMs };
|
|
25083
|
+
}
|
|
25084
|
+
|
|
25003
25085
|
// src/connector-runtime.ts
|
|
25004
25086
|
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
25005
25087
|
var DEADLINE_GRACE_MS = 5e3;
|
|
25006
25088
|
var RUNNER_GOG_FAILED = 422;
|
|
25007
25089
|
var RUNNER_DRAINING = 503;
|
|
25008
|
-
function makeFlyExecutor(endpoint, key) {
|
|
25090
|
+
function makeFlyExecutor(endpoint, key, readAccessToken) {
|
|
25009
25091
|
return async (args, opts) => {
|
|
25010
25092
|
const deadlineMs = (opts?.timeout ?? DEFAULT_TIMEOUT_MS) + DEADLINE_GRACE_MS;
|
|
25093
|
+
const accessToken = await readAccessToken?.();
|
|
25011
25094
|
let res;
|
|
25012
25095
|
try {
|
|
25013
25096
|
res = await fetch(endpoint + "/run", {
|
|
@@ -25016,7 +25099,7 @@ function makeFlyExecutor(endpoint, key) {
|
|
|
25016
25099
|
Authorization: "Bearer " + key,
|
|
25017
25100
|
"Content-Type": "application/json"
|
|
25018
25101
|
},
|
|
25019
|
-
body: JSON.stringify({ args }),
|
|
25102
|
+
body: JSON.stringify(accessToken ? { args, accessToken } : { args }),
|
|
25020
25103
|
signal: AbortSignal.timeout(deadlineMs)
|
|
25021
25104
|
});
|
|
25022
25105
|
} catch (err) {
|
|
@@ -25057,7 +25140,9 @@ function useRemoteGogRunner(env = process.env) {
|
|
|
25057
25140
|
const endpoint = readEnvVar("GOG_RUNNER_URL", { env });
|
|
25058
25141
|
const key = readEnvVar("GOG_RUNNER_KEY", { env });
|
|
25059
25142
|
if (!endpoint || !key) return false;
|
|
25060
|
-
setDefaultGogExecutor(
|
|
25143
|
+
setDefaultGogExecutor(
|
|
25144
|
+
makeFlyExecutor(endpoint.replace(/\/+$/, ""), key, makeAccessTokenSource(env))
|
|
25145
|
+
);
|
|
25061
25146
|
return true;
|
|
25062
25147
|
}
|
|
25063
25148
|
export {
|
package/manifest.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"manifest_version": "0.3",
|
|
4
4
|
"name": "gogcli-mcp",
|
|
5
5
|
"display_name": "gogcli",
|
|
6
|
-
"version": "2.
|
|
6
|
+
"version": "2.21.0",
|
|
7
7
|
"description": "Google Sheets (and more) for Claude via gogcli — read, write, and manage spreadsheets",
|
|
8
8
|
"author": {
|
|
9
9
|
"name": "Chris Hall",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gogcli-mcp",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.21.0",
|
|
4
4
|
"mcpName": "io.github.chrischall/gogcli-mcp",
|
|
5
5
|
"description": "MCP server wrapping gogcli for Google service access",
|
|
6
6
|
"author": "Claude Code (AI) <https://www.anthropic.com/claude>",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"test:coverage": "vitest run --coverage"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"@chrischall/mcp-utils": "^0.
|
|
45
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
44
|
+
"@chrischall/mcp-utils": "^0.14.0",
|
|
45
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
46
46
|
"zod": "^4.4.3"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"source": "github",
|
|
8
8
|
"subfolder": "packages/gogcli-mcp"
|
|
9
9
|
},
|
|
10
|
-
"version": "2.
|
|
10
|
+
"version": "2.21.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "gogcli-mcp",
|
|
15
|
-
"version": "2.
|
|
15
|
+
"version": "2.21.0",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
},
|
package/src/connector-runtime.ts
CHANGED
|
@@ -60,9 +60,34 @@ const RUNNER_DRAINING = 503;
|
|
|
60
60
|
// clever here (flattening to `--flag=<inline>`, truncating, re-encoding) would
|
|
61
61
|
// put the payload straight back into argv and re-create the size cap this whole
|
|
62
62
|
// change exists to escape.
|
|
63
|
-
|
|
63
|
+
// `readAccessToken` is how a hosted gog acts as its CALLER rather than as
|
|
64
|
+
// whoever seeded the backend's volume (#230). The backend holds one Google
|
|
65
|
+
// identity; a token supplied with the request overrides it for that one `gog`
|
|
66
|
+
// invocation, and `gog` already prefers a directly-passed token over its store.
|
|
67
|
+
//
|
|
68
|
+
// A FUNCTION, read per call, not a string captured at construction. The whole
|
|
69
|
+
// claim being made is "this token belongs to this request", and an executor
|
|
70
|
+
// outlives any one request — on the Worker path a single isolate serves many
|
|
71
|
+
// callers, so a captured token would pin the first caller's identity onto
|
|
72
|
+
// everyone who followed. That is the same shared-identity bug this closes,
|
|
73
|
+
// rebuilt one layer up.
|
|
74
|
+
//
|
|
75
|
+
// Absent when there is no token, rather than null or "": the backend has to
|
|
76
|
+
// tell "act as this caller" from "act as the box", and an empty third state is
|
|
77
|
+
// one neither side has a meaning for.
|
|
78
|
+
export function makeFlyExecutor(
|
|
79
|
+
endpoint: string,
|
|
80
|
+
key: string,
|
|
81
|
+
readAccessToken?: () => string | undefined | Promise<string | undefined>,
|
|
82
|
+
): GogExecutor {
|
|
64
83
|
return async (args: GogArg[], opts) => {
|
|
65
84
|
const deadlineMs = (opts?.timeout ?? DEFAULT_TIMEOUT_MS) + DEADLINE_GRACE_MS;
|
|
85
|
+
// Awaited, because the token may have to be MINTED (#241): a refresh token
|
|
86
|
+
// is what a registration stores, and the access token it yields lives about
|
|
87
|
+
// an hour. Deliberately NOT caught here — if the source throws, the call
|
|
88
|
+
// fails, because the alternative is running it as the backend's identity
|
|
89
|
+
// and handing this caller someone else's account.
|
|
90
|
+
const accessToken = await readAccessToken?.();
|
|
66
91
|
let res: Response;
|
|
67
92
|
try {
|
|
68
93
|
res = await fetch(endpoint + '/run', {
|
|
@@ -71,7 +96,7 @@ export function makeFlyExecutor(endpoint: string, key: string): GogExecutor {
|
|
|
71
96
|
Authorization: 'Bearer ' + key,
|
|
72
97
|
'Content-Type': 'application/json',
|
|
73
98
|
},
|
|
74
|
-
body: JSON.stringify({ args }),
|
|
99
|
+
body: JSON.stringify(accessToken ? { args, accessToken } : { args }),
|
|
75
100
|
signal: AbortSignal.timeout(deadlineMs),
|
|
76
101
|
});
|
|
77
102
|
} catch (err) {
|