mcp-google-multi 6.0.0-beta.1 → 6.0.0-beta.2

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.
Files changed (103) hide show
  1. package/README.md +5 -3
  2. package/dist/accounts.d.ts +5 -0
  3. package/dist/accounts.js +12 -1
  4. package/dist/api-probe.js +3 -0
  5. package/dist/arg-normalize.d.ts +61 -4
  6. package/dist/arg-normalize.js +228 -4
  7. package/dist/arg-strict.d.ts +54 -0
  8. package/dist/arg-strict.js +163 -0
  9. package/dist/auth.js +15 -72
  10. package/dist/client.js +15 -5
  11. package/dist/config-file.d.ts +1 -0
  12. package/dist/config-file.js +4 -0
  13. package/dist/discover.js +35 -14
  14. package/dist/discovery-client.d.ts +8 -1
  15. package/dist/discovery-client.js +96 -17
  16. package/dist/doctor.d.ts +9 -1
  17. package/dist/doctor.js +32 -4
  18. package/dist/env-load.d.ts +11 -0
  19. package/dist/env-load.js +23 -1
  20. package/dist/executor.js +12 -1
  21. package/dist/fanout.js +43 -8
  22. package/dist/http-transport.d.ts +11 -2
  23. package/dist/http-transport.js +10 -5
  24. package/dist/index.js +112 -12
  25. package/dist/metrics-cli.d.ts +21 -0
  26. package/dist/metrics-cli.js +206 -0
  27. package/dist/metrics-tap.d.ts +12 -0
  28. package/dist/metrics-tap.js +95 -0
  29. package/dist/oauth-consent.d.ts +25 -10
  30. package/dist/oauth-consent.js +85 -39
  31. package/dist/outbound-allowlist.d.ts +34 -0
  32. package/dist/outbound-allowlist.js +108 -0
  33. package/dist/registry.d.ts +49 -2
  34. package/dist/registry.js +147 -7
  35. package/dist/scope-catalog.js +16 -0
  36. package/dist/services.d.ts +8 -0
  37. package/dist/services.js +28 -1
  38. package/dist/setup-prompt.d.ts +1 -1
  39. package/dist/setup-prompt.js +1 -1
  40. package/dist/tools/_coerce.d.ts +5 -0
  41. package/dist/tools/_coerce.js +11 -0
  42. package/dist/tools/_errors.d.ts +31 -3
  43. package/dist/tools/_errors.js +307 -43
  44. package/dist/tools/_local-files.js +7 -2
  45. package/dist/tools/account-wizard.d.ts +24 -1
  46. package/dist/tools/account-wizard.js +152 -51
  47. package/dist/tools/admin.js +7 -6
  48. package/dist/tools/analytics.d.ts +18 -0
  49. package/dist/tools/analytics.js +284 -0
  50. package/dist/tools/calendar.d.ts +7 -0
  51. package/dist/tools/calendar.js +75 -15
  52. package/dist/tools/chat.js +7 -4
  53. package/dist/tools/contacts.js +57 -30
  54. package/dist/tools/docs.js +56 -54
  55. package/dist/tools/drive.js +132 -70
  56. package/dist/tools/forms.js +7 -7
  57. package/dist/tools/generated/_shared.d.ts +6 -0
  58. package/dist/tools/generated/_shared.js +11 -1
  59. package/dist/tools/generated/admin.js +345 -214
  60. package/dist/tools/generated/analytics.d.ts +2 -0
  61. package/dist/tools/generated/analytics.js +981 -0
  62. package/dist/tools/generated/appsmarket.js +4 -4
  63. package/dist/tools/generated/calendar.js +28 -28
  64. package/dist/tools/generated/chat.js +83 -56
  65. package/dist/tools/generated/classroom.js +258 -216
  66. package/dist/tools/generated/cloudidentity.js +76 -68
  67. package/dist/tools/generated/cloudsearch.js +40 -39
  68. package/dist/tools/generated/contacts.js +20 -11
  69. package/dist/tools/generated/drive.js +62 -53
  70. package/dist/tools/generated/drivelabels.js +60 -32
  71. package/dist/tools/generated/forms.js +7 -7
  72. package/dist/tools/generated/gmail.js +136 -109
  73. package/dist/tools/generated/groupsmigration.js +1 -1
  74. package/dist/tools/generated/groupssettings.js +3 -3
  75. package/dist/tools/generated/index.js +2 -0
  76. package/dist/tools/generated/keep.js +8 -7
  77. package/dist/tools/generated/licensing.js +39 -23
  78. package/dist/tools/generated/meet.js +14 -14
  79. package/dist/tools/generated/method-map.d.ts +2 -0
  80. package/dist/tools/generated/method-map.js +894 -0
  81. package/dist/tools/generated/postmaster.js +3 -3
  82. package/dist/tools/generated/reseller.js +34 -28
  83. package/dist/tools/generated/script.js +29 -19
  84. package/dist/tools/generated/searchconsole.js +5 -3
  85. package/dist/tools/generated/sheets.js +10 -9
  86. package/dist/tools/generated/tasks.js +10 -4
  87. package/dist/tools/generated/vault.js +65 -56
  88. package/dist/tools/generated/workspaceevents.js +17 -16
  89. package/dist/tools/gmail.d.ts +10 -1
  90. package/dist/tools/gmail.js +50 -18
  91. package/dist/tools/google-api.js +48 -19
  92. package/dist/tools/meet.js +5 -2
  93. package/dist/tools/searchconsole.js +10 -10
  94. package/dist/tools/sheets.js +36 -48
  95. package/dist/tools/slides.js +6 -6
  96. package/dist/tools/tasks.js +16 -16
  97. package/dist/trim.d.ts +21 -0
  98. package/dist/trim.js +47 -0
  99. package/dist/usage-metrics.d.ts +155 -0
  100. package/dist/usage-metrics.js +603 -0
  101. package/dist/write-control.d.ts +27 -1
  102. package/dist/write-control.js +90 -11
  103. package/package.json +8 -3
