@dguido/google-workspace-mcp 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -21,6 +21,56 @@ var init_logging = __esm({
21
21
  }
22
22
  });
23
23
 
24
+ // src/config/services.ts
25
+ function getEnabledServices() {
26
+ if (enabledServices !== null) return enabledServices;
27
+ const envValue = process.env.GOOGLE_WORKSPACE_SERVICES;
28
+ if (envValue === void 0) {
29
+ enabledServices = new Set(SERVICE_NAMES);
30
+ return enabledServices;
31
+ }
32
+ if (envValue.trim() === "") {
33
+ enabledServices = /* @__PURE__ */ new Set();
34
+ return enabledServices;
35
+ }
36
+ const requested = envValue.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
37
+ const valid = /* @__PURE__ */ new Set();
38
+ const unknown = [];
39
+ for (const service of requested) {
40
+ if (SERVICE_NAMES.includes(service)) {
41
+ valid.add(service);
42
+ } else {
43
+ unknown.push(service);
44
+ }
45
+ }
46
+ if (unknown.length > 0) {
47
+ console.warn(
48
+ `[google-workspace-mcp] Unknown services: ${unknown.join(", ")}. Valid: ${SERVICE_NAMES.join(", ")}`
49
+ );
50
+ }
51
+ enabledServices = valid;
52
+ return enabledServices;
53
+ }
54
+ function isServiceEnabled(service) {
55
+ return getEnabledServices().has(service);
56
+ }
57
+ function areUnifiedToolsEnabled() {
58
+ const enabled = getEnabledServices();
59
+ return UNIFIED_REQUIRED_SERVICES.every((s) => enabled.has(s));
60
+ }
61
+ function isToonEnabled() {
62
+ return process.env.GOOGLE_WORKSPACE_TOON_FORMAT === "true";
63
+ }
64
+ var SERVICE_NAMES, UNIFIED_REQUIRED_SERVICES, enabledServices;
65
+ var init_services = __esm({
66
+ "src/config/services.ts"() {
67
+ "use strict";
68
+ SERVICE_NAMES = ["drive", "docs", "sheets", "slides", "calendar", "gmail"];
69
+ UNIFIED_REQUIRED_SERVICES = ["drive", "docs", "sheets", "slides"];
70
+ enabledServices = null;
71
+ }
72
+ });
73
+
24
74
  // src/utils/responses.ts
