mcp-google-multi 6.0.0-alpha.13 → 6.0.0-alpha.15

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
@@ -1,6 +1,6 @@
1
1
  # mcp-google-multi
2
2
 
3
- The most complete **local Google Workspace MCP server**: Gmail, Drive, Calendar, Sheets, Docs, Slides, Forms, Contacts, Tasks, Chat, Meet, Classroom, Vault, Admin and more — **every OAuth-reachable Workspace API method** as a tool, across **multiple Google accounts** at once, from Claude Code or any MCP client.
3
+ The most complete **local Google Workspace MCP server**: Gmail, Drive, Calendar, Sheets, Docs, Slides, Forms, Contacts, Tasks, Chat, Meet, Analytics (GA4), Search Console, Classroom, Vault, Admin and more — **every OAuth-reachable API method** as a tool, across **multiple Google accounts** at once, from Claude Code or any MCP client.
4
4
 
5
5
  [![npm](https://img.shields.io/npm/v/mcp-google-multi?label=npm&color=cb3837)](https://www.npmjs.com/package/mcp-google-multi)
6
6
 
@@ -1,4 +1,4 @@
1
- export declare const WORKSPACE_APIS: Record<string, {
1
+ export declare const SUPPORTED_APIS: Record<string, {
2
2
  id: string;
3
3
  version: string;
4
4
  }>;
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
3
  import * as os from 'node:os';
4
- export const WORKSPACE_APIS = {
4
+ export const SUPPORTED_APIS = {
5
5
  gmail: { id: 'gmail', version: 'v1' },
6
6
  drive: { id: 'drive', version: 'v3' },
7
7
  calendar: { id: 'calendar', version: 'v3' },
@@ -98,9 +98,9 @@ export async function loadMethodIndex(api, deps = {}) {
98
98
  const cached = memoryCache.get(api);
99
99
  if (cached && now() < cached.refreshAfter)
100
100
  return cached.index;
101
- const spec = WORKSPACE_APIS[api];
101
+ const spec = SUPPORTED_APIS[api];
102
102
  if (!spec) {
103
- throw new Error(`Unknown api "${api}". Known: ${Object.keys(WORKSPACE_APIS).join(', ')}`);
103
+ throw new Error(`Unknown api "${api}". Known: ${Object.keys(SUPPORTED_APIS).join(', ')}`);
104
104
  }
105
105
  const fetchFn = deps.fetchFn ?? ((url) => fetch(url, { signal: AbortSignal.timeout(FETCH_TIMEOUT_MS) }));
106
106
  const cacheDir = deps.cacheDir ?? discoveryCacheDir();
@@ -126,20 +126,34 @@ export async function loadMethodIndex(api, deps = {}) {
126
126
  if (cacheFresh())
127
127
  doc = readCache();
128
128
  if (!doc) {
129
- const url = `https://www.googleapis.com/discovery/v1/apis/${spec.id}/${spec.version}/rest`;
130
- try {
131
- const res = await fetchFn(url);
132
- if (!res.ok)
133
- throw new Error(`HTTP ${res.status}`);
134
- doc = (await res.json());
135
- fs.mkdirSync(cacheDir, { recursive: true });
136
- fs.writeFileSync(cacheFile, JSON.stringify(doc), { mode: 0o600 });
129
+ // Newer APIs (analyticsadmin/analyticsdata) are absent from the central
130
+ // discovery directory (404); each service's own $discovery endpoint is
131
+ // authoritative, so try both — same fallback as scripts/fetch-discovery.
132
+ const urls = [
133
+ `https://www.googleapis.com/discovery/v1/apis/${spec.id}/${spec.version}/rest`,
134
+ `https://${spec.id}.googleapis.com/$discovery/rest?version=${spec.version}`,
135
+ ];
136
+ let fetchError;
137
+ for (const url of urls) {
138
+ try {
139
+ const res = await fetchFn(url);
140
+ if (!res.ok)
141
+ throw new Error(`HTTP ${res.status}`);
142
+ doc = (await res.json());
143
+ fs.mkdirSync(cacheDir, { recursive: true });
144
+ fs.writeFileSync(cacheFile, JSON.stringify(doc), { mode: 0o600 });
145
+ break;
146
+ }
147
+ catch (err) {
148
+ fetchError = err;
149
+ doc = undefined;
150
+ }
137
151
  }
138
- catch (err) {
152
+ if (!doc) {
139
153
  doc = readCache();
140
154
  staleFallback = true;
141
155
  if (!doc) {
142
- throw new Error(`Could not fetch the Google API Discovery document for "${api}" and no local cache exists (${err.message}). Retry when online.`, { cause: err });
156
+ throw new Error(`Could not fetch the Google API Discovery document for "${api}" and no local cache exists (${fetchError?.message ?? 'fetch failed'}). Retry when online.`, { cause: fetchError });
143
157
  }
144
158
  }
145
159
  }
@@ -108,13 +108,22 @@ export const BUNDLE_CATALOG = {
108
108
  description: 'Read Gmail Postmaster Tools deliverability data.',
109
109
  risk: 'low',
110
110
  },
111
- // Read-only by design: GA4 admin writes need analytics.edit, which ships as
112
- // a separate opt-in bundle only if real demand appears (plan 6.0.0 GA-1).
113
111
  analytics: {
114
112
  scopes: ['https://www.googleapis.com/auth/analytics.readonly'],
115
113
  description: 'Read Google Analytics (GA4): run reports and inspect accounts, properties and their configuration.',
116
114
  risk: 'low',
117
115
  },
116
+ // Includes readonly so it is self-sufficient: analytics.edit alone does not
117
+ // authorize Data API reads. The smaller `analytics` bundle stays the hint
118
+ // target for read scopes (scope-observability sorts bundles by size).
119
+ analytics_write: {
120
+ scopes: [
121
+ 'https://www.googleapis.com/auth/analytics.readonly',
122
+ 'https://www.googleapis.com/auth/analytics.edit',
123
+ ],
124
+ description: 'Edit Google Analytics (GA4) configuration: properties, data streams, key events, custom dimensions and metrics. Includes read access.',
125
+ risk: 'medium',
126
+ },
118
127
  groupssettings: {
119
128
  scopes: ['https://www.googleapis.com/auth/apps.groups.settings'],
120
129
  description: 'Change Google Groups settings for the domain.',
package/dist/services.js CHANGED
@@ -26,7 +26,7 @@ export const SERVICES = [
26
26
  { name: 'slides', register: registerSlidesTools, enabled: () => new Set(getOptionalBundles()).has('slides') },
27
27
  { name: 'forms', register: registerFormsTools, enabled: () => new Set(getOptionalBundles()).has('forms') },
28
28
  { name: 'chat', register: registerChatTools, enabled: () => new Set(getOptionalBundles()).has('chat') },
29
- { name: 'analytics', register: registerAnalyticsTools, enabled: () => new Set(getOptionalBundles()).has('analytics') },
29
+ { name: 'analytics', register: registerAnalyticsTools, enabled: () => { const b = new Set(getOptionalBundles()); return b.has('analytics') || b.has('analytics_write'); } },
30
30
  { name: 'admin', register: registerAdminTools, enabled: () => getAdminAccounts().length > 0 },
31
31
  ];
32
32
  // Generated-only services with opt-in scopes; admin/forms/chat/analytics reuse their curated gate in buildRegistry,
@@ -129,6 +129,19 @@ export function mapGoogleError(error, account, forbiddenHint, scopeContext) {
129
129
  }
130
130
  if (status === 429) {
131
131
  const retryAfter = error?.response?.headers?.['retry-after'];
132
+ // GA4 quota exhaustion ("Exhausted property tokens ...") is a per-property
133
+ // token bucket, not a transient rate spike: shrinking the request is the
134
+ // lever that helps, and a blind immediate retry only burns more tokens.
135
+ if (/property tokens/i.test(message)) {
136
+ return {
137
+ error: 'rate_limited',
138
+ message,
139
+ hint: 'GA4 quotas are per-property token buckets that refill over the hour/day. ' +
140
+ 'Narrow the date range, request fewer dimensions/metrics/rows, and pass returnPropertyQuota to see the remaining tokens before retrying.',
141
+ retriable: true,
142
+ account,
143
+ };
144
+ }
132
145
  return {
133
146
  error: 'rate_limited',
134
147
  message,
@@ -6,7 +6,7 @@ import { coerceJson } from './_coerce.js';
6
6
  import { getToolsets, toolsetEnabled } from '../toolsets.js';
7
7
  import { editDistance } from '../scope-catalog.js';
8
8
  import { executeApiMethod, jsonResult } from '../executor.js';
9
- import { WORKSPACE_APIS, cudFromMethod, loadMethodIndex, searchMethods, } from '../discovery-client.js';
9
+ import { SUPPORTED_APIS, cudFromMethod, loadMethodIndex, searchMethods, } from '../discovery-client.js';
10
10
  const accountEnum = accountAliasSchema.optional();
11
11
  // Policy/toolset namespace for each API alias must match the NAMED tools' service
12
12
  // names, or user deny globs and GOOGLE_TOOLSETS silently miss escape-hatch calls.
@@ -61,7 +61,7 @@ export function registerEscapeTools(registry, policy, deps = {}) {
61
61
  const apiEnabled = (alias) => {
62
62
  return toolsetEnabled(toolsets, serviceForAlias(alias));
63
63
  };
64
- const enabledApis = Object.keys(WORKSPACE_APIS).filter(apiEnabled);
64
+ const enabledApis = Object.keys(SUPPORTED_APIS).filter(apiEnabled);
65
65
  const apiList = enabledApis.join(', ');
66
66
  const toolsetDisabled = (alias) => jsonResult({
67
67
  error: 'toolset_disabled',
@@ -80,7 +80,7 @@ export function registerEscapeTools(registry, policy, deps = {}) {
80
80
  },
81
81
  annotations: { openWorldHint: true },
82
82
  }, async ({ query, api, maxResults }) => {
83
- if (api && !WORKSPACE_APIS[api]) {
83
+ if (api && !SUPPORTED_APIS[api]) {
84
84
  return jsonResult({ error: 'unknown_api', message: `Unknown api "${api}".`, hint: `Known APIs: ${apiList}`, retriable: false }, true);
85
85
  }
86
86
  if (api && !apiEnabled(api))
@@ -129,7 +129,7 @@ export function registerEscapeTools(registry, policy, deps = {}) {
129
129
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
130
130
  _meta: { 'anthropic/maxResultSizeChars': 100_000 },
131
131
  }, async ({ account, api, methodId, pathParams, queryParams, body }) => {
132
- if (!WORKSPACE_APIS[api]) {
132
+ if (!SUPPORTED_APIS[api]) {
133
133
  return jsonResult({ error: 'unknown_api', message: `Unknown api "${api}".`, hint: `Known APIs: ${apiList}`, retriable: false, account }, true);
134
134
  }
135
135
  if (!apiEnabled(api))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-google-multi",
3
- "version": "6.0.0-alpha.13",
3
+ "version": "6.0.0-alpha.15",
4
4
  "description": "Local MCP server for Google Workspace (Gmail, Drive, Calendar, Sheets, Docs, Contacts, Tasks, Meet, Search Console, +Forms/Chat/Admin) across multiple accounts — OAuth-only, encrypted token storage, deny-by-default writes.",
5
5
  "type": "module",
6
6
  "license": "MIT",