mcp-google-multi 6.0.0-alpha.12 → 6.0.0-alpha.14

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();
package/dist/services.js CHANGED
@@ -11,6 +11,7 @@ import { registerSlidesTools } from './tools/slides.js';
11
11
  import { registerFormsTools } from './tools/forms.js';
12
12
  import { registerChatTools } from './tools/chat.js';
13
13
  import { registerAdminTools } from './tools/admin.js';
14
+ import { registerAnalyticsTools } from './tools/analytics.js';
14
15
  import { getOptionalBundles, getAdminAccounts } from './auth.js';
15
16
  export const SERVICES = [
16
17
  { name: 'gmail', register: registerGmailTools },
@@ -25,16 +26,16 @@ export const SERVICES = [
25
26
  { name: 'slides', register: registerSlidesTools, enabled: () => new Set(getOptionalBundles()).has('slides') },
26
27
  { name: 'forms', register: registerFormsTools, enabled: () => new Set(getOptionalBundles()).has('forms') },
27
28
  { name: 'chat', register: registerChatTools, enabled: () => new Set(getOptionalBundles()).has('chat') },
29
+ { name: 'analytics', register: registerAnalyticsTools, enabled: () => new Set(getOptionalBundles()).has('analytics') },
28
30
  { name: 'admin', register: registerAdminTools, enabled: () => getAdminAccounts().length > 0 },
29
31
  ];
30
- // Generated-only services with opt-in scopes; admin/forms/chat reuse their curated gate in buildRegistry,
32
+ // Generated-only services with opt-in scopes; admin/forms/chat/analytics reuse their curated gate in buildRegistry,
31
33
  // and workspaceevents is deliberately absent — no dedicated scope (subscriptions use resource scopes).
32
34
  const bundleGate = (name) => ({
33
35
  enabled: () => new Set(getOptionalBundles()).has(name),
34
36
  hint: `add "${name}" to an account's scope profile (or legacy GOOGLE_OPTIONAL_SCOPES)`,
35
37
  });
36
38
  export const GENERATED_GATES = {
37
- analytics: bundleGate('analytics'),
38
39
  appsmarket: bundleGate('appsmarket'),
39
40
  classroom: bundleGate('classroom'),
40
41
  cloudidentity: bundleGate('cloudidentity'),
@@ -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,
@@ -0,0 +1,18 @@
1
+ import type { ToolRegistry } from '../registry.js';
2
+ /** Accepts "213025502" or "properties/213025502"; rejects the identifiers
3
+ * people paste by mistake (G-… measurement IDs, UA-… properties) with a
4
+ * pointer to the right one. */
5
+ export declare function normalizeProperty(input: string): {
6
+ name: string;
7
+ } | {
8
+ hint: string;
9
+ };
10
+ /** GA4 report rows ({dimensionValues:[{value}], metricValues:[{value}]}) are
11
+ * verbose; merge each row into one {name: value} object (dimension and metric
12
+ * API names never collide). Shared by runReport and runRealtimeReport. */
13
+ export declare function shapeReport(data: any): Record<string, unknown>;
14
+ export declare function shapeAccountSummaries(data: any): Record<string, unknown>;
15
+ /** Full metadata descriptions run to paragraphs; the first ~160 chars carry
16
+ * the disambiguation the model needs without bloating a ~300-entry list. */
17
+ export declare function shapeMetadata(data: any): Record<string, unknown>;
18
+ export declare function registerAnalyticsTools(server: ToolRegistry): void;
@@ -0,0 +1,279 @@
1
+ import { z } from 'zod';
2
+ import { coerceArray, coerceBoolean, coerceJson } from './_coerce.js';
3
+ import { analyticsdata as analyticsdataClient } from '@googleapis/analyticsdata';
4
+ import { analyticsadmin as analyticsadminClient } from '@googleapis/analyticsadmin';
5
+ import { accountAliasSchema } from '../accounts.js';
6
+ import { getClient } from '../client.js';
7
+ import { handleGoogleApiError } from './_errors.js';
8
+ const accountEnum = accountAliasSchema.optional();
9
+ /** Accepts "213025502" or "properties/213025502"; rejects the identifiers
10
+ * people paste by mistake (G-… measurement IDs, UA-… properties) with a
11
+ * pointer to the right one. */
12
+ export function normalizeProperty(input) {
13
+ const t = input.trim();
14
+ if (/^properties\/\d+$/.test(t))
15
+ return { name: t };
16
+ if (/^\d+$/.test(t))
17
+ return { name: `properties/${t}` };
18
+ if (/^G-[A-Z0-9]+$/i.test(t)) {
19
+ return {
20
+ hint: `"${t}" is a measurement ID (a web data-stream tag), not a GA4 property ID. ` +
21
+ 'Use the numeric property ID from GA Admin > Property settings, or find it with analytics_account_summaries.',
22
+ };
23
+ }
24
+ if (/^UA-/i.test(t)) {
25
+ return {
26
+ hint: `"${t}" is a Universal Analytics property, which the GA4 APIs cannot query. ` +
27
+ 'Use the numeric ID of a GA4 property (find yours with analytics_account_summaries).',
28
+ };
29
+ }
30
+ return {
31
+ hint: `"${t}" is not a GA4 property reference. Pass the numeric property ID ` +
32
+ '(e.g. "213025502" or "properties/213025502"); find yours with analytics_account_summaries.',
33
+ };
34
+ }
35
+ /** GA4 report rows ({dimensionValues:[{value}], metricValues:[{value}]}) are
36
+ * verbose; merge each row into one {name: value} object (dimension and metric
37
+ * API names never collide). Shared by runReport and runRealtimeReport. */
38
+ export function shapeReport(data) {
39
+ const dimensionHeaders = (data.dimensionHeaders ?? []).map((h) => h.name);
40
+ const metricHeaders = (data.metricHeaders ?? []).map((h) => ({ name: h.name, type: h.type }));
41
+ const mergeRow = (r) => {
42
+ const out = {};
43
+ dimensionHeaders.forEach((name, i) => {
44
+ out[name] = r.dimensionValues?.[i]?.value ?? '';
45
+ });
46
+ metricHeaders.forEach((h, i) => {
47
+ out[h.name] = r.metricValues?.[i]?.value ?? '';
48
+ });
49
+ return out;
50
+ };
51
+ const shaped = {
52
+ rowCount: data.rowCount ?? data.rows?.length ?? 0,
53
+ dimensionHeaders,
54
+ metricHeaders,
55
+ rows: (data.rows ?? []).map(mergeRow),
56
+ };
57
+ if (data.totals?.length)
58
+ shaped.totals = data.totals.map(mergeRow);
59
+ if (data.maximums?.length)
60
+ shaped.maximums = data.maximums.map(mergeRow);
61
+ if (data.minimums?.length)
62
+ shaped.minimums = data.minimums.map(mergeRow);
63
+ if (data.metadata)
64
+ shaped.metadata = data.metadata;
65
+ if (data.propertyQuota)
66
+ shaped.propertyQuota = data.propertyQuota;
67
+ return shaped;
68
+ }
69
+ export function shapeAccountSummaries(data) {
70
+ const accounts = (data.accountSummaries ?? []).map((a) => ({
71
+ account: a.account,
72
+ displayName: a.displayName,
73
+ properties: (a.propertySummaries ?? []).map((p) => ({
74
+ property: p.property,
75
+ displayName: p.displayName,
76
+ ...(p.propertyType && p.propertyType !== 'PROPERTY_TYPE_ORDINARY' ? { propertyType: p.propertyType } : {}),
77
+ })),
78
+ }));
79
+ return { accounts, ...(data.nextPageToken ? { nextPageToken: data.nextPageToken } : {}) };
80
+ }
81
+ const DESCRIPTION_CAP = 160;
82
+ /** Full metadata descriptions run to paragraphs; the first ~160 chars carry
83
+ * the disambiguation the model needs without bloating a ~300-entry list. */
84
+ export function shapeMetadata(data) {
85
+ const cap = (s) => typeof s === 'string' && s.length > DESCRIPTION_CAP ? `${s.slice(0, DESCRIPTION_CAP - 3)}...` : s || undefined;
86
+ return {
87
+ dimensions: (data.dimensions ?? []).map((d) => ({
88
+ apiName: d.apiName,
89
+ uiName: d.uiName,
90
+ category: d.category,
91
+ ...(d.customDefinition ? { custom: true } : {}),
92
+ description: cap(d.description),
93
+ })),
94
+ metrics: (data.metrics ?? []).map((m) => ({
95
+ apiName: m.apiName,
96
+ uiName: m.uiName,
97
+ category: m.category,
98
+ ...(m.type ? { type: m.type } : {}),
99
+ ...(m.expression ? { expression: m.expression } : {}),
100
+ ...(m.customDefinition ? { custom: true } : {}),
101
+ description: cap(m.description),
102
+ })),
103
+ };
104
+ }
105
+ const propertySchema = z
106
+ .string()
107
+ .describe('GA4 property: numeric ID like "213025502" or "properties/213025502" — NOT a "G-..." measurement ID and not "UA-...". Find yours with analytics_account_summaries.');
108
+ export function registerAnalyticsTools(server) {
109
+ server.registerTool('analytics_account_summaries', {
110
+ description: 'List every Google Analytics (GA4) account and property this Google account can access, with their numeric property IDs. The starting point for any Analytics question ("what properties do I have?").',
111
+ inputSchema: {
112
+ account: accountEnum.describe('Google account alias'),
113
+ pageSize: z.number().min(1).max(200).optional().describe('Summaries per page (default 50, max 200)'),
114
+ pageToken: z.string().optional().describe('Token from a previous page'),
115
+ },
116
+ }, async ({ account, pageSize, pageToken }) => {
117
+ try {
118
+ const auth = await getClient(account);
119
+ const admin = analyticsadminClient({ version: 'v1beta', auth });
120
+ const res = await admin.accountSummaries.list({ pageSize, pageToken });
121
+ return {
122
+ content: [{ type: 'text', text: JSON.stringify(shapeAccountSummaries(res.data), null, 2) }],
123
+ };
124
+ }
125
+ catch (error) {
126
+ return handleAnalyticsError(error, account);
127
+ }
128
+ });
129
+ server.registerTool('analytics_run_report', {
130
+ description: 'Run a Google Analytics (GA4) report: metrics over a date range, optionally grouped by dimensions — the workhorse for questions like "how many users last week, by country". Dates accept YYYY-MM-DD or relative forms ("today", "yesterday", "28daysAgo"). Unsure which dimension/metric names are valid? Call analytics_get_metadata first.',
131
+ inputSchema: {
132
+ account: accountEnum.describe('Google account alias'),
133
+ property: propertySchema,
134
+ startDate: z.string().describe('Start date: YYYY-MM-DD or relative ("today", "yesterday", "NdaysAgo" e.g. "28daysAgo")'),
135
+ endDate: z.string().describe('End date: YYYY-MM-DD or relative ("today", "yesterday", "NdaysAgo")'),
136
+ metrics: coerceArray(z.string()).describe('Metric API names, e.g. ["activeUsers","sessions","screenPageViews"]. Max 10. Valid names (including custom ones) come from analytics_get_metadata.'),
137
+ dimensions: coerceArray(z.string())
138
+ .optional()
139
+ .describe('Dimension API names to group by, e.g. ["date"] or ["country","deviceCategory"]. Max 9. Omit for a single total row.'),
140
+ dimensionFilter: coerceJson(z.record(z.string(), z.unknown()))
141
+ .optional()
142
+ .describe('FilterExpression on dimensions (applies independently of metricFilter). Simple: {"filter":{"fieldName":"country","stringFilter":{"matchType":"EXACT","value":"France"}}}. AND of two: {"andGroup":{"expressions":[{"filter":{"fieldName":"country","stringFilter":{"matchType":"EXACT","value":"France"}}},{"filter":{"fieldName":"deviceCategory","stringFilter":{"matchType":"EXACT","value":"mobile"}}}]}}. matchType: EXACT | BEGINS_WITH | ENDS_WITH | CONTAINS | FULL_REGEXP (add "caseSensitive":true for case-sensitive). Also available: inListFilter, notExpression, orGroup.'),
143
+ metricFilter: coerceJson(z.record(z.string(), z.unknown()))
144
+ .optional()
145
+ .describe('FilterExpression on metric values, e.g. {"filter":{"fieldName":"sessions","numericFilter":{"operation":"GREATER_THAN","value":{"int64Value":"100"}}}}. operation: EQUAL | LESS_THAN | LESS_THAN_OR_EQUAL | GREATER_THAN | GREATER_THAN_OR_EQUAL; betweenFilter takes fromValue/toValue.'),
146
+ orderBys: coerceJson(z.array(z.record(z.string(), z.unknown())))
147
+ .optional()
148
+ .describe('Sort order, e.g. [{"metric":{"metricName":"sessions"},"desc":true}] or [{"dimension":{"dimensionName":"date"}}]. Default: unordered.'),
149
+ limit: z.number().min(1).max(250000).optional().describe('Max rows to return (API default 10000). Keep small for readable output.'),
150
+ offset: z.number().min(0).optional().describe('Zero-based row offset for pagination'),
151
+ metricAggregations: coerceArray(z.enum(['TOTAL', 'MINIMUM', 'MAXIMUM', 'COUNT']))
152
+ .optional()
153
+ .describe('Also return aggregate rows across all matching data (surfaced as totals/maximums/minimums)'),
154
+ keepEmptyRows: coerceBoolean.optional().describe('Include rows whose metrics are all zero (default false)'),
155
+ returnPropertyQuota: coerceBoolean.optional().describe("Include this property's remaining quota tokens in the response"),
156
+ },
157
+ }, async ({ account, property, startDate, endDate, metrics, dimensions, dimensionFilter, metricFilter, orderBys, limit, offset, metricAggregations, keepEmptyRows, returnPropertyQuota }) => {
158
+ const prop = normalizeProperty(property);
159
+ if ('hint' in prop)
160
+ return invalidProperty(prop.hint, account);
161
+ try {
162
+ const auth = await getClient(account);
163
+ const dataApi = analyticsdataClient({ version: 'v1beta', auth });
164
+ const requestBody = {
165
+ dateRanges: [{ startDate, endDate }],
166
+ metrics: metrics.map((name) => ({ name })),
167
+ };
168
+ if (dimensions?.length)
169
+ requestBody.dimensions = dimensions.map((name) => ({ name }));
170
+ if (dimensionFilter)
171
+ requestBody.dimensionFilter = dimensionFilter;
172
+ if (metricFilter)
173
+ requestBody.metricFilter = metricFilter;
174
+ if (orderBys?.length)
175
+ requestBody.orderBys = orderBys;
176
+ if (limit !== undefined)
177
+ requestBody.limit = limit;
178
+ if (offset !== undefined)
179
+ requestBody.offset = offset;
180
+ if (metricAggregations?.length)
181
+ requestBody.metricAggregations = metricAggregations;
182
+ if (keepEmptyRows !== undefined)
183
+ requestBody.keepEmptyRows = keepEmptyRows;
184
+ if (returnPropertyQuota !== undefined)
185
+ requestBody.returnPropertyQuota = returnPropertyQuota;
186
+ const res = await dataApi.properties.runReport({ property: prop.name, requestBody });
187
+ return {
188
+ content: [{ type: 'text', text: JSON.stringify(shapeReport(res.data), null, 2) }],
189
+ };
190
+ }
191
+ catch (error) {
192
+ return handleAnalyticsError(error, account);
193
+ }
194
+ });
195
+ server.registerTool('analytics_run_realtime_report', {
196
+ description: 'Run a GA4 realtime report: who is on the site right now (last 30 minutes). Realtime supports a restricted set of names, e.g. metrics activeUsers, screenPageViews, eventCount, keyEvents; dimensions country, city, deviceCategory, unifiedScreenName, eventName.',
197
+ inputSchema: {
198
+ account: accountEnum.describe('Google account alias'),
199
+ property: propertySchema,
200
+ metrics: coerceArray(z.string()).describe('Realtime metric API names, e.g. ["activeUsers"]'),
201
+ dimensions: coerceArray(z.string()).optional().describe('Realtime dimension API names, e.g. ["country"] or ["unifiedScreenName"]'),
202
+ dimensionFilter: coerceJson(z.record(z.string(), z.unknown()))
203
+ .optional()
204
+ .describe('FilterExpression on dimensions — same shape as analytics_run_report'),
205
+ metricFilter: coerceJson(z.record(z.string(), z.unknown()))
206
+ .optional()
207
+ .describe('FilterExpression on metric values — same shape as analytics_run_report'),
208
+ minuteRanges: coerceJson(z.array(z.record(z.string(), z.unknown())))
209
+ .optional()
210
+ .describe('Up to 2 ranges of minutes-ago, e.g. [{"startMinutesAgo":29,"endMinutesAgo":0}] (default: last 30 minutes)'),
211
+ limit: z.number().min(1).max(250000).optional().describe('Max rows to return'),
212
+ returnPropertyQuota: coerceBoolean.optional().describe("Include this property's remaining realtime quota tokens"),
213
+ },
214
+ }, async ({ account, property, metrics, dimensions, dimensionFilter, metricFilter, minuteRanges, limit, returnPropertyQuota }) => {
215
+ const prop = normalizeProperty(property);
216
+ if ('hint' in prop)
217
+ return invalidProperty(prop.hint, account);
218
+ try {
219
+ const auth = await getClient(account);
220
+ const dataApi = analyticsdataClient({ version: 'v1beta', auth });
221
+ const requestBody = { metrics: metrics.map((name) => ({ name })) };
222
+ if (dimensions?.length)
223
+ requestBody.dimensions = dimensions.map((name) => ({ name }));
224
+ if (dimensionFilter)
225
+ requestBody.dimensionFilter = dimensionFilter;
226
+ if (metricFilter)
227
+ requestBody.metricFilter = metricFilter;
228
+ if (minuteRanges?.length)
229
+ requestBody.minuteRanges = minuteRanges;
230
+ if (limit !== undefined)
231
+ requestBody.limit = limit;
232
+ if (returnPropertyQuota !== undefined)
233
+ requestBody.returnPropertyQuota = returnPropertyQuota;
234
+ const res = await dataApi.properties.runRealtimeReport({ property: prop.name, requestBody });
235
+ return {
236
+ content: [{ type: 'text', text: JSON.stringify(shapeReport(res.data), null, 2) }],
237
+ };
238
+ }
239
+ catch (error) {
240
+ return handleAnalyticsError(error, account);
241
+ }
242
+ });
243
+ server.registerTool('analytics_get_metadata', {
244
+ description: 'List every valid dimension and metric API name for a GA4 property, including its custom definitions. Call this before analytics_run_report when unsure which names exist. Property "0" returns the standard set without property access.',
245
+ inputSchema: {
246
+ account: accountEnum.describe('Google account alias'),
247
+ property: propertySchema,
248
+ },
249
+ }, async ({ account, property }) => {
250
+ const prop = normalizeProperty(property);
251
+ if ('hint' in prop)
252
+ return invalidProperty(prop.hint, account);
253
+ try {
254
+ const auth = await getClient(account);
255
+ const dataApi = analyticsdataClient({ version: 'v1beta', auth });
256
+ const res = await dataApi.properties.getMetadata({ name: `${prop.name}/metadata` });
257
+ return {
258
+ content: [{ type: 'text', text: JSON.stringify(shapeMetadata(res.data), null, 2) }],
259
+ };
260
+ }
261
+ catch (error) {
262
+ return handleAnalyticsError(error, account);
263
+ }
264
+ });
265
+ }
266
+ function invalidProperty(hint, account) {
267
+ return {
268
+ content: [
269
+ {
270
+ type: 'text',
271
+ text: JSON.stringify({ error: 'invalid_params', message: 'Invalid GA4 property reference.', hint, retriable: false, account }),
272
+ },
273
+ ],
274
+ isError: true,
275
+ };
276
+ }
277
+ function handleAnalyticsError(error, account) {
278
+ return handleGoogleApiError(error, account, 'Needs the "analytics" bundle on this account (add it to the scope profile, then re-auth), and the Google account must have access to this GA4 property.');
279
+ }
@@ -118,20 +118,6 @@ export function registerAnalyticsGeneratedTools(registry) {
118
118
  fields: z.string().optional().describe('Response field mask.'),
119
119
  },
120
120
  });
