@dguido/google-workspace-mcp 3.1.0 → 3.1.1

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 CHANGED
@@ -162,6 +162,7 @@ To enable additional services, add them to `GOOGLE_WORKSPACE_SERVICES`:
162
162
 
163
163
  - Omit `GOOGLE_WORKSPACE_SERVICES` entirely to enable all services
164
164
  - Unified tools (`create_file`, `update_file`, `get_file_content`) require `drive`, `docs`, `sheets`, and `slides`
165
+ - When you limit services, only the OAuth scopes for those services are requested during authentication. If you change enabled services, re-authenticate to update granted scopes.
165
166
 
166
167
  See [Advanced Configuration](docs/ADVANCED.md) for named profiles, multi-account setup, and environment variables.
167
168
 
@@ -266,22 +267,6 @@ If you have a Google Workspace account:
266
267
 
267
268
  Use `get_status` to check token age. Tokens older than 6 days show a warning automatically.
268
269
 
269
- ## Scope Filtering
270
-
271
- When you limit services via `GOOGLE_WORKSPACE_SERVICES`, only the OAuth scopes for those services are requested during authentication. This provides:
272
-
273
- - **Cleaner consent screen** - Users see only the permissions they need
274
- - **Principle of least privilege** - App only has access to enabled services
275
-
276
- For example, setting `GOOGLE_WORKSPACE_SERVICES=drive,calendar` will only request Drive and Calendar scopes, not Gmail or Contacts.
277
-
278
- **Note:** If you change enabled services, re-authenticate to update granted scopes:
279
-
280
- ```bash
281
- rm ~/.config/google-workspace-mcp/tokens.json
282
- npx @dguido/google-workspace-mcp auth
283
- ```
284
-
285
270
  ## Development
286
271
 
