@alanse/mcp-server-google-workspace 0.2.1 → 1.0.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.
- package/README.md +250 -17
- package/dist/auth.js +6 -0
- package/dist/index.js +1 -1
- package/dist/lib/calendar-helpers.js +197 -0
- package/dist/lib/document-id-resolver.js +76 -0
- package/dist/lib/drive-helpers.js +263 -0
- package/dist/lib/gmail-helpers.js +204 -0
- package/dist/lib/response-formatter.js +82 -0
- package/dist/lib/validation.js +112 -0
- package/dist/tools/calendar/acl/calendar_acl_insert.js +80 -0
- package/dist/tools/calendar/acl/calendar_acl_list.js +82 -0
- package/dist/tools/calendar/basic/calendar_create_event.js +113 -0
- package/dist/tools/calendar/basic/calendar_delete_event.js +52 -0
- package/dist/tools/calendar/basic/calendar_get_event.js +52 -0
- package/dist/tools/calendar/basic/calendar_list_events.js +86 -0
- package/dist/tools/calendar/basic/calendar_update_event.js +116 -0
- package/dist/tools/calendar/calendarlist/calendar_calendarlist_get.js +73 -0
- package/dist/tools/calendar/calendarlist/calendar_calendarlist_list.js +87 -0
- package/dist/tools/calendar/calendars/calendar_calendars_get.js +52 -0
- package/dist/tools/calendar/calendars/calendar_calendars_insert.js +66 -0
- package/dist/tools/calendar/calendars/calendar_calendars_update.js +85 -0
- package/dist/tools/calendar/colors/calendar_colors_get.js +46 -0
- package/dist/tools/calendar/events_advanced/calendar_events_instances.js +81 -0
- package/dist/tools/calendar/events_advanced/calendar_events_move.js +63 -0
- package/dist/tools/calendar/events_advanced/calendar_events_quickadd.js +52 -0
- package/dist/tools/calendar/freebusy/calendar_freebusy_query.js +69 -0
- package/dist/tools/calendar/settings/calendar_settings_list.js +81 -0
- package/dist/tools/docs/basic/gdocs_create.js +37 -0
- package/dist/tools/docs/basic/gdocs_get_metadata.js +45 -0
- package/dist/tools/docs/basic/gdocs_list_documents.js +59 -0
- package/dist/tools/docs/basic/gdocs_read.js +62 -0
- package/dist/tools/docs/content/gdocs_append_text.js +57 -0
- package/dist/tools/docs/content/gdocs_apply_style.js +86 -0
- package/dist/tools/docs/content/gdocs_create_heading.js +89 -0
- package/dist/tools/docs/content/gdocs_create_list.js +86 -0
- package/dist/tools/docs/content/gdocs_delete_text.js +64 -0
- package/dist/tools/docs/content/gdocs_format_text.js +137 -0
- package/dist/tools/docs/content/gdocs_insert_text.js +62 -0
- package/dist/tools/docs/content/gdocs_replace_text.js +64 -0
- package/dist/tools/docs/content/gdocs_set_alignment.js +76 -0
- package/dist/tools/docs/content/gdocs_update_text.js +78 -0
- package/dist/tools/docs/elements/gdocs_batch_update.js +108 -0
- package/dist/tools/docs/elements/gdocs_create_table.js +73 -0
- package/dist/tools/docs/elements/gdocs_export.js +62 -0
- package/dist/tools/docs/elements/gdocs_insert_image.js +96 -0
- package/dist/tools/docs/elements/gdocs_insert_link.js +77 -0
- package/dist/tools/docs/elements/gdocs_insert_page_break.js +55 -0
- package/dist/tools/docs/elements/gdocs_insert_toc.js +71 -0
- package/dist/tools/docs/elements/gdocs_merge_documents.js +104 -0
- package/dist/tools/docs/elements/gdocs_suggest_mode.js +41 -0
- package/dist/tools/drive/advanced/drive_empty_trash.js +56 -0
- package/dist/tools/drive/advanced/drive_export_file.js +158 -0
- package/dist/tools/drive/advanced/drive_list_revisions.js +80 -0
- package/dist/tools/drive/basic/drive_get_metadata.js +49 -0
- package/dist/tools/drive/basic/drive_list_files.js +76 -0
- package/dist/tools/drive/file/drive_copy_file.js +79 -0
- package/dist/tools/drive/file/drive_create_file.js +72 -0
- package/dist/tools/drive/file/drive_delete_file.js +48 -0
- package/dist/tools/drive/file/drive_move_file.js +79 -0
- package/dist/tools/drive/file/drive_rename_file.js +58 -0
- package/dist/tools/drive/file/drive_update_file.js +106 -0
- package/dist/tools/drive/file/drive_upload_file.js +80 -0
- package/dist/tools/drive/folder/drive_create_folder.js +67 -0
- package/dist/tools/drive/folder/drive_list_folder_contents.js +68 -0
- package/dist/tools/drive/folder/drive_move_to_folder.js +59 -0
- package/dist/tools/drive/permissions/drive_list_permissions.js +115 -0
- package/dist/tools/drive/permissions/drive_remove_permission.js +71 -0
- package/dist/tools/drive/permissions/drive_share_file.js +116 -0
- package/dist/tools/drive/permissions/drive_update_permission.js +79 -0
- package/dist/tools/gmail/basic/gmail_get_message.js +95 -0
- package/dist/tools/gmail/basic/gmail_get_thread.js +46 -0
- package/dist/tools/gmail/basic/gmail_list_labels.js +54 -0
- package/dist/tools/gmail/basic/gmail_search_messages.js +59 -0
- package/dist/tools/gmail/batch/gmail_batch_modify_labels.js +74 -0
- package/dist/tools/gmail/batch/gmail_get_messages_batch.js +120 -0
- package/dist/tools/gmail/batch/gmail_get_threads_batch.js +102 -0
- package/dist/tools/gmail/labels/gmail_manage_label.js +131 -0
- package/dist/tools/gmail/labels/gmail_modify_labels.js +65 -0
- package/dist/tools/gmail/send/gmail_draft_message.js +117 -0
- package/dist/tools/gmail/send/gmail_send_message.js +109 -0
- package/dist/tools/index.js +386 -3
- package/package.json +8 -3
- package/dist/tools/basic/gsheets_add_sheet.js +0 -65
- package/dist/tools/basic/gsheets_copy_sheet.js +0 -56
- package/dist/tools/basic/gsheets_copy_to.js +0 -113
- package/dist/tools/basic/gsheets_create_spreadsheet.js +0 -88
- package/dist/tools/basic/gsheets_delete_columns.js +0 -69
- package/dist/tools/basic/gsheets_delete_rows.js +0 -69
- package/dist/tools/basic/gsheets_delete_sheet.js +0 -56
- package/dist/tools/basic/gsheets_duplicate_sheet.js +0 -72
- package/dist/tools/basic/gsheets_insert_columns.js +0 -69
- package/dist/tools/basic/gsheets_insert_rows.js +0 -69
- package/dist/tools/basic/gsheets_list_sheets.js +0 -53
- package/dist/tools/basic/gsheets_read.js +0 -120
- package/dist/tools/basic/gsheets_rename_sheet.js +0 -64
- package/dist/tools/charts/gsheets_add_bubble.js +0 -176
- package/dist/tools/charts/gsheets_add_candlestick.js +0 -192
- package/dist/tools/charts/gsheets_add_chart.js +0 -162
- package/dist/tools/charts/gsheets_add_combo.js +0 -169
- package/dist/tools/charts/gsheets_add_histogram.js +0 -143
- package/dist/tools/charts/gsheets_add_org_chart.js +0 -160
- package/dist/tools/charts/gsheets_add_treemap.js +0 -177
- package/dist/tools/charts/gsheets_add_waterfall.js +0 -155
- package/dist/tools/charts/gsheets_delete_chart.js +0 -56
- package/dist/tools/charts/gsheets_update_chart.js +0 -118
- package/dist/tools/data/gsheets_append_data.js +0 -68
- package/dist/tools/data/gsheets_batch_clear.js +0 -53
- package/dist/tools/data/gsheets_batch_update.js +0 -81
- package/dist/tools/data/gsheets_clear_data.js +0 -53
- package/dist/tools/data/gsheets_create_filter.js +0 -81
- package/dist/tools/data/gsheets_find_replace.js +0 -124
- package/dist/tools/data/gsheets_set_data_validation.js +0 -153
- package/dist/tools/data/gsheets_sort_range.js +0 -102
- package/dist/tools/data/gsheets_update_cell.js +0 -44
- package/dist/tools/formatting/gsheets_auto_resize.js +0 -75
- package/dist/tools/formatting/gsheets_format_cells.js +0 -161
- package/dist/tools/formatting/gsheets_freeze_columns.js +0 -67
- package/dist/tools/formatting/gsheets_freeze_rows.js +0 -67
- package/dist/tools/formatting/gsheets_merge_cells.js +0 -85
- package/dist/tools/formatting/gsheets_set_number_format.js +0 -116
- package/dist/tools/formatting/gsheets_unmerge_cells.js +0 -79
- package/dist/tools/formatting/gsheets_update_borders.js +0 -212
- package/dist/tools/gdrive/gdrive_read_file.js +0 -77
- package/dist/tools/gdrive/gdrive_search.js +0 -71
- package/dist/tools/gdrive_read_file.js +0 -77
- package/dist/tools/gdrive_search.js +0 -71
- package/dist/tools/gsheets_add_bubble.js +0 -176
- package/dist/tools/gsheets_add_candlestick.js +0 -192
- package/dist/tools/gsheets_add_chart.js +0 -162
- package/dist/tools/gsheets_add_combo.js +0 -169
- package/dist/tools/gsheets_add_conditional_format.js +0 -175
- package/dist/tools/gsheets_add_histogram.js +0 -143
- package/dist/tools/gsheets_add_named_range.js +0 -87
- package/dist/tools/gsheets_add_org_chart.js +0 -160
- package/dist/tools/gsheets_add_protected_range.js +0 -127
- package/dist/tools/gsheets_add_sheet.js +0 -65
- package/dist/tools/gsheets_add_treemap.js +0 -177
- package/dist/tools/gsheets_add_waterfall.js +0 -155
- package/dist/tools/gsheets_append_data.js +0 -68
- package/dist/tools/gsheets_auto_resize.js +0 -75
- package/dist/tools/gsheets_batch_clear.js +0 -53
- package/dist/tools/gsheets_batch_update.js +0 -81
- package/dist/tools/gsheets_clear_data.js +0 -53
- package/dist/tools/gsheets_copy_sheet.js +0 -56
- package/dist/tools/gsheets_copy_to.js +0 -113
- package/dist/tools/gsheets_create_filter.js +0 -81
- package/dist/tools/gsheets_create_spreadsheet.js +0 -88
- package/dist/tools/gsheets_delete_chart.js +0 -56
- package/dist/tools/gsheets_delete_columns.js +0 -69
- package/dist/tools/gsheets_delete_named_range.js +0 -56
- package/dist/tools/gsheets_delete_protected_range.js +0 -56
- package/dist/tools/gsheets_delete_rows.js +0 -69
- package/dist/tools/gsheets_delete_sheet.js +0 -56
- package/dist/tools/gsheets_duplicate_sheet.js +0 -72
- package/dist/tools/gsheets_find_replace.js +0 -124
- package/dist/tools/gsheets_format_cells.js +0 -161
- package/dist/tools/gsheets_freeze_columns.js +0 -67
- package/dist/tools/gsheets_freeze_rows.js +0 -67
- package/dist/tools/gsheets_insert_columns.js +0 -69
- package/dist/tools/gsheets_insert_rows.js +0 -69
- package/dist/tools/gsheets_list_sheets.js +0 -53
- package/dist/tools/gsheets_merge_cells.js +0 -85
- package/dist/tools/gsheets_read.js +0 -120
- package/dist/tools/gsheets_rename_sheet.js +0 -64
- package/dist/tools/gsheets_set_data_validation.js +0 -153
- package/dist/tools/gsheets_set_number_format.js +0 -116
- package/dist/tools/gsheets_sort_range.js +0 -102
- package/dist/tools/gsheets_unmerge_cells.js +0 -79
- package/dist/tools/gsheets_update_borders.js +0 -212
- package/dist/tools/gsheets_update_cell.js +0 -44
- package/dist/tools/gsheets_update_chart.js +0 -118
- package/dist/tools/gsheets_update_named_range.js +0 -112
- package/dist/tools/gsheets_update_protected_range.js +0 -110
- package/dist/tools/protection/gsheets_add_conditional_format.js +0 -175
- package/dist/tools/protection/gsheets_add_named_range.js +0 -87
- package/dist/tools/protection/gsheets_add_protected_range.js +0 -127
- package/dist/tools/protection/gsheets_delete_named_range.js +0 -56
- package/dist/tools/protection/gsheets_delete_protected_range.js +0 -56
- package/dist/tools/protection/gsheets_update_named_range.js +0 -112
- package/dist/tools/protection/gsheets_update_protected_range.js +0 -110
- /package/dist/tools/drive/{drive_read_file.js → basic/drive_read_file.js} +0 -0
- /package/dist/tools/drive/{drive_search.js → basic/drive_search.js} +0 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Google Drive Helper Functions
|
|
3
|
+
*
|
|
4
|
+
* Utilities for Google Drive API operations including:
|
|
5
|
+
* - File metadata formatting
|
|
6
|
+
* - Permission formatting
|
|
7
|
+
* - MIME type conversion
|
|
8
|
+
* - URL generation
|
|
9
|
+
* - File upload handling
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Format file metadata for display
|
|
13
|
+
*/
|
|
14
|
+
export function formatFileMetadata(file, detailed = false) {
|
|
15
|
+
const id = file.id || "";
|
|
16
|
+
const name = file.name || "Unnamed";
|
|
17
|
+
const mimeType = file.mimeType || "unknown";
|
|
18
|
+
const size = file.size ? formatFileSize(parseInt(file.size)) : "N/A";
|
|
19
|
+
const modifiedTime = file.modifiedTime
|
|
20
|
+
? new Date(file.modifiedTime).toLocaleString()
|
|
21
|
+
: "Unknown";
|
|
22
|
+
const webViewLink = file.webViewLink || "";
|
|
23
|
+
if (!detailed) {
|
|
24
|
+
return `${name} (${id}) - ${mimeType}`;
|
|
25
|
+
}
|
|
26
|
+
let output = `📄 ${name}\n`;
|
|
27
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
|
28
|
+
output += `ID: ${id}\n`;
|
|
29
|
+
output += `MIME Type: ${mimeType}\n`;
|
|
30
|
+
output += `Size: ${size}\n`;
|
|
31
|
+
output += `Modified: ${modifiedTime}\n`;
|
|
32
|
+
if (webViewLink) {
|
|
33
|
+
output += `Web Link: ${webViewLink}\n`;
|
|
34
|
+
}
|
|
35
|
+
if (file.parents && file.parents.length > 0) {
|
|
36
|
+
output += `Parent Folder(s): ${file.parents.join(", ")}\n`;
|
|
37
|
+
}
|
|
38
|
+
output += `━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
|
39
|
+
return output;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Format file list for display
|
|
43
|
+
*/
|
|
44
|
+
export function formatFileList(files, nextPageToken) {
|
|
45
|
+
if (files.length === 0) {
|
|
46
|
+
return "No files found.";
|
|
47
|
+
}
|
|
48
|
+
let output = `Found ${files.length} file(s):\n\n`;
|
|
49
|
+
files.forEach((file, index) => {
|
|
50
|
+
const name = file.name || "Unnamed";
|
|
51
|
+
const id = file.id || "";
|
|
52
|
+
const mimeType = file.mimeType || "unknown";
|
|
53
|
+
const size = file.size ? formatFileSize(parseInt(file.size)) : "N/A";
|
|
54
|
+
output += `${index + 1}. ${name}\n`;
|
|
55
|
+
output += ` ID: ${id}\n`;
|
|
56
|
+
output += ` Type: ${mimeType}\n`;
|
|
57
|
+
output += ` Size: ${size}\n`;
|
|
58
|
+
if (file.webViewLink) {
|
|
59
|
+
output += ` Link: ${file.webViewLink}\n`;
|
|
60
|
+
}
|
|
61
|
+
output += `\n`;
|
|
62
|
+
});
|
|
63
|
+
if (nextPageToken) {
|
|
64
|
+
output += `\nMore results available. Use pageToken: ${nextPageToken}`;
|
|
65
|
+
}
|
|
66
|
+
return output;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Format permission for display
|
|
70
|
+
*/
|
|
71
|
+
export function formatPermission(permission) {
|
|
72
|
+
const role = permission.role || "unknown";
|
|
73
|
+
const type = permission.type || "unknown";
|
|
74
|
+
const emailAddress = permission.emailAddress || "";
|
|
75
|
+
const displayName = permission.displayName || "";
|
|
76
|
+
const id = permission.id || "";
|
|
77
|
+
let output = `Permission ID: ${id}\n`;
|
|
78
|
+
output += ` Role: ${role}\n`;
|
|
79
|
+
output += ` Type: ${type}\n`;
|
|
80
|
+
if (emailAddress) {
|
|
81
|
+
output += ` Email: ${emailAddress}\n`;
|
|
82
|
+
}
|
|
83
|
+
if (displayName) {
|
|
84
|
+
output += ` Name: ${displayName}\n`;
|
|
85
|
+
}
|
|
86
|
+
return output;
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Format permission list for display
|
|
90
|
+
*/
|
|
91
|
+
export function formatPermissionList(permissions) {
|
|
92
|
+
if (permissions.length === 0) {
|
|
93
|
+
return "No permissions found.";
|
|
94
|
+
}
|
|
95
|
+
let output = `Total permissions: ${permissions.length}\n\n`;
|
|
96
|
+
permissions.forEach((permission, index) => {
|
|
97
|
+
output += `${index + 1}. ${formatPermission(permission)}\n`;
|
|
98
|
+
});
|
|
99
|
+
return output;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Format revision for display
|
|
103
|
+
*/
|
|
104
|
+
export function formatRevision(revision) {
|
|
105
|
+
const id = revision.id || "";
|
|
106
|
+
const modifiedTime = revision.modifiedTime
|
|
107
|
+
? new Date(revision.modifiedTime).toLocaleString()
|
|
108
|
+
: "Unknown";
|
|
109
|
+
const lastModifyingUser = revision.lastModifyingUser?.displayName || "Unknown";
|
|
110
|
+
const size = revision.size ? formatFileSize(parseInt(revision.size)) : "N/A";
|
|
111
|
+
let output = `Revision ID: ${id}\n`;
|
|
112
|
+
output += ` Modified: ${modifiedTime}\n`;
|
|
113
|
+
output += ` Modified by: ${lastModifyingUser}\n`;
|
|
114
|
+
output += ` Size: ${size}\n`;
|
|
115
|
+
return output;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Format revision list for display
|
|
119
|
+
*/
|
|
120
|
+
export function formatRevisionList(revisions) {
|
|
121
|
+
if (revisions.length === 0) {
|
|
122
|
+
return "No revisions found.";
|
|
123
|
+
}
|
|
124
|
+
let output = `Total revisions: ${revisions.length}\n\n`;
|
|
125
|
+
revisions.forEach((revision, index) => {
|
|
126
|
+
output += `${index + 1}. ${formatRevision(revision)}\n`;
|
|
127
|
+
});
|
|
128
|
+
return output;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Convert MIME type for export
|
|
132
|
+
*/
|
|
133
|
+
export function convertMimeType(sourceMimeType, targetFormat) {
|
|
134
|
+
// Google Workspace document types
|
|
135
|
+
const googleMimeTypes = {
|
|
136
|
+
"application/vnd.google-apps.document": {
|
|
137
|
+
pdf: "application/pdf",
|
|
138
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
139
|
+
html: "text/html",
|
|
140
|
+
txt: "text/plain",
|
|
141
|
+
odt: "application/vnd.oasis.opendocument.text",
|
|
142
|
+
rtf: "application/rtf",
|
|
143
|
+
epub: "application/epub+zip",
|
|
144
|
+
markdown: "text/markdown",
|
|
145
|
+
},
|
|
146
|
+
"application/vnd.google-apps.spreadsheet": {
|
|
147
|
+
pdf: "application/pdf",
|
|
148
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
149
|
+
csv: "text/csv",
|
|
150
|
+
ods: "application/vnd.oasis.opendocument.spreadsheet",
|
|
151
|
+
tsv: "text/tab-separated-values",
|
|
152
|
+
html: "text/html",
|
|
153
|
+
},
|
|
154
|
+
"application/vnd.google-apps.presentation": {
|
|
155
|
+
pdf: "application/pdf",
|
|
156
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
157
|
+
odp: "application/vnd.oasis.opendocument.presentation",
|
|
158
|
+
txt: "text/plain",
|
|
159
|
+
},
|
|
160
|
+
"application/vnd.google-apps.drawing": {
|
|
161
|
+
pdf: "application/pdf",
|
|
162
|
+
png: "image/png",
|
|
163
|
+
jpeg: "image/jpeg",
|
|
164
|
+
svg: "image/svg+xml",
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
if (!targetFormat) {
|
|
168
|
+
// Default export formats
|
|
169
|
+
const defaults = {
|
|
170
|
+
"application/vnd.google-apps.document": "application/pdf",
|
|
171
|
+
"application/vnd.google-apps.spreadsheet": "text/csv",
|
|
172
|
+
"application/vnd.google-apps.presentation": "application/pdf",
|
|
173
|
+
"application/vnd.google-apps.drawing": "image/png",
|
|
174
|
+
};
|
|
175
|
+
return defaults[sourceMimeType] || "application/pdf";
|
|
176
|
+
}
|
|
177
|
+
const formats = googleMimeTypes[sourceMimeType];
|
|
178
|
+
if (!formats) {
|
|
179
|
+
throw new Error(`Unsupported source MIME type: ${sourceMimeType}`);
|
|
180
|
+
}
|
|
181
|
+
const targetMimeType = formats[targetFormat.toLowerCase()];
|
|
182
|
+
if (!targetMimeType) {
|
|
183
|
+
throw new Error(`Unsupported export format '${targetFormat}' for MIME type '${sourceMimeType}'`);
|
|
184
|
+
}
|
|
185
|
+
return targetMimeType;
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Generate Google Drive web URL for a file or folder
|
|
189
|
+
*/
|
|
190
|
+
export function generateDriveWebUrl(fileId) {
|
|
191
|
+
return `https://drive.google.com/file/d/${fileId}/view`;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Generate folder web URL
|
|
195
|
+
*/
|
|
196
|
+
export function generateFolderWebUrl(folderId) {
|
|
197
|
+
return `https://drive.google.com/drive/folders/${folderId}`;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Format file size to human-readable format
|
|
201
|
+
*/
|
|
202
|
+
function formatFileSize(bytes) {
|
|
203
|
+
if (bytes === 0)
|
|
204
|
+
return "0 Bytes";
|
|
205
|
+
const k = 1024;
|
|
206
|
+
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
|
207
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
208
|
+
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get MIME type for folder
|
|
212
|
+
*/
|
|
213
|
+
export const FOLDER_MIME_TYPE = "application/vnd.google-apps.folder";
|
|
214
|
+
/**
|
|
215
|
+
* Check if MIME type is a Google Workspace document
|
|
216
|
+
*/
|
|
217
|
+
export function isGoogleWorkspaceDocument(mimeType) {
|
|
218
|
+
return mimeType.startsWith("application/vnd.google-apps.");
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Get icon for MIME type
|
|
222
|
+
*/
|
|
223
|
+
export function getFileIcon(mimeType) {
|
|
224
|
+
const icons = {
|
|
225
|
+
"application/vnd.google-apps.folder": "📁",
|
|
226
|
+
"application/vnd.google-apps.document": "📝",
|
|
227
|
+
"application/vnd.google-apps.spreadsheet": "📊",
|
|
228
|
+
"application/vnd.google-apps.presentation": "📊",
|
|
229
|
+
"application/vnd.google-apps.drawing": "🎨",
|
|
230
|
+
"application/pdf": "📄",
|
|
231
|
+
"image/png": "🖼️",
|
|
232
|
+
"image/jpeg": "🖼️",
|
|
233
|
+
"text/plain": "📃",
|
|
234
|
+
"application/zip": "🗜️",
|
|
235
|
+
};
|
|
236
|
+
return icons[mimeType] || "📄";
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Format permission role for display
|
|
240
|
+
*/
|
|
241
|
+
export function getRoleLabel(role) {
|
|
242
|
+
const labels = {
|
|
243
|
+
owner: "Owner",
|
|
244
|
+
organizer: "Organizer",
|
|
245
|
+
fileOrganizer: "File Organizer",
|
|
246
|
+
writer: "Editor",
|
|
247
|
+
commenter: "Commenter",
|
|
248
|
+
reader: "Viewer",
|
|
249
|
+
};
|
|
250
|
+
return labels[role] || role;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Format permission type for display
|
|
254
|
+
*/
|
|
255
|
+
export function getTypeLabel(type) {
|
|
256
|
+
const labels = {
|
|
257
|
+
user: "User",
|
|
258
|
+
group: "Group",
|
|
259
|
+
domain: "Domain",
|
|
260
|
+
anyone: "Anyone with link",
|
|
261
|
+
};
|
|
262
|
+
return labels[type] || type;
|
|
263
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gmail Helper Functions
|
|
3
|
+
*
|
|
4
|
+
* Utilities for Gmail API operations including:
|
|
5
|
+
* - Message body extraction and formatting
|
|
6
|
+
* - HTML to text conversion
|
|
7
|
+
* - MIME message preparation
|
|
8
|
+
* - Attachment handling
|
|
9
|
+
* - URL generation
|
|
10
|
+
*/
|
|
11
|
+
import nodemailer from "nodemailer";
|
|
12
|
+
import { convert } from "html-to-text";
|
|
13
|
+
/**
|
|
14
|
+
* Extract text and HTML bodies from Gmail message payload
|
|
15
|
+
*/
|
|
16
|
+
export function extractMessageBodies(payload) {
|
|
17
|
+
let textBody = "";
|
|
18
|
+
let htmlBody = "";
|
|
19
|
+
function extractParts(part) {
|
|
20
|
+
if (part.mimeType === "text/plain" && part.body?.data) {
|
|
21
|
+
textBody = Buffer.from(part.body.data, "base64").toString("utf-8");
|
|
22
|
+
}
|
|
23
|
+
else if (part.mimeType === "text/html" && part.body?.data) {
|
|
24
|
+
htmlBody = Buffer.from(part.body.data, "base64").toString("utf-8");
|
|
25
|
+
}
|
|
26
|
+
// Recursively process multipart messages
|
|
27
|
+
if (part.parts) {
|
|
28
|
+
for (const subPart of part.parts) {
|
|
29
|
+
extractParts(subPart);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
extractParts(payload);
|
|
34
|
+
return { textBody, htmlBody };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Format body content, preferring plain text but converting HTML if needed
|
|
38
|
+
*/
|
|
39
|
+
export function formatBodyContent(textBody, htmlBody) {
|
|
40
|
+
if (textBody) {
|
|
41
|
+
return textBody;
|
|
42
|
+
}
|
|
43
|
+
if (htmlBody) {
|
|
44
|
+
// Convert HTML to readable plain text
|
|
45
|
+
return convert(htmlBody, {
|
|
46
|
+
wordwrap: 80,
|
|
47
|
+
selectors: [
|
|
48
|
+
{ selector: "a", options: { ignoreHref: false } },
|
|
49
|
+
{ selector: "img", format: "skip" },
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return "[No text content found]";
|
|
54
|
+
}
|
|
55
|
+
export function extractAttachments(payload) {
|
|
56
|
+
const attachments = [];
|
|
57
|
+
function extractParts(part) {
|
|
58
|
+
if (part.filename && part.body?.attachmentId) {
|
|
59
|
+
attachments.push({
|
|
60
|
+
filename: part.filename,
|
|
61
|
+
mimeType: part.mimeType || "application/octet-stream",
|
|
62
|
+
size: part.body.size || 0,
|
|
63
|
+
attachmentId: part.body.attachmentId,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
if (part.parts) {
|
|
67
|
+
for (const subPart of part.parts) {
|
|
68
|
+
extractParts(subPart);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
extractParts(payload);
|
|
73
|
+
return attachments;
|
|
74
|
+
}
|
|
75
|
+
export function prepareGmailMessage(options) {
|
|
76
|
+
const { to, subject, body, bodyFormat, from, cc, bcc, inReplyTo, references, } = options;
|
|
77
|
+
// Build message using nodemailer's createTransport (for MIME creation only)
|
|
78
|
+
const message = {
|
|
79
|
+
from: from || "me",
|
|
80
|
+
to,
|
|
81
|
+
subject,
|
|
82
|
+
[bodyFormat === "html" ? "html" : "text"]: body,
|
|
83
|
+
};
|
|
84
|
+
if (cc)
|
|
85
|
+
message.cc = cc;
|
|
86
|
+
if (bcc)
|
|
87
|
+
message.bcc = bcc;
|
|
88
|
+
if (inReplyTo)
|
|
89
|
+
message.inReplyTo = inReplyTo;
|
|
90
|
+
if (references)
|
|
91
|
+
message.references = references;
|
|
92
|
+
// Create MIME message
|
|
93
|
+
const transport = nodemailer.createTransport({ jsonTransport: true });
|
|
94
|
+
// We'll use a synchronous approach here
|
|
95
|
+
let mimeMessage = "";
|
|
96
|
+
mimeMessage += `From: ${message.from}\r\n`;
|
|
97
|
+
mimeMessage += `To: ${message.to}\r\n`;
|
|
98
|
+
if (message.cc)
|
|
99
|
+
mimeMessage += `Cc: ${message.cc}\r\n`;
|
|
100
|
+
if (message.bcc)
|
|
101
|
+
mimeMessage += `Bcc: ${message.bcc}\r\n`;
|
|
102
|
+
mimeMessage += `Subject: ${message.subject}\r\n`;
|
|
103
|
+
if (message.inReplyTo)
|
|
104
|
+
mimeMessage += `In-Reply-To: ${message.inReplyTo}\r\n`;
|
|
105
|
+
if (message.references)
|
|
106
|
+
mimeMessage += `References: ${message.references}\r\n`;
|
|
107
|
+
mimeMessage += `Content-Type: text/${bodyFormat}; charset=utf-8\r\n`;
|
|
108
|
+
mimeMessage += `\r\n`;
|
|
109
|
+
mimeMessage += body;
|
|
110
|
+
// Base64url encode
|
|
111
|
+
return Buffer.from(mimeMessage)
|
|
112
|
+
.toString("base64")
|
|
113
|
+
.replace(/\+/g, "-")
|
|
114
|
+
.replace(/\//g, "_")
|
|
115
|
+
.replace(/=+$/, "");
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Generate Gmail web URL for a message or thread
|
|
119
|
+
*/
|
|
120
|
+
export function generateGmailWebUrl(messageId, threadId) {
|
|
121
|
+
if (messageId) {
|
|
122
|
+
return `https://mail.google.com/mail/u/0/#all/${messageId}`;
|
|
123
|
+
}
|
|
124
|
+
if (threadId) {
|
|
125
|
+
return `https://mail.google.com/mail/u/0/#all/${threadId}`;
|
|
126
|
+
}
|
|
127
|
+
return "https://mail.google.com/mail/u/0/";
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Extract header value from message headers
|
|
131
|
+
*/
|
|
132
|
+
export function getHeader(headers, name) {
|
|
133
|
+
if (!headers)
|
|
134
|
+
return "";
|
|
135
|
+
const header = headers.find((h) => h.name?.toLowerCase() === name.toLowerCase());
|
|
136
|
+
return header?.value || "";
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Format thread content for display
|
|
140
|
+
*/
|
|
141
|
+
export function formatThreadContent(thread) {
|
|
142
|
+
const messages = thread.messages || [];
|
|
143
|
+
const threadId = thread.id || "";
|
|
144
|
+
if (messages.length === 0) {
|
|
145
|
+
return "No messages in thread";
|
|
146
|
+
}
|
|
147
|
+
// Extract thread subject from first message
|
|
148
|
+
const firstMessage = messages[0];
|
|
149
|
+
const subject = getHeader(firstMessage.payload?.headers, "Subject");
|
|
150
|
+
let output = `Thread ID: ${threadId}\n`;
|
|
151
|
+
output += `Subject: ${subject}\n`;
|
|
152
|
+
output += `Messages: ${messages.length}\n`;
|
|
153
|
+
output += `Web URL: ${generateGmailWebUrl(undefined, threadId)}\n\n`;
|
|
154
|
+
output += "=".repeat(80) + "\n\n";
|
|
155
|
+
// Format each message
|
|
156
|
+
messages.forEach((message, index) => {
|
|
157
|
+
const headers = message.payload?.headers || [];
|
|
158
|
+
const from = getHeader(headers, "From");
|
|
159
|
+
const date = getHeader(headers, "Date");
|
|
160
|
+
const msgSubject = getHeader(headers, "Subject");
|
|
161
|
+
output += `Message ${index + 1}/${messages.length}\n`;
|
|
162
|
+
output += `-`.repeat(40) + "\n";
|
|
163
|
+
output += `From: ${from}\n`;
|
|
164
|
+
output += `Date: ${date}\n`;
|
|
165
|
+
// Only show subject if different from thread subject
|
|
166
|
+
if (msgSubject && msgSubject !== subject) {
|
|
167
|
+
output += `Subject: ${msgSubject}\n`;
|
|
168
|
+
}
|
|
169
|
+
output += `\n`;
|
|
170
|
+
// Extract and format body
|
|
171
|
+
if (message.payload) {
|
|
172
|
+
const { textBody, htmlBody } = extractMessageBodies(message.payload);
|
|
173
|
+
const bodyContent = formatBodyContent(textBody, htmlBody);
|
|
174
|
+
// Truncate very long messages
|
|
175
|
+
const maxLength = 2000;
|
|
176
|
+
if (bodyContent.length > maxLength) {
|
|
177
|
+
output += bodyContent.substring(0, maxLength);
|
|
178
|
+
output += `\n\n[... truncated ${bodyContent.length - maxLength} characters ...]`;
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
output += bodyContent;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
output += "\n\n";
|
|
185
|
+
});
|
|
186
|
+
return output;
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Format message list for search results
|
|
190
|
+
*/
|
|
191
|
+
export function formatMessageList(messages, nextPageToken) {
|
|
192
|
+
let output = `Found ${messages.length} message(s)\n\n`;
|
|
193
|
+
messages.forEach((msg, index) => {
|
|
194
|
+
const messageId = msg.id || "";
|
|
195
|
+
const threadId = msg.threadId || "";
|
|
196
|
+
output += `${index + 1}. Message ID: ${messageId}\n`;
|
|
197
|
+
output += ` Thread ID: ${threadId}\n`;
|
|
198
|
+
output += ` Web URL: ${generateGmailWebUrl(messageId)}\n\n`;
|
|
199
|
+
});
|
|
200
|
+
if (nextPageToken) {
|
|
201
|
+
output += `\nMore results available. Use pageToken: ${nextPageToken}`;
|
|
202
|
+
}
|
|
203
|
+
return output;
|
|
204
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ResponseFormatter - Consistent response formatting across all tools
|
|
3
|
+
*/
|
|
4
|
+
export class ResponseFormatter {
|
|
5
|
+
/**
|
|
6
|
+
* Create a success response
|
|
7
|
+
*/
|
|
8
|
+
static success(data, message) {
|
|
9
|
+
let text;
|
|
10
|
+
if (message) {
|
|
11
|
+
// If message is provided, combine it with data
|
|
12
|
+
if (typeof data === "object" && data !== null) {
|
|
13
|
+
text = `${message}\n\n${JSON.stringify(data, null, 2)}`;
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
text = message;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
// Format data only
|
|
21
|
+
text = typeof data === "object" && data !== null
|
|
22
|
+
? JSON.stringify(data, null, 2)
|
|
23
|
+
: String(data);
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
content: [{ type: "text", text }],
|
|
27
|
+
isError: false,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Create an error response
|
|
32
|
+
*/
|
|
33
|
+
static error(error) {
|
|
34
|
+
const message = error instanceof Error ? error.message : error;
|
|
35
|
+
return {
|
|
36
|
+
content: [
|
|
37
|
+
{
|
|
38
|
+
type: "text",
|
|
39
|
+
text: `Error: ${message}`,
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
isError: true,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Format Google Docs document data
|
|
47
|
+
*/
|
|
48
|
+
static formatDocumentData(doc) {
|
|
49
|
+
const formatted = {
|
|
50
|
+
documentId: doc.documentId,
|
|
51
|
+
title: doc.title,
|
|
52
|
+
revisionId: doc.revisionId,
|
|
53
|
+
suggestionsViewMode: doc.suggestionsViewMode,
|
|
54
|
+
};
|
|
55
|
+
return JSON.stringify(formatted, null, 2);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Format metadata
|
|
59
|
+
*/
|
|
60
|
+
static formatMetadata(metadata) {
|
|
61
|
+
return JSON.stringify(metadata, null, 2);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Format a simple success message
|
|
65
|
+
*/
|
|
66
|
+
static simpleSuccess(message) {
|
|
67
|
+
return {
|
|
68
|
+
content: [{ type: "text", text: message }],
|
|
69
|
+
isError: false,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Format operation result with details
|
|
74
|
+
*/
|
|
75
|
+
static operationResult(operation, details) {
|
|
76
|
+
const text = `${operation} completed successfully\n\n${JSON.stringify(details, null, 2)}`;
|
|
77
|
+
return {
|
|
78
|
+
content: [{ type: "text", text }],
|
|
79
|
+
isError: false,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validation - Shared validation logic for tool inputs
|
|
3
|
+
*/
|
|
4
|
+
export class Validator {
|
|
5
|
+
/**
|
|
6
|
+
* Validate that required fields exist in an object
|
|
7
|
+
*/
|
|
8
|
+
static validateRequired(obj, fields) {
|
|
9
|
+
const missing = fields.filter((field) => obj[field] === undefined);
|
|
10
|
+
if (missing.length > 0) {
|
|
11
|
+
throw new Error(`Missing required fields: ${missing.join(", ")}`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Validate RGB color values (0-1 range)
|
|
16
|
+
*/
|
|
17
|
+
static validateColor(color) {
|
|
18
|
+
const values = [color.red, color.green, color.blue];
|
|
19
|
+
return values.every((v) => typeof v === "number" && v >= 0 && v <= 1);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Validate dimension type
|
|
23
|
+
*/
|
|
24
|
+
static validateDimension(dimension) {
|
|
25
|
+
return dimension === "ROWS" || dimension === "COLUMNS";
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Validate text style object
|
|
29
|
+
*/
|
|
30
|
+
static validateTextStyle(style) {
|
|
31
|
+
// Check for at least one style property
|
|
32
|
+
const validProperties = [
|
|
33
|
+
"bold",
|
|
34
|
+
"italic",
|
|
35
|
+
"underline",
|
|
36
|
+
"fontSize",
|
|
37
|
+
"fontFamily",
|
|
38
|
+
"foregroundColor",
|
|
39
|
+
"backgroundColor",
|
|
40
|
+
];
|
|
41
|
+
const hasValidProperty = validProperties.some((prop) => style[prop] !== undefined);
|
|
42
|
+
if (!hasValidProperty) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
// Validate color properties if they exist
|
|
46
|
+
if (style.foregroundColor && !this.validateColor(style.foregroundColor)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
if (style.backgroundColor && !this.validateColor(style.backgroundColor)) {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Validate index is a positive integer
|
|
56
|
+
*/
|
|
57
|
+
static validateIndex(index, allowZero = false) {
|
|
58
|
+
if (!Number.isInteger(index)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
return allowZero ? index >= 0 : index > 0;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Validate range (startIndex < endIndex)
|
|
65
|
+
*/
|
|
66
|
+
static validateRange(startIndex, endIndex) {
|
|
67
|
+
return (this.validateIndex(startIndex) &&
|
|
68
|
+
this.validateIndex(endIndex) &&
|
|
69
|
+
startIndex < endIndex);
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Validate alignment value
|
|
73
|
+
*/
|
|
74
|
+
static validateAlignment(alignment) {
|
|
75
|
+
return ["START", "CENTER", "END", "JUSTIFIED"].includes(alignment);
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Validate named style type
|
|
79
|
+
*/
|
|
80
|
+
static validateNamedStyleType(styleType) {
|
|
81
|
+
const validTypes = [
|
|
82
|
+
"NORMAL_TEXT",
|
|
83
|
+
"HEADING_1",
|
|
84
|
+
"HEADING_2",
|
|
85
|
+
"HEADING_3",
|
|
86
|
+
"HEADING_4",
|
|
87
|
+
"HEADING_5",
|
|
88
|
+
"HEADING_6",
|
|
89
|
+
"TITLE",
|
|
90
|
+
"SUBTITLE",
|
|
91
|
+
];
|
|
92
|
+
return validTypes.includes(styleType);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Validate URL format
|
|
96
|
+
*/
|
|
97
|
+
static validateUrl(url) {
|
|
98
|
+
try {
|
|
99
|
+
new URL(url);
|
|
100
|
+
return url.startsWith("http://") || url.startsWith("https://");
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Validate positive number
|
|
108
|
+
*/
|
|
109
|
+
static validatePositiveNumber(value) {
|
|
110
|
+
return typeof value === "number" && value > 0 && !isNaN(value);
|
|
111
|
+
}
|
|
112
|
+
}
|