gogcli-mcp-sheets 4.5.1 → 4.6.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 Sheets for Claude via gogcli — auth + full Sheets support",
10
- "version": "4.5.1"
10
+ "version": "4.6.0"
11
11
  },
12
12
  "plugins": [
13
13
  {
@@ -15,7 +15,7 @@
15
15
  "displayName": "gogcli (Sheets)",
16
16
  "source": "./",
17
17
  "description": "Extended Google Sheets for Claude via gogcli — auth + full Sheets support",
18
- "version": "4.5.1",
18
+ "version": "4.6.0",
19
19
  "author": {
20
20
  "name": "Chris Hall"
21
21
  },
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "gogcli-mcp-sheets",
3
3
  "displayName": "gogcli (Sheets)",
4
- "version": "4.5.1",
4
+ "version": "4.6.0",
5
5
  "description": "Extended Google Sheets for Claude via gogcli — auth + full Sheets support",
6
6
  "author": {
7
7
  "name": "Chris Hall",
package/dist/index.js CHANGED
@@ -19790,7 +19790,7 @@ function date4(params) {
19790
19790
  return _coercedDate(ZodDate, params);
19791
19791
  }
19792
19792
 
19793
- // ../../node_modules/@modelcontextprotocol/core/dist/auth-CUe6YdwF.mjs
19793
+ // ../../node_modules/@modelcontextprotocol/server/node_modules/@modelcontextprotocol/core/dist/auth-CGP0BDVq.mjs
19794
19794
  var LATEST_PROTOCOL_VERSION = "2025-11-25";
19795
19795
  var SUPPORTED_PROTOCOL_VERSIONS = [
19796
19796
  LATEST_PROTOCOL_VERSION,
@@ -20627,7 +20627,8 @@ var OAuthMetadataSchema = looseObject({
20627
20627
  introspection_endpoint_auth_signing_alg_values_supported: array(string2()).optional(),
20628
20628
  code_challenge_methods_supported: array(string2()).optional(),
20629
20629
  client_id_metadata_document_supported: boolean2().optional(),
20630
- authorization_response_iss_parameter_supported: boolean2().optional().catch(void 0)
20630
+ authorization_response_iss_parameter_supported: boolean2().optional().catch(void 0),
20631
+ dpop_signing_alg_values_supported: array(string2()).optional()
20631
20632
  });
20632
20633
  var OpenIdProviderMetadataSchema = looseObject({
20633
20634
  issuer: string2(),
@@ -20728,7 +20729,7 @@ var OAuthTokenRevocationRequestSchema = object({
20728
20729
  token_type_hint: string2().optional()
20729
20730
  }).strip();
20730
20731
 
20731
- // ../../node_modules/@modelcontextprotocol/server/dist/src-CX2iR2pK.mjs
20732
+ // ../../node_modules/@modelcontextprotocol/server/dist/src-D-y6h4N7.mjs
20732
20733
  var BRANDS = /* @__PURE__ */ Symbol.for("mcp.sdk.errorBrands");
20733
20734
  function stampErrorBrands(instance, ctor) {
20734
20735
  const brands = /* @__PURE__ */ new Set();
@@ -20842,8 +20843,18 @@ var SdkError = class extends Error {
20842
20843
  if (typeof this !== "function") throw new TypeError("isInstance must be called on the class (e.g. `SdkError.isInstance(value)`); for callbacks use `v => SdkError.isInstance(v)`");
20843
20844
  return brandedHasInstance(this, value);
20844
20845
  }
20845
- constructor(code, message, data) {
20846
- super(message);
20846
+ /**
20847
+ * @param code - Stable string code identifying the failure ({@linkcode SdkErrorCode}).
20848
+ * @param message - Human-readable description.
20849
+ * @param data - Optional structured payload (for example the HTTP status carried by
20850
+ * {@linkcode SdkHttpError}). Opaque to the SDK: a `cause` key inside `data` is not
20851
+ * promoted to `Error.cause`.
20852
+ * @param options - Standard `ErrorOptions`, forwarded to `Error`. Pass the underlying
20853
+ * failure as `{ cause }` so it is reachable through the `Error.cause` chain that
20854
+ * loggers and error trackers walk.
20855
+ */
20856
+ constructor(code, message, data, options) {
20857
+ super(message, options);
20847
20858
  this.code = code;
20848
20859
  this.data = data;
20849
20860
  this.name = "SdkError";
@@ -20854,8 +20865,11 @@ var SdkHttpError = class extends SdkError {
20854
20865
  static {
20855
20866
  Object.defineProperty(this, "mcpBrand", { value: "mcp.SdkHttpError" });
20856
20867
  }
20857
- constructor(code, message, data) {
20858
- super(code, message, data);
20868
+ /**
20869
+ * @param options - Standard `ErrorOptions`, forwarded to `Error` (see {@linkcode SdkError}).
20870
+ */
20871
+ constructor(code, message, data, options) {
20872
+ super(code, message, data, options);
20859
20873
  this.name = "SdkHttpError";
20860
20874
  }
20861
20875
  get status() {
@@ -23806,7 +23820,7 @@ var INBOUND_VALIDATION_LADDER = [
23806
23820
  evaluatedAt: "pre-dispatch",
23807
23821
  codes: [HEADER_MISMATCH_ERROR_CODE],
23808
23822
  conformance: ["http-header-validation"],
23809
- rationale: "SEP-2243 standard `Mcp-Method` / `Mcp-Name` headers \u2014 presence, sentinel decoding, and `Mcp-Name` \u2194 body cross-check \u2014 are validated by the HTTP entry on a modern-classified request after the supported-revision gate and before dispatch. The classifier\u2019s own header-mismatch cells (protocol-version, `Mcp-Method` mismatch) stay on the edge `era-classification` rung; this rung carries the entry-layer presence/`Mcp-Name` half. Evaluated before the capability gate, the factory call, and the `Mcp-Param-*` rung so a request that fails several rungs is answered by the standard-header rung first. The documented order (after method-registry 5 and request-params 6) is NOT the observed precedence: serveModern evaluates this rung immediately after the supported-revision gate, so a request that also fails a dispatch rung is answered here before the dispatch rungs (5\u20136) are consulted."
23823
+ rationale: "SEP-2243 standard `MCP-Protocol-Version` / `Mcp-Method` / `Mcp-Name` headers \u2014 presence, sentinel decoding, and `Mcp-Name` \u2194 body cross-check \u2014 are validated by the HTTP entry on a modern-classified request after the supported-revision gate and before dispatch. The spec requires `MCP-Protocol-Version` and `Mcp-Method` on every modern *request* POST (`Mcp-Name` only for the methods that mirror `params.name` / `params.uri` / `params.taskId`, see `MCP_NAME_HEADER_SOURCE`) and names them in that order, so a request missing several is answered by the earliest. Notification POSTs are exempt: the presence half runs on requests only, so a modern-enveloped notification is dispatched even with no standard headers at all. The classifier\u2019s own header-mismatch cells (protocol-version, `Mcp-Method` mismatch) stay on the edge `era-classification` rung; this rung carries the entry-layer presence/`Mcp-Name` half \u2014 including the missing `MCP-Protocol-Version` cell, which cannot live on the edge rung without breaking body-primary classification. Evaluated before the capability gate, the factory call, and the `Mcp-Param-*` rung so a request that fails several rungs is answered by the standard-header rung first. The documented order (after method-registry 5 and request-params 6) is NOT the observed precedence: serveModern evaluates this rung immediately after the supported-revision gate, so a request that also fails a dispatch rung is answered here before the dispatch rungs (5\u20136) are consulted."
23810
23824
  },
23811
23825
  {
23812
23826
  rung: "client-capabilities",
@@ -24607,7 +24621,7 @@ var Protocol = class {
24607
24621
  return this._requestHandlers.get(method);
24608
24622
  }
24609
24623
  async _oncancel(notification) {
24610
- if (!notification.params.requestId) return;
24624
+ if (notification.params.requestId === void 0) return;
24611
24625
  this._requestHandlerAbortControllers.get(notification.params.requestId)?.abort(notification.params.reason);
24612
24626
  }
24613
24627
  _setupTimeout(messageId, timeout, maxTotalTimeout, onTimeout, resetTimeoutOnProgress = false) {
@@ -25006,19 +25020,20 @@ var Protocol = class {
25006
25020
  const cancel = (reason) => {
25007
25021
  if (responseReceived) return;
25008
25022
  this._progressHandlers.delete(messageId);
25009
- if (requestAbort === void 0) this._transport?.send(this._envelopeOutbound({
25010
- jsonrpc: "2.0",
25011
- method: "notifications/cancelled",
25012
- params: {
25013
- requestId: messageId,
25014
- reason: String(reason)
25015
- }
25016
- }), {
25017
- relatedRequestId,
25018
- resumptionToken,
25019
- onresumptiontoken
25020
- }).catch((error62) => this._onerror(/* @__PURE__ */ new Error(`Failed to send cancellation: ${error62}`)));
25021
- else requestAbort.abort();
25023
+ if (requestAbort === void 0) {
25024
+ if (request.method !== "initialize") this._transport?.send(this._envelopeOutbound({
25025
+ jsonrpc: "2.0",
25026
+ method: "notifications/cancelled",
25027
+ params: {
25028
+ requestId: messageId,
25029
+ reason: String(reason)
25030
+ }
25031
+ }), {
25032
+ relatedRequestId,
25033
+ resumptionToken,
25034
+ onresumptiontoken
25035
+ }).catch((error62) => this._onerror(/* @__PURE__ */ new Error(`Failed to send cancellation: ${error62}`)));
25036
+ } else requestAbort.abort();
25022
25037
  reject(reason instanceof SdkError ? reason : new SdkError(SdkErrorCode.RequestTimeout, String(reason)));
25023
25038
  };
25024
25039
  this._responseHandlers.set(messageId, (response) => {
@@ -25098,7 +25113,7 @@ var Protocol = class {
25098
25113
  jsonrpc: "2.0",
25099
25114
  ...notification
25100
25115
  });
25101
- if ((this._options?.debouncedNotificationMethods ?? []).includes(notification.method) && !notification.params && !options?.relatedRequestId) {
25116
+ if ((this._options?.debouncedNotificationMethods ?? []).includes(notification.method) && !notification.params && options?.relatedRequestId === void 0) {
25102
25117
  if (this._pendingDebouncedNotifications.has(notification.method)) return;
25103
25118
  this._pendingDebouncedNotifications.add(notification.method);
25104
25119
  Promise.resolve().then(() => {
@@ -32401,7 +32416,7 @@ var Ajv = import_ajv.Ajv;
32401
32416
  // ../../node_modules/@modelcontextprotocol/server/dist/shimsNode.mjs
32402
32417
  import process2 from "node:process";
32403
32418
 
32404
- // ../../node_modules/@modelcontextprotocol/server/dist/mcp-DXXb3Vv3.mjs
32419
+ // ../../node_modules/@modelcontextprotocol/server/dist/mcp-Dw2OlZ1f.mjs
32405
32420
  var COMPLETABLE_SYMBOL = /* @__PURE__ */ Symbol.for("mcp.completable");
32406
32421
  function isCompletable(schema) {
32407
32422
  return !!schema && typeof schema === "object" && COMPLETABLE_SYMBOL in schema;
@@ -32594,6 +32609,9 @@ function notificationToServerEvent(method, uri) {
32594
32609
  };
32595
32610
  }
32596
32611
  }
32612
+ function supportsScopeChallengeResolver(transport) {
32613
+ return typeof transport === "object" && transport !== null && "setScopeChallengeResolver" in transport && typeof transport.setScopeChallengeResolver === "function";
32614
+ }
32597
32615
  var DEFAULT_LEGACY_SHIM_MAX_ROUNDS = 8;
32598
32616
  var DEFAULT_LEGACY_SHIM_ROUND_TIMEOUT_MS = 6e5;
32599
32617
  function resolveLegacyShimOptions(options) {
@@ -33404,6 +33422,7 @@ var McpServer = class {
33404
33422
  * ```
33405
33423
  */
33406
33424
  async connect(transport) {
33425
+ if (supportsScopeChallengeResolver(transport)) transport.setScopeChallengeResolver((context) => this.resolveScopeChallenge(context));
33407
33426
  return await this.server.connect(transport);
33408
33427
  }
33409
33428
  /**
@@ -33412,6 +33431,41 @@ var McpServer = class {
33412
33431
  async close() {
33413
33432
  await this.server.close();
33414
33433
  }
33434
+ /** @internal */
33435
+ resolveScopeChallenge = (context) => {
33436
+ switch (context.request.method) {
33437
+ case "tools/call": {
33438
+ const toolName = context.request.params?.name;
33439
+ if (typeof toolName !== "string") return;
33440
+ const tool = this._registeredTools[toolName];
33441
+ if (tool === void 0 || !tool.enabled) return;
33442
+ return tool.scopeChallenge?.(context);
33443
+ }
33444
+ case "resources/read": {
33445
+ const resourceUri = context.request.params?.uri;
33446
+ if (typeof resourceUri !== "string") return;
33447
+ let uri;
33448
+ try {
33449
+ uri = new URL(resourceUri);
33450
+ } catch {
33451
+ return;
33452
+ }
33453
+ const resource = this._registeredResources[uri.toString()];
33454
+ if (resource !== void 0) return resource.enabled ? resource.scopeChallenge?.(context) : void 0;
33455
+ for (const template of Object.values(this._registeredResourceTemplates)) if (template.resourceTemplate.uriTemplate.match(uri.toString())) return template.enabled ? template.scopeChallenge?.(context) : void 0;
33456
+ return;
33457
+ }
33458
+ case "prompts/get": {
33459
+ const promptName = context.request.params?.name;
33460
+ if (typeof promptName !== "string") return;
33461
+ const prompt = this._registeredPrompts[promptName];
33462
+ if (prompt === void 0 || !prompt.enabled) return;
33463
+ return prompt.scopeChallenge?.(context);
33464
+ }
33465
+ default:
33466
+ return;
33467
+ }
33468
+ };
33415
33469
  _toolHandlersInitialized = false;
33416
33470
  setToolRequestHandlers() {
33417
33471
  if (this._toolHandlersInitialized) return;
@@ -33578,7 +33632,10 @@ var McpServer = class {
33578
33632
  }
33579
33633
  for (const template of Object.values(this._registeredResourceTemplates)) {
33580
33634
  const variables = template.resourceTemplate.uriTemplate.match(uri.toString());
33581
- if (variables) return attachCacheHintFallback(await template.readCallback(uri, variables, ctx), template.cacheHint);
33635
+ if (variables) {
33636
+ if (!template.enabled) throw new ProtocolError(ProtocolErrorCode.InvalidParams, `Resource template ${template.resourceTemplate.uriTemplate} disabled`);
33637
+ return attachCacheHintFallback(await template.readCallback(uri, variables, ctx), template.cacheHint);
33638
+ }
33582
33639
  }
33583
33640
  throw new ResourceNotFoundError(request.params.uri);
33584
33641
  });
@@ -33609,36 +33666,32 @@ var McpServer = class {
33609
33666
  this._promptHandlersInitialized = true;
33610
33667
  }
33611
33668
  registerResource(name, uriOrTemplate, config2, readCallback) {
33612
- const cacheHint = config2.cacheHint;
33613
- let metadata = config2;
33614
- if (cacheHint !== void 0) {
33615
- assertValidCacheHint(cacheHint, `resource ${name}`);
33616
- const rest = { ...config2 };
33617
- delete rest.cacheHint;
33618
- metadata = rest;
33619
- }
33669
+ const { cacheHint, scopeChallenge, ...resourceMetadata } = config2;
33670
+ const metadata = resourceMetadata;
33671
+ if (cacheHint !== void 0) assertValidCacheHint(cacheHint, `resource ${name}`);
33620
33672
  if (typeof uriOrTemplate === "string") {
33621
33673
  if (this._registeredResources[uriOrTemplate]) throw new Error(`Resource ${uriOrTemplate} is already registered`);
33622
- const registeredResource = this._createRegisteredResource(name, config2.title, uriOrTemplate, metadata, readCallback);
33674
+ const registeredResource = this._createRegisteredResource(name, config2.title, uriOrTemplate, metadata, scopeChallenge, readCallback);
33623
33675
  if (cacheHint !== void 0) registeredResource.cacheHint = cacheHint;
33624
33676
  this.setResourceRequestHandlers();
33625
33677
  this.sendResourceListChanged();
33626
33678
  return registeredResource;
33627
33679
  } else {
33628
33680
  if (this._registeredResourceTemplates[name]) throw new Error(`Resource template ${name} is already registered`);
33629
- const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, config2.title, uriOrTemplate, metadata, readCallback);
33681
+ const registeredResourceTemplate = this._createRegisteredResourceTemplate(name, config2.title, uriOrTemplate, metadata, scopeChallenge, readCallback);
33630
33682
  if (cacheHint !== void 0) registeredResourceTemplate.cacheHint = cacheHint;
33631
33683
  this.setResourceRequestHandlers();
33632
33684
  this.sendResourceListChanged();
33633
33685
  return registeredResourceTemplate;
33634
33686
  }
33635
33687
  }
33636
- _createRegisteredResource(name, title, uri, metadata, readCallback) {
33688
+ _createRegisteredResource(name, title, uri, metadata, scopeChallenge, readCallback) {
33637
33689
  const registeredResource = {
33638
33690
  name,
33639
33691
  title,
33640
33692
  metadata,
33641
33693
  readCallback,
33694
+ scopeChallenge,
33642
33695
  enabled: true,
33643
33696
  disable: () => registeredResource.update({ enabled: false }),
33644
33697
  enable: () => registeredResource.update({ enabled: true }),
@@ -33652,6 +33705,7 @@ var McpServer = class {
33652
33705
  if (updates.title !== void 0) registeredResource.title = updates.title;
33653
33706
  if (updates.metadata !== void 0) registeredResource.metadata = updates.metadata;
33654
33707
  if (updates.callback !== void 0) registeredResource.readCallback = updates.callback;
33708
+ if (updates.scopeChallenge !== void 0) registeredResource.scopeChallenge = updates.scopeChallenge === null ? void 0 : updates.scopeChallenge;
33655
33709
  if (updates.enabled !== void 0) registeredResource.enabled = updates.enabled;
33656
33710
  this.sendResourceListChanged();
33657
33711
  }
@@ -33659,12 +33713,13 @@ var McpServer = class {
33659
33713
  this._registeredResources[uri] = registeredResource;
33660
33714
  return registeredResource;
33661
33715
  }
33662
- _createRegisteredResourceTemplate(name, title, template, metadata, readCallback) {
33716
+ _createRegisteredResourceTemplate(name, title, template, metadata, scopeChallenge, readCallback) {
33663
33717
  const registeredResourceTemplate = {
33664
33718
  resourceTemplate: template,
33665
33719
  title,
33666
33720
  metadata,
33667
33721
  readCallback,
33722
+ scopeChallenge,
33668
33723
  enabled: true,
33669
33724
  disable: () => registeredResourceTemplate.update({ enabled: false }),
33670
33725
  enable: () => registeredResourceTemplate.update({ enabled: true }),
@@ -33678,6 +33733,7 @@ var McpServer = class {
33678
33733
  if (updates.template !== void 0) registeredResourceTemplate.resourceTemplate = updates.template;
33679
33734
  if (updates.metadata !== void 0) registeredResourceTemplate.metadata = updates.metadata;
33680
33735
  if (updates.callback !== void 0) registeredResourceTemplate.readCallback = updates.callback;
33736
+ if (updates.scopeChallenge !== void 0) registeredResourceTemplate.scopeChallenge = updates.scopeChallenge === null ? void 0 : updates.scopeChallenge;
33681
33737
  if (updates.enabled !== void 0) registeredResourceTemplate.enabled = updates.enabled;
33682
33738
  this.sendResourceListChanged();
33683
33739
  }
@@ -33687,7 +33743,7 @@ var McpServer = class {
33687
33743
  if (Array.isArray(variableNames) && variableNames.some((v) => !!template.completeCallback(v))) this.setCompletionRequestHandler();
33688
33744
  return registeredResourceTemplate;
33689
33745
  }
33690
- _createRegisteredPrompt(name, title, description, argsSchema, callback, icons, _meta) {
33746
+ _createRegisteredPrompt(name, title, description, argsSchema, callback, icons, scopeChallenge, _meta) {
33691
33747
  let currentArgsSchema = argsSchema;
33692
33748
  let currentCallback = callback;
33693
33749
  const registeredPrompt = {
@@ -33695,6 +33751,7 @@ var McpServer = class {
33695
33751
  description,
33696
33752
  argsSchema,
33697
33753
  icons,
33754
+ scopeChallenge,
33698
33755
  _meta,
33699
33756
  handler: createPromptHandler(name, argsSchema, callback),
33700
33757
  enabled: true,
@@ -33709,6 +33766,7 @@ var McpServer = class {
33709
33766
  if (updates.title !== void 0) registeredPrompt.title = updates.title;
33710
33767
  if (updates.description !== void 0) registeredPrompt.description = updates.description;
33711
33768
  if (updates.icons !== void 0) registeredPrompt.icons = updates.icons;
33769
+ if (updates.scopeChallenge !== void 0) registeredPrompt.scopeChallenge = updates.scopeChallenge === null ? void 0 : updates.scopeChallenge;
33712
33770
  if (updates._meta !== void 0) registeredPrompt._meta = updates._meta;
33713
33771
  let needsHandlerRegen = false;
33714
33772
  if (updates.argsSchema !== void 0) {
@@ -33736,7 +33794,7 @@ var McpServer = class {
33736
33794
  }
33737
33795
  return registeredPrompt;
33738
33796
  }
33739
- _createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, icons, execution, _meta, handler) {
33797
+ _createRegisteredTool(name, title, description, inputSchema, outputSchema, annotations, icons, execution, scopeChallenge, _meta, handler) {
33740
33798
  validateAndWarnToolName(name);
33741
33799
  if (inputSchema !== void 0) try {
33742
33800
  const json2 = standardSchemaToJsonSchema(inputSchema, "input");
@@ -33755,6 +33813,7 @@ var McpServer = class {
33755
33813
  annotations,
33756
33814
  icons,
33757
33815
  execution,
33816
+ scopeChallenge,
33758
33817
  _meta,
33759
33818
  handler,
33760
33819
  executor: createToolExecutor(inputSchema, handler),
@@ -33793,6 +33852,7 @@ var McpServer = class {
33793
33852
  }
33794
33853
  if (updates.annotations !== void 0) registeredTool.annotations = updates.annotations;
33795
33854
  if (updates.icons !== void 0) registeredTool.icons = updates.icons;
33855
+ if (updates.scopeChallenge !== void 0) registeredTool.scopeChallenge = updates.scopeChallenge === null ? void 0 : updates.scopeChallenge;
33796
33856
  if (updates._meta !== void 0) registeredTool._meta = updates._meta;
33797
33857
  if (updates.enabled !== void 0) registeredTool.enabled = updates.enabled;
33798
33858
  this.sendToolListChanged();
@@ -33805,13 +33865,13 @@ var McpServer = class {
33805
33865
  }
33806
33866
  registerTool(name, config2, cb) {
33807
33867
  if (this._registeredTools[name]) throw new Error(`Tool ${name} is already registered`);
33808
- const { title, description, inputSchema, outputSchema, annotations, icons, _meta } = config2;
33809
- return this._createRegisteredTool(name, title, description, normalizeRawShapeSchema(inputSchema), normalizeRawShapeSchema(outputSchema), annotations, icons, void 0, _meta, cb);
33868
+ const { title, description, inputSchema, outputSchema, annotations, icons, scopeChallenge, _meta } = config2;
33869
+ return this._createRegisteredTool(name, title, description, normalizeRawShapeSchema(inputSchema), normalizeRawShapeSchema(outputSchema), annotations, icons, void 0, scopeChallenge, _meta, cb);
33810
33870
  }
33811
33871
  registerPrompt(name, config2, cb) {
33812
33872
  if (this._registeredPrompts[name]) throw new Error(`Prompt ${name} is already registered`);
33813
- const { title, description, argsSchema, icons, _meta } = config2;
33814
- const registeredPrompt = this._createRegisteredPrompt(name, title, description, normalizeRawShapeSchema(argsSchema), cb, icons, _meta);
33873
+ const { title, description, argsSchema, icons, scopeChallenge, _meta } = config2;
33874
+ const registeredPrompt = this._createRegisteredPrompt(name, title, description, normalizeRawShapeSchema(argsSchema), cb, icons, scopeChallenge, _meta);
33815
33875
  this.setPromptRequestHandlers();
33816
33876
  this.sendPromptListChanged();
33817
33877
  return registeredPrompt;
@@ -33923,6 +33983,7 @@ function unwrapOptionalSchema(schema) {
33923
33983
  }
33924
33984
 
33925
33985
  // ../../node_modules/@modelcontextprotocol/server/dist/stdio.mjs
33986
+ var swallowsErrorsAfterClose = /* @__PURE__ */ Symbol("swallowsErrorsAfterClose");
33926
33987
  var StdioServerTransport = class {
33927
33988
  _readBuffer;
33928
33989
  _started = false;
@@ -33949,18 +34010,27 @@ var StdioServerTransport = class {
33949
34010
  this.onerror?.(error62);
33950
34011
  };
33951
34012
  _onstdouterror = (error62) => {
34013
+ if (this._closed) return;
33952
34014
  this.onerror?.(error62);
33953
34015
  this.close().catch(() => {
33954
34016
  });
33955
34017
  };
34018
+ _onstdinclose = () => {
34019
+ this.close().catch(() => {
34020
+ });
34021
+ };
33956
34022
  /**
33957
34023
  * Starts listening for messages on `stdin`.
33958
34024
  */
33959
34025
  async start() {
33960
34026
  if (this._started) throw new Error("StdioServerTransport already started! If using Server class, note that connect() calls start() automatically.");
33961
34027
  this._started = true;
34028
+ for (const listener of this._stdout.listeners("error")) if (listener[swallowsErrorsAfterClose]) this._stdout.off("error", listener);
34029
+ if (this._stdin.readableEnded || this._stdin.destroyed) setImmediate(this._onstdinclose);
33962
34030
  this._stdin.on("data", this._ondata);
33963
34031
  this._stdin.on("error", this._onerror);
34032
+ this._stdin.on("end", this._onstdinclose);
34033
+ this._stdin.on("close", this._onstdinclose);
33964
34034
  this._stdout.on("error", this._onstdouterror);
33965
34035
  }
33966
34036
  processReadBuffer() {
@@ -33977,7 +34047,9 @@ var StdioServerTransport = class {
33977
34047
  this._closed = true;
33978
34048
  this._stdin.off("data", this._ondata);
33979
34049
  this._stdin.off("error", this._onerror);
33980
- this._stdout.off("error", this._onstdouterror);
34050
+ this._stdin.off("end", this._onstdinclose);
34051
+ this._stdin.off("close", this._onstdinclose);
34052
+ this._onstdouterror[swallowsErrorsAfterClose] = true;
33981
34053
  if (this._stdin.listenerCount("data") === 0) this._stdin.pause();
33982
34054
  this._readBuffer.clear();
33983
34055
  this.onclose?.();
@@ -34254,6 +34326,7 @@ function serveStdio(factory, options = {}) {
34254
34326
  return;
34255
34327
  }
34256
34328
  if (state.era === "modern" && await tryServeListen(message)) return;
34329
+ if (isTornDown()) return;
34257
34330
  state.instance.channel.deliver(message);
34258
34331
  return;
34259
34332
  }
@@ -34320,6 +34393,7 @@ function serveStdio(factory, options = {}) {
34320
34393
  };
34321
34394
  }
34322
34395
  if (await tryServeListen(message)) return;
34396
+ if (isTornDown()) return;
34323
34397
  state.instance.channel.deliver(message, { classification: opening.classification });
34324
34398
  return;
34325
34399
  case "legacy": {
@@ -34406,6 +34480,9 @@ function toError(value) {
34406
34480
  return value instanceof Error ? value : new Error(String(value));
34407
34481
  }
34408
34482
 
34483
+ // ../../node_modules/@modelcontextprotocol/server/dist/index.mjs
34484
+ var DEFAULT_MAX_REQUEST_BODY_SIZE = 4 * 1024 * 1024;
34485
+
34409
34486
  // ../../node_modules/@chrischall/mcp-utils/dist/errors/index.js
34410
34487
  var McpToolError = class extends Error {
34411
34488
  /** Actionable remediation text, when one applies. */
@@ -36292,6 +36369,9 @@ function countNonEmptyCells(getOutput) {
36292
36369
  var DATASOURCE_ALIASES = /* @__PURE__ */ new Set(["datasource", "data-source", "data-sources", "connected-sheets"]);
36293
36370
  var DATASOURCE_BILLED = /* @__PURE__ */ new Set(["add", "update", "refresh"]);
36294
36371
  function vetSheetsRun(subcommand, args) {
36372
+ if (subcommand.toLowerCase() === "batch-request") {
36373
+ return gatedElsewhere("gog sheets batch-request", "gog_sheets_run", "applies structural changes to a spreadsheet", "gog_sheets_batch_request");
36374
+ }
36295
36375
  if (!DATASOURCE_ALIASES.has(subcommand.toLowerCase())) return void 0;
36296
36376
  const word = hasCommandWord(args, DATASOURCE_BILLED)?.toLowerCase();
36297
36377
  return word ? gatedElsewhere(`gog sheets datasource ${word}`, "gog_sheets_run", "runs a billed BigQuery job", `gog_sheets_datasource_${word}`) : void 0;
@@ -36415,7 +36495,7 @@ function registerSheetsTools(server) {
36415
36495
  }
36416
36496
 
36417
36497
  // ../gogcli-mcp/src/server.ts
36418
- var VERSION = true ? "4.5.1" : "0.0.0";
36498
+ var VERSION = true ? "4.6.0" : "0.0.0";
36419
36499
 
36420
36500
  // ../gogcli-mcp/src/bootstrap-auth.ts
36421
36501
  import { join as join5 } from "node:path";
@@ -36578,6 +36658,35 @@ async function confirmBilledExecution(ctx, o) {
36578
36658
  });
36579
36659
  }
36580
36660
  function registerExtraSheetsTools(server) {
36661
+ server.registerTool("gog_sheets_batch_begin", {
36662
+ description: "Open a persisted, revision-locked Sheets request batch. Pass its returned batchId to gog_sheets_batch_request using batch=<id>; no spreadsheet changes are submitted until gog_sheets_batch_end.",
36663
+ annotations: { destructiveHint: false },
36664
+ inputSchema: external_exports.object({ spreadsheetId: external_exports.string().describe("Spreadsheet ID the batch is locked to"), name: external_exports.string().optional().describe("Optional batch label"), account: accountParam })
36665
+ }, async ({ spreadsheetId, name, account }) => {
36666
+ const args = ["batch", "begin", `--service=sheets`, `--spreadsheet=${spreadsheetId}`];
36667
+ if (name) args.push(`--name=${name}`);
36668
+ return runOrDiagnose(args, { account });
36669
+ });
36670
+ server.registerTool("gog_sheets_batch_end", {
36671
+ description: "Submit a persisted Sheets request batch atomically against its locked revision. The queued requests are shown for review and require confirmation before they are applied.",
36672
+ annotations: { destructiveHint: true },
36673
+ inputSchema: external_exports.object({ batchId: external_exports.string().describe("Batch ID returned by gog_sheets_batch_begin"), account: accountParam, confirmToken: confirmTokenParam })
36674
+ }, async ({ batchId, account, confirmToken }, ctx) => {
36675
+ const previewResult = await runOrDiagnose(["batch", "show", pos(batchId)], { account });
36676
+ if (previewResult.isError) return previewResult;
36677
+ const preview = resultText2(previewResult);
36678
+ if (preview === void 0) throw new Error("gog returned no queued batch preview; refusing to submit an unreviewed batch.");
36679
+ if (Buffer.byteLength(preview, "utf8") > 1e5) throw new Error("The queued batch is too large to review safely through this tool; inspect and submit it with gog directly.");
36680
+ const confirmation = await requireDispatchConfirmation(ctx, {
36681
+ action: "sheets.batch-end",
36682
+ message: "Review and confirm applying the queued Sheets requests:",
36683
+ confirmationLabel: "Confirm applying this atomic Sheets batch.",
36684
+ details: { batchId, queuedBatch: preview },
36685
+ fallback: { tool: "gog_sheets_batch_end", account, confirmToken, subject: () => ({ target: batchId, payload: { queuedBatch: preview }, preview: { batchId, queuedBatch: preview } }) }
36686
+ });
36687
+ if (confirmation) return confirmation;
36688
+ return runOrDiagnose(["batch", "end", pos(batchId), "--force"], { account });
36689
+ });
36581
36690
  server.registerTool("gog_sheets_list_tabs", {
36582
36691
  description: "List tabs (sheets) in a spreadsheet with their titles, sheetIds, and indices. A friendlier view than gog_sheets_metadata when you only need the tab list \u2014 useful for restructuring a workbook over a long agent session without losing track of names.",
36583
36692
  annotations: { readOnlyHint: true },
@@ -37103,6 +37212,54 @@ ${resultText2(result) ?? ""}` }] };
37103
37212
  if (responseDateTimeRender) args.push(`--response-date-time-render=${responseDateTimeRender}`);
37104
37213
  return runOrDiagnose(args, { account });
37105
37214
  });
37215
+ server.registerTool("gog_sheets_batch_request", {
37216
+ description: "Submit a Google Sheets API structural request array atomically (for example addDimension, repeatCell, mergeCells, or updateSheetProperties). requestsJson must be a JSON array of Sheets API Request objects. Requests are previewed and require confirmation before submission. Use batch=<id> to append these requests to a persisted Sheets batch instead of submitting immediately.",
37217
+ annotations: { destructiveHint: true },
37218
+ inputSchema: external_exports.object({
37219
+ spreadsheetId: external_exports.string().describe("Spreadsheet ID"),
37220
+ requestsJson: external_exports.string().describe('JSON array of Google Sheets API Request objects, e.g. [{"addSheet":{"properties":{"title":"Archive"}}}]'),
37221
+ batch: external_exports.string().optional().describe("Persisted Sheets batch ID; append requests without submitting yet"),
37222
+ account: accountParam,
37223
+ confirmToken: confirmTokenParam
37224
+ })
37225
+ }, async ({ spreadsheetId, requestsJson, batch, account, confirmToken }, ctx) => {
37226
+ confineAtFile(requestsJson, "requestsJson");
37227
+ let requests;
37228
+ try {
37229
+ requests = JSON.parse(requestsJson);
37230
+ } catch {
37231
+ throw new Error("requestsJson must be valid JSON");
37232
+ }
37233
+ if (!Array.isArray(requests) || requests.length === 0) throw new Error("requestsJson must be a non-empty JSON array");
37234
+ if (Buffer.byteLength(requestsJson, "utf8") > 1e5) throw new Error("requestsJson exceeds the 100 KB inline limit; split the requests across persisted batches");
37235
+ const args = ["sheets", "batch-request", pos(spreadsheetId), `--requests-json=${requestsJson}`];
37236
+ if (batch) {
37237
+ args.push(`--batch=${batch}`);
37238
+ return runOrDiagnose(args, { account });
37239
+ }
37240
+ const got = await runOrDiagnose(["sheets", "metadata", pos(spreadsheetId), "--select=properties.title"], { account });
37241
+ if (got.isError) return got;
37242
+ const title = spreadsheetTitle(resultText2(got));
37243
+ const confirmation = await requireDispatchConfirmation(ctx, {
37244
+ action: "sheets.batch-request",
37245
+ message: "Review and confirm these structural changes to the spreadsheet:",
37246
+ confirmationLabel: "Confirm submitting this atomic Sheets request batch.",
37247
+ details: {
37248
+ spreadsheet: { id: spreadsheetId, ...title !== void 0 ? { title } : {} },
37249
+ requestCount: requests.length,
37250
+ requests
37251
+ },
37252
+ fallback: {
37253
+ tool: "gog_sheets_batch_request",
37254
+ account,
37255
+ confirmToken,
37256
+ subject: () => ({ target: spreadsheetId, payload: { requests }, preview: { requestCount: requests.length, requests } })
37257
+ }
37258
+ });
37259
+ if (confirmation) return confirmation;
37260
+ args.push("--force");
37261
+ return runOrDiagnose(args, { account });
37262
+ });
37106
37263
  server.registerTool("gog_sheets_reorder_tab", {
37107
37264
  description: "Move a tab to a specific 0-based position. `tab` is the tab name or numeric sheetId; `to=0` is the leftmost slot.",
37108
37265
  annotations: { destructiveHint: true },
@@ -37627,7 +37784,7 @@ await runMcp({
37627
37784
  });
37628
37785
  /*! Bundled license information:
37629
37786
 
37630
- @modelcontextprotocol/server/dist/src-CX2iR2pK.mjs:
37787
+ @modelcontextprotocol/server/dist/src-D-y6h4N7.mjs:
37631
37788
  (*!
37632
37789
  * content-type
37633
37790
  * Copyright(c) 2015 Douglas Christopher Wilson
package/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifest_version": "0.3",
4
4
  "name": "gogcli-mcp-sheets",
5
5
  "display_name": "gogcli (Sheets)",
6
- "version": "4.5.1",
6
+ "version": "4.6.0",
7
7
  "description": "Extended Google Sheets for Claude via gogcli — auth + full Sheets support",
8
8
  "author": {
9
9
  "name": "Chris Hall",
@@ -362,6 +362,18 @@
362
362
  {
363
363
  "name": "gog_sheets_datasource_delete",
364
364
  "description": "Delete a Connected Sheets data source and its linked sheet"
365
+ },
366
+ {
367
+ "name": "gog_sheets_batch_request",
368
+ "description": "Submit an atomic array of Google Sheets structural requests after confirmation."
369
+ },
370
+ {
371
+ "name": "gog_sheets_batch_begin",
372
+ "description": "Open a revision-locked Sheets request batch."
373
+ },
374
+ {
375
+ "name": "gog_sheets_batch_end",
376
+ "description": "Review and atomically submit a Sheets request batch after confirmation."
365
377
  }
366
378
  ],
367
379
  "compatibility": {
package/mint.yaml CHANGED
@@ -92,7 +92,7 @@ dependencies:
92
92
  # bumping these nine pins, and the pin stored on each mcp-host registration.
93
93
  - kind: github-release
94
94
  repo: openclaw/gogcli
95
- tag: v0.41.0
95
+ tag: v0.43.0
96
96
  asset: "gogcli_*_linux_amd64.tar.gz"
97
97
  bin: [gog]
98
98
  state:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gogcli-mcp-sheets",
3
- "version": "4.5.1",
3
+ "version": "4.6.0",
4
4
  "mcpName": "io.github.chrischall/gogcli-mcp-sheets",
5
5
  "description": "Extended Google Sheets MCP server via gogcli — all base tools plus full Sheets support",
6
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
@@ -25,7 +25,7 @@
25
25
  },
26
26
  "dependencies": {
27
27
  "@chrischall/mcp-utils": "^2.6.0",
28
- "@modelcontextprotocol/server": "^2.0.0",
28
+ "@modelcontextprotocol/server": "^2.1.0",
29
29
  "zod": "^4.6.1"
30
30
  },
31
31
  "license": "MIT",
package/server.json CHANGED
@@ -7,12 +7,12 @@
7
7
  "source": "github",
8
8
  "subfolder": "packages/gogcli-mcp-sheets"
9
9
  },
10
- "version": "4.5.1",
10
+ "version": "4.6.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "npm",
14
14
  "identifier": "gogcli-mcp-sheets",
15
- "version": "4.5.1",
15
+ "version": "4.6.0",
16
16
  "transport": {
17
17
  "type": "stdio"
18
18
  },
@@ -166,6 +166,37 @@ async function confirmBilledExecution(
166
166
 
167
167
  export function registerExtraSheetsTools(server: McpServer): void {
168
168
 
169
+ server.registerTool('gog_sheets_batch_begin', {
170
+ description: 'Open a persisted, revision-locked Sheets request batch. Pass its returned batchId to gog_sheets_batch_request using batch=<id>; no spreadsheet changes are submitted until gog_sheets_batch_end.',
171
+ annotations: { destructiveHint: false },
172
+ inputSchema: z.object({ spreadsheetId: z.string().describe('Spreadsheet ID the batch is locked to'), name: z.string().optional().describe('Optional batch label'), account: accountParam }),
173
+ }, async ({ spreadsheetId, name, account }) => {
174
+ const args: GogArg[] = ['batch', 'begin', `--service=sheets`, `--spreadsheet=${spreadsheetId}`];
175
+ if (name) args.push(`--name=${name}`);
176
+ return runOrDiagnose(args, { account });
177
+ });
178
+
179
+ server.registerTool('gog_sheets_batch_end', {
180
+ description: 'Submit a persisted Sheets request batch atomically against its locked revision. The queued requests are shown for review and require confirmation before they are applied.',
181
+ annotations: { destructiveHint: true },
182
+ inputSchema: z.object({ batchId: z.string().describe('Batch ID returned by gog_sheets_batch_begin'), account: accountParam, confirmToken: confirmTokenParam }),
183
+ }, async ({ batchId, account, confirmToken }, ctx) => {
184
+ const previewResult = await runOrDiagnose(['batch', 'show', pos(batchId)], { account });
185
+ if (previewResult.isError) return previewResult;
186
+ const preview = resultText(previewResult);
187
+ if (preview === undefined) throw new Error('gog returned no queued batch preview; refusing to submit an unreviewed batch.');
188
+ if (Buffer.byteLength(preview, 'utf8') > 100_000) throw new Error('The queued batch is too large to review safely through this tool; inspect and submit it with gog directly.');
189
+ const confirmation = await requireDispatchConfirmation(ctx, {
190
+ action: 'sheets.batch-end',
191
+ message: 'Review and confirm applying the queued Sheets requests:',
192
+ confirmationLabel: 'Confirm applying this atomic Sheets batch.',
193
+ details: { batchId, queuedBatch: preview },
194
+ fallback: { tool: 'gog_sheets_batch_end', account, confirmToken, subject: () => ({ target: batchId, payload: { queuedBatch: preview }, preview: { batchId, queuedBatch: preview } }) },
195
+ });
196
+ if (confirmation) return confirmation;
197
+ return runOrDiagnose(['batch', 'end', pos(batchId), '--force'], { account });
198
+ });
199
+
169
200
  server.registerTool('gog_sheets_list_tabs', {
170
201
  description: 'List tabs (sheets) in a spreadsheet with their titles, sheetIds, and indices. A friendlier view than gog_sheets_metadata when you only need the tab list — useful for restructuring a workbook over a long agent session without losing track of names.',
171
202
  annotations: { readOnlyHint: true },
@@ -734,6 +765,49 @@ export function registerExtraSheetsTools(server: McpServer): void {
734
765
  return runOrDiagnose(args, { account });
735
766
  });
736
767
 
768
+ server.registerTool('gog_sheets_batch_request', {
769
+ description: 'Submit a Google Sheets API structural request array atomically (for example addDimension, repeatCell, mergeCells, or updateSheetProperties). requestsJson must be a JSON array of Sheets API Request objects. Requests are previewed and require confirmation before submission. Use batch=<id> to append these requests to a persisted Sheets batch instead of submitting immediately.',
770
+ annotations: { destructiveHint: true },
771
+ inputSchema: z.object({
772
+ spreadsheetId: z.string().describe('Spreadsheet ID'),
773
+ requestsJson: z.string().describe('JSON array of Google Sheets API Request objects, e.g. [{"addSheet":{"properties":{"title":"Archive"}}}]'),
774
+ batch: z.string().optional().describe('Persisted Sheets batch ID; append requests without submitting yet'),
775
+ account: accountParam,
776
+ confirmToken: confirmTokenParam,
777
+ }),
778
+ }, async ({ spreadsheetId, requestsJson, batch, account, confirmToken }, ctx) => {
779
+ confineAtFile(requestsJson, 'requestsJson');
780
+ let requests: unknown;
781
+ try { requests = JSON.parse(requestsJson); } catch { throw new Error('requestsJson must be valid JSON'); }
782
+ if (!Array.isArray(requests) || requests.length === 0) throw new Error('requestsJson must be a non-empty JSON array');
783
+ if (Buffer.byteLength(requestsJson, 'utf8') > 100_000) throw new Error('requestsJson exceeds the 100 KB inline limit; split the requests across persisted batches');
784
+ const args: GogArg[] = ['sheets', 'batch-request', pos(spreadsheetId), `--requests-json=${requestsJson}`];
785
+ if (batch) {
786
+ args.push(`--batch=${batch}`);
787
+ return runOrDiagnose(args, { account });
788
+ }
789
+ const got = await runOrDiagnose(['sheets', 'metadata', pos(spreadsheetId), '--select=properties.title'], { account });
790
+ if (got.isError) return got;
791
+ const title = spreadsheetTitle(resultText(got));
792
+ const confirmation = await requireDispatchConfirmation(ctx, {
793
+ action: 'sheets.batch-request',
794
+ message: 'Review and confirm these structural changes to the spreadsheet:',
795
+ confirmationLabel: 'Confirm submitting this atomic Sheets request batch.',
796
+ details: {
797
+ spreadsheet: { id: spreadsheetId, ...(title !== undefined ? { title } : {}) },
798
+ requestCount: requests.length,
799
+ requests,
800
+ },
801
+ fallback: {
802
+ tool: 'gog_sheets_batch_request', account, confirmToken,
803
+ subject: () => ({ target: spreadsheetId, payload: { requests }, preview: { requestCount: requests.length, requests } }),
804
+ },
805
+ });
806
+ if (confirmation) return confirmation;
807
+ args.push('--force');
808
+ return runOrDiagnose(args, { account });
809
+ });
810
+
737
811
  server.registerTool('gog_sheets_reorder_tab', {
738
812
  description: 'Move a tab to a specific 0-based position. `tab` is the tab name or numeric sheetId; `to=0` is the leftmost slot.',
739
813
  annotations: { destructiveHint: true },
@@ -894,6 +894,121 @@ describe('gog_sheets_batch_update', () => {
894
894
  });
895
895
  });
896
896
 
897
+ describe('gog_sheets_batch_request', () => {
898
+ it('opens a revision-locked Sheets batch', async () => {
899
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
900
+ const harness = await setupHandlers();
901
+ await harness.callTool('gog_sheets_batch_begin', { spreadsheetId: 'sid', name: 'quarterly' });
902
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
903
+ ['batch', 'begin', '--service=sheets', '--spreadsheet=sid', '--name=quarterly'], { account: undefined });
904
+ await harness.callTool('gog_sheets_batch_begin', { spreadsheetId: 'sid' });
905
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
906
+ ['batch', 'begin', '--service=sheets', '--spreadsheet=sid'], { account: undefined });
907
+ });
908
+
909
+ it('shows and confirms the queued requests before ending a batch', async () => {
910
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{"requests":[{"addSheet":{}}]}'));
911
+ const harness = await setupHandlers();
912
+ await harness.callTool('gog_sheets_batch_end', { batchId: 'batch-1' });
913
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['batch', 'show', pos('batch-1')], { account: undefined });
914
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['batch', 'end', pos('batch-1'), '--force'], { account: undefined });
915
+ });
916
+
917
+ it('does not end a batch when the preview fails or is too large', async () => {
918
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue({ isError: true, content: [{ type: 'text', text: 'failed' }] });
919
+ const harness = await setupHandlers();
920
+ expect((await harness.callTool('gog_sheets_batch_end', { batchId: 'batch-1' })).isError).toBe(true);
921
+ expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
922
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('x'.repeat(100_001)));
923
+ expect((await harness.callTool('gog_sheets_batch_end', { batchId: 'batch-1' })).isError).toBe(true);
924
+ expect(lib.runOrDiagnose).toHaveBeenCalledTimes(2);
925
+ });
926
+
927
+ it('refuses to submit when gog returns no batch preview', async () => {
928
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue({ content: [] });
929
+ const harness = await setupHandlers();
930
+ expect((await harness.callTool('gog_sheets_batch_end', { batchId: 'batch-1' })).isError).toBe(true);
931
+ expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
932
+ });
933
+
934
+ it('confirms an unprompted client using a one-time token bound to the queued requests', async () => {
935
+ process.env.MCP_CONFIRM_MODE = 'ask-user';
936
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{"requests":[{"addSheet":{}}]}'));
937
+ const harness = await createTestHarness(registerExtraSheetsTools);
938
+ const preview = await harness.callTool('gog_sheets_batch_end', { batchId: 'batch-1' });
939
+ const payload = JSON.parse(preview.content[0]!.text!);
940
+ await harness.callTool('gog_sheets_batch_end', { batchId: 'batch-1', confirmToken: payload.confirmToken });
941
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(['batch', 'end', pos('batch-1'), '--force'], { account: undefined });
942
+ delete process.env.MCP_CONFIRM_MODE;
943
+ });
944
+
945
+ it('confirms and submits a structural request array atomically', async () => {
946
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{"properties":{"title":"Plan"}}'));
947
+ const harness = await setupHandlers();
948
+ const requestsJson = '[{"addSheet":{"properties":{"title":"Archive"}}}]';
949
+ await harness.callTool('gog_sheets_batch_request', { spreadsheetId: 'sid', requestsJson });
950
+ expect(lib.runOrDiagnose).toHaveBeenNthCalledWith(1,
951
+ ['sheets', 'metadata', pos('sid'), '--select=properties.title'], { account: undefined });
952
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
953
+ ['sheets', 'batch-request', pos('sid'), `--requests-json=${requestsJson}`, '--force'], { account: undefined });
954
+ });
955
+
956
+ it('appends requests to a persisted batch without submitting or prompting', async () => {
957
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
958
+ const harness = await setupHandlers();
959
+ const requestsJson = '[{"addSheet":{"properties":{"title":"Archive"}}}]';
960
+ await harness.callTool('gog_sheets_batch_request', { spreadsheetId: 'sid', requestsJson, batch: 'batch-1' });
961
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
962
+ ['sheets', 'batch-request', pos('sid'), `--requests-json=${requestsJson}`, '--batch=batch-1'], { account: undefined });
963
+ });
964
+
965
+ it('rejects malformed or empty request arrays before calling gog', async () => {
966
+ const harness = await setupHandlers();
967
+ const malformed = await harness.callTool('gog_sheets_batch_request', { spreadsheetId: 'sid', requestsJson: '{' });
968
+ expect(malformed.isError).toBe(true);
969
+ const empty = await harness.callTool('gog_sheets_batch_request', { spreadsheetId: 'sid', requestsJson: '[]' });
970
+ expect(empty.isError).toBe(true);
971
+ expect(lib.runOrDiagnose).not.toHaveBeenCalled();
972
+ });
973
+
974
+ it('rejects request payloads that exceed the safe argv size', async () => {
975
+ const harness = await setupHandlers();
976
+ const result = await harness.callTool('gog_sheets_batch_request', {
977
+ spreadsheetId: 'sid', requestsJson: JSON.stringify([{ updateCells: { userEnteredValue: { stringValue: 'x'.repeat(100_001) } } }]),
978
+ });
979
+ expect(result.isError).toBe(true);
980
+ expect(lib.runOrDiagnose).not.toHaveBeenCalled();
981
+ });
982
+
983
+ it('does not submit if the title lookup fails', async () => {
984
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue({ isError: true, content: [{ type: 'text', text: 'failed' }] });
985
+ const harness = await setupHandlers();
986
+ await harness.callTool('gog_sheets_batch_request', { spreadsheetId: 'sid', requestsJson: '[{"addSheet":{}}]' });
987
+ expect(lib.runOrDiagnose).toHaveBeenCalledTimes(1);
988
+ });
989
+
990
+ it('confirms by spreadsheet ID when metadata has no title', async () => {
991
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{}'));
992
+ const harness = await setupHandlers();
993
+ await harness.callTool('gog_sheets_batch_request', { spreadsheetId: 'sid', requestsJson: '[{"addSheet":{}}]' });
994
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
995
+ ['sheets', 'batch-request', pos('sid'), '--requests-json=[{"addSheet":{}}]', '--force'], { account: undefined });
996
+ });
997
+
998
+ it('uses a one-time token when the client cannot prompt', async () => {
999
+ process.env.MCP_CONFIRM_MODE = 'ask-user';
1000
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(rawTextResult('{"properties":{"title":"Plan"}}'));
1001
+ const harness = await createTestHarness(registerExtraSheetsTools);
1002
+ const args = { spreadsheetId: 'sid', requestsJson: '[{"addSheet":{"properties":{"title":"Archive"}}}]' };
1003
+ const preview = await harness.callTool('gog_sheets_batch_request', args);
1004
+ const payload = JSON.parse(preview.content[0]!.text!);
1005
+ await harness.callTool('gog_sheets_batch_request', { ...args, confirmToken: payload.confirmToken });
1006
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
1007
+ ['sheets', 'batch-request', pos('sid'), `--requests-json=${args.requestsJson}`, '--force'], { account: undefined });
1008
+ delete process.env.MCP_CONFIRM_MODE;
1009
+ });
1010
+ });
1011
+
897
1012
  // 24. reorder-tab (gog 0.18.0)
898
1013
  describe('gog_sheets_reorder_tab', () => {
899
1014
  it('passes --tab and --to', async () => {