@@ -3,9 +3,12 @@ import { people as peopleClient } from '@googleapis/people';
3
3
  import { accountAliasSchema } from '../accounts.js';
4
4
  import { getClient } from '../client.js';
5
5
  import { coerceBoolean } from './_coerce.js';
6
- import { handleGoogleApiError } from './_errors.js';
6
+ import { handleGoogleApiError, invalidParams } from './_errors.js';
7
+ import { listResult } from '../trim.js';
7
8
  const accountEnum = accountAliasSchema.optional();
8
9
  const PERSON_FIELDS = 'names,emailAddresses,phoneNumbers,organizations,addresses,photos,memberships';
10
+ // people.getBatchGet rejects more than 200 resource names per call.
11
+ const BATCH_GET_LIMIT = 200;
9
12
  function formatContact(person) {
10
13
  return {
11
14
  resourceName: person.resourceName,
@@ -143,9 +146,15 @@ export function registerContactsTools(server) {
143
146
  readMask: PERSON_FIELDS,
144
147
  pageSize: pageSize ?? 10,
145
148
  });
149
+ const limit = pageSize ?? 10;
146
150
  const contacts = (res.data.results ?? []).map((r) => formatContact(r.person));
147
151
  return {
148
- content: [{ type: 'text', text: JSON.stringify(contacts, null, 2) }],
152
+ content: [{ type: 'text', text: JSON.stringify(listResult('contacts', contacts, {
153
+ // searchContacts returns no token and no total, so a full page is
154
+ // the only evidence that the cap, not the address book, ended it.
155
+ capped: contacts.length >= limit,
156
+ hint: `The page is full at ${limit} of a possible 30, so more contacts may match. Raise pageSize or narrow the query. This search endpoint has no pageToken, so there is no next page to fetch.`,
157
+ }), null, 2) }],
149
158
  };
150
159
  }
