@dguido/google-workspace-mcp 1.0.0 → 1.1.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 Dan Guido
3
+ Copyright (c) 2026 Dan Guido
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -95,6 +95,28 @@ Create a presentation called "Product Roadmap" with slides for Q1 milestones.
95
95
 
96
96
  Tokens are stored at `~/.config/google-workspace-mcp/tokens.json` by default.
97
97
 
98
+ ### Token-Efficient Output (TOON)
99
+
100
+ For LLM-optimized responses that reduce token usage by 20-50%, enable TOON format:
101
+
102
+ ```json
103
+ {
104
+ "mcpServers": {
105
+ "google-workspace": {
106
+ "command": "npx",
107
+ "args": ["@dguido/google-workspace-mcp"],
108
+ "env": {
109
+ "GOOGLE_DRIVE_OAUTH_CREDENTIALS": "/path/to/gcp-oauth.keys.json",
110
+ "GOOGLE_WORKSPACE_SERVICES": "drive,gmail,calendar",
111
+ "GOOGLE_WORKSPACE_TOON_FORMAT": "true"
112
+ }
113
+ }
114
+ }
115
+ }
116
+ ```
117
+
118
+ TOON (Token-Oriented Object Notation) encodes structured responses more compactly than JSON by eliminating repeated field names. Savings are highest for list operations (calendars, events, emails, filters).
119
+
98
120
  ### Service Configuration
99
121
 
100
122
  By default, we recommend enabling only the core services (`drive,gmail,calendar`) as shown in Quick Start. This provides file management, email, and calendar capabilities without the complexity of document editing tools.
@@ -125,25 +147,33 @@ See [Advanced Configuration](docs/ADVANCED.md) for multi-account setup and envir
125
147
 
126
148
  ## Available Tools
127
149
 
128
- ### Drive Operations
150
+ ### Drive (29 tools)
151
+
152
+ `search` `listFolder` `createFolder` `createTextFile` `updateTextFile` `deleteItem` `renameItem` `moveItem` `copyFile` `getFileMetadata` `exportFile` `shareFile` `getSharing` `removePermission` `listRevisions` `restoreRevision` `downloadFile` `uploadFile` `getStorageQuota` `starFile` `resolveFilePath` `batchDelete` `batchRestore` `batchMove` `batchShare` `listTrash` `restoreFromTrash` `emptyTrash` `getFolderTree`
129
153
 
130
- `search` `listFolder` `createFolder` `deleteItem` `renameItem` `moveItem` `copyFile`
154
+ ### Google Docs (8 tools)
131
155
 
132
- ### File Operations
156
+ `createGoogleDoc` `updateGoogleDoc` `getGoogleDocContent` `appendToDoc` `insertTextInDoc` `deleteTextInDoc` `replaceTextInDoc` `formatGoogleDocRange`
133
157
 
134
- `createTextFile` `updateTextFile`
158
+ ### Google Sheets (7 tools)
135
159
 
136
- ### Google Docs
160
+ `createGoogleSheet` `updateGoogleSheet` `getGoogleSheetContent` `formatGoogleSheetCells` `mergeGoogleSheetCells` `addGoogleSheetConditionalFormat` `sheetTabs`
137
161
 
138
- `createGoogleDoc` `updateGoogleDoc` `getGoogleDocContent` `formatGoogleDocRange`
162
+ ### Google Slides (10 tools)
139
163
 
140
- ### Google Sheets
164
+ `createGoogleSlides` `updateGoogleSlides` `getGoogleSlidesContent` `formatSlidesText` `formatSlidesShape` `formatSlideBackground` `createGoogleSlidesTextBox` `createGoogleSlidesShape` `slidesSpeakerNotes` `listSlidePages`
141
165
 
142
- `createGoogleSheet` `updateGoogleSheet` `getGoogleSheetContent` `formatGoogleSheetCells` `formatGoogleSheetText` `formatGoogleSheetNumbers` `setGoogleSheetBorders` `mergeGoogleSheetCells` `addGoogleSheetConditionalFormat`
166
+ ### Calendar (7 tools)
143
167
 
144
- ### Google Slides
168
+ `listCalendars` `listEvents` `getEvent` `createEvent` `updateEvent` `deleteEvent` `findFreeTime`
145
169
 
146
- `createGoogleSlides` `updateGoogleSlides` `getGoogleSlidesContent` `formatGoogleSlidesElement` `createGoogleSlidesTextBox` `createGoogleSlidesShape` `getGoogleSlidesSpeakerNotes` `updateGoogleSlidesSpeakerNotes`
170
+ ### Gmail (14 tools)
171
+
172
+ `sendEmail` `draftEmail` `readEmail` `searchEmails` `deleteEmail` `modifyEmail` `downloadAttachment` `listLabels` `getOrCreateLabel` `updateLabel` `deleteLabel` `createFilter` `listFilters` `deleteFilter`
173
+
174
+ ### Unified (3 tools)
175
+
176
+ `createFile` `updateFile` `getFileContent`
147
177
 