121
- registerGeneratedTool(registry, {
122
- name: "analytics_account_summaries_list",
123
- cud: "read",
124
- description: "Returns summaries of all accounts accessible by the caller.",
125
- method: { id: "analyticsadmin.accountSummaries.list", httpMethod: "GET", path: "v1beta/accountSummaries", baseUrl: "https://analyticsadmin.googleapis.com/", requiredParams: [], scopes: S_analyticsadmin_v1beta[1] },
126
- params: [{ "field": "pageSize", "api": "pageSize", "location": "query" }, { "field": "pageToken", "api": "pageToken", "location": "query" }, { "field": "fields", "api": "fields", "location": "query" }],
127
- hasBody: false,
128
- shape: {
129
- account: accountField(),
130
- pageSize: z.number().describe("Optional. The maximum number of AccountSummary resources to return. The service may return fewer than this value, even if there are additional pages. If unspecified, at most 50 resources will be retur").optional(),
131
- pageToken: z.string().describe("Optional. A page token, received from a previous `ListAccountSummaries` call. Provide this to retrieve the subsequent page. When paginating, all other parameters provided to `ListAccountSummaries` mus").optional(),
132
- fields: z.string().optional().describe('Response field mask.'),
133
- },
134
- });
135
121
  registerGeneratedTool(registry, {
136
122
  name: "analytics_properties_acknowledge_user_data_collection",
137
123
  cud: "update",
@@ -881,19 +867,6 @@ export function registerAnalyticsGeneratedTools(registry) {
881
867
  fields: z.string().optional().describe('Response field mask.'),
882
868
  },
883
869
  });
