gogcli-mcp-drive 2.24.0 → 2.26.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": "Extended Google Drive for Claude via gogcli — auth + full Drive support (upload, download, permissions, comments, shared drives)",
10
- "version": "2.24.0"
10
+ "version": "2.26.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "gogcli (Drive)",
16
16
  "source": "./",
17
17
  "description": "Extended Google Drive for Claude via gogcli — auth + full Drive support (upload, download, permissions, comments, shared drives)",
18
- "version": "2.24.0",
18
+ "version": "2.26.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gogcli-mcp-drive",
3
3
  "displayName": "gogcli (Drive)",
4
- "version": "2.24.0",
4
+ "version": "2.26.0",
5
5
  "description": "Extended Google Drive for Claude via gogcli — auth + full Drive support (upload, download, permissions, comments, shared drives)",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/index.js CHANGED
@@ -31000,9 +31000,82 @@ var StdioServerTransport = class {
31000
31000
  }
31001
31001
  };
31002
31002
 
31003
+ // ../../node_modules/@chrischall/mcp-utils/dist/errors/index.js
31004
+ var McpToolError = class extends Error {
31005
+ /** Actionable remediation text, when one applies. */
31006
+ hint;
31007
+ constructor(message, opts) {
31008
+ super(message, opts?.cause !== void 0 ? { cause: opts.cause } : void 0);
31009
+ this.name = "McpToolError";
31010
+ if (opts?.hint !== void 0)
31011
+ this.hint = opts.hint;
31012
+ Object.setPrototypeOf(this, new.target.prototype);
31013
+ }
31014
+ };
31015
+ var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
31016
+ var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
31017
+ var BASIC_AUTH_RE = /(authorization\s*:\s*basic\s+)[A-Za-z0-9+/=_-]{6,}/gi;
31018
+ var SET_COOKIE_RE = /(\bset-cookie\s*:\s*)([^=;,\s]+)=[^;,\s]*/gi;
31019
+ var COOKIE_HEADER_RE = /((?<!set-)\bcookie\s*:\s*)((?:[^=;,\s]+=[^;,\s]*)(?:;\s*[^=;,\s]+=[^;,\s]*)*)/gi;
31020
+ var API_KEY_RE = new RegExp([
31021
+ "sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
31022
+ // OpenAI / Anthropic (incl. sk-ant-…)
31023
+ "gh[pousr]_[A-Za-z0-9]{36,}\\b",
31024
+ // GitHub ghp_/gho_/ghu_/ghs_/ghr_
31025
+ "xox[baprs]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])",
31026
+ // Slack
31027
+ "AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])",
31028
+ // Google API key (39 chars total)
31029
+ "AKIA[0-9A-Z]{16}\\b",
31030
+ // AWS access key id (20 chars total)
31031
+ "whsec_[A-Za-z0-9]{16,}\\b"
31032
+ // webhook signing secret (Stripe-style)
31033
+ ].map((p) => `\\b${p}`).join("|"), "g");
31034
+ var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
31035
+ var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
31036
+ var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
31037
+ var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
31038
+ var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
31039
+ function redactSecrets(text) {
31040
+ return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
31041
+ }
31042
+
31043
+ // ../../node_modules/@chrischall/mcp-utils/dist/response/index.js
31044
+ function rawTextResult(text) {
31045
+ return { content: [{ type: "text", text }] };
31046
+ }
31047
+ function errorResult(message) {
31048
+ return {
31049
+ content: [{ type: "text", text: redactSecrets(message) }],
31050
+ isError: true
31051
+ };
31052
+ }
31053
+
31003
31054
  // ../../node_modules/@chrischall/mcp-utils/dist/server/index.js
