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 +1 -1
- package/dist/discovery-client.d.ts +1 -1
- package/dist/discovery-client.js +27 -13
- package/dist/scope-catalog.js +11 -2
- package/dist/services.js +1 -1
- package/dist/tools/_errors.js +13 -0
- package/dist/tools/google-api.js +4 -4
- package/package.json +1 -1
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
|
|
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
|
[](https://www.npmjs.com/package/mcp-google-multi)
|
|
6
6
|
|
package/dist/discovery-client.js
CHANGED
|
@@ -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
|
|
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 =
|
|
101
|
+
const spec = SUPPORTED_APIS[api];
|
|
102
102
|
if (!spec) {
|
|
103
|
-
throw new Error(`Unknown api "${api}". Known: ${Object.keys(
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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 (${
|
|
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
|
}
|
package/dist/scope-catalog.js
CHANGED
|
@@ -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,
|
package/dist/tools/_errors.js
CHANGED
|
@@ -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,
|
package/dist/tools/google-api.js
CHANGED
|
@@ -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 {
|
|
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(
|
|
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 && !
|
|
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 (!
|
|
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.
|
|
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",
|