@dguido/google-workspace-mcp 1.1.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
@@ -925,12 +925,6 @@ import * as fs from "fs/promises";
925
925
  // src/auth/utils.ts
926
926
  import * as path from "path";
927
927
  import * as os from "os";
928
- import { fileURLToPath } from "url";
929
- function getProjectRoot() {
930
- const __dirname2 = path.dirname(fileURLToPath(import.meta.url));
931
- const projectRoot = path.join(__dirname2, "..", "..");
932
- return path.resolve(projectRoot);
933
- }
934
928
  function getSecureTokenPath() {
935
929
  const customTokenPath = process.env.GOOGLE_WORKSPACE_MCP_TOKEN_PATH;
936
930
  if (customTokenPath) {
@@ -944,25 +938,12 @@ function getSecureTokenPath() {
944
938
  const tokenDir = path.join(configHome, "google-workspace-mcp");
945
939
  return path.join(tokenDir, "tokens.json");
946
940
  }
947
- function getLegacyTokenPath() {
948
- const projectRoot = getProjectRoot();
949
- return path.join(projectRoot, ".gcp-saved-tokens.json");
950
- }
951
- function getAdditionalLegacyPaths() {
952
- return [
953
- process.env.GOOGLE_TOKEN_PATH,
954
- path.join(process.cwd(), "google-tokens.json"),
955
- path.join(process.cwd(), ".gcp-saved-tokens.json")
956
- ].filter(Boolean);
957
- }
958
941
  function getKeysFilePath() {
959
942
  const envCredentialsPath = process.env.GOOGLE_DRIVE_OAUTH_CREDENTIALS;
960
943
  if (envCredentialsPath) {
961
944
  return path.resolve(envCredentialsPath);
962
945
  }
963
- const projectRoot = getProjectRoot();
964
- const keysPath = path.join(projectRoot, "gcp-oauth.keys.json");
965
- return keysPath;
946
+ return path.join(process.cwd(), "gcp-oauth.keys.json");
966
947
  }
967
948
  function generateCredentialsErrorMessage() {
968
949
  return `
@@ -1127,46 +1108,13 @@ var TokenManager = class {
1127
1108
  }
1128
1109
  });
1129
1110
  }
1130
- async migrateLegacyTokens() {
1131
- const legacyPaths = [getLegacyTokenPath(), ...getAdditionalLegacyPaths()];
1132
- for (const legacyPath of legacyPaths) {
1133
- try {
1134
- if (!await fs2.access(legacyPath).then(() => true).catch(() => false)) {
1135
- continue;
1136
- }
1137
- const legacyTokens = JSON.parse(await fs2.readFile(legacyPath, "utf-8"));
1138
- if (!legacyTokens || typeof legacyTokens !== "object") {
1139
- log(`Invalid legacy token format at ${legacyPath}, skipping`);
1140
- continue;
1141
- }
1142
- await this.ensureTokenDirectoryExists();
1143
- await fs2.writeFile(this.tokenPath, JSON.stringify(legacyTokens, null, 2), {
1144
- mode: 384
1145
- });
1146
- log(`Migrated tokens from legacy location: ${legacyPath} to: ${this.tokenPath}`);
1147
- try {
1148
- await fs2.unlink(legacyPath);
1149
- log("Removed legacy token file");
1150
- } catch (unlinkErr) {
1151
- log("Warning: Could not remove legacy token file:", unlinkErr);
1152
- }
1153
- return true;
1154
- } catch (error) {
1155
- log(`Error migrating legacy tokens from ${legacyPath}`, error);
1156
- }
1157
- }
1158
- return false;
1159
- }
1160
1111
  async loadSavedTokens() {
1161
1112
  try {
1162
1113
  await this.ensureTokenDirectoryExists();
1163
1114
  const tokenExists = await fs2.access(this.tokenPath).then(() => true).catch(() => false);
1164
1115
  if (!tokenExists) {
1165
- const migrated = await this.migrateLegacyTokens();
1166
- if (!migrated) {
1167
- log("No token file found at:", this.tokenPath);
1168
- return false;
1169
- }
1116
+ log("No token file found at:", this.tokenPath);
1117
+ return false;
1170
1118
  }
1171
1119
  const tokens = JSON.parse(await fs2.readFile(this.tokenPath, "utf-8"));
1172
1120
  if (!tokens || typeof tokens !== "object") {
@@ -1529,9 +1477,9 @@ async function authenticate() {
1529
1477
 
1530
1478
  // src/index.ts
1531
1479
  init_utils();
1532
- import { fileURLToPath as fileURLToPath2 } from "url";
1480
+ import { fileURLToPath } from "url";
1533
1481
  import { readFileSync } from "fs";
1534
- import { join as join3, dirname as dirname3 } from "path";
1482
+ import { join as join3, dirname as dirname2 } from "path";
1535
1483
 
1536
1484
  // src/config/index.ts
1537
1485
  init_services();
@@ -4500,13 +4448,13 @@ var gmailTools = [
4500
4448
  },
4501
4449
  {
4502
4450
  name: "modify_email",
4503
- description: "Add/remove labels (max 1000 IDs per request)",
4451
+ description: "Add/remove labels on threads (max 1000 IDs per request)",
4504
4452
  inputSchema: {
4505
4453
  type: "object",
4506
4454
  properties: {
4507
- messageId: {
4455
+ threadId: {
4508
4456
  oneOf: [{ type: "string" }, { type: "array", items: { type: "string" }, maxItems: 1e3 }],
4509
- description: "Message ID or array of IDs (max 1000)"
4457
+ description: "Thread ID or array of IDs (max 1000)"
4510
4458
  },
4511
4459
  addLabelIds: {
4512
4460
  type: "array",
@@ -4519,14 +4467,14 @@ var gmailTools = [
4519
4467
  description: "(optional) Label IDs to remove"
4520
4468
  }
4521
4469
  },
4522
- required: ["messageId"]
4470
+ required: ["threadId"]
4523
4471
  },
4524
4472
  outputSchema: {
4525
4473
  type: "object",
4526
4474
  properties: {
4527
- modified: { type: "number", description: "Number of messages modified" },
4528
- addedLabels: { type: "array", items: { type: "string" }, description: "Labels added" },
4529
- 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" }
4530
4478
  }
4531
4479
  }
4532
4480
  },
@@ -5875,7 +5823,7 @@ var DeleteEmailSchema = z7.object({
5875
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)")
5876
5824
  });
5877
5825
  var ModifyEmailSchema = z7.object({
5878
- 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)"),
5879
5827
  addLabelIds: z7.array(z7.string()).optional().describe("Label IDs to add"),
5880
5828
  removeLabelIds: z7.array(z7.string()).optional().describe("Label IDs to remove")
5881
5829
  });
@@ -10230,6 +10178,25 @@ var SYSTEM_LABELS = /* @__PURE__ */ new Set([
10230
10178
  "CATEGORY_UPDATES",
10231
10179
  "CATEGORY_FORUMS"
10232
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
+ }
10233
10200
  function extractEmailBody(payload) {
10234
10201
  if (!payload) return { text: "", html: "" };
10235
10202
  const result = { text: "", html: "" };
@@ -10498,36 +10465,92 @@ async function handleDeleteEmail(gmail, args) {
10498
10465
  async function handleModifyEmail(gmail, args) {
10499
10466
  const validation = validateArgs(ModifyEmailSchema, args);
10500
10467
  if (!validation.success) return validation.response;
10501
- const { messageId, addLabelIds, removeLabelIds } = validation.data;
10502
- const messageIds = Array.isArray(messageId) ? messageId : [messageId];
10503
- if (messageIds.length === 1) {
10504
- 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({
10505
10472
  userId: "me",
10506
- id: messageIds[0],
10473
+ id: ids[0],
10507
10474
  requestBody: { addLabelIds, removeLabelIds }
10508
10475
  });
10509
- 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;
10510
10478
  return structuredResponse(
10511
- `Email ${messageIds[0]} labels updated.
10512
- 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"}`,
10513
10481
  {
10514
10482
  id: response.data.id,
10515
- threadId: response.data.threadId,
10516
- labelIds: response.data.labelIds
10483
+ historyId: response.data.historyId,
10484
+ messageCount,
10485
+ labelIds: response.data.messages?.[0]?.labelIds
10517
10486
  }
10518
10487
  );
10519
10488
  }
10520
- await gmail.users.messages.batchModify({
10521
- userId: "me",
10522
- requestBody: {
10523
- ids: messageIds,
10524
- addLabelIds,
10525
- removeLabelIds
10526
- }
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
+ };
10527
10515
  });
10528
- 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
+ }
10529
10552
  return successResponse(
10530
- `Successfully modified labels for ${messageIds.length} email(s).` + (addLabelIds ? `
10553
+ `Successfully modified labels for ${ids.length} thread(s).` + (addLabelIds ? `
10531
10554
  Added labels: ${addLabelIds.join(", ")}` : "") + (removeLabelIds ? `
10532
10555
  Removed labels: ${removeLabelIds.join(", ")}` : "")
10533
10556
  );
@@ -10939,8 +10962,8 @@ async function handleListTools(args) {
10939
10962
  var drive = null;
10940
10963
  var authClient = null;
10941
10964
  var authenticationPromise = null;
10942
- var __filename = fileURLToPath2(import.meta.url);
10943
- var __dirname = dirname3(__filename);
10965
+ var __filename = fileURLToPath(import.meta.url);
10966
+ var __dirname = dirname2(__filename);
10944
10967
  var packageJsonPath = join3(__dirname, "..", "package.json");
10945
10968
  var packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
10946
10969
  var VERSION = packageJson.version;