287
272
  ```bash
@@ -293,12 +278,6 @@ npm test # Run tests
293
278
 
294
279
  See [Contributing Guide](CONTRIBUTING.md) for project structure and development workflow.
295
280
 
296
- ## Links
297
-
298
- - [GitHub Repository](https://github.com/dguido/google-workspace-mcp)
299
- - [Issue Tracker](https://github.com/dguido/google-workspace-mcp/issues)
300
- - [Model Context Protocol](https://modelcontextprotocol.io)
301
-
302
281
  ## Origin
303
282
 
304
283
  This project is a substantial rewrite of [piotr-agier/google-drive-mcp](https://github.com/piotr-agier/google-drive-mcp), originally created by Piotr Agier.
package/dist/index.js CHANGED
@@ -5253,16 +5253,50 @@ var gmailTools = [
5253
5253
  },
5254
5254
  {
5255
5255
  name: "search_emails",
5256
- description: "Search emails using Gmail query syntax (max 500 per request)",
5256
+ description: "Search emails using structured parameters or Gmail query syntax (max 500 per request). At least one search parameter required.",
5257
5257
  inputSchema: {
5258
5258
  type: "object",
5259
5259
  properties: {
5260
- query: { type: "string", description: "Gmail search query" },
5260
+ query: {
5261
+ type: "string",
5262
+ description: "Gmail search query. Operators: from: to: subject: has:attachment is:unread after:YYYY/MM/DD before:YYYY/MM/DD larger: smaller: label:. Gmail ignores special characters like $ and commas \u2014 use plain numbers (5149 not $5,149)."
5263
+ },
5264
+ from: {
5265
+ type: "string",
5266
+ description: "Sender email or name"
5267
+ },
5268
+ to: {
5269
+ type: "string",
5270
+ description: "Recipient email or name"
5271
+ },
5272
+ subject: {
5273
+ type: "string",
5274
+ description: "Subject line text"
5275
+ },
5276
+ after: {
5277
+ type: "string",
5278
+ description: "After date (YYYY/MM/DD)"
5279
+ },
5280
+ before: {
5281
+ type: "string",
5282
+ description: "Before date (YYYY/MM/DD)"
5283
+ },
5284
+ hasAttachment: {
5285
+ type: "boolean",
5286
+ description: "Filter for messages with attachments"
5287
+ },
5288
+ label: {
5289
+ type: "string",
5290
+ description: "Gmail label name"
5291
+ },
5261
5292
  maxResults: {
5262
5293
  type: "number",
5263
5294
  description: "(optional, default: 50) Maximum results (max 500)"
5264
5295
  },
5265
- pageToken: { type: "string", description: "(optional) Pagination token" },
5296
+ pageToken: {
5297
+ type: "string",
5298
+ description: "(optional) Pagination token"
5299
+ },
5266
5300
  labelIds: {
5267
5301
  type: "array",
5268
5302
  items: { type: "string" },
@@ -5272,8 +5306,7 @@ var gmailTools = [
5272
5306
  type: "boolean",
5273
5307
  description: "(optional, default: false) Include spam and trash"
5274
5308
  }
5275
- },
5276
- required: ["query"]
5309
+ }
5277
5310
  },
5278
5311
  outputSchema: {
5279
5312
  type: "object",
@@ -7138,12 +7171,24 @@ var ReadEmailSchema = z8.object({
7138
7171
  contentFormat: z8.enum(["full", "text", "headers"]).optional().default("full").describe("Content format: 'full' (text+HTML), 'text' (plain text only), 'headers' (no body)")
7139
7172
  });
7140
7173
  var SearchEmailsSchema = z8.object({
7141
- query: z8.string().min(1, "Search query required").describe("Gmail search query (e.g., 'from:sender@example.com', 'is:unread', 'subject:hello')"),
7174
+ query: z8.string().max(500).optional().describe(
7175
+ "Gmail search query. Operators: from: to: subject: has:attachment is:unread after:YYYY/MM/DD before:YYYY/MM/DD larger: smaller: label:. Gmail ignores special characters like $ and commas \u2014 use plain numbers (5149 not $5,149)."
7176
+ ),
7177
+ from: z8.string().max(254).optional().describe("Sender email or name"),
7178
+ to: z8.string().max(254).optional().describe("Recipient email or name"),
7179
+ subject: z8.string().max(500).optional().describe("Subject line text"),
7180
+ after: z8.string().max(10).optional().describe("After date (YYYY/MM/DD)"),
7181
+ before: z8.string().max(10).optional().describe("Before date (YYYY/MM/DD)"),
7182
+ hasAttachment: z8.boolean().optional().describe("Filter for messages with attachments"),
7183
+ label: z8.string().max(225).optional().describe("Gmail label name"),
7142
7184
  maxResults: z8.number().int().min(1).max(500).optional().default(50).describe("Maximum results"),
7143
7185
  pageToken: z8.string().optional().describe("Token for pagination"),
7144
7186
  labelIds: z8.array(z8.string()).optional().describe("Filter by label IDs"),
7145
7187
  includeSpamTrash: z8.boolean().optional().default(false).describe("Include spam and trash")
7146
- });
7188
+ }).refine(
7189
+ (d) => d.query || d.from || d.to || d.subject || d.after || d.before || d.hasAttachment || d.label,
7190
+ { message: "At least one search parameter required" }
7191
+ );
7147
7192
  var DeleteEmailSchema = z8.object({
7148
7193
  messageId: z8.union([z8.string().min(1), z8.array(z8.string().min(1)).min(1).max(1e3)]).describe("Message ID or array of IDs (max 1000 for batch)")
7149
7194
  });
@@ -11891,10 +11936,45 @@ async function handleReadEmail(gmail, args) {
11891
11936
  truncated
11892
11937
  });
11893
11938
  }
11939
+ function buildSearchQuery(args) {
11940
+ const parts = [];
11941
+ if (args.from) parts.push(`from:${args.from}`);
11942
+ if (args.to) parts.push(`to:${args.to}`);
11943
+ if (args.subject) parts.push(`subject:${args.subject}`);
11944
+ if (args.after) parts.push(`after:${args.after}`);
11945
+ if (args.before) parts.push(`before:${args.before}`);
11946
+ if (args.hasAttachment) parts.push("has:attachment");
11947
+ if (args.label) parts.push(`label:${args.label}`);
11948
+ if (args.query) parts.push(args.query);
11949
+ return parts.join(" ");
11950
+ }
11951
+ function buildSearchHints(args) {
11952
+ const hints = [];
11953
+ if (args.query && /[$#,]/.test(args.query)) {
11954
+ hints.push(
11955
+ "Gmail ignores special characters like $, #, and commas \u2014 use plain numbers (e.g. 5149 not $5,149)"
11956
+ );
11957
+ }
11958
+ const dateFormat = /^\d{4}\/\d{2}\/\d{2}$/;
11959
+ if (args.after && !dateFormat.test(args.after)) {
11960
+ hints.push(`Date format for 'after' should be YYYY/MM/DD (got: ${args.after})`);
11961
+ }
11962
+ if (args.before && !dateFormat.test(args.before)) {
11963
+ hints.push(`Date format for 'before' should be YYYY/MM/DD (got: ${args.before})`);
11964
+ }
11965
+ if (args.query && /\d{4}[-/]\d{2}[-/]\d{2}/.test(args.query) && !/(?:after|before):/.test(args.query)) {
11966
+ hints.push("Dates in query need operators: use after:YYYY/MM/DD or before:YYYY/MM/DD");
11967
+ }
11968
+ if (args.query && args.query.length > 200) {
11969
+ hints.push("Try simplifying \u2014 shorter queries often match more");
11970
+ }
11971
+ return hints;
11972
+ }
11894
11973
  async function handleSearchEmails(gmail, args) {
11895
11974
  const validation = validateArgs(SearchEmailsSchema, args);
11896
11975
  if (!validation.success) return validation.response;
11897
- const { query, maxResults, pageToken, labelIds, includeSpamTrash } = validation.data;
11976
+ const { maxResults, pageToken, labelIds, includeSpamTrash } = validation.data;
11977
+ const query = buildSearchQuery(validation.data);
11898
11978
  const response = await gmail.users.messages.list({
11899
11979
  userId: "me",
11900
11980
  q: query,
@@ -11905,7 +11985,12 @@ async function handleSearchEmails(gmail, args) {
11905
11985
  });
11906
11986
  const messages = response.data.messages || [];
11907
11987
  if (messages.length === 0) {
11908
- return structuredResponse(`No emails found matching: ${query}`, { messages: [] });
11988
+ const hints = buildSearchHints(validation.data);
11989
+ const text = `No emails found matching: ${query}` + (hints.length ? `
11990
+
11991
+ Hints:
11992
+ ${hints.map((h) => `- ${h}`).join("\n")}` : "");
11993
+ return structuredResponse(text, { messages: [] });
11909
11994
  }
11910
11995
  const messageDetails = await Promise.all(
11911
11996
  messages.slice(0, 50).map(async (msg) => {
@@ -13303,10 +13388,12 @@ async function ensureAuthenticated() {
13303
13388
  authenticationPromise = authenticate();
13304
13389
  try {
13305
13390
  authClient = await authenticationPromise;
13391
+ const hasCredentials = !!authClient?.credentials;
13392
+ const hasAccessToken = !!authClient?.credentials?.access_token;
13306
13393
  log("Authentication complete", {
13307
13394
  authClientType: authClient?.constructor?.name,
13308
- hasCredentials: !!authClient?.credentials,
13309
- hasAccessToken: !!authClient?.credentials?.access_token
13395
+ hasCredentials,
13396
+ hasAccessToken
13310
13397
  });
13311
13398
  ensureDriveService();
13312
13399
  const healthy = await verifyAuthHealth();