151
160
  catch (error) {
@@ -200,7 +209,7 @@ export function registerContactsTools(server) {
200
209
  description: 'Get a single contact by resource name',
201
210
  inputSchema: {
202
211
  account: accountEnum.describe('Google account alias'),
203
- resourceName: z.string().describe('Contact resource name (e.g. "people/c1234567890")'),
212
+ resourceName: z.string().min(1).describe('Contact resource name (e.g. "people/c1234567890")'),
204
213
  },
205
214
  }, async ({ account, resourceName }) => {
206
215
  try {
@@ -307,7 +316,7 @@ export function registerContactsTools(server) {
307
316
  description: 'Update an existing contact (reads current etag automatically)',
308
317
  inputSchema: {
309
318
  account: accountEnum.describe('Google account alias'),
310
- resourceName: z.string().describe('Contact resource name (e.g. "people/c1234567890")'),
319
+ resourceName: z.string().min(1).describe('Contact resource name (e.g. "people/c1234567890")'),
311
320
  givenName: z.string().optional().describe('Updated first name'),
312
321
  familyName: z.string().optional().describe('Updated last name'),
313
322
  email: z.string().optional().describe('Updated email address'),
@@ -354,12 +363,7 @@ export function registerContactsTools(server) {
354
363
  updateFields.push('organizations');
355
364
  }
356
365
  if (updateFields.length === 0) {
357
- return {
358
- content: [{ type: 'text', text: JSON.stringify({
359
- error: 'No fields to update. Provide at least one of: givenName, familyName, email, phone, organization, jobTitle',
360
- }, null, 2) }],
361
- isError: true,
362
- };
366
+ return invalidParams(account, 'No fields to update: every optional field was omitted, so the request would have been a no-op.', 'Pass at least one of: givenName, familyName, email, phone, organization, jobTitle.');
363
367
  }
364
368
  const res = await people.people.updateContact({
365
369
  resourceName,
@@ -379,7 +383,7 @@ export function registerContactsTools(server) {
379
383
  description: 'Delete a contact (permanent, cannot be undone)',
380
384
  inputSchema: {
381
385
  account: accountEnum.describe('Google account alias'),
382
- resourceName: z.string().describe('Contact resource name (e.g. "people/c1234567890")'),
386
+ resourceName: z.string().min(1).describe('Contact resource name (e.g. "people/c1234567890")'),
383
387
  },
384
388
  }, async ({ account, resourceName }) => {
385
389
  try {
@@ -429,7 +433,7 @@ export function registerContactsTools(server) {
429
433
  description: 'List members of a contact group',
430
434
  inputSchema: {
431
435
  account: accountEnum.describe('Google account alias'),
432
- groupResourceName: z.string().describe('Contact group resource name (e.g. "contactGroups/abc123")'),
436
+ groupResourceName: z.string().min(1).describe('Contact group resource name (e.g. "contactGroups/abc123")'),
433
437
  maxMembers: z.number().min(1).max(1000).default(100).optional()
434
438
  .describe('Max member resource names to return (default: 100)'),
435
439
  },
@@ -442,26 +446,49 @@ export function registerContactsTools(server) {
442
446
  maxMembers: maxMembers ?? 100,
443
447
  });
444
448
  const memberResourceNames = groupRes.data.memberResourceNames ?? [];
445
- if (memberResourceNames.length === 0) {
446
- return {
447
- content: [{ type: 'text', text: JSON.stringify({
448
- group: groupRes.data.name,
449
- members: [],
450
- }, null, 2) }],
451
- };
449
+ const members = [];
450
+ let fetchFailures = 0;
451
+ let firstError;
452
+ // getBatchGet caps at 200 names while maxMembers goes to 1000, so a
453
+ // large group used to fail outright rather than come back in pages.
454
+ for (let i = 0; i < memberResourceNames.length; i += BATCH_GET_LIMIT) {
455
+ const chunk = memberResourceNames.slice(i, i + BATCH_GET_LIMIT);
456
+ try {
457
+ const membersRes = await people.people.getBatchGet({
458
+ resourceNames: chunk,
459
+ personFields: PERSON_FIELDS,
460
+ });
461
+ const got = (membersRes.data.responses ?? []).filter((r) => r.person);
462
+ fetchFailures += chunk.length - got.length;
463
+ for (const r of got)
464
+ members.push(formatContact(r.person));
465
+ }
466
+ catch (chunkError) {
467
+ firstError ??= chunkError;
468
+ fetchFailures += chunk.length;
469
+ }
452
470
  }
453
- const membersRes = await people.people.getBatchGet({
454
- resourceNames: memberResourceNames,
455
- personFields: PERSON_FIELDS,
456
- });
457
- const members = (membersRes.data.responses ?? [])
458
- .filter((r) => r.person)
459
- .map((r) => formatContact(r.person));
471
+ // Every chunk failed, so there is no partial answer to report honestly.
472
+ if (firstError && members.length === 0 && memberResourceNames.length > 0)
473
+ throw firstError;
474
+ const total = groupRes.data.memberCount ?? memberResourceNames.length;
475
+ const namesTruncated = total > memberResourceNames.length;
476
+ const causes = [];
477
+ if (namesTruncated)
478
+ causes.push(`the maxMembers cap returned ${memberResourceNames.length} of ${total} member names`);
479
+ if (fetchFailures > 0)
480
+ causes.push(`${fetchFailures} member record(s) could not be fetched`);
460
481
  return {
461
- content: [{ type: 'text', text: JSON.stringify({
462
- group: groupRes.data.name,
463
- members,
464
- }, null, 2) }],
482
+ content: [{ type: 'text', text: JSON.stringify(listResult('members', members, {
483
+ totalItems: total,
484
+ extra: {
485
+ group: groupRes.data.name,
486
+ ...(fetchFailures > 0 ? { fetchFailures } : {}),
487
+ },
488
+ hint: causes.length > 0
489
+ ? `Incomplete because ${causes.join(' and ')}.${namesTruncated ? ' Raise maxMembers to see the rest.' : ''}`
490
+ : undefined,
491
+ }), null, 2) }],
465
492
  };
466
493
  }
467
494
  catch (error) {
@@ -3,7 +3,7 @@ import { coerceBoolean, coerceJson } from './_coerce.js';
3
3
  import { docs as docsClient } from '@googleapis/docs';
4
4
  import { accountAliasSchema } from '../accounts.js';
5
5
  import { getClient } from '../client.js';
6
- import { handleGoogleApiError } from './_errors.js';
6
+ import { handleGoogleApiError, invalidParams } from './_errors.js';
7
7
  import { capText } from '../trim.js';
8
8
  const accountEnum = accountAliasSchema.optional();
9
9
  // A single very large insertText has been observed to apply only partially while
@@ -199,7 +199,7 @@ export function registerDocsTools(server) {
199
199
  description: 'Get document metadata (title, revision, named ranges). Optionally include tab content and choose a suggestionsViewMode.',
200
200
  inputSchema: {
201
201
  account: accountEnum.describe('Google account alias'),
202
- documentId: z.string().describe('Google Docs document ID'),
202
+ documentId: z.string().min(1).describe('Google Docs document ID'),
203
203
  includeTabsContent: coerceBoolean.optional()
204
204
  .describe('Include full content for every tab (default: false — first tab only)'),
205
205
  suggestionsViewMode: z.enum([
@@ -236,7 +236,7 @@ export function registerDocsTools(server) {
236
236
  description: 'Read document content as plain text with a heading index (returns up to maxChars characters per call). Pass heading or headingIndex to read one section, tabId to read a specific tab.',
237
237
  inputSchema: {
238
238
  account: accountEnum.describe('Google account alias'),
239
- documentId: z.string().describe('Google Docs document ID'),
239
+ documentId: z.string().min(1).describe('Google Docs document ID'),
240
240
  heading: z.string().optional()
241
241
  .describe('Read only the section under this heading (case-insensitive; exact match first, then substring)'),
242
242
  headingIndex: z.number().int().min(0).optional()
@@ -251,13 +251,7 @@ export function registerDocsTools(server) {
251
251
  }, async ({ account, documentId, heading, headingIndex, tabId, maxChars, offset }) => {
252
252
  try {
253
253
  if (heading !== undefined && headingIndex !== undefined) {
254
- return {
255
- content: [{ type: 'text', text: JSON.stringify({
256
- error: 'invalid_params',
257
- message: 'Provide either heading or headingIndex, not both',
258
- }, null, 2) }],
259
- isError: true,
260
- };
254
+ return invalidParams(account, 'Both heading and headingIndex were supplied, and they address different sections.', 'Pass heading to look a section up by its text, or headingIndex to address one by position. Not both.');
261
255
  }
262
256
  const auth = await getClient(account);
263
257
  const docs = docsClient({ version: 'v1', auth });
@@ -272,7 +266,10 @@ export function registerDocsTools(server) {
272
266
  return {
273
267
  content: [{ type: 'text', text: JSON.stringify({
274
268
  error: 'not_found',
275
- message: `Tab "${tabId}" not found in document ${documentId}`,
269
+ message: `Tab "${tabId}" was not found in document ${documentId}.`,
270
+ hint: 'Pick a tabId from availableTabs below, or omit tabId to read the first tab.',
271
+ retriable: false,
272
+ account: account,
276
273
  availableTabs: listTabs(res.data.tabs),
277
274
  }, null, 2) }],
278
275
  isError: true,
@@ -288,7 +285,10 @@ export function registerDocsTools(server) {
288
285
  return {
289
286
  content: [{ type: 'text', text: JSON.stringify({
290
287
  error: 'invalid_params',
291
- message: `headingIndex ${headingIndex} is out of range (document has ${headings.length} headings)`,
288
+ message: `headingIndex ${headingIndex} is out of range: the document has ${headings.length} heading(s).`,
289
+ hint: 'Indexes are zero-based. Pick one from the headings list below, or omit headingIndex to read the whole document.',
290
+ retriable: false,
291
+ account: account,
292
292
  headings: headings.map(({ i, level, text }) => ({ i, level, text })),
293
293
  }, null, 2) }],
294
294
  isError: true,
@@ -300,10 +300,17 @@ export function registerDocsTools(server) {
300
300
  if ('error' in resolved) {
301
301
  return {
302
302
  content: [{ type: 'text', text: JSON.stringify({
303
- error: resolved.error === 'ambiguous' ? 'ambiguous_heading' : 'not_found',
303
+ // 'ambiguous', not 'ambiguous_heading': the slug vocabulary is
304
+ // shared, and a one-off name buckets as `other` in metrics.
305
+ error: resolved.error === 'ambiguous' ? 'ambiguous' : 'not_found',
304
306
  message: resolved.error === 'ambiguous'
305
- ? `Heading "${heading}" matches multiple headings; use headingIndex to disambiguate`
306
- : `Heading "${heading}" not found in document ${documentId}`,
307
+ ? `Heading "${heading}" matches ${resolved.candidates.length} headings in document ${documentId}.`
308
+ : `Heading "${heading}" was not found in document ${documentId}.`,
309
+ hint: resolved.error === 'ambiguous'
310
+ ? 'Pass headingIndex with the i value of the one you want, from candidates below.'
311
+ : 'Pick a heading from candidates below (it lists every heading in the document), or pass headingIndex.',
312
+ retriable: false,
313
+ account: account,
307
314
  candidates: resolved.candidates.map(({ i, level, text }) => ({ i, level, text })),
308
315
  }, null, 2) }],
309
316
  isError: true,
@@ -338,7 +345,7 @@ export function registerDocsTools(server) {
338
345
  description: 'Insert text into a document at a specific position or at the end',
339
346
  inputSchema: {
340
347
  account: accountEnum.describe('Google account alias'),
341
- documentId: z.string().describe('Google Docs document ID'),
348
+ documentId: z.string().min(1).describe('Google Docs document ID'),
342
349
  text: z.string().describe('Text to insert'),
343
350
  index: z.number().min(1).optional()
344
351
  .describe('Character index to insert at (1-based). Omit to append at the end'),
@@ -377,7 +384,7 @@ export function registerDocsTools(server) {
377
384
  description: 'Find and replace all occurrences of text in a document',
378
385
  inputSchema: {
379
386
  account: accountEnum.describe('Google account alias'),
380
- documentId: z.string().describe('Google Docs document ID'),
387
+ documentId: z.string().min(1).describe('Google Docs document ID'),
381
388
  findText: z.string().describe('Text to search for'),
382
389
  replaceText: z.string().describe('Replacement text'),
383
390
  matchCase: coerceBoolean.default(true).optional()
@@ -414,7 +421,7 @@ export function registerDocsTools(server) {
414
421
  description: 'Delete content in a character index range within a document',
415
422
  inputSchema: {
416
423
  account: accountEnum.describe('Google account alias'),
417
- documentId: z.string().describe('Google Docs document ID'),
424
+ documentId: z.string().min(1).describe('Google Docs document ID'),
418
425
  startIndex: z.number().min(1).describe('Start index (inclusive, 1-based)'),
419
426
  endIndex: z.number().min(2).describe('End index (exclusive)'),
420
427
  },
@@ -447,7 +454,7 @@ export function registerDocsTools(server) {
447
454
  description: 'Update text formatting (bold, italic, underline, font, size) for a range',
448
455
  inputSchema: {
449
456
  account: accountEnum.describe('Google account alias'),
450
- documentId: z.string().describe('Google Docs document ID'),
457
+ documentId: z.string().min(1).describe('Google Docs document ID'),
451
458
  startIndex: z.number().min(1).describe('Start index (inclusive, 1-based)'),
452
459
  endIndex: z.number().min(2).describe('End index (exclusive)'),
453
460
  bold: coerceBoolean.optional().describe('Set bold'),
@@ -483,12 +490,7 @@ export function registerDocsTools(server) {
483
490
  fields.push('weightedFontFamily');
484
491
  }
485
492
  if (fields.length === 0) {
486
- return {
487
- content: [{ type: 'text', text: JSON.stringify({
488
- error: 'No style properties provided. Set at least one of: bold, italic, underline, fontSize, fontFamily',
489
- }, null, 2) }],
490
- isError: true,
491
- };
493
+ return invalidParams(account, 'No style properties to apply: every optional property was omitted.', 'Pass at least one of: bold, italic, underline, fontSize, fontFamily.');
492
494
  }
493
495
  const res = await docs.documents.batchUpdate({
494
496
  documentId,
@@ -518,7 +520,7 @@ export function registerDocsTools(server) {
518
520
  description: 'Insert a table into a document at a specific position',
519
521
  inputSchema: {
520
522
  account: accountEnum.describe('Google account alias'),
521
- documentId: z.string().describe('Google Docs document ID'),
523
+ documentId: z.string().min(1).describe('Google Docs document ID'),
522
524
  rows: z.number().min(1).describe('Number of rows'),
523
525
  columns: z.number().min(1).describe('Number of columns'),
524
526
  index: z.number().min(1).optional()
@@ -555,7 +557,7 @@ export function registerDocsTools(server) {
555
557
  description: 'Tag a range of text with a name. Multiple ranges can share the same name. Used as a template anchor for docs_replace_named_range_content.',
556
558
  inputSchema: {
557
559
  account: accountEnum.describe('Google account alias'),
558
- documentId: z.string().describe('Google Docs document ID'),
560
+ documentId: z.string().min(1).describe('Google Docs document ID'),
559
561
  name: z.string().describe('Name (1-256 chars). Need not be unique.'),
560
562
  startIndex: z.number().min(1).describe('Range start (inclusive)'),
561
563
  endIndex: z.number().min(2).describe('Range end (exclusive)'),
@@ -588,14 +590,14 @@ export function registerDocsTools(server) {
588
590
  description: 'Delete a named range by its ID or by name. Removes every range matching the identifier.',
589
591
  inputSchema: {
590
592
  account: accountEnum.describe('Google account alias'),
591
- documentId: z.string().describe('Google Docs document ID'),
593
+ documentId: z.string().min(1).describe('Google Docs document ID'),
592
594
  namedRangeId: z.string().optional().describe('Specific named range ID'),
593
595
  name: z.string().optional().describe('Name (deletes ALL named ranges with this name)'),
594
596
  },
595
597
  }, async ({ account, documentId, namedRangeId, name }) => {
596
598
  try {
597
599
  if (!namedRangeId && !name) {
598
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'Either namedRangeId or name must be supplied' }) }], isError: true };
600
+ return invalidParams(account, 'Neither namedRangeId nor name was supplied, so no named range is addressed.', 'Pass namedRangeId to delete one specific range, or name to delete every range carrying that name.');
599
601
  }
600
602
  const auth = await getClient(account);
601
603
  const docs = docsClient({ version: 'v1', auth });
@@ -620,7 +622,7 @@ export function registerDocsTools(server) {
620
622
  description: 'Replace the content of every named range matching the identifier with the supplied text. Real mail-merge primitive — far more robust than replaceAllText for templated docs.',
621
623
  inputSchema: {
622
624
  account: accountEnum.describe('Google account alias'),
623
- documentId: z.string().describe('Google Docs document ID'),
625
+ documentId: z.string().min(1).describe('Google Docs document ID'),
624
626
  namedRangeId: z.string().optional().describe('Specific named range ID'),
625
627
  namedRangeName: z.string().optional().describe('Name to match (replaces ALL ranges with this name)'),
626
628
  text: z.string().describe('Replacement text'),
@@ -628,7 +630,7 @@ export function registerDocsTools(server) {
628
630
  }, async ({ account, documentId, namedRangeId, namedRangeName, text }) => {
629
631
  try {
630
632
  if (!namedRangeId && !namedRangeName) {
631
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'Either namedRangeId or namedRangeName must be supplied' }) }], isError: true };
633
+ return invalidParams(account, 'Neither namedRangeId nor namedRangeName was supplied, so no named range is addressed.', 'Pass namedRangeId to replace one specific range, or namedRangeName to replace every range carrying that name.');
632
634
  }
633
635
  const auth = await getClient(account);
634
636
  const docs = docsClient({ version: 'v1', auth });
@@ -654,7 +656,7 @@ export function registerDocsTools(server) {
654
656
  description: 'Update paragraph styling (alignment, heading, indents, spacing) across a range. Only supplied fields are applied.',
655
657
  inputSchema: {
656
658
  account: accountEnum.describe('Google account alias'),
657
- documentId: z.string().describe('Google Docs document ID'),
659
+ documentId: z.string().min(1).describe('Google Docs document ID'),
658
660
  startIndex: z.number().min(1),
659
661
  endIndex: z.number().min(2),
660
662
  namedStyleType: z.enum([
@@ -679,7 +681,7 @@ export function registerDocsTools(server) {
679
681
  const docs = docsClient({ version: 'v1', auth });
680
682
  const built = buildParagraphStyle(style);
681
683
  if (built.fields.length === 0) {
682
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'No paragraph style properties supplied' }) }], isError: true };
684
+ return invalidParams(account, 'No paragraph style properties to apply: every optional property was omitted.', 'Pass at least one of: namedStyleType, alignment, lineSpacing, spaceAbove, spaceBelow, indentStart, indentEnd, indentFirstLine, direction, keepLinesTogether, keepWithNext, avoidWidowAndOrphan.');
683
685
  }
684
686
  await docs.documents.batchUpdate({
685
687
  documentId,
@@ -705,7 +707,7 @@ export function registerDocsTools(server) {
705
707
  description: 'Update document-level styling (page size, margins, headers/footers behavior). Only supplied fields are applied.',
706
708
  inputSchema: {
707
709
  account: accountEnum.describe('Google account alias'),
708
- documentId: z.string().describe('Google Docs document ID'),
710
+ documentId: z.string().min(1).describe('Google Docs document ID'),
709
711
  pageWidth: z.number().optional().describe('Page width in points (e.g. 595 = A4)'),
710
712
  pageHeight: z.number().optional().describe('Page height in points (e.g. 842 = A4)'),
711
713
  marginTop: z.number().optional().describe('Margin in points'),
@@ -725,7 +727,7 @@ export function registerDocsTools(server) {
725
727
  const docs = docsClient({ version: 'v1', auth });
726
728
  const built = buildDocumentStyle(style);
727
729
  if (built.fields.length === 0) {
728
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'No document style properties supplied' }) }], isError: true };
730
+ return invalidParams(account, 'No document style properties to apply: every optional property was omitted.', 'Pass at least one of: pageWidth, pageHeight, marginTop, marginBottom, marginLeft, marginRight, marginHeader, marginFooter, pageNumberStart, useFirstPageHeaderFooter, useEvenPageHeaderFooter, useCustomHeaderFooterMargins.');
729
731
  }
730
732
  await docs.documents.batchUpdate({
731
733
  documentId,
@@ -751,7 +753,7 @@ export function registerDocsTools(server) {
751
753
  description: 'Turn paragraphs in a range into a bulleted or numbered list',
752
754
  inputSchema: {
753
755
  account: accountEnum.describe('Google account alias'),
754
- documentId: z.string().describe('Google Docs document ID'),
756
+ documentId: z.string().min(1).describe('Google Docs document ID'),
755
757
  startIndex: z.number().min(1),
756
758
  endIndex: z.number().min(2),
757
759
  bulletPreset: z.enum([
@@ -799,7 +801,7 @@ export function registerDocsTools(server) {
799
801
  description: 'Strip bullet/number formatting from paragraphs in a range',
800
802
  inputSchema: {
801
803
  account: accountEnum.describe('Google account alias'),
802
- documentId: z.string().describe('Google Docs document ID'),
804
+ documentId: z.string().min(1).describe('Google Docs document ID'),
803
805
  startIndex: z.number().min(1),
804
806
  endIndex: z.number().min(2),
805
807
  },
@@ -830,7 +832,7 @@ export function registerDocsTools(server) {
830
832
  description: 'Insert a publicly-accessible image (PNG/JPEG/GIF, <50MB, <25MP, URL <2KB) inline at an index or at the document end',
831
833
  inputSchema: {
832
834
  account: accountEnum.describe('Google account alias'),
833
- documentId: z.string().describe('Google Docs document ID'),
835
+ documentId: z.string().min(1).describe('Google Docs document ID'),
834
836
  uri: z.string().url().describe('Public image URL'),
835
837
  index: z.number().min(1).optional().describe('Insert position; omit to append at the end'),
836
838
  width: z.number().optional().describe('Width in points (omit for natural size)'),
@@ -869,7 +871,7 @@ export function registerDocsTools(server) {
869
871
  description: 'Insert a page break at a position or at the document end',
870
872
  inputSchema: {
871
873
  account: accountEnum.describe('Google account alias'),
872
- documentId: z.string().describe('Google Docs document ID'),
874
+ documentId: z.string().min(1).describe('Google Docs document ID'),
873
875
  index: z.number().min(1).optional().describe('Insert position; omit to append'),
874
876
  },
875
877
  }, async ({ account, documentId, index }) => {
@@ -897,7 +899,7 @@ export function registerDocsTools(server) {
897
899
  description: 'Insert a section break at a position. CONTINUOUS keeps the same page; NEXT_PAGE starts a new page.',
898
900
  inputSchema: {
899
901
  account: accountEnum.describe('Google account alias'),
900
- documentId: z.string().describe('Google Docs document ID'),
902
+ documentId: z.string().min(1).describe('Google Docs document ID'),
901
903
  index: z.number().min(1).optional(),
902
904
  sectionType: z.enum(['CONTINUOUS', 'NEXT_PAGE']).default('NEXT_PAGE').optional(),
903
905
  },
@@ -927,7 +929,7 @@ export function registerDocsTools(server) {
927
929
  description: 'Create a header for the document or for a specific section. The new header is empty; insert text into it with docs_insert_text targeting its segmentId.',
928
930
  inputSchema: {
929
931
  account: accountEnum.describe('Google account alias'),
930
- documentId: z.string().describe('Google Docs document ID'),
932
+ documentId: z.string().min(1).describe('Google Docs document ID'),
931
933
  sectionBreakIndex: z.number().min(1).optional().describe('Anchor to a specific section break; omit to apply to the document'),
932
934
  },
933
935
  }, async ({ account, documentId, sectionBreakIndex }) => {
@@ -954,8 +956,8 @@ export function registerDocsTools(server) {
954
956
  description: 'Delete a header by its headerId',
955
957
  inputSchema: {
956
958
  account: accountEnum.describe('Google account alias'),
957
- documentId: z.string().describe('Google Docs document ID'),
958
- headerId: z.string().describe('Header ID (from docs_get or createHeader reply)'),
959
+ documentId: z.string().min(1).describe('Google Docs document ID'),
960
+ headerId: z.string().min(1).describe('Header ID (from docs_get or createHeader reply)'),
959
961
  },
960
962
  }, async ({ account, documentId, headerId }) => {
961
963
  try {
@@ -977,7 +979,7 @@ export function registerDocsTools(server) {
977
979
  description: 'Create a footer for the document or a specific section',
978
980
  inputSchema: {
979
981
  account: accountEnum.describe('Google account alias'),
980
- documentId: z.string().describe('Google Docs document ID'),
982
+ documentId: z.string().min(1).describe('Google Docs document ID'),
981
983
  sectionBreakIndex: z.number().min(1).optional(),
982
984
  },
983
985
  }, async ({ account, documentId, sectionBreakIndex }) => {
@@ -1004,8 +1006,8 @@ export function registerDocsTools(server) {
1004
1006
  description: 'Delete a footer by its footerId',
1005
1007
  inputSchema: {
1006
1008
  account: accountEnum.describe('Google account alias'),
1007
- documentId: z.string().describe('Google Docs document ID'),
1008
- footerId: z.string().describe('Footer ID'),
1009
+ documentId: z.string().min(1).describe('Google Docs document ID'),
1010
+ footerId: z.string().min(1).describe('Footer ID'),
1009
1011
  },
1010
1012
  }, async ({ account, documentId, footerId }) => {
1011
1013
  try {
@@ -1030,7 +1032,7 @@ export function registerDocsTools(server) {
1030
1032
  description: 'Mutate a table by operation: insertRow, insertColumn, deleteRow, deleteColumn, mergeCells, unmergeCells. Locate the cell with tableStartIndex (the table\'s start index in the doc) plus rowIndex/columnIndex (0-based).',
1031
1033
  inputSchema: {
1032
1034
  account: accountEnum.describe('Google account alias'),
1033
- documentId: z.string().describe('Google Docs document ID'),
1035
+ documentId: z.string().min(1).describe('Google Docs document ID'),
1034
1036
  operation: z.enum(['insertRow', 'insertColumn', 'deleteRow', 'deleteColumn', 'mergeCells', 'unmergeCells']),
1035
1037
  tableStartIndex: z.number().min(1).describe('Start index of the table in the document'),
1036
1038
  rowIndex: z.number().min(0).describe('Target row (0-based)'),
@@ -1103,7 +1105,7 @@ export function registerDocsTools(server) {
1103
1105
  description: 'Add a new tab to a document. Can be a top-level tab or nested as a child of an existing tab.',
1104
1106
  inputSchema: {
1105
1107
  account: accountEnum.describe('Google account alias'),
1106
- documentId: z.string().describe('Google Docs document ID'),
1108
+ documentId: z.string().min(1).describe('Google Docs document ID'),
1107
1109
  title: z.string().describe('Tab title'),
1108
1110
  parentTabId: z.string().optional().describe('Parent tab ID (omit for top-level)'),
1109
1111
  index: z.number().min(0).optional().describe('Position among siblings'),
@@ -1134,8 +1136,8 @@ export function registerDocsTools(server) {
1134
1136
  description: 'Delete a tab and all of its descendant tabs',
1135
1137
  inputSchema: {
1136
1138
  account: accountEnum.describe('Google account alias'),
1137
- documentId: z.string().describe('Google Docs document ID'),
1138
- tabId: z.string().describe('Tab ID'),
1139
+ documentId: z.string().min(1).describe('Google Docs document ID'),
1140
+ tabId: z.string().min(1).describe('Tab ID'),
1139
1141
  },
1140
1142
  }, async ({ account, documentId, tabId }) => {
1141
1143
  try {
@@ -1157,8 +1159,8 @@ export function registerDocsTools(server) {
1157
1159
  description: 'Rename a tab, change its position, or move it under a different parent',
1158
1160
  inputSchema: {
1159
1161
  account: accountEnum.describe('Google account alias'),
1160
- documentId: z.string().describe('Google Docs document ID'),
1161
- tabId: z.string().describe('Tab ID'),
1162
+ documentId: z.string().min(1).describe('Google Docs document ID'),
1163
+ tabId: z.string().min(1).describe('Tab ID'),
1162
1164
  title: z.string().optional(),
1163
1165
  index: z.number().min(0).optional(),
1164
1166
  parentTabId: z.string().optional(),
@@ -1182,7 +1184,7 @@ export function registerDocsTools(server) {
1182
1184
  fields.push('parentTabId');
1183
1185
  }
1184
1186
  if (fields.length === 0) {
1185
- return { content: [{ type: 'text', text: JSON.stringify({ error: 'No tab property supplied' }) }], isError: true };
1187
+ return invalidParams(account, 'No tab property to update: title, index and parentTabId were all omitted.', 'Pass at least one of: title, index, parentTabId.');
1186
1188
  }
1187
1189
  await docs.documents.batchUpdate({
1188
1190
  documentId,
@@ -1210,7 +1212,7 @@ export function registerDocsTools(server) {
1210
1212
  description: 'Generic documents.batchUpdate pass-through. Accepts the full Request union (40 types). See https://developers.google.com/workspace/docs/api/reference/rest/v1/documents/request',
1211
1213
  inputSchema: {
1212
1214
  account: accountEnum.describe('Google account alias'),
1213
- documentId: z.string().describe('Google Docs document ID'),
1215
+ documentId: z.string().min(1).describe('Google Docs document ID'),
1214
1216
  requests: coerceJson(z.array(z.record(z.string(), z.any()))).describe('Array of Request objects, each with one request-type key'),
1215
1217
  writeControl: z.object({
1216
1218
  requiredRevisionId: z.string().optional(),