148
178
  [Full API Reference](docs/API.md)
149
179
 
@@ -194,10 +224,6 @@ npm test # Run tests
194
224
 
195
225
  See [Contributing Guide](CONTRIBUTING.md) for project structure and development workflow.
196
226
 
197
- ## Docker
198
-
199
- See [Docker Usage](docs/DOCKER.md) for containerized deployment.
200
-
201
227
  ## Links
202
228
 
203
229
  - [GitHub Repository](https://github.com/dguido/google-workspace-mcp)
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
 
@@ -1458,46 +1533,8 @@ import { fileURLToPath as fileURLToPath2 } from "url";
1458
1533
  import { readFileSync } from "fs";
1459
1534
  import { join as join3, dirname as dirname3 } from "path";
1460
1535
 
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
- }
1536
+ // src/config/index.ts
1537
+ init_services();
1501
1538
 
1502
1539
  // src/tools/definitions.ts
1503
1540
  var driveTools = [
@@ -5109,9 +5146,6 @@ function formatBytes(bytes, options = {}) {
5109
5146
  const formatted = value.toFixed(precision);
5110
5147
  return `${formatted} ${BYTE_UNITS[unitIndex]}`;
5111
5148
  }
5112
- function formatBytesCompact(bytes, nullValue = "N/A") {
5113
- return formatBytes(bytes, { precision: 1, nullValue });
5114
- }
5115
5149
 
5116
5150
  // src/schemas/drive.ts
5117
5151
  import { z } from "zod";
@@ -6261,19 +6295,29 @@ async function handleSearch(drive2, args) {
6261
6295
  includeItemsFromAllDrives: true,
6262
6296
  supportsAllDrives: true
6263
6297
  });
6264
- const fileList = res.data.files?.map((f) => `${f.name} (${f.mimeType})`).join("\n") || "";
6298
+ const files = res.data.files?.map((f) => ({
6299
+ id: f.id,
6300
+ name: f.name,
6301
+ mimeType: f.mimeType,
6302
+ modifiedTime: f.modifiedTime,
6303
+ size: f.size
6304
+ })) || [];
6265
6305
  log("Search results", {
6266
6306
  query: userQuery,
6267
- resultCount: res.data.files?.length
6307
+ resultCount: files.length
6268
6308
  });
6269
- let response = `Found ${res.data.files?.length ?? 0} files:
6270
- ${fileList}`;
6309
+ let textResponse = `Found ${files.length} files:
6310
+
6311
+ ${toToon({ files })}`;
6271
6312
  if (res.data.nextPageToken) {
6272
- response += `
6313
+ textResponse += `
6273
6314
 
6274
6315
  More results available. Use pageToken: ${res.data.nextPageToken}`;
6275
6316
  }
6276
- return successResponse(response);
6317
+ return structuredResponse(textResponse, {
6318
+ files,
6319
+ nextPageToken: res.data.nextPageToken || null
6320
+ });
6277
6321
  }
6278
6322
  async function handleCreateTextFile(drive2, args) {
6279
6323
  const validation = validateArgs(CreateTextFileSchema, args);
@@ -6736,33 +6780,23 @@ async function handleGetSharing(drive2, args) {
6736
6780
  "List permissions"
6737
6781
  );
6738
6782
  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");
6783
+ const permissionData = permissionList.map((p) => ({
6784
+ id: p.id,
6785
+ role: p.role,
6786
+ type: p.type,
6787
+ emailAddress: p.emailAddress,
6788
+ domain: p.domain,
6789
+ displayName: p.displayName
6790
+ }));
6750
6791
  const textResponse = `Sharing settings for "${file.data.name}":
6751
6792
 
6752
- ${formattedPermissions}
6793
+ ${toToon({ permissions: permissionData })}
6753
6794
 
6754
6795
  Link: ${file.data.webViewLink}`;
6755
6796
  return structuredResponse(textResponse, {
6756
6797
  fileName: file.data.name,
6757
6798
  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
- }))
6799
+ permissions: permissionData
6766
6800
  });
6767
6801
  }
6768
6802
  async function handleListRevisions(drive2, args) {
@@ -6796,27 +6830,22 @@ async function handleListRevisions(drive2, args) {
6796
6830
  if (revisionList.length === 0) {
6797
6831
  return successResponse(`No revisions found for "${file.data.name}".`);
6798
6832
  }
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");
6833
+ const revisionData = revisionList.map((r) => ({
6834
+ id: r.id,
6835
+ modifiedTime: r.modifiedTime,
6836
+ size: r.size,
6837
+ keepForever: r.keepForever,
6838
+ lastModifyingUser: r.lastModifyingUser ? {
6839
+ displayName: r.lastModifyingUser.displayName,
6840
+ emailAddress: r.lastModifyingUser.emailAddress
6841
+ } : void 0
6842
+ }));
6805
6843
  const textResponse = `Revisions for "${file.data.name}" (${revisionList.length} found):
6806
6844
 
6807
- ${formattedRevisions}`;
6845
+ ${toToon({ revisions: revisionData })}`;
6808
6846
  return structuredResponse(textResponse, {
6809
6847
  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
- }))
6848
+ revisions: revisionData
6820
6849
  });