884
- registerGeneratedTool(registry, {
885
- name: "analytics_properties_get_metadata",
886
- cud: "read",
887
- description: "Returns metadata for dimensions and metrics available in reporting methods. Used to explore the dimensions and metrics. In this method, a Google Analytics prope",
888
- method: { id: "analyticsdata.properties.getMetadata", httpMethod: "GET", path: "v1beta/{+name}", baseUrl: "https://analyticsdata.googleapis.com/", requiredParams: ["name"], scopes: S_analyticsdata_v1beta[0] },
889
- params: [{ "field": "name", "api": "name", "location": "path" }, { "field": "fields", "api": "fields", "location": "query" }],
890
- hasBody: false,
891
- shape: {
892
- account: accountField(),
893
- name: z.string().describe("Required. The resource name of the metadata to retrieve. This name field is specified in the URL path and not URL parameters. Property is a numeric Google Analytics property identifier. To learn more,"),
894
- fields: z.string().optional().describe('Response field mask.'),
895
- },
896
- });
897
870
  registerGeneratedTool(registry, {
898
871
  name: "analytics_properties_run_pivot_report",
899
872
  cud: "read",
@@ -908,32 +881,4 @@ export function registerAnalyticsGeneratedTools(registry) {
908
881
  fields: z.string().optional().describe('Response field mask.'),
909
882
  },
910
883
  });