31055
+ function hintResultOrRethrow(err) {
31056
+ if (err instanceof McpToolError && err.hint) {
31057
+ return errorResult(`${err.message}
31058
+
31059
+ Hint: ${err.hint}`);
31060
+ }
31061
+ throw err;
31062
+ }
31063
+ function surfaceToolHints(server) {
31064
+ const register = server.registerTool.bind(server);
31065
+ server.registerTool = (name, config2, cb) => register(name, config2, (...args) => {
31066
+ let result;
31067
+ try {
31068
+ result = cb(...args);
31069
+ } catch (err) {
31070
+ return hintResultOrRethrow(err);
31071
+ }
31072
+ return result instanceof Promise ? result.catch(hintResultOrRethrow) : result;
31073
+ });
31074
+ }
31004
31075
  async function createMcpServer(opts) {
31005
31076
  const server = new McpServer({ name: opts.name, version: opts.version });
31077
+ if (opts.surfaceHints !== false)
31078
+ surfaceToolHints(server);
31006
31079
  if (opts.banner !== void 0) {
31007
31080
  console.error(opts.banner);
31008
31081
  }
@@ -31047,46 +31120,6 @@ async function runMcp(opts) {
31047
31120
  return server;
31048
31121
  }
31049
31122
 
31050
- // ../../node_modules/@chrischall/mcp-utils/dist/errors/index.js
31051
- var BEARER_RE = /(bearer\s+)[A-Za-z0-9._~+/=-]{8,}/gi;
31052
- var JWT_RE = /\b[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{8,}\b/g;
31053
- var BASIC_AUTH_RE = /(authorization\s*:\s*basic\s+)[A-Za-z0-9+/=_-]{6,}/gi;
31054
- var SET_COOKIE_RE = /(\bset-cookie\s*:\s*)([^=;,\s]+)=[^;,\s]*/gi;
31055
- var COOKIE_HEADER_RE = /((?<!set-)\bcookie\s*:\s*)((?:[^=;,\s]+=[^;,\s]*)(?:;\s*[^=;,\s]+=[^;,\s]*)*)/gi;
31056
- var API_KEY_RE = new RegExp([
31057
- "sk-[A-Za-z0-9_-]{20,}(?![A-Za-z0-9_-])",
31058
- // OpenAI / Anthropic (incl. sk-ant-…)
31059
- "gh[pousr]_[A-Za-z0-9]{36,}\\b",
31060
- // GitHub ghp_/gho_/ghu_/ghs_/ghr_
31061
- "xox[baprs]-[A-Za-z0-9-]{10,}(?![A-Za-z0-9-])",
31062
- // Slack
31063
- "AIza[0-9A-Za-z_-]{35}(?![0-9A-Za-z_-])",
31064
- // Google API key (39 chars total)
31065
- "AKIA[0-9A-Z]{16}\\b",
31066
- // AWS access key id (20 chars total)
31067
- "whsec_[A-Za-z0-9]{16,}\\b"
31068
- // webhook signing secret (Stripe-style)
31069
- ].map((p) => `\\b${p}`).join("|"), "g");
31070
- var QUERY_SECRET_RE = /([?&](?:access_token|refresh_token|client_secret|api_?key|signature|token|key|sig)=)[^&#\s"'<>`]+/gi;
31071
- var AWS_SIGV4_RE = /([?&]X-Amz-(?:Signature|Security-Token|Credential)=)[^&#\s"'<>`]+/gi;
31072
- var JSON_SECRET_KEYS = "access_token|refresh_token|client_secret|api_?key|password|passwd|secret|token";
31073
- var JSON_SECRET_DQ_RE = new RegExp(`("(?:${JSON_SECRET_KEYS})"\\s*:\\s*")[^"]*(")`, "gi");
31074
- var JSON_SECRET_SQ_RE = new RegExp(`('(?:${JSON_SECRET_KEYS})'\\s*:\\s*')[^']*(')`, "gi");
31075
- function redactSecrets(text) {
31076
- return text.replace(BEARER_RE, "$1[REDACTED]").replace(BASIC_AUTH_RE, "$1[REDACTED]").replace(SET_COOKIE_RE, "$1$2=[REDACTED]").replace(COOKIE_HEADER_RE, (_m, prefix, pairs) => `${prefix}${pairs.replace(/=[^;,\s]*/g, "=[REDACTED]")}`).replace(API_KEY_RE, "[REDACTED]").replace(QUERY_SECRET_RE, "$1[REDACTED]").replace(AWS_SIGV4_RE, "$1[REDACTED]").replace(JSON_SECRET_DQ_RE, "$1[REDACTED]$2").replace(JSON_SECRET_SQ_RE, "$1[REDACTED]$2").replace(JWT_RE, "[REDACTED]");
31077
- }
31078
-
31079
- // ../../node_modules/@chrischall/mcp-utils/dist/response/index.js
31080
- function rawTextResult(text) {
31081
- return { content: [{ type: "text", text }] };
31082
- }
31083
- function errorResult(message) {
31084
- return {
31085
- content: [{ type: "text", text: redactSecrets(message) }],
31086
- isError: true
31087
- };
31088
- }
31089
-
31090
31123
  // ../../node_modules/@chrischall/mcp-utils/dist/config/index.js
31091
31124
  var PLACEHOLDER_RE = /^\$\{[^}]*\}$/;
31092
31125
  function readEnvVar(key, opts = {}) {
@@ -31177,10 +31210,11 @@ function sanitizedEnv() {
31177
31210
  }
31178
31211
  return result;
31179
31212
  }
31213
+ var TOKEN_LEFT_BOUNDARY = "(?<![A-Za-z0-9+/])";
31180
31214
  var GOOGLE_TOKEN_PATTERNS = [
31181
- /ya29\.[A-Za-z0-9._\-]+/g,
31215
+ new RegExp(`${TOKEN_LEFT_BOUNDARY}ya29\\.[A-Za-z0-9._\\-]+`, "g"),
31182
31216
  // OAuth2 access tokens
31183
- /1\/\/[A-Za-z0-9._\-]+/g
31217
+ new RegExp(`${TOKEN_LEFT_BOUNDARY}1//[A-Za-z0-9._\\-]+`, "g")
31184
31218
  // OAuth2 refresh tokens
31185
31219
  ];
31186
31220
  function redactGoogleTokens(text) {
@@ -31193,6 +31227,26 @@ function redactGoogleTokens(text) {
31193
31227
  function redactSecrets2(text) {
31194
31228
  return redactGoogleTokens(redactSecrets(text));
31195
31229
  }
31230
+ var OPAQUE_FIELD_VALUE = "[A-Za-z0-9+/_-]{16,}={0,2}";
31231
+ var opaquePlaceholder = (i) => `\0gogOpaque${i}\0`;
31232
+ function redactPreservingOpaqueFields(text, fields, redact) {
31233
+ const lifted = [];
31234
+ let staged = text;
31235
+ for (const field of fields) {
31236
+ const escaped = field.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
31237
+ const re = new RegExp(`("${escaped}"\\s*:\\s*")(${OPAQUE_FIELD_VALUE})(")`, "g");
31238
+ staged = staged.replace(re, (_m, open, value, close) => {
31239
+ lifted.push(value);
31240
+ return `${open}${opaquePlaceholder(lifted.length - 1)}${close}`;
31241
+ });
31242
+ }
31243
+ if (lifted.length === 0) return redact(text);
31244
+ let redacted = redact(staged);
31245
+ lifted.forEach((value, i) => {
31246
+ redacted = redacted.split(opaquePlaceholder(i)).join(value);
31247
+ });
31248
+ return redacted;
31249
+ }
31196
31250
  function augmentedPath() {
31197
31251
  const home = process.env.HOME;
31198
31252
  const candidates = [
@@ -31223,19 +31277,24 @@ function formatTimeout(ms) {
31223
31277
  return `${ms}ms`;
31224
31278
  }
31225
31279
  async function spawnWithTempFiles(args, opts) {
31226
- const { mkdtemp, writeFile, rm } = await import("node:fs/promises");
31280
+ const { mkdtemp, mkdir, writeFile, rm } = await import("node:fs/promises");
31227
31281
  const { tmpdir } = await import("node:os");
31228
31282
  const dir = await mkdtemp(join(tmpdir(), "gogcli-mcp-"));
31229
31283
  try {
31230
31284
  const argv = [];
31285
+ let seq = 0;
31231
31286
  for (const arg of args) {
31232
31287
  if (!isGogFileArg(arg)) {
31233
31288
  argv.push(arg);
31234
31289
  continue;
31235
31290
  }
31236
- const path = join(dir, `${arg.flag}.${arg.ext ?? "txt"}`);
31237
- await writeFile(path, arg.contents, { encoding: "utf8", mode: 384 });
31238
- argv.push(`--${arg.flag}=${path}`);
31291
+ const sub = join(dir, String(seq));
31292
+ seq += 1;
31293
+ await mkdir(sub, { recursive: true, mode: 448 });
31294
+ const path = join(sub, arg.filename ?? `${arg.flag}.${arg.ext ?? "txt"}`);
31295
+ const data = arg.encoding === "base64" ? Buffer.from(arg.contents, "base64") : Buffer.from(arg.contents, "utf8");
31296
+ await writeFile(path, data, { mode: 384 });
31297
+ argv.push(arg.positional ? path : `--${arg.flag}=${path}`);
31239
31298
  }
31240
31299
  return await spawnGog(argv, opts);
31241
31300
  } finally {
@@ -31320,8 +31379,9 @@ function assembleArgs(args, opts) {
31320
31379
  return fullArgs;
31321
31380
  }
31322
31381
  async function run(args, options = {}) {
31323
- const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full" } = options;
31324
- const redact = redactMode === "tokens" ? redactGoogleTokens : redactSecrets2;
31382
+ const { account, spawner, interactive = false, timeout, readonly: readonly2 = false, redactMode = "full", opaqueFields } = options;
31383
+ const base = redactMode === "tokens" ? redactGoogleTokens : redactSecrets2;
31384
+ const redact = opaqueFields?.length ? (text) => redactPreservingOpaqueFields(text, opaqueFields, base) : base;
31325
31385
  const fullArgs = assembleArgs(args, { account, interactive, readonly: readonly2 });
31326
31386
  const store = activeExecutor();
31327
31387
  try {
@@ -31335,7 +31395,7 @@ async function run(args, options = {}) {
31335
31395
  }
31336
31396
  return redact(output);
31337
31397
  } catch (err) {
31338
- const message = redact(err instanceof Error ? err.message : String(err));
31398
+ const message = base(err instanceof Error ? err.message : String(err));
31339
31399
  if (isRunnerTransportError(err)) {
31340
31400
  throw new RunnerTransportError(message, err.kind, err.status);
31341
31401
  }
@@ -31871,6 +31931,71 @@ function authToolsFor(defaultServices) {
31871
31931
  return (server) => registerAuthToolsWith(server, defaultServices);
31872
31932
  }
31873
31933
 
31934
+ // ../gogcli-mcp/src/tools/calendar.ts
31935
+ var reminderParams = {
31936
+ reminders: external_exports.array(external_exports.string()).max(5).optional().describe(
31937
+ `Reminders as method:duration, e.g. ["popup:30m", "email:1d"]. Method is popup or email; duration accepts m/h/d (max 40320 minutes = 4 weeks). Google allows at most 5. These REPLACE the event's reminders \u2014 on update, pass an EMPTY array to drop custom reminders and go back to the calendar's defaults. Cannot be combined with noReminders.`
31938
+ ),
31939
+ noReminders: external_exports.boolean().optional().describe(
31940
+ "Give the event no reminders at all, overriding the calendar's defaults. Different from an empty reminders array, which RESTORES those defaults. Cannot be combined with reminders."
31941
+ )
31942
+ };
31943
+
31944
+ // ../gogcli-mcp/src/attachments.ts
31945
+ var MAX_INLINE_ATTACHMENT_BYTES = 8 * 1024 * 1024;
31946
+ var RUNNER_MAX_BODY_BYTES = 32 * 1024 * 1024;
31947
+ var RUNNER_BODY_JSON_RESERVE_BYTES = 256 * 1024;
31948
+ var MAX_REQUEST_PAYLOAD_WIRE_BYTES = RUNNER_MAX_BODY_BYTES - RUNNER_BODY_JSON_RESERVE_BYTES;
31949
+ var MAX_INLINE_ATTACHMENT_TOTAL_BYTES = Math.floor(MAX_REQUEST_PAYLOAD_WIRE_BYTES * 3 / 4);
31950
+ var formatMiB = (bytes) => `${Math.floor(bytes / (1024 * 1024))} MiB`;
31951
+ var INLINE_ATTACHMENT_LIMITS_TEXT = `up to ${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)} per file and ${formatMiB(MAX_INLINE_ATTACHMENT_TOTAL_BYTES)} in total`;
31952
+ var inlineAttachmentSchema = external_exports.object({
31953
+ filename: external_exports.string().min(1).describe(
31954
+ `Filename the recipient will see, e.g. "pendant-layouts.png". gog infers the attachment's MIME type from this extension, so give it the right one \u2014 a .png sent as "layouts" arrives as an untyped blob. Must be a single filename, not a path.`
31955
+ ),
31956
+ contentBase64: external_exports.string().min(1).describe(
31957
+ "The file's bytes, base64-encoded (standard alphabet, with padding). This is the whole point of this parameter: the bytes travel with the request, so nothing needs to exist on the gog server's filesystem."
31958
+ )
31959
+ });
31960
+ var attachInlineParam = external_exports.array(inlineAttachmentSchema).optional().describe(
31961
+ `Attachments supplied as BYTES rather than as server-side paths \u2014 use this whenever you hold a file and the gog server does not, which is always the case on the hosted connector and on any remote deployment. Each entry is {filename, contentBase64} (${INLINE_ATTACHMENT_LIMITS_TEXT}). Can be combined with \`attach\`: the two name disjoint files (paths read on the server vs. bytes sent with the call), and both end up as ordinary attachments on the message.`
31962
+ );
31963
+ function validateFilename(filename, where) {
31964
+ if (/[/\\]/.test(filename)) {
31965
+ throw new Error(
31966
+ `${where}: filename ${JSON.stringify(filename)} must be a bare filename, not a path. Pass just the name the recipient should see, e.g. "report.pdf".`
31967
+ );
31968
+ }
31969
+ if (/[\x00-\x1f]/.test(filename) || /^\.+$/.test(filename) || filename.length > 200) {
31970
+ throw new Error(
31971
+ `${where}: filename ${JSON.stringify(filename)} is not a usable filename (no control characters, not "."/"..", 200 characters max).`
31972
+ );
31973
+ }
31974
+ }
31975
+ function decodedLength(contentBase64) {
31976
+ const buf = Buffer.from(contentBase64, "base64");
31977
+ return buf.toString("base64") === contentBase64 ? buf.length : null;
31978
+ }
31979
+ function inlineFileArg(flag, attachment, opts = {}) {
31980
+ const { filename, contentBase64 } = attachment;
31981
+ const where = opts.where ?? `attachInline entry ${JSON.stringify(filename)}`;
31982
+ validateFilename(filename, where);
31983
+ const bytes = decodedLength(contentBase64);
31984
+ if (bytes === null) {
31985
+ throw new Error(
31986
+ `${where}: contents are not valid base64. Send the standard alphabet with padding and no line breaks \u2014 the value must survive a decode/re-encode round trip unchanged.`
31987
+ );
31988
+ }
31989
+ if (bytes > MAX_INLINE_ATTACHMENT_BYTES) {
31990
+ throw new Error(
31991
+ `${where}: ${bytes} bytes exceeds the ${MAX_INLINE_ATTACHMENT_BYTES}-byte (${formatMiB(MAX_INLINE_ATTACHMENT_BYTES)}) per-file limit for inline content. Upload it to Drive and link to it instead, or send it from a local (stdio) deployment using a real server-side path.`
31992
+ );
31993
+ }
31994
+ const arg = { kind: "file", flag, contents: contentBase64, encoding: "base64", filename };
31995
+ if (opts.positional) arg.positional = true;
31996
+ return { arg, bytes };
31997
+ }
31998
+
31874
31999
  // ../gogcli-mcp/src/tools/drive.ts
31875
32000
  var GOOGLE_DOC_MIME = "application/vnd.google-apps.document";
31876
32001
  function fileMeta(raw) {
@@ -32096,7 +32221,7 @@ var failIfNotEmptyParam = external_exports.boolean().optional().describe(
32096
32221
  );
32097
32222
 
32098
32223
  // ../gogcli-mcp/src/server.ts
32099
- var VERSION = true ? "2.24.0" : "0.0.0";
32224
+ var VERSION = true ? "2.26.0" : "0.0.0";
32100
32225
 
32101
32226
  // ../gogcli-mcp/src/auth-log.ts
32102
32227
  var FAILURES = /* @__PURE__ */ new Set([
@@ -32602,10 +32727,11 @@ function registerExtraDriveTools(server) {
32602
32727
  return runOrDiagnose(args, { account });
32603
32728
  });
32604
32729
  server.registerTool("gog_drive_upload", {
32605
- description: 'Upload a local file to Drive. Use replace to replace the content of an existing file (preserves link/permissions), or convert to auto-convert to a Google format. Pair replace with ifVersion to make the overwrite conditional: gog sends an atomic If-Match precondition and reports a conflict \u2014 applying nothing \u2014 if the file changed since you read it, instead of silently clobbering a concurrent edit. No read tool surfaces the version number: fetch it immediately before uploading with gog_drive_run { subcommand: "raw", args: ["<fileId>", "--fields=version"] } \u2014 it comes back as a JSON string, so pass it on as a number. Conditional replacement refuses Google Workspace files (Docs/Sheets/Slides), which have no replaceable binary content.',
32730
+ description: `Upload a file to Drive. Supply it EITHER as localPath (a path on the gog server's own filesystem \u2014 local stdio deployments only) OR as content (the bytes, base64-encoded, carried with the request), which is what works when this server runs remotely from you and no path you can name exists on it. Use replace to replace the content of an existing file (preserves link/permissions), or convert to auto-convert to a Google format. Pair replace with ifVersion to make the overwrite conditional: gog sends an atomic If-Match precondition and reports a conflict \u2014 applying nothing \u2014 if the file changed since you read it, instead of silently clobbering a concurrent edit. No read tool surfaces the version number: fetch it immediately before uploading with gog_drive_run { subcommand: "raw", args: ["<fileId>", "--fields=version"] } \u2014 it comes back as a JSON string, so pass it on as a number. Conditional replacement refuses Google Workspace files (Docs/Sheets/Slides), which have no replaceable binary content.`,
32606
32731
  annotations: { destructiveHint: true },
32607
32732
  inputSchema: {
32608
- localPath: external_exports.string().describe("Path to the local file to upload"),
32733
+ localPath: external_exports.string().optional().describe(`Path to the file to upload, resolved ON THE GOG SERVER's filesystem \u2014 NOT this client's. Only usable when gog runs on the same machine you do (local stdio); on the hosted connector or any GOG_RUNNER_URL backend this path does not exist and the call fails with "no such file or directory" \u2014 use content there. Exactly one of localPath / content is required.`),
32734
+ content: external_exports.string().optional().describe("The file's bytes, base64-encoded (standard alphabet, with padding) \u2014 upload a file you hold without it existing anywhere on the gog server. This is the only route that works when the caller and gog share no filesystem. Requires name (there is no path to take a filename from). Max 8 MiB; for anything larger use localPath from a local deployment. Exactly one of localPath / content is required \u2014 supplying both is an error, not a precedence rule."),
32609
32735
  name: external_exports.string().optional().describe("Override filename (create) or rename target (replace)"),
32610
32736
  parent: external_exports.string().optional().describe("Destination folder ID (create only)"),
32611
32737
  replace: external_exports.string().optional().describe("Replace content of an existing Drive file ID (preserves link/permissions). Unconditional unless ifVersion is set."),
@@ -32618,11 +32744,29 @@ function registerExtraDriveTools(server) {
32618
32744
  convertTo: external_exports.string().optional().describe("Convert to a specific Google format: doc | sheet | slides (create only)"),
32619
32745
  account: accountParam
32620
32746
  }
32621
- }, async ({ localPath, name, parent, replace, ifVersion, mimeType, keepRevisionForever, convert, convertTo, account }) => {
32747
+ }, async ({ localPath, content, name, parent, replace, ifVersion, mimeType, keepRevisionForever, convert, convertTo, account }) => {
32622
32748
  if (ifVersion !== void 0 && !replace) {
32623
32749
  throw new Error("ifVersion requires replace: a version precondition only applies when replacing an existing Drive file.");
32624
32750
  }
32625
- const args = ["drive", "upload", localPath];
32751
+ if (localPath === void 0 === (content === void 0)) {
32752
+ throw new Error(
32753
+ "Pass exactly one of localPath or content. localPath reads a file on the GOG SERVER's filesystem (local stdio deployments only); content carries the bytes with the request, base64-encoded, and is what works when this server runs remotely from you."
32754
+ );
32755
+ }
32756
+ let pathArg;
32757
+ if (localPath !== void 0) {
32758
+ pathArg = localPath;
32759
+ } else {
32760
+ if (!name) {
32761
+ throw new Error("content requires name: there is no path to derive the Drive filename from.");
32762
+ }
32763
+ pathArg = inlineFileArg(
32764
+ "localPath",
32765
+ { filename: name, contentBase64: content },
32766
+ { positional: true, where: "content" }
32767
+ ).arg;
32768
+ }
32769
+ const args = ["drive", "upload", pathArg];
32626
32770
  if (name) args.push(`--name=${name}`);
32627
32771
  if (parent) args.push(`--parent=${parent}`);
32628
32772
  if (replace) args.push(`--replace=${replace}`);
package/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifest_version": "0.3",
4
4
  "name": "gogcli-mcp-drive",
5
5
  "display_name": "gogcli (Drive)",
6
- "version": "2.24.0",
6
+ "version": "2.26.0",
7
7
  "description": "Extended Google Drive for Claude via gogcli — auth + full Drive support (upload, download, permissions, comments, shared drives)",
8
8
  "author": {
9
9
  "name": "Chris Hall",
@@ -139,7 +139,7 @@
139
139
  },
140
140
  {
141
141
  "name": "gog_drive_upload",
142
- "description": "Upload a local file, optionally replacing an existing file (unconditionally, or only at a given version) or converting to Google format"
142
+ "description": "Upload a file to Drive from a server-side path (localPath) or from base64 bytes sent with the call (content, for remote deployments with no shared filesystem), optionally replacing an existing file (unconditionally, or only at a given version) or converting to Google format"
143
143
  },
144
144
  {
145
145
  "name": "gog_drive_sync_push",
package/mint.yaml ADDED
@@ -0,0 +1,107 @@
1
+ version: 1
2
+ name: Google Drive (gogcli)
3
+ slug: gogcli-mcp-drive
4
+ summary: >-
5
+ Extended Google Drive MCP server via gogcli — auth + full Drive support
6
+ (upload/download/permissions/comments/shared drives)
7
+ command:
8
+ # This package publishes a single bin; naming it keeps the install
9
+ # unambiguous alongside its eight sibling packages.
10
+ bin: gogcli-mcp-drive
11
+ env:
12
+ - name: GOG_CLIENT_ID
13
+ required: false
14
+ help: >-
15
+ Google OAuth client id. NOTE: on the local-spawn path the child `gog`
16
+ receives a sanitized env — runner.ts drops GOG_ACCESS_TOKEN and every
17
+ *_TOKEN / *_SECRET / *_API_KEY / *_PRIVATE_KEY variable — so the CLI
18
+ authenticates from its own stored credentials under $HOME (see
19
+ state.dataDir), not from these variables being passed through.
20
+ - name: GOG_CLIENT_SECRET
21
+ secret: true
22
+ required: false
23
+ help: >-
24
+ Google OAuth client secret. Stripped from the spawned CLI's environment
25
+ by runner.ts's *_SECRET rule — see GOG_CLIENT_ID.
26
+ - name: GOG_REFRESH_TOKEN
27
+ secret: true
28
+ required: false
29
+ help: >-
30
+ Google OAuth refresh token. Stripped from the spawned CLI's environment
31
+ by runner.ts's *_TOKEN rule — see GOG_CLIENT_ID. A hosted deployment must
32
+ therefore carry gog's authorised-account state in its persisted data dir,
33
+ or drive a remote runner via GOG_RUNNER_URL.
34
+ - name: GOG_ACCESS_TOKEN
35
+ secret: true
36
+ required: false
37
+ help: >-
38
+ Deliberately removed from the spawned CLI's environment by runner.ts, so
39
+ that a stale directly-passed token cannot shadow gog's stored refresh
40
+ credential. Leave unset.
41
+ - name: GOG_ACCOUNT
42
+ required: false
43
+ help: >-
44
+ Which configured Google account to act as, when more than one is
45
+ authorised. Defaults to the single/most recent account.
46
+ - name: GOG_READONLY
47
+ required: false
48
+ help: >-
49
+ Set to 1 to refuse every mutating operation. Recommended when the
50
+ connector is shared or you only need reads.
51
+ - name: GOG_PATH
52
+ required: false
53
+ help: >-
54
+ Path to the `gog` binary. Leave unset when the dependency below supplies
55
+ it; set it only to point at a binary you manage yourself.
56
+ - name: GOG_RUNNER_URL
57
+ required: false
58
+ help: >-
59
+ URL of a remote gog runner to execute against instead of spawning the
60
+ local binary. If you set this, add its host to egress.allow.
61
+ - name: GOG_RUNNER_KEY
62
+ secret: true
63
+ required: false
64
+ help: >-
65
+ Shared key authenticating calls to GOG_RUNNER_URL. Required whenever that
66
+ is set.
67
+ - name: GOG_TIMEZONE
68
+ required: false
69
+ help: >-
70
+ The IANA zone gog itself formats its naive timestamps in. The wrapper
71
+ reads it (naiveSourceTimeZone) to re-attach the correct offset, so it
72
+ should match gog's own configuration. Falls back to DISPLAY_TZ, then to
73
+ America/New_York.
74
+ - name: DISPLAY_TZ
75
+ required: false
76
+ help: >-
77
+ The IANA zone every rendered *Display field uses (displayTimeZone,
78
+ default America/New_York). It does not read GOG_TIMEZONE — the fallback
79
+ runs the other way, so this is also what GOG_TIMEZONE falls back to. An
80
+ invalid value degrades to the default rather than throwing.
81
+ dependencies:
82
+ # Every tool shells out to the `gog` CLI; without it the server starts and
83
+ # then fails on the first call. This tag must track
84
+ # packages/gogcli-mcp/src/runner.ts's MIN_GOG_VERSION (the floor the tools
85
+ # assume) and the fly-gog-runner/Dockerfile GOG_VERSION build arg. See
86
+ # CLAUDE.md "Required gog version" — bumping the floor means bumping these
87
+ # nine pins too.
88
+ - kind: github-release
89
+ repo: openclaw/gogcli
90
+ tag: v0.38.1
91
+ asset: "gogcli_*_linux_amd64.tar.gz"
92
+ bin: [gog]
93
+ state:
94
+ dataDir: true
95
+ reason: >-
96
+ `gog` keeps its authorised-account state and token cache under $HOME.
97
+ Without a persistent data dir every cold start has no account configured and
98
+ every tool call fails until the credentials are re-supplied.
99
+ egress:
100
+ allow:
101
+ # Google OAuth token exchange and the Google REST APIs the CLI calls.
102
+ - oauth2.googleapis.com
103
+ - www.googleapis.com
104
+ - googleapis.com
105
+ #
106
+ # NOTE: if you set GOG_RUNNER_URL to run against a remote gog runner, add
107
+ # that host here too — it is supplied at runtime and cannot be declared.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-drive",
3
- "version": "2.24.0",
3
+ "version": "2.26.0",
4
4
  "mcpName": "io.github.chrischall/gogcli-mcp-drive",
5
5
  "description": "Extended Google Drive MCP server via gogcli — auth + full Drive support (upload/download/permissions/comments/shared drives)",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -24,7 +24,7 @@
24
24
  "test:coverage": "vitest run --coverage"
25
25
  },
26
26
  "dependencies": {
27
- "@chrischall/mcp-utils": "^0.14.0",
27
+ "@chrischall/mcp-utils": "^0.15.0",
28
28
  "@modelcontextprotocol/sdk": "^1.30.0",
29
29
  "zod": "^4.4.3"
30
30
  },
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "source": "github",
8
8
  "subfolder": "packages/gogcli-mcp-drive"
9
9
  },
10
- "version": "2.24.0",
10
+ "version": "2.26.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp-drive",
15
- "version": "2.24.0",
15
+ "version": "2.26.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -1,6 +1,7 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { z } from 'zod';
3
- import { accountParam, runOrDiagnose, paginationParams, pushPaginationFlags, pageTokenParam, pageAliasParam, resolvePageToken} from '../../../gogcli-mcp/src/lib.js';
3
+ import { accountParam, runOrDiagnose, paginationParams, pushPaginationFlags, pageTokenParam, pageAliasParam, resolvePageToken, inlineFileArg} from '../../../gogcli-mcp/src/lib.js';
4
+ import type { GogArg } from '../../../gogcli-mcp/src/lib.js';
4
5
 
5
6
  export function registerExtraDriveTools(server: McpServer): void {
6
7
  server.registerTool('gog_drive_download', {
@@ -23,7 +24,10 @@ export function registerExtraDriveTools(server: McpServer): void {
23
24
 
24
25
  server.registerTool('gog_drive_upload', {
25
26
  description:
26
- 'Upload a local file to Drive. Use replace to replace the content of an existing file (preserves link/permissions), ' +
27
+ 'Upload a file to Drive. Supply it EITHER as localPath (a path on the gog server\'s own filesystem local ' +
28
+ 'stdio deployments only) OR as content (the bytes, base64-encoded, carried with the request), which is what ' +
29
+ 'works when this server runs remotely from you and no path you can name exists on it. ' +
30
+ 'Use replace to replace the content of an existing file (preserves link/permissions), ' +
27
31
  'or convert to auto-convert to a Google format. ' +
28
32
  'Pair replace with ifVersion to make the overwrite conditional: gog sends an atomic If-Match precondition and reports ' +
29
33
  'a conflict — applying nothing — if the file changed since you read it, instead of silently clobbering a concurrent edit. ' +
@@ -32,7 +36,8 @@ export function registerExtraDriveTools(server: McpServer): void {
32
36
  'Conditional replacement refuses Google Workspace files (Docs/Sheets/Slides), which have no replaceable binary content.',
33
37
  annotations: { destructiveHint: true },
34
38
  inputSchema: {
35
- localPath: z.string().describe('Path to the local file to upload'),
39
+ localPath: z.string().optional().describe('Path to the file to upload, resolved ON THE GOG SERVER\'s filesystem — NOT this client\'s. Only usable when gog runs on the same machine you do (local stdio); on the hosted connector or any GOG_RUNNER_URL backend this path does not exist and the call fails with "no such file or directory" — use content there. Exactly one of localPath / content is required.'),
40
+ content: z.string().optional().describe('The file\'s bytes, base64-encoded (standard alphabet, with padding) — upload a file you hold without it existing anywhere on the gog server. This is the only route that works when the caller and gog share no filesystem. Requires name (there is no path to take a filename from). Max 8 MiB; for anything larger use localPath from a local deployment. Exactly one of localPath / content is required — supplying both is an error, not a precedence rule.'),
36
41
  name: z.string().optional().describe('Override filename (create) or rename target (replace)'),
37
42
  parent: z.string().optional().describe('Destination folder ID (create only)'),
38
43
  replace: z.string().optional().describe('Replace content of an existing Drive file ID (preserves link/permissions). Unconditional unless ifVersion is set.'),
@@ -47,11 +52,37 @@ export function registerExtraDriveTools(server: McpServer): void {
47
52
  convertTo: z.string().optional().describe('Convert to a specific Google format: doc | sheet | slides (create only)'),
48
53
  account: accountParam,
49
54
  },
50
- }, async ({ localPath, name, parent, replace, ifVersion, mimeType, keepRevisionForever, convert, convertTo, account }) => {
55
+ }, async ({ localPath, content, name, parent, replace, ifVersion, mimeType, keepRevisionForever, convert, convertTo, account }) => {
51
56
  if (ifVersion !== undefined && !replace) {
52
57
  throw new Error('ifVersion requires replace: a version precondition only applies when replacing an existing Drive file.');
53
58
  }
54
- const args = ['drive', 'upload', localPath];
59
+ // Explicit either/or, never a precedence rule: a caller who sends both has
60
+ // two different files in mind and silently picking one would upload the
61
+ // wrong bytes under the right name.
62
+ if ((localPath === undefined) === (content === undefined)) {
63
+ throw new Error(
64
+ 'Pass exactly one of localPath or content. localPath reads a file on the GOG SERVER\'s filesystem '
65
+ + '(local stdio deployments only); content carries the bytes with the request, base64-encoded, and '
66
+ + 'is what works when this server runs remotely from you.',
67
+ );
68
+ }
69
+ // The positional <localPath> argument: either the caller's real server-side
70
+ // path, or a temp file the executor materializes next to gog from the bytes
71
+ // they sent. gog cannot tell the two apart, which is the point.
72
+ let pathArg: GogArg;
73
+ if (localPath !== undefined) {
74
+ pathArg = localPath;
75
+ } else {
76
+ if (!name) {
77
+ throw new Error('content requires name: there is no path to derive the Drive filename from.');
78
+ }
79
+ pathArg = inlineFileArg(
80
+ 'localPath',
81
+ { filename: name, contentBase64: content as string },
82
+ { positional: true, where: 'content' },
83
+ ).arg;
84
+ }
85
+ const args: GogArg[] = ['drive', 'upload', pathArg];
55
86
  if (name) args.push(`--name=${name}`);
56
87
  if (parent) args.push(`--parent=${parent}`);
57
88
  if (replace) args.push(`--replace=${replace}`);
@@ -104,6 +104,85 @@ describe('gog_drive_upload', () => {
104
104
  expect(lib.runOrDiagnose).not.toHaveBeenCalled();
105
105
  });
106
106
 
107
+ // ==========================================================================
108
+ // INLINE CONTENT — uploading a file the caller holds but the gog server does
109
+ // not, which is every remote deployment. `content` replaces the positional
110
+ // localPath with a temp file the executor materializes beside gog.
111
+ // ==========================================================================
112
+ describe('content (inline bytes)', () => {
113
+ const PNG = Buffer.from([0x89, 0x50, 0x4e, 0x47]).toString('base64');
114
+
115
+ it('materializes content as the POSITIONAL path argument', async () => {
116
+ await harness.callTool('gog_drive_upload', { content: PNG, name: 'pendant-layouts.png' });
117
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
118
+ [
119
+ 'drive', 'upload',
120
+ { kind: 'file', flag: 'localPath', contents: PNG, encoding: 'base64', filename: 'pendant-layouts.png', positional: true },
121
+ '--name=pendant-layouts.png',
122
+ ],
123
+ { account: undefined },
124
+ );
125
+ });
126
+
127
+ it('carries the caller mimeType through, which gog CAN honour here', async () => {
128
+ await harness.callTool('gog_drive_upload', { content: PNG, name: 'a.png', mimeType: 'image/png' });
129
+ const args = vi.mocked(lib.runOrDiagnose).mock.calls[0][0];
130
+ expect(args).toContain('--mime-type=image/png');
131
+ });
132
+
133
+ it('rejects supplying BOTH localPath and content — never a precedence rule', async () => {
134
+ const result = await harness.callTool('gog_drive_upload', { localPath: '/tmp/x.png', content: PNG, name: 'x.png' });
135
+ expect(result.isError).toBe(true);
136
+ expect(result.content[0].text).toMatch(/Pass exactly one of localPath or content/);
137
+ expect(lib.runOrDiagnose).not.toHaveBeenCalled();
138
+ });
139
+
140
+ it('rejects supplying NEITHER localPath nor content', async () => {
141
+ const result = await harness.callTool('gog_drive_upload', { name: 'x.png' });
142
+ expect(result.isError).toBe(true);
143
+ expect(result.content[0].text).toMatch(/Pass exactly one of localPath or content/);
144
+ expect(lib.runOrDiagnose).not.toHaveBeenCalled();
145
+ });
146
+
147
+ it('requires name with content, since there is no path to derive it from', async () => {
148
+ const result = await harness.callTool('gog_drive_upload', { content: PNG });
149
+ expect(result.isError).toBe(true);
150
+ expect(result.content[0].text).toMatch(/content requires name/);
151
+ expect(lib.runOrDiagnose).not.toHaveBeenCalled();
152
+ });
153
+
154
+ it('rejects invalid base64 rather than uploading a corrupt file', async () => {
155
+ const result = await harness.callTool('gog_drive_upload', { content: 'not!base64!', name: 'a.png' });
156
+ expect(result.isError).toBe(true);
157
+ expect(result.content[0].text).toMatch(/not valid base64/);
158
+ expect(lib.runOrDiagnose).not.toHaveBeenCalled();
159
+ });
160
+
161
+ it('rejects content over the 8 MiB ceiling with a message naming the limit', async () => {
162
+ const result = await harness.callTool('gog_drive_upload', {
163
+ content: Buffer.alloc(8 * 1024 * 1024 + 1).toString('base64'),
164
+ name: 'huge.bin',
165
+ });
166
+ expect(result.isError).toBe(true);
167
+ expect(result.content[0].text).toMatch(/per-file limit/);
168
+ expect(lib.runOrDiagnose).not.toHaveBeenCalled();
169
+ });
170
+
171
+ it('keeps a spaced, non-ASCII name intact as the Drive filename', async () => {
172
+ await harness.callTool('gog_drive_upload', { content: PNG, name: 'Reçu — étude 2026.png' });
173
+ const args = vi.mocked(lib.runOrDiagnose).mock.calls[0][0];
174
+ expect(args[2]).toMatchObject({ filename: 'Reçu — étude 2026.png' });
175
+ expect(args).toContain('--name=Reçu — étude 2026.png');
176
+ });
177
+
178
+ // The existing path-based contract must be untouched: a plain string
179
+ // positional, no file arg, for every local stdio caller that has one.
180
+ it('leaves the localPath path unchanged', async () => {
181
+ await harness.callTool('gog_drive_upload', { localPath: '/tmp/x.txt' });
182
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['drive', 'upload', '/tmp/x.txt'], { account: undefined });
183
+ });
184
+ });
185
+
107
186
  // No gog read command surfaces the Drive `version` int: driveFileGetFields
108
187
  // and info_via_drive.go both omit it, and `drive upload` itself is the only
109
188
  // caller that requests it. `drive raw` (fields=*) is the sole way to read it,