6821
6850
  }
6822
6851
  async function handleRestoreRevision(drive2, args) {
@@ -7438,22 +7467,18 @@ async function handleListTrash(drive2, args) {
7438
7467
  nextPageToken: null
7439
7468
  });
7440
7469
  }
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");
7470
+ const fileData = files.map((f) => ({
7471
+ id: f.id,
7472
+ name: f.name,
7473
+ mimeType: f.mimeType,
7474
+ size: f.size,
7475
+ trashedTime: f.trashedTime
7476
+ }));
7446
7477
  const textResponse = `Trash contents (${files.length} items):
7447
7478
 
7448
- ${fileList}` + (response.data.nextPageToken ? "\n\n(More items available - use nextPageToken to continue)" : "");
7479
+ ${toToon({ files: fileData })}` + (response.data.nextPageToken ? "\n\n(More items available - use nextPageToken to continue)" : "");
7449
7480
  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
- })),
7481
+ files: fileData,
7457
7482
  nextPageToken: response.data.nextPageToken || null
7458
7483
  });
7459
7484
  }
@@ -8538,9 +8563,9 @@ async function listSheetTabs(sheets, spreadsheetId) {
8538
8563
  rowCount: sheet.properties?.gridProperties?.rowCount,
8539
8564
  columnCount: sheet.properties?.gridProperties?.columnCount
8540
8565
  })) || [];
8541
- const tabList = tabs.map((t) => `${t.index}: ${t.title} (${t.rowCount}x${t.columnCount})`).join("\n");
8542
8566
  return structuredResponse(`Spreadsheet has ${tabs.length} tab(s):
8543
- ${tabList}`, {
8567
+
8568
+ ${toToon({ tabs })}`, {
8544
8569
  spreadsheetId,
8545
8570
  tabs
8546
8571
  });
@@ -9749,13 +9774,6 @@ function formatEventTime(eventTime) {
9749
9774
  }
9750
9775
  return "Unknown";
9751
9776
  }
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
9777
  async function handleListCalendars(calendar, args) {
9760
9778
  const validation = validateArgs(ListCalendarsSchema, args);
9761
9779
  if (!validation.success) return validation.response;
@@ -9768,26 +9786,23 @@ async function handleListCalendars(calendar, args) {
9768
9786
  if (calendars.length === 0) {
9769
9787
  return structuredResponse("No calendars found.", { calendars: [] });
9770
9788
  }
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");
9789
+ const calendarData = calendars.map((cal) => ({
9790
+ id: cal.id,
9791
+ summary: cal.summary,
9792
+ description: cal.description,
9793
+ primary: cal.primary,
9794
+ accessRole: cal.accessRole,
9795
+ backgroundColor: cal.backgroundColor,
9796
+ foregroundColor: cal.foregroundColor,
9797
+ timeZone: cal.timeZone
9798
+ }));
9776
9799
  log("Listed calendars", { count: calendars.length });
9777
- return structuredResponse(`Found ${calendars.length} calendar(s):
9800
+ return structuredResponse(
9801
+ `Found ${calendars.length} calendar(s):
9778
9802
 
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
- });
9803
+ ${toToon({ calendars: calendarData })}`,
9804
+ { calendars: calendarData }
9805
+ );
9791
9806
  }
9792
9807
  async function handleListEvents(calendar, args) {
9793
9808
  const validation = validateArgs(ListEventsSchema, args);
@@ -9810,10 +9825,29 @@ async function handleListEvents(calendar, args) {
9810
9825
  nextPageToken: null
9811
9826
  });
9812
9827
  }
9813
- const formattedEvents = events.map((event) => `- ${formatEventSummary(event)}`).join("\n");
9828
+ const eventData = events.map((event) => ({
9829
+ id: event.id,
9830
+ summary: event.summary,
9831
+ description: event.description,
9832
+ location: event.location,
9833
+ start: event.start,
9834
+ end: event.end,
9835
+ status: event.status,
9836
+ htmlLink: event.htmlLink,
9837
+ hangoutLink: event.hangoutLink,
9838
+ attendees: event.attendees?.map((a) => ({
9839
+ email: a.email,
9840
+ displayName: a.displayName,
9841
+ responseStatus: a.responseStatus,
9842
+ optional: a.optional
9843
+ })),
9844
+ organizer: event.organizer,
9845
+ creator: event.creator,
9846
+ recurringEventId: event.recurringEventId
9847
+ }));
9814
9848
  let textResponse = `Found ${events.length} event(s):
9815
9849
 
9816
- ${formattedEvents}`;
9850
+ ${toToon({ events: eventData })}`;
9817
9851
  if (response.data.nextPageToken) {
9818
9852
  textResponse += `
9819
9853
 
@@ -9821,26 +9855,7 @@ More events available. Use pageToken: ${response.data.nextPageToken}`;
9821
9855
  }
9822
9856
  log("Listed events", { calendarId, count: events.length });
9823
9857
  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
- })),
9858
+ events: eventData,
9844
9859
  nextPageToken: response.data.nextPageToken || null