911
- registerGeneratedTool(registry, {
912
- name: "analytics_properties_run_realtime_report",
913
- cud: "read",
914
- description: "Returns a customized report of realtime event data for your property. Events appear in realtime reports seconds after they have been sent to the Google Analytic",
915
- method: { id: "analyticsdata.properties.runRealtimeReport", httpMethod: "POST", path: "v1beta/{+property}:runRealtimeReport", baseUrl: "https://analyticsdata.googleapis.com/", requiredParams: ["property"], scopes: S_analyticsdata_v1beta[0] },
916
- params: [{ "field": "property", "api": "property", "location": "path" }, { "field": "fields", "api": "fields", "location": "query" }],
917
- hasBody: true,
918
- shape: {
919
- account: accountField(),
920
- property: z.string().describe("A Google Analytics property identifier whose events are tracked. Specified in the URL path and not the body. To learn more, see [where to find your Property ID](https://developers.google.com/analytics"),
921
- body: coerceJson(z.record(z.string(), z.unknown())).describe("RunRealtimeReportRequest JSON request body. Top-level fields: dimensionFilter, dimensions, limit, metricAggregations, metricFilter, metrics, minuteRanges, orderBys, returnPropertyQuota."),
922
- fields: z.string().optional().describe('Response field mask.'),
923
- },
924
- });
925
- registerGeneratedTool(registry, {
926
- name: "analytics_properties_run_report",
927
- cud: "read",
928
- description: "Returns a customized report of your Google Analytics event data. Reports contain statistics derived from data collected by the Google Analytics tracking code. T",
929
- method: { id: "analyticsdata.properties.runReport", httpMethod: "POST", path: "v1beta/{+property}:runReport", baseUrl: "https://analyticsdata.googleapis.com/", requiredParams: ["property"], scopes: S_analyticsdata_v1beta[0] },
930
- params: [{ "field": "property", "api": "property", "location": "path" }, { "field": "fields", "api": "fields", "location": "query" }],
931
- hasBody: true,
932
- shape: {
933
- account: accountField(),
934
- property: z.string().describe("A Google Analytics property identifier whose events are tracked. Specified in the URL path and not the body. To learn more, see [where to find your Property ID](https://developers.google.com/analytics"),
935
- body: coerceJson(z.record(z.string(), z.unknown())).describe("RunReportRequest JSON request body. Top-level fields: cohortSpec, comparisons, currencyCode, dateRanges, dimensionFilter, dimensions, keepEmptyRows, limit, metricAggregations, metricFilter, metrics, offset, +3 more."),
936
- fields: z.string().optional().describe('Response field mask.'),
937
- },
938
- });
939
884
  }
@@ -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.12",
3
+ "version": "6.0.0-alpha.14",
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",
@@ -63,6 +63,8 @@
63
63
  },
64
64
  "dependencies": {
65
65
  "@googleapis/admin": "^37.0.0",
66
+ "@googleapis/analyticsadmin": "^22.0.0",
67
+ "@googleapis/analyticsdata": "^10.0.0",
66
68
  "@googleapis/calendar": "^20.0.0",
67
69
  "@googleapis/chat": "^51.0.0",
68
70
  "@googleapis/docs": "^14.0.0",