25
75
  function truncateResponse(content) {
26
76
  if (content.length <= CHARACTER_LIMIT) {
@@ -38,11 +88,14 @@ function successResponse(text) {
38
88
  return { content: [{ type: "text", text }], isError: false };
39
89
  }
40
90
  function structuredResponse(text, data) {
41
- return {
91
+ const response = {
42
92
  content: [{ type: "text", text }],
43
- structuredContent: data,
44
93
  isError: false
45
94
  };
95
+ if (!isToonEnabled()) {
96
+ response.structuredContent = data;
97
+ }
98
+ return response;
46
99
  }
47
100
  function errorResponse(message, options) {
48
101
  log("Error", { message, ...options });
@@ -62,6 +115,7 @@ var CHARACTER_LIMIT;
62
115
  var init_responses = __esm({
63
116
  "src/utils/responses.ts"() {
64
117
  "use strict";
118
+ init_services();
65
119
  init_logging();
66
120
  CHARACTER_LIMIT = 25e3;
67
121
  }
@@ -137,7 +191,7 @@ function getGmailService(authClient2) {
137
191
  return gmailService;
138
192
  }
139
193
  var docsService, sheetsService, slidesService, calendarService, gmailService, lastAuthClient;
140
- var init_services = __esm({
194
+ var init_services2 = __esm({
141
195
  "src/utils/services.ts"() {
142
196
  "use strict";
143
197
  docsService = null;
@@ -776,6 +830,25 @@ var init_mime = __esm({
776
830
  }
777
831
  });
778
832
 
833
+ // src/utils/toon.ts
834
+ import { encode } from "@toon-format/toon";
835
+ function toToon(data) {
836
+ if (!isToonEnabled()) {
837
+ return JSON.stringify(data, null, 2);
838
+ }
839
+ try {
840
+ return encode(data);
841
+ } catch {
842
+ return JSON.stringify(data, null, 2);
843
+ }
844
+ }
845
+ var init_toon = __esm({
846
+ "src/utils/toon.ts"() {
847
+ "use strict";
848
+ init_services();
849
+ }
850
+ });
851
+
779
852
  // src/utils/index.ts
780
853
  var utils_exports = {};
781
854
  __export(utils_exports, {
@@ -807,6 +880,7 @@ __export(utils_exports, {
807
880
  structuredResponse: () => structuredResponse,
808
881
  successResponse: () => successResponse,
809
882
  supportsFormElicitation: () => supportsFormElicitation,
883
+ toToon: () => toToon,
810
884
  truncateResponse: () => truncateResponse,
811
885
  validateArgs: () => validateArgs,
812
886
  withProgressReporting: () => withProgressReporting,
@@ -820,13 +894,14 @@ var init_utils = __esm({
820
894
  init_logging();
821
895
  init_responses();
822
896
  init_validation();
823
- init_services();
897
+ init_services2();
824
898
  init_timeout();
825
899
  init_retry();
826
900
  init_elicitation();
827
901
  init_progress();
828
902
  init_pathCache();
829
903
  init_mime();
904
+ init_toon();
830
905
  }
831
906
  });
832
907
 
@@ -850,12 +925,6 @@ import * as fs from "fs/promises";
850
925
  // src/auth/utils.ts
851
926
  import * as path from "path";
852
927
  import * as os from "os";
853
- import { fileURLToPath } from "url";
854
- function getProjectRoot() {
855
- const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
856
- const projectRoot = path.join(__dirname2, "..", "..");
857
- return path.resolve(projectRoot);
858
- }
859
928
  function getSecureTokenPath() {
860
929
  const customTokenPath = process.env.GOOGLE_WORKSPACE_MCP_TOKEN_PATH;
861
930
  if (customTokenPath) {
@@ -869,25 +938,12 @@ function getSecureTokenPath() {
869
938
  const tokenDir = path.join(configHome, "google-workspace-mcp");
870
939
  return path.join(tokenDir, "tokens.json");
871
940
  }
872
- function getLegacyTokenPath() {
873
- const projectRoot = getProjectRoot();
874
- return path.join(projectRoot, ".gcp-saved-tokens.json");
875
- }
876
- function getAdditionalLegacyPaths() {
877
- return [
878
- process.env.GOOGLE_TOKEN_PATH,
879
- path.join(process.cwd(), "google-tokens.json"),
880
- path.join(process.cwd(), ".gcp-saved-tokens.json")
881
- ].filter(Boolean);
882
- }
883
941
  function getKeysFilePath() {
884
942
  const envCredentialsPath = process.env.GOOGLE_DRIVE_OAUTH_CREDENTIALS;
885
943
  if (envCredentialsPath) {
886
944
  return path.resolve(envCredentialsPath);
887
945
  }
888
- const projectRoot = getProjectRoot();
889
- const keysPath = path.join(projectRoot, "gcp-oauth.keys.json");
890
- return keysPath;
946
+ return path.join(process.cwd(), "gcp-oauth.keys.json");
891
947
  }
892
948
  function generateCredentialsErrorMessage() {
893
949
  return `
@@ -1052,46 +1108,13 @@ var TokenManager = class {
1052
1108
  }
1053
1109
  });
1054
1110
  }
1055
- async migrateLegacyTokens() {
1056
- const legacyPaths = [getLegacyTokenPath(), ...getAdditionalLegacyPaths()];
1057
- for (const legacyPath of legacyPaths) {
1058
- try {
1059
- if (!await fs2.access(legacyPath).then(() => true).catch(() => false)) {
1060
- continue;
1061
- }
1062
- const legacyTokens = JSON.parse(await fs2.readFile(legacyPath, "utf-8"));
1063
- if (!legacyTokens || typeof legacyTokens !== "object") {
1064
- log(`Invalid legacy token format at ${legacyPath}, skipping`);
1065
- continue;
1066
- }
1067
- await this.ensureTokenDirectoryExists();
1068
- await fs2.writeFile(this.tokenPath, JSON.stringify(legacyTokens, null, 2), {
1069
- mode: 384
1070
- });
1071
- log(`Migrated tokens from legacy location: ${legacyPath} to: ${this.tokenPath}`);
1072
- try {
1073
- await fs2.unlink(legacyPath);
1074
- log("Removed legacy token file");
1075
- } catch (unlinkErr) {
1076
- log("Warning: Could not remove legacy token file:", unlinkErr);
1077
- }
1078
- return true;
1079
- } catch (error) {
1080
- log(`Error migrating legacy tokens from ${legacyPath}`, error);
1081
- }
1082
- }
1083
- return false;
1084
- }
1085
1111
  async loadSavedTokens() {
1086
1112
  try {
1087
1113
  await this.ensureTokenDirectoryExists();
1088
1114
  const tokenExists = await fs2.access(this.tokenPath).then(() => true).catch(() => false);
1089
1115
  if (!tokenExists) {
1090
- const migrated = await this.migrateLegacyTokens();
1091
- if (!migrated) {
1092
- log("No token file found at:", this.tokenPath);
1093
- return false;
1094
- }
1116
+ log("No token file found at:", this.tokenPath);
1117
+ return false;
1095
1118
  }
1096
1119
  const tokens = JSON.parse(await fs2.readFile(this.tokenPath, "utf-8"));
1097
1120
  if (!tokens || typeof tokens !== "object") {
@@ -1454,50 +1477,12 @@ async function authenticate() {
1454
1477
 
1455
1478
  // src/index.ts
1456
1479
  init_utils();
1457
- import { fileURLToPath as fileURLToPath2 } from "url";
1480
+ import { fileURLToPath } from "url";
1458
1481
  import { readFileSync } from "fs";
1459
- import { join as join3, dirname as dirname3 } from "path";
1482
+ import { join as join3, dirname as dirname2 } from "path";
1460
1483
 
1461
- // src/config/services.ts
1462
- var SERVICE_NAMES = ["drive", "docs", "sheets", "slides", "calendar", "gmail"];
1463
- var UNIFIED_REQUIRED_SERVICES = ["drive", "docs", "sheets", "slides"];
1464
- var enabledServices = null;
1465
- function getEnabledServices() {
1466
- if (enabledServices !== null) return enabledServices;
1467
- const envValue = process.env.GOOGLE_WORKSPACE_SERVICES;
1468
- if (envValue === void 0) {
1469
- enabledServices = new Set(SERVICE_NAMES);
1470
- return enabledServices;
1471
- }
1472
- if (envValue.trim() === "") {
1473
- enabledServices = /* @__PURE__ */ new Set();
1474
- return enabledServices;
1475
- }
1476
- const requested = envValue.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
1477
- const valid = /* @__PURE__ */ new Set();
1478
- const unknown = [];
1479
- for (const service of requested) {
1480
- if (SERVICE_NAMES.includes(service)) {
1481
- valid.add(service);
1482
- } else {
1483
- unknown.push(service);
1484
- }
1485
- }
1486
- if (unknown.length > 0) {
1487
- console.warn(
1488
- `[google-workspace-mcp] Unknown services: ${unknown.join(", ")}. Valid: ${SERVICE_NAMES.join(", ")}`
1489
- );
1490
- }
1491
- enabledServices = valid;
1492
- return enabledServices;
1493
- }
1494
- function isServiceEnabled(service) {
1495
- return getEnabledServices().has(service);
1496
- }
1497
- function areUnifiedToolsEnabled() {
1498
- const enabled = getEnabledServices();
1499
- return UNIFIED_REQUIRED_SERVICES.every((s) => enabled.has(s));
1500
- }
1484
+ // src/config/index.ts
1485
+ init_services();
1501
1486
 
1502
1487
  // src/tools/definitions.ts
1503
1488
  var driveTools = [
@@ -4463,13 +4448,13 @@ var gmailTools = [
4463
4448
  },
4464
4449
  {
4465
4450
  name: "modify_email",
4466
- description: "Add/remove labels (max 1000 IDs per request)",
4451
+ description: "Add/remove labels on threads (max 1000 IDs per request)",
4467
4452
  inputSchema: {
4468
4453
  type: "object",
4469
4454
  properties: {
4470
- messageId: {
4455
+ threadId: {
4471
4456
  oneOf: [{ type: "string" }, { type: "array", items: { type: "string" }, maxItems: 1e3 }],
4472
- description: "Message ID or array of IDs (max 1000)"
4457
+ description: "Thread ID or array of IDs (max 1000)"
4473
4458
  },
4474
4459
  addLabelIds: {
4475
4460
  type: "array",
@@ -4482,14 +4467,14 @@ var gmailTools = [
4482
4467
  description: "(optional) Label IDs to remove"
4483
4468
  }
4484
4469
  },
4485
- required: ["messageId"]
4470
+ required: ["threadId"]
4486
4471
  },
4487
4472
  outputSchema: {
4488
4473
  type: "object",
4489
4474
  properties: {
4490
- modified: { type: "number", description: "Number of messages modified" },
4491
- addedLabels: { type: "array", items: { type: "string" }, description: "Labels added" },
4492
- removedLabels: { type: "array", items: { type: "string" }, description: "Labels removed" }
4475
+ id: { type: "string", description: "Thread ID" },
4476
+ messageCount: { type: "number", description: "Number of messages in thread" },
4477
+ labelIds: { type: "array", items: { type: "string" }, description: "Current labels" }
4493
4478
  }
4494
4479
  }
4495
4480
  },
@@ -5109,9 +5094,6 @@ function formatBytes(bytes, options = {}) {
5109
5094
  const formatted = value.toFixed(precision);
5110
5095
  return `${formatted} ${BYTE_UNITS[unitIndex]}`;
5111
5096
  }
5112
- function formatBytesCompact(bytes, nullValue = "N/A") {
5113
- return formatBytes(bytes, { precision: 1, nullValue });
5114
- }
5115
5097
 
5116
5098
  // src/schemas/drive.ts
5117
5099
  import { z } from "zod";
@@ -5841,7 +5823,7 @@ var DeleteEmailSchema = z7.object({
5841
5823
  messageId: z7.union([z7.string().min(1), z7.array(z7.string().min(1)).min(1).max(1e3)]).describe("Message ID or array of IDs (max 1000 for batch)")
5842
5824
  });
5843
5825
  var ModifyEmailSchema = z7.object({
5844
- messageId: z7.union([z7.string().min(1), z7.array(z7.string().min(1)).min(1).max(1e3)]).describe("Message ID or array of IDs (max 1000 for batch)"),
5826
+ threadId: z7.union([z7.string().min(1), z7.array(z7.string().min(1)).min(1).max(1e3)]).describe("Thread ID or array of IDs (max 1000 for batch)"),
5845
5827
  addLabelIds: z7.array(z7.string()).optional().describe("Label IDs to add"),
5846
5828
  removeLabelIds: z7.array(z7.string()).optional().describe("Label IDs to remove")
5847
5829
  });
@@ -6261,19 +6243,29 @@ async function handleSearch(drive2, args) {
6261
6243
  includeItemsFromAllDrives: true,
6262
6244
  supportsAllDrives: true
6263
6245
  });
6264
- const fileList = res.data.files?.map((f) => `${f.name} (${f.mimeType})`).join("\n") || "";
6246
+ const files = res.data.files?.map((f) => ({
6247
+ id: f.id,
6248
+ name: f.name,
6249
+ mimeType: f.mimeType,
6250
+ modifiedTime: f.modifiedTime,
6251
+ size: f.size
6252
+ })) || [];
6265
6253
  log("Search results", {
6266
6254
  query: userQuery,
6267
- resultCount: res.data.files?.length
6255
+ resultCount: files.length
6268
6256
  });
6269
- let response = `Found ${res.data.files?.length ?? 0} files:
6270
- ${fileList}`;
6257
+ let textResponse = `Found ${files.length} files:
6258
+
6259
+ ${toToon({ files })}`;
6271
6260
  if (res.data.nextPageToken) {
6272
- response += `
6261
+ textResponse += `
6273
6262
 
6274
6263
  More results available. Use pageToken: ${res.data.nextPageToken}`;
6275
6264
  }
6276
- return successResponse(response);
6265
+ return structuredResponse(textResponse, {
6266
+ files,
6267
+ nextPageToken: res.data.nextPageToken || null
6268
+ });
6277
6269
  }
6278
6270
  async function handleCreateTextFile(drive2, args) {
6279
6271
  const validation = validateArgs(CreateTextFileSchema, args);
@@ -6736,33 +6728,23 @@ async function handleGetSharing(drive2, args) {
6736
6728
  "List permissions"
6737
6729
  );
6738
6730
  const permissionList = permissions.data.permissions || [];
6739
- const formattedPermissions = permissionList.map((p) => {
6740
- let target = "";
6741
- if (p.type === "anyone") {
6742
- target = "Anyone with the link";
6743
- } else if (p.type === "domain") {
6744
- target = `Anyone in ${p.domain}`;
6745
- } else {
6746
- target = p.emailAddress || p.displayName || "Unknown";
6747
- }
6748
- return `\u2022 ${target}: ${p.role} (ID: ${p.id})`;
6749
- }).join("\n");
6731
+ const permissionData = permissionList.map((p) => ({
6732
+ id: p.id,
6733
+ role: p.role,
6734
+ type: p.type,
6735
+ emailAddress: p.emailAddress,
6736
+ domain: p.domain,
6737
+ displayName: p.displayName
6738
+ }));
6750
6739
  const textResponse = `Sharing settings for "${file.data.name}":
6751
6740
 
6752
- ${formattedPermissions}
6741
+ ${toToon({ permissions: permissionData })}
6753
6742
 
6754
6743
  Link: ${file.data.webViewLink}`;
6755
6744
  return structuredResponse(textResponse, {
6756
6745
  fileName: file.data.name,
6757
6746
  webViewLink: file.data.webViewLink,
6758
- permissions: permissionList.map((p) => ({
6759
- id: p.id,
6760
- role: p.role,
6761
- type: p.type,
6762
- emailAddress: p.emailAddress,
6763
- domain: p.domain,
6764
- displayName: p.displayName
6765
- }))
6747
+ permissions: permissionData
6766
6748
  });
6767
6749
  }
6768
6750
  async function handleListRevisions(drive2, args) {
@@ -6796,27 +6778,22 @@ async function handleListRevisions(drive2, args) {
6796
6778
  if (revisionList.length === 0) {
6797
6779
  return successResponse(`No revisions found for "${file.data.name}".`);
6798
6780
  }
6799
- const formattedRevisions = revisionList.map((r, idx) => {
6800
- const author = r.lastModifyingUser?.displayName || r.lastModifyingUser?.emailAddress || "Unknown";
6801
- const sizeStr = formatBytes(r.size);
6802
- const keepForever = r.keepForever ? " (pinned)" : "";
6803
- return `${idx + 1}. ID: ${r.id} | ${r.modifiedTime} | ${author} | ${sizeStr}${keepForever}`;
6804
- }).join("\n");
6781
+ const revisionData = revisionList.map((r) => ({
6782
+ id: r.id,
6783
+ modifiedTime: r.modifiedTime,
6784
+ size: r.size,
6785
+ keepForever: r.keepForever,
6786
+ lastModifyingUser: r.lastModifyingUser ? {
6787
+ displayName: r.lastModifyingUser.displayName,
6788
+ emailAddress: r.lastModifyingUser.emailAddress
6789
+ } : void 0
6790
+ }));
6805
6791
  const textResponse = `Revisions for "${file.data.name}" (${revisionList.length} found):
6806
6792
 
6807
- ${formattedRevisions}`;
6793
+ ${toToon({ revisions: revisionData })}`;
6808
6794
  return structuredResponse(textResponse, {
6809
6795
  fileName: file.data.name,
6810
- revisions: revisionList.map((r) => ({
6811
- id: r.id,
6812
- modifiedTime: r.modifiedTime,
6813
- size: r.size,
6814
- keepForever: r.keepForever,
6815
- lastModifyingUser: r.lastModifyingUser ? {
6816
- displayName: r.lastModifyingUser.displayName,
6817
- emailAddress: r.lastModifyingUser.emailAddress
6818
- } : void 0
6819
- }))
6796
+ revisions: revisionData
6820
6797
  });
6821
6798
  }
6822
6799
  async function handleRestoreRevision(drive2, args) {
@@ -7438,22 +7415,18 @@ async function handleListTrash(drive2, args) {
7438
7415
  nextPageToken: null
7439
7416
  });
7440
7417
  }
7441
- const fileList = files.map((f) => {
7442
- const icon = f.mimeType === FOLDER_MIME_TYPE ? "\u{1F4C1}" : "\u{1F4C4}";
7443
- const size = f.mimeType === FOLDER_MIME_TYPE ? "" : ` (${formatBytesCompact(f.size)})`;
7444
- return `${icon} ${f.name}${size} - ID: ${f.id}`;
7445
- }).join("\n");
7418
+ const fileData = files.map((f) => ({
7419
+ id: f.id,
7420
+ name: f.name,
7421
+ mimeType: f.mimeType,
7422
+ size: f.size,
7423
+ trashedTime: f.trashedTime
7424
+ }));
7446
7425
  const textResponse = `Trash contents (${files.length} items):
7447
7426
 
7448
- ${fileList}` + (response.data.nextPageToken ? "\n\n(More items available - use nextPageToken to continue)" : "");
7427
+ ${toToon({ files: fileData })}` + (response.data.nextPageToken ? "\n\n(More items available - use nextPageToken to continue)" : "");
7449
7428
  return structuredResponse(textResponse, {
7450
- files: files.map((f) => ({
7451
- id: f.id,
7452
- name: f.name,
7453
- mimeType: f.mimeType,
7454
- size: f.size,
7455
- trashedTime: f.trashedTime
7456
- })),
7429
+ files: fileData,
7457
7430
  nextPageToken: response.data.nextPageToken || null
7458
7431
  });
7459
7432
  }
@@ -8538,9 +8511,9 @@ async function listSheetTabs(sheets, spreadsheetId) {
8538
8511
  rowCount: sheet.properties?.gridProperties?.rowCount,
8539
8512
  columnCount: sheet.properties?.gridProperties?.columnCount
8540
8513
  })) || [];
8541
- const tabList = tabs.map((t) => `${t.index}: ${t.title} (${t.rowCount}x${t.columnCount})`).join("\n");
8542
8514
  return structuredResponse(`Spreadsheet has ${tabs.length} tab(s):
8543
- ${tabList}`, {
8515
+
8516
+ ${toToon({ tabs })}`, {
8544
8517
  spreadsheetId,
8545
8518
  tabs
8546
8519
  });
@@ -9749,13 +9722,6 @@ function formatEventTime(eventTime) {
9749
9722
  }
9750
9723
  return "Unknown";
9751
9724
  }
9752
- function formatEventSummary(event) {
9753
- const start = formatEventTime(event.start);
9754
- const end = formatEventTime(event.end);
9755
- const isAllDay = !!event.start?.date;
9756
- const timeStr = isAllDay ? start : `${start} - ${end}`;
9757
- return `${event.summary || "(No title)"} | ${timeStr}`;
9758
- }
9759
9725
  async function handleListCalendars(calendar, args) {
9760
9726
  const validation = validateArgs(ListCalendarsSchema, args);
9761
9727
  if (!validation.success) return validation.response;
@@ -9768,26 +9734,23 @@ async function handleListCalendars(calendar, args) {
9768
9734
  if (calendars.length === 0) {
9769
9735
  return structuredResponse("No calendars found.", { calendars: [] });
9770
9736
  }
9771
- const formattedCalendars = calendars.map((cal) => {
9772
- const primary = cal.primary ? " (Primary)" : "";
9773
- const access2 = cal.accessRole ? ` [${cal.accessRole}]` : "";
9774
- return `${cal.summary || cal.id}${primary}${access2} - ID: ${cal.id}`;
9775
- }).join("\n");
9737
+ const calendarData = calendars.map((cal) => ({
9738
+ id: cal.id,
9739
+ summary: cal.summary,
9740
+ description: cal.description,
9741
+ primary: cal.primary,
9742
+ accessRole: cal.accessRole,
9743
+ backgroundColor: cal.backgroundColor,
9744
+ foregroundColor: cal.foregroundColor,
9745
+ timeZone: cal.timeZone
9746
+ }));
9776
9747
  log("Listed calendars", { count: calendars.length });
9777
- return structuredResponse(`Found ${calendars.length} calendar(s):
9748
+ return structuredResponse(
9749
+ `Found ${calendars.length} calendar(s):
9778
9750
 
9779
- ${formattedCalendars}`, {
9780
- calendars: calendars.map((cal) => ({
9781
- id: cal.id,
9782
- summary: cal.summary,
9783
- description: cal.description,
9784
- primary: cal.primary,
9785
- accessRole: cal.accessRole,
9786
- backgroundColor: cal.backgroundColor,
9787
- foregroundColor: cal.foregroundColor,
9788
- timeZone: cal.timeZone
9789
- }))
9790
- });
9751
+ ${toToon({ calendars: calendarData })}`,
9752
+ { calendars: calendarData }
9753
+ );
9791
9754
  }
9792
9755
  async function handleListEvents(calendar, args) {
9793
9756
  const validation = validateArgs(ListEventsSchema, args);
@@ -9810,10 +9773,29 @@ async function handleListEvents(calendar, args) {
9810
9773
  nextPageToken: null
9811
9774
  });
9812
9775
  }
9813
- const formattedEvents = events.map((event) => `- ${formatEventSummary(event)}`).join("\n");
9776
+ const eventData = events.map((event) => ({
9777
+ id: event.id,
9778
+ summary: event.summary,
9779
+ description: event.description,
9780
+ location: event.location,
9781
+ start: event.start,
9782
+ end: event.end,
9783
+ status: event.status,
9784
+ htmlLink: event.htmlLink,
9785
+ hangoutLink: event.hangoutLink,
9786
+ attendees: event.attendees?.map((a) => ({
9787
+ email: a.email,
9788
+ displayName: a.displayName,
9789
+ responseStatus: a.responseStatus,
9790
+ optional: a.optional
9791
+ })),
9792
+ organizer: event.organizer,
9793
+ creator: event.creator,
9794
+ recurringEventId: event.recurringEventId
9795
+ }));
9814
9796
  let textResponse = `Found ${events.length} event(s):
9815
9797
 
9816
- ${formattedEvents}`;
9798
+ ${toToon({ events: eventData })}`;
9817
9799
  if (response.data.nextPageToken) {
9818
9800
  textResponse += `
9819
9801
 
@@ -9821,26 +9803,7 @@ More events available. Use pageToken: ${response.data.nextPageToken}`;
9821
9803
  }
9822
9804
  log("Listed events", { calendarId, count: events.length });
9823
9805
  return structuredResponse(textResponse, {
9824
- events: events.map((event) => ({
9825
- id: event.id,
9826
- summary: event.summary,
9827
- description: event.description,
9828
- location: event.location,
9829
- start: event.start,
9830
- end: event.end,
9831
- status: event.status,
9832
- htmlLink: event.htmlLink,
9833
- hangoutLink: event.hangoutLink,
9834
- attendees: event.attendees?.map((a) => ({
9835
- email: a.email,
9836
- displayName: a.displayName,
9837
- responseStatus: a.responseStatus,
9838
- optional: a.optional
9839
- })),
9840
- organizer: event.organizer,
9841
- creator: event.creator,
9842
- recurringEventId: event.recurringEventId
9843
- })),
9806
+ events: eventData,
9844
9807
  nextPageToken: response.data.nextPageToken || null
9845
9808
  });
9846
9809
  }
@@ -10215,6 +10178,25 @@ var SYSTEM_LABELS = /* @__PURE__ */ new Set([
10215
10178
  "CATEGORY_UPDATES",
10216
10179
  "CATEGORY_FORUMS"
10217
10180
  ]);
10181
+ var THREAD_ID_PATTERN = /^[0-9a-f]{16}$/i;
10182
+ var CATEGORY_SUGGESTIONS = {
10183
+ INVALID_FORMAT: "Use search_emails to get valid thread IDs",
10184
+ NOT_FOUND: "Thread may have been deleted. Use search_emails to refresh",
10185
+ PERMISSION_DENIED: "You don't have access to this thread",
10186
+ RATE_LIMITED: "Too many requests. Wait and retry with smaller batches",
10187
+ UNKNOWN: "Check the thread ID and try again"
10188
+ };
10189
+ function isValidThreadIdFormat(id) {
10190
+ return THREAD_ID_PATTERN.test(id);
10191
+ }
10192
+ function categorizeError(errorMessage) {
10193
+ const msg = errorMessage.toLowerCase();
10194
+ if (msg.includes("invalid id") || msg.includes("invalid value")) return "INVALID_FORMAT";
10195
+ if (msg.includes("not found")) return "NOT_FOUND";
10196
+ if (msg.includes("permission") || msg.includes("forbidden")) return "PERMISSION_DENIED";
10197
+ if (msg.includes("rate") || msg.includes("quota")) return "RATE_LIMITED";
10198
+ return "UNKNOWN";
10199
+ }
10218
10200
  function extractEmailBody(payload) {
10219
10201
  if (!payload) return { text: "", html: "" };
10220
10202
  const result = { text: "", html: "" };
@@ -10425,10 +10407,9 @@ async function handleSearchEmails(gmail, args) {
10425
10407
  };
10426
10408
  })
10427
10409
  );
10428
- const formattedList = messageDetails.map((m) => `- [${m.id}] ${m.subject || "(No subject)"} from ${m.from || "Unknown"}`).join("\n");
10429
10410
  let textResponse = `Found ${response.data.resultSizeEstimate} email(s):
10430
10411
 
10431
- ${formattedList}`;
10412
+ ${toToon({ messages: messageDetails })}`;
10432
10413
  if (response.data.nextPageToken) {
10433
10414
  textResponse += `
10434
10415
 
@@ -10484,36 +10465,92 @@ async function handleDeleteEmail(gmail, args) {
10484
10465
  async function handleModifyEmail(gmail, args) {
10485
10466
  const validation = validateArgs(ModifyEmailSchema, args);
10486
10467
  if (!validation.success) return validation.response;
10487
- const { messageId, addLabelIds, removeLabelIds } = validation.data;
10488
- const messageIds = Array.isArray(messageId) ? messageId : [messageId];
10489
- if (messageIds.length === 1) {
10490
- const response = await gmail.users.messages.modify({
10468
+ const { threadId, addLabelIds, removeLabelIds } = validation.data;
10469
+ const ids = Array.isArray(threadId) ? threadId : [threadId];
10470
+ if (ids.length === 1) {
10471
+ const response = await gmail.users.threads.modify({
10491
10472
  userId: "me",
10492
- id: messageIds[0],
10473
+ id: ids[0],
10493
10474
  requestBody: { addLabelIds, removeLabelIds }
10494
10475
  });
10495
- log("Modified email labels", { messageId: messageIds[0], addLabelIds, removeLabelIds });
10476
+ log("Modified thread labels", { threadId: ids[0], addLabelIds, removeLabelIds });
10477
+ const messageCount = response.data.messages?.length || 0;
10496
10478
  return structuredResponse(
10497
- `Email ${messageIds[0]} labels updated.
10498
- Current labels: ${response.data.labelIds?.join(", ") || "None"}`,
10479
+ `Thread ${ids[0]} labels updated (${messageCount} message(s) affected).
10480
+ Current labels: ${response.data.messages?.[0]?.labelIds?.join(", ") || "None"}`,
10499
10481
  {
10500
10482
  id: response.data.id,
10501
- threadId: response.data.threadId,
10502
- labelIds: response.data.labelIds
10483
+ historyId: response.data.historyId,
10484
+ messageCount,
10485
+ labelIds: response.data.messages?.[0]?.labelIds
10503
10486
  }
10504
10487
  );
10505
10488
  }
10506
- await gmail.users.messages.batchModify({
10507
- userId: "me",
10508
- requestBody: {
10509
- ids: messageIds,
10510
- addLabelIds,
10511
- removeLabelIds
10512
- }
10489
+ const validIds = ids.filter(isValidThreadIdFormat);
10490
+ const invalidIds = ids.filter((id) => !isValidThreadIdFormat(id));
10491
+ const formatFailures = invalidIds.map((id) => ({
10492
+ threadId: id,
10493
+ category: "INVALID_FORMAT",
10494
+ error: "Invalid thread ID format"
10495
+ }));
10496
+ const results = await Promise.allSettled(
10497
+ validIds.map(
10498
+ (id) => gmail.users.threads.modify({
10499
+ userId: "me",
10500
+ id,
10501
+ requestBody: { addLabelIds, removeLabelIds }
10502
+ })
10503
+ )
10504
+ );
10505
+ const apiSucceeded = results.filter((r) => r.status === "fulfilled").length;
10506
+ const apiFailures = results.map((r, i) => ({ id: validIds[i], result: r })).filter(
10507
+ (item) => item.result.status === "rejected"
10508
+ ).map((item) => {
10509
+ const errorMsg = item.result.reason?.message || String(item.result.reason) || "Unknown error";
10510
+ return {
10511
+ threadId: item.id,
10512
+ category: categorizeError(errorMsg),
10513
+ error: errorMsg
10514
+ };
10513
10515
  });
10514
- log("Batch modified emails", { count: messageIds.length, addLabelIds, removeLabelIds });
10516
+ const allFailures = [...formatFailures, ...apiFailures];
10517
+ const succeeded = apiSucceeded;
10518
+ log("Batch modified threads", {
10519
+ count: ids.length,
10520
+ succeeded,
10521
+ failed: allFailures.length,
10522
+ preFiltered: invalidIds.length,
10523
+ addLabelIds,
10524
+ removeLabelIds
10525
+ });
10526
+ if (allFailures.length > 0) {
10527
+ const failuresByCategory = allFailures.reduce(
10528
+ (acc, f) => {
10529
+ if (!acc[f.category]) acc[f.category] = [];
10530
+ acc[f.category].push(f.threadId);
10531
+ return acc;
10532
+ },
10533
+ {}
10534
+ );
10535
+ const categoryLines = Object.entries(failuresByCategory).map(([category, threadIds]) => {
10536
+ const suggestion = CATEGORY_SUGGESTIONS[category];
10537
+ const idList = threadIds.slice(0, 5).join(", ");
10538
+ const moreText = threadIds.length > 5 ? ` (+${threadIds.length - 5} more)` : "";
10539
+ return `- ${category} (${threadIds.length}): ${suggestion}
10540
+ ${idList}${moreText}`;
10541
+ }).join("\n");
10542
+ return structuredResponse(
10543
+ `Partially completed: ${succeeded} thread(s) modified, ${allFailures.length} failed.` + (addLabelIds ? `
10544
+ Added labels: ${addLabelIds.join(", ")}` : "") + (removeLabelIds ? `
10545
+ Removed labels: ${removeLabelIds.join(", ")}` : "") + `
10546
+
10547
+ Failures:
10548
+ ${categoryLines}`,
10549
+ { succeeded, failed: allFailures.length, total: ids.length, failuresByCategory }
10550
+ );
10551
+ }
10515
10552
  return successResponse(
10516
- `Successfully modified labels for ${messageIds.length} email(s).` + (addLabelIds ? `
10553
+ `Successfully modified labels for ${ids.length} thread(s).` + (addLabelIds ? `
10517
10554
  Added labels: ${addLabelIds.join(", ")}` : "") + (removeLabelIds ? `
10518
10555
  Removed labels: ${removeLabelIds.join(", ")}` : "")
10519
10556
  );
@@ -10629,28 +10666,31 @@ async function handleListLabels(gmail, args) {
10629
10666
  if (!includeSystemLabels) {
10630
10667
  labels = userLabels;
10631
10668
  }
10632
- const formattedLabels = labels.map((l) => `- ${l.name} [${l.id}] (${l.type})`).join("\n");
10669
+ const labelData = labels.map((l) => ({
10670
+ id: l.id,
10671
+ name: l.name,
10672
+ type: l.type,
10673
+ messageListVisibility: l.messageListVisibility,
10674
+ labelListVisibility: l.labelListVisibility,
10675
+ color: l.color,
10676
+ messagesTotal: l.messagesTotal,
10677
+ messagesUnread: l.messagesUnread
10678
+ }));
10633
10679
  log("Listed labels", {
10634
10680
  total: labels.length,
10635
10681
  system: systemLabels.length,
10636
10682
  user: userLabels.length
10637
10683
  });
10638
- return structuredResponse(`Found ${labels.length} label(s):
10684
+ return structuredResponse(
10685
+ `Found ${labels.length} label(s):
10639
10686
 
10640
- ${formattedLabels}`, {
10641
- labels: labels.map((l) => ({
10642
- id: l.id,
10643
- name: l.name,
10644
- type: l.type,
10645
- messageListVisibility: l.messageListVisibility,
10646
- labelListVisibility: l.labelListVisibility,
10647
- color: l.color,
10648
- messagesTotal: l.messagesTotal,
10649
- messagesUnread: l.messagesUnread
10650
- })),
10651
- systemLabelCount: systemLabels.length,
10652
- userLabelCount: userLabels.length
10653
- });
10687
+ ${toToon({ labels: labelData })}`,
10688
+ {
10689
+ labels: labelData,
10690
+ systemLabelCount: systemLabels.length,
10691
+ userLabelCount: userLabels.length
10692
+ }
10693
+ );
10654
10694
  }
10655
10695
  async function handleGetOrCreateLabel(gmail, args) {
10656
10696
  const validation = validateArgs(GetOrCreateLabelSchema, args);
@@ -10823,26 +10863,20 @@ ${actionStr || "None"}`,
10823
10863
  if (filters.length === 0) {
10824
10864
  return structuredResponse("No filters found.", { filters: [] });
10825
10865
  }
10826
- const formattedFilters = filters.map((f) => {
10827
- const criteria = f.criteria || {};
10828
- const criteriaStr = [
10829
- criteria.from ? `from:${criteria.from}` : null,
10830
- criteria.to ? `to:${criteria.to}` : null,
10831
- criteria.subject ? `subject:${criteria.subject}` : null,
10832
- criteria.query ? `query:${criteria.query}` : null
10833
- ].filter(Boolean).join(", ");
10834
- return `- [${f.id}] ${criteriaStr || "(no criteria)"}`;
10835
- }).join("\n");
10866
+ const filterData = filters.map((f) => ({
10867
+ id: f.id,
10868
+ criteria: f.criteria,
10869
+ action: f.action
10870
+ }));
10836
10871
  log("Listed filters", { count: filters.length });
10837
- return structuredResponse(`Found ${filters.length} filter(s):
10872
+ return structuredResponse(
10873
+ `Found ${filters.length} filter(s):
10838
10874
 
10839
- ${formattedFilters}`, {
10840
- filters: filters.map((f) => ({
10841
- id: f.id,
10842
- criteria: f.criteria,
10843
- action: f.action
10844
- }))
10845
- });
10875
+ ${toToon({ filters: filterData })}`,
10876
+ {
10877
+ filters: filterData
10878
+ }
10879
+ );
10846
10880
  }
10847
10881
  async function handleDeleteFilter(gmail, args) {
10848
10882
  const validation = validateArgs(DeleteFilterSchema, args);
@@ -10928,8 +10962,8 @@ async function handleListTools(args) {
10928
10962
  var drive = null;
10929
10963
  var authClient = null;
10930
10964
  var authenticationPromise = null;
10931
- var __filename = fileURLToPath2(import.meta.url);
10932
- var __dirname = dirname3(__filename);
10965
+ var __filename = fileURLToPath(import.meta.url);
10966
+ var __dirname = dirname2(__filename);
10933
10967
  var packageJsonPath = join3(__dirname, "..", "package.json");
10934
10968
  var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
10935
10969
  var VERSION = packageJson.version;