9845
9860
  });
9846
9861
  }
@@ -10425,10 +10440,9 @@ async function handleSearchEmails(gmail, args) {
10425
10440
  };
10426
10441
  })
10427
10442
  );
10428
- const formattedList = messageDetails.map((m) => `- [${m.id}] ${m.subject || "(No subject)"} from ${m.from || "Unknown"}`).join("\n");
10429
10443
  let textResponse = `Found ${response.data.resultSizeEstimate} email(s):
10430
10444
 
10431
- ${formattedList}`;
10445
+ ${toToon({ messages: messageDetails })}`;
10432
10446
  if (response.data.nextPageToken) {
10433
10447
  textResponse += `
10434
10448
 
@@ -10629,28 +10643,31 @@ async function handleListLabels(gmail, args) {
10629
10643
  if (!includeSystemLabels) {
10630
10644
  labels = userLabels;
10631
10645
  }
10632
- const formattedLabels = labels.map((l) => `- ${l.name} [${l.id}] (${l.type})`).join("\n");
10646
+ const labelData = labels.map((l) => ({
10647
+ id: l.id,
10648
+ name: l.name,
10649
+ type: l.type,
10650
+ messageListVisibility: l.messageListVisibility,
10651
+ labelListVisibility: l.labelListVisibility,
10652
+ color: l.color,
10653
+ messagesTotal: l.messagesTotal,
10654
+ messagesUnread: l.messagesUnread
10655
+ }));
10633
10656
  log("Listed labels", {
10634
10657
  total: labels.length,
10635
10658
  system: systemLabels.length,
10636
10659
  user: userLabels.length
10637
10660
  });
10638
- return structuredResponse(`Found ${labels.length} label(s):
10661
+ return structuredResponse(
10662
+ `Found ${labels.length} label(s):
10639
10663
 
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
- });
10664
+ ${toToon({ labels: labelData })}`,
10665
+ {
10666
+ labels: labelData,
10667
+ systemLabelCount: systemLabels.length,
10668
+ userLabelCount: userLabels.length
10669
+ }
10670
+ );
10654
10671
  }
10655
10672
  async function handleGetOrCreateLabel(gmail, args) {
10656
10673
  const validation = validateArgs(GetOrCreateLabelSchema, args);
@@ -10823,26 +10840,20 @@ ${actionStr || "None"}`,
10823
10840
  if (filters.length === 0) {
10824
10841
  return structuredResponse("No filters found.", { filters: [] });
10825
10842
  }
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");
10843
+ const filterData = filters.map((f) => ({
10844
+ id: f.id,
10845
+ criteria: f.criteria,
10846
+ action: f.action
10847
+ }));
10836
10848
  log("Listed filters", { count: filters.length });
10837
- return structuredResponse(`Found ${filters.length} filter(s):
10849
+ return structuredResponse(
10850
+ `Found ${filters.length} filter(s):
10838
10851
 
10839
- ${formattedFilters}`, {
10840
- filters: filters.map((f) => ({
10841
- id: f.id,
10842
- criteria: f.criteria,
10843
- action: f.action
10844
- }))
10845
- });
10852
+ ${toToon({ filters: filterData })}`,
10853
+ {
10854
+ filters: filterData
10855
+ }
10856
+ );
10846
10857
  }
10847
10858
  async function handleDeleteFilter(gmail, args) {
10848
10859
  const validation = validateArgs(DeleteFilterSchema, args);