mcp-google-multi 6.0.0-alpha.10 → 6.0.0-alpha.12
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 +2 -2
- package/dist/api-probe.js +3 -0
- package/dist/arg-normalize.d.ts +9 -3
- package/dist/arg-normalize.js +14 -4
- package/dist/discovery-client.js +8 -2
- package/dist/http-transport.d.ts +2 -1
- package/dist/registry.d.ts +4 -2
- package/dist/registry.js +24 -4
- package/dist/scope-catalog.js +7 -0
- package/dist/services.js +1 -0
- package/dist/tools/generated/analytics.d.ts +2 -0
- package/dist/tools/generated/analytics.js +939 -0
- package/dist/tools/generated/cloudsearch.js +1 -1
- package/dist/tools/generated/index.js +2 -0
- package/dist/tools/google-api.js +2 -0
- package/package.json +5 -2
package/README.md
CHANGED
|
@@ -4,10 +4,10 @@ The most complete **local Google Workspace MCP server**: Gmail, Drive, Calendar,
|
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/mcp-google-multi)
|
|
6
6
|
|
|
7
|
-
- 🧰 **Exhaustive** —
|
|
7
|
+
- 🧰 **Exhaustive** — 940 tools across 29 services, now including Google Analytics (GA4), + an escape hatch for anything else → [COVERAGE.md](./COVERAGE.md)
|
|
8
8
|
- 🔑 **Multi-account** — drive any number of Google accounts by alias, or fan one call out across all of them
|
|
9
9
|
- 🔒 **Private by design** — your own OAuth app, tokens encrypted at rest (AES-256-GCM), writes deny-by-default, no telemetry, no metering — it talks only to Google
|
|
10
|
-
- 🌐 **Local or remote** — runs locally over stdio, or self-hosted over HTTP with its own built-in OAuth 2.1 server (Claude Code's `/mcp` login and the claude.ai connector, zero custom UI) → [remote setup](./docs/http-setup.md)
|
|
10
|
+
- 🌐 **Local or remote** — runs locally over stdio, or self-hosted over HTTP with its own built-in OAuth 2.1 server (Claude Code's `/mcp` login and the claude.ai connector, zero custom UI). Pull-and-up Docker Compose with optional automatic HTTPS → [remote setup](./docs/http-setup.md)
|
|
11
11
|
- ✉️ **Built for real work** — send and read email in Markdown with attachments and one-call replies, an interactive setup wizard with a `doctor` self-check, and per-account scope profiles → [features tour](./docs/features.md)
|
|
12
12
|
|
|
13
13
|
## Quick setup
|
package/dist/api-probe.js
CHANGED
|
@@ -17,6 +17,9 @@ export const API_PROBES = [
|
|
|
17
17
|
{ service: 'chat', api: 'chat', url: 'https://chat.googleapis.com/v1/spaces?pageSize=1', scopePrefixes: [`${P}chat.`] },
|
|
18
18
|
{ service: 'meet', api: 'meet', url: 'https://meet.googleapis.com/v2/conferenceRecords?pageSize=1', scopePrefixes: [`${P}meetings.`] },
|
|
19
19
|
{ service: 'forms', api: 'forms', url: `https://forms.googleapis.com/v1/forms/${BOGUS_ID}`, scopePrefixes: [`${P}forms.`], notFoundMeansEnabled: true },
|
|
20
|
+
// Probes the Admin API only: the Data API has no no-arg read (every call
|
|
21
|
+
// needs a property id), so its enablement surfaces on first report instead.
|
|
22
|
+
{ service: 'analytics', api: 'analyticsadmin', url: 'https://analyticsadmin.googleapis.com/v1beta/accountSummaries?pageSize=1', scopePrefixes: [`${P}analytics`] },
|
|
20
23
|
];
|
|
21
24
|
export function planProbes(granted, probes = API_PROBES) {
|
|
22
25
|
return probes.filter((p) => granted.some((s) => p.scopePrefixes.some((prefix) => s.startsWith(prefix))));
|
package/dist/arg-normalize.d.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import type { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
|
|
2
2
|
import type { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js';
|
|
3
3
|
export declare function argNormalizationEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
4
|
-
|
|
4
|
+
/** Declared scalar kind per schema key; drives value coercion on RENAMED keys
|
|
5
|
+
* only. Clients string-encode values for keys absent from the advertised
|
|
6
|
+
* schema, so a renamed key almost always arrives as a string — without
|
|
7
|
+
* coercion the rename would just move the -32602 from the key to the value. */
|
|
8
|
+
export type ArgKind = 'number' | 'boolean' | 'other';
|
|
9
|
+
export type ArgShape = ReadonlyMap<string, ArgKind>;
|
|
10
|
+
export declare function normalizeCallArguments(shape: ArgShape, args: Record<string, unknown>): {
|
|
5
11
|
args: Record<string, unknown>;
|
|
6
12
|
renamed: [string, string][];
|
|
7
13
|
};
|
|
8
|
-
export declare function normalizeMessage(msg: JSONRPCMessage, shapeFor: (tool: string) =>
|
|
14
|
+
export declare function normalizeMessage(msg: JSONRPCMessage, shapeFor: (tool: string) => ArgShape | undefined, log?: (line: string) => void): JSONRPCMessage;
|
|
9
15
|
/** Wrap a server-side transport so tools/call argument keys are normalized
|
|
10
16
|
* before the SDK validates them. The Protocol assigns `onmessage` during
|
|
11
17
|
* connect(); the interceptor lives in that setter, so the wrapper works
|
|
12
18
|
* identically for stdio and (per-request, stateless) HTTP transports. */
|
|
13
|
-
export declare function withArgNormalization(transport: Transport, shapeFor: (tool: string) =>
|
|
19
|
+
export declare function withArgNormalization(transport: Transport, shapeFor: (tool: string) => ArgShape | undefined, log?: (line: string) => void): Transport;
|
package/dist/arg-normalize.js
CHANGED
|
@@ -10,16 +10,26 @@ export function argNormalizationEnabled(env = process.env) {
|
|
|
10
10
|
return !/^(0|false|off|no)$/i.test((env.GOOGLE_ARG_NORMALIZE ?? '').trim());
|
|
11
11
|
}
|
|
12
12
|
const snakeToCamel = (key) => key.replace(/_([a-z0-9])/g, (_m, c) => c.toUpperCase());
|
|
13
|
-
|
|
13
|
+
function coerceRenamedValue(value, kind) {
|
|
14
|
+
if (typeof value !== 'string')
|
|
15
|
+
return value;
|
|
16
|
+
const v = value.trim();
|
|
17
|
+
if (kind === 'number' && /^-?\d+(\.\d+)?$/.test(v))
|
|
18
|
+
return Number(v);
|
|
19
|
+
if (kind === 'boolean' && /^(true|false)$/i.test(v))
|
|
20
|
+
return v.toLowerCase() === 'true';
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
export function normalizeCallArguments(shape, args) {
|
|
14
24
|
const renamed = [];
|
|
15
25
|
let out;
|
|
16
26
|
for (const key of Object.keys(args)) {
|
|
17
|
-
if (
|
|
27
|
+
if (shape.has(key) || !key.includes('_'))
|
|
18
28
|
continue;
|
|
19
29
|
const camel = snakeToCamel(key);
|
|
20
|
-
if (camel !== key &&
|
|
30
|
+
if (camel !== key && shape.has(camel) && !(camel in args)) {
|
|
21
31
|
out ??= { ...args };
|
|
22
|
-
out[camel] = out[key];
|
|
32
|
+
out[camel] = coerceRenamedValue(out[key], shape.get(camel));
|
|
23
33
|
delete out[key];
|
|
24
34
|
renamed.push([key, camel]);
|
|
25
35
|
}
|
package/dist/discovery-client.js
CHANGED
|
@@ -20,6 +20,8 @@ export const WORKSPACE_APIS = {
|
|
|
20
20
|
admin_reports: { id: 'admin', version: 'reports_v1' },
|
|
21
21
|
admin_datatransfer: { id: 'admin', version: 'datatransfer_v1' },
|
|
22
22
|
groupssettings: { id: 'groupssettings', version: 'v1' },
|
|
23
|
+
analyticsadmin: { id: 'analyticsadmin', version: 'v1beta' },
|
|
24
|
+
analyticsdata: { id: 'analyticsdata', version: 'v1beta' },
|
|
23
25
|
appsmarket: { id: 'appsmarket', version: 'v2' },
|
|
24
26
|
classroom: { id: 'classroom', version: 'v1' },
|
|
25
27
|
cloudidentity: { id: 'cloudidentity', version: 'v1' },
|
|
@@ -148,9 +150,13 @@ export async function loadMethodIndex(api, deps = {}) {
|
|
|
148
150
|
export function clearDiscoveryMemoryCache() {
|
|
149
151
|
memoryCache.clear();
|
|
150
152
|
}
|
|
151
|
-
|
|
153
|
+
// GA4-style report execution (runReport, batchRunPivotReports, runAccessReport)
|
|
154
|
+
// and check* predicates are POSTs purely for the request-body size — reads.
|
|
155
|
+
const POST_READ_VERB = /^(get|list|search|query|lookup|count|batchGet|generateIds|export|download|inspect|check|(batch)?run\w*report)/i;
|
|
152
156
|
const POST_UPDATE_VERB = /^(untrash|undelete|restore|modify|move|set|sort|merge|unmerge|replace|resize|publish|resolve|update|patch|write|format)/i;
|
|
153
|
-
|
|
157
|
+
// archive sits with the deletes: in GA4 archiving a custom dimension/metric is
|
|
158
|
+
// permanent, so the most restrictive write class is the safe classification.
|
|
159
|
+
const POST_DELETE_VERB = /^(batch)?(delete|remove|trash|clear|empty|obliterate|purge|revoke|wipeout|archive)/i;
|
|
154
160
|
export function cudFromMethod(method) {
|
|
155
161
|
switch (method.httpMethod) {
|
|
156
162
|
case 'GET':
|
package/dist/http-transport.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type IncomingMessage, type ServerResponse } from 'node:http';
|
|
2
2
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import type { HttpConfig } from './http-config.js';
|
|
4
|
+
import { type ArgShape } from './arg-normalize.js';
|
|
4
5
|
export type AuthOutcome = {
|
|
5
6
|
ok: true;
|
|
6
7
|
} | {
|
|
@@ -29,7 +30,7 @@ export interface HttpHostOptions {
|
|
|
29
30
|
* shared lock instead of wedging the transport (default 120s). */
|
|
30
31
|
dispatchTimeoutMs?: number;
|
|
31
32
|
/** tools/call argument-key normalization lookup (arg-normalize.ts); absent = off. */
|
|
32
|
-
argShapeFor?: (tool: string) =>
|
|
33
|
+
argShapeFor?: (tool: string) => ArgShape | undefined;
|
|
33
34
|
}
|
|
34
35
|
export declare function parseOwnerEmails(env?: NodeJS.ProcessEnv): string[];
|
|
35
36
|
/** Front guard: an Origin, if present, must be allowlisted; a Host must be
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { type Policy } from './write-control.js';
|
|
4
|
+
import type { ArgShape } from './arg-normalize.js';
|
|
4
5
|
export type Cud = 'read' | 'create' | 'update' | 'delete';
|
|
5
6
|
export type DiscoveryMode = 'lazy' | 'curated' | 'eager';
|
|
6
7
|
export declare function resolveDiscoveryMode(env?: NodeJS.ProcessEnv): DiscoveryMode;
|
|
@@ -49,8 +50,9 @@ export declare class ToolRegistry {
|
|
|
49
50
|
constructor(server: McpServer, policy: Policy, mode?: DiscoveryMode);
|
|
50
51
|
registerMeta: McpServer['registerTool'];
|
|
51
52
|
services(): string[];
|
|
52
|
-
/** Declared input-schema keys for one tool (tools/call arg
|
|
53
|
-
|
|
53
|
+
/** Declared input-schema keys + scalar kinds for one tool (tools/call arg
|
|
54
|
+
* normalization; the kind drives value coercion on renamed keys). */
|
|
55
|
+
argShape(name: string): ArgShape | undefined;
|
|
54
56
|
catalog(service: string, query?: string): CatalogOperation[];
|
|
55
57
|
reveal(service: string): boolean;
|
|
56
58
|
/** discover_all: advertise the full curated set at once. Idempotent. */
|
package/dist/registry.js
CHANGED
|
@@ -38,6 +38,23 @@ const SERVICE_OVERRIDES = {
|
|
|
38
38
|
};
|
|
39
39
|
// read tools that write local files — same savePath fanned across accounts would clobber
|
|
40
40
|
const FANOUT_EXCLUDE = new Set(['gmail_download_attachment', 'drive_download', 'drive_export']);
|
|
41
|
+
/** Unwrap optional/default/nullable to the declared scalar kind (zod 4 defs). */
|
|
42
|
+
function scalarKindOf(field) {
|
|
43
|
+
let cur = field;
|
|
44
|
+
for (let i = 0; i < 4 && cur?._zod?.def; i++) {
|
|
45
|
+
const def = cur._zod.def;
|
|
46
|
+
if (def.type === 'number')
|
|
47
|
+
return 'number';
|
|
48
|
+
if (def.type === 'boolean')
|
|
49
|
+
return 'boolean';
|
|
50
|
+
if (def.type === 'optional' || def.type === 'default' || def.type === 'nullable') {
|
|
51
|
+
cur = def.innerType;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
return 'other';
|
|
55
|
+
}
|
|
56
|
+
return 'other';
|
|
57
|
+
}
|
|
41
58
|
function isAccountEnum(field) {
|
|
42
59
|
const def = field?._zod?.def;
|
|
43
60
|
if (!def)
|
|
@@ -193,7 +210,8 @@ export class ToolRegistry {
|
|
|
193
210
|
services() {
|
|
194
211
|
return [...new Set(this.tools.filter((t) => !t.meta).map((t) => t.service))];
|
|
195
212
|
}
|
|
196
|
-
/** Declared input-schema keys for one tool (tools/call arg
|
|
213
|
+
/** Declared input-schema keys + scalar kinds for one tool (tools/call arg
|
|
214
|
+
* normalization; the kind drives value coercion on renamed keys). */
|
|
197
215
|
argShape(name) {
|
|
198
216
|
const cached = this.argShapeCache.get(name);
|
|
199
217
|
if (cached)
|
|
@@ -201,9 +219,11 @@ export class ToolRegistry {
|
|
|
201
219
|
const entry = this.tools.find((t) => t.name === name);
|
|
202
220
|
if (!entry)
|
|
203
221
|
return undefined;
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
222
|
+
const shape = new Map();
|
|
223
|
+
for (const [key, field] of Object.entries(entry.inputShape))
|
|
224
|
+
shape.set(key, scalarKindOf(field));
|
|
225
|
+
this.argShapeCache.set(name, shape);
|
|
226
|
+
return shape;
|
|
207
227
|
}
|
|
208
228
|
catalog(service, query) {
|
|
209
229
|
const q = query?.trim().toLowerCase();
|
package/dist/scope-catalog.js
CHANGED
|
@@ -108,6 +108,13 @@ 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
|
+
analytics: {
|
|
114
|
+
scopes: ['https://www.googleapis.com/auth/analytics.readonly'],
|
|
115
|
+
description: 'Read Google Analytics (GA4): run reports and inspect accounts, properties and their configuration.',
|
|
116
|
+
risk: 'low',
|
|
117
|
+
},
|
|
111
118
|
groupssettings: {
|
|
112
119
|
scopes: ['https://www.googleapis.com/auth/apps.groups.settings'],
|
|
113
120
|
description: 'Change Google Groups settings for the domain.',
|
package/dist/services.js
CHANGED
|
@@ -34,6 +34,7 @@ const bundleGate = (name) => ({
|
|
|
34
34
|
hint: `add "${name}" to an account's scope profile (or legacy GOOGLE_OPTIONAL_SCOPES)`,
|
|
35
35
|
});
|
|
36
36
|
export const GENERATED_GATES = {
|
|
37
|
+
analytics: bundleGate('analytics'),
|
|
37
38
|
appsmarket: bundleGate('appsmarket'),
|
|
38
39
|
classroom: bundleGate('classroom'),
|
|
39
40
|
cloudidentity: bundleGate('cloudidentity'),
|