mcp-google-multi 6.0.0-alpha.2 → 6.0.0-alpha.3

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.
@@ -0,0 +1,18 @@
1
+ import type { ApiProbeResult } from './doctor.js';
2
+ export interface ApiProbeSpec {
3
+ service: string;
4
+ /** console library id for the enable deep-link, e.g. "calendar-json". */
5
+ api: string;
6
+ url: string;
7
+ scopePrefixes: string[];
8
+ /** id-required APIs have no no-arg read; a 404 on a nonexistent id still
9
+ * proves the API is enabled (accessNotConfigured wins before routing). */
10
+ notFoundMeansEnabled?: boolean;
11
+ }
12
+ export declare const API_PROBES: ApiProbeSpec[];
13
+ export declare function planProbes(granted: string[], probes?: ApiProbeSpec[]): ApiProbeSpec[];
14
+ export interface ApiProbeDeps {
15
+ grantedScopes: (alias: string) => string[];
16
+ request: (alias: string, url: string) => Promise<void>;
17
+ }
18
+ export declare function probeApiEnablement(alias: string, deps?: ApiProbeDeps): Promise<ApiProbeResult[]>;
@@ -0,0 +1,65 @@
1
+ import { getClient } from './client.js';
2
+ import { readToken } from './token-store.js';
3
+ import { mapGoogleError } from './tools/_errors.js';
4
+ const P = 'https://www.googleapis.com/auth/';
5
+ const BOGUS_ID = 'mcp-google-multi-probe-nonexistent';
6
+ export const API_PROBES = [
7
+ { service: 'gmail', api: 'gmail', url: 'https://gmail.googleapis.com/gmail/v1/users/me/profile', scopePrefixes: [`${P}gmail.`] },
8
+ { service: 'drive', api: 'drive', url: 'https://www.googleapis.com/drive/v3/about?fields=user', scopePrefixes: [`${P}drive`] },
9
+ { service: 'calendar', api: 'calendar-json', url: 'https://www.googleapis.com/calendar/v3/users/me/calendarList?maxResults=1', scopePrefixes: [`${P}calendar`] },
10
+ // people/me needs profile scopes, not contacts; connections is the read the
11
+ // contacts grant actually authorizes.
12
+ { service: 'contacts', api: 'people', url: 'https://people.googleapis.com/v1/people/me/connections?personFields=names&pageSize=1', scopePrefixes: [`${P}contacts`] },
13
+ { service: 'sheets', api: 'sheets', url: `https://sheets.googleapis.com/v4/spreadsheets/${BOGUS_ID}`, scopePrefixes: [`${P}spreadsheets`], notFoundMeansEnabled: true },
14
+ { service: 'docs', api: 'docs', url: `https://docs.googleapis.com/v1/documents/${BOGUS_ID}`, scopePrefixes: [`${P}documents`], notFoundMeansEnabled: true },
15
+ { service: 'searchconsole', api: 'searchconsole', url: 'https://www.googleapis.com/webmasters/v3/sites', scopePrefixes: [`${P}webmasters`] },
16
+ { service: 'tasks', api: 'tasks', url: 'https://tasks.googleapis.com/tasks/v1/users/@me/lists?maxResults=1', scopePrefixes: [`${P}tasks`] },
17
+ { service: 'chat', api: 'chat', url: 'https://chat.googleapis.com/v1/spaces?pageSize=1', scopePrefixes: [`${P}chat.`] },
18
+ { service: 'meet', api: 'meet', url: 'https://meet.googleapis.com/v2/conferenceRecords?pageSize=1', scopePrefixes: [`${P}meetings.`] },
19
+ { service: 'forms', api: 'forms', url: `https://forms.googleapis.com/v1/forms/${BOGUS_ID}`, scopePrefixes: [`${P}forms.`], notFoundMeansEnabled: true },
20
+ ];
21
+ export function planProbes(granted, probes = API_PROBES) {
22
+ return probes.filter((p) => granted.some((s) => p.scopePrefixes.some((prefix) => s.startsWith(prefix))));
23
+ }
24
+ const DEFAULT_DEPS = {
25
+ grantedScopes: (alias) => {
26
+ try {
27
+ const scope = readToken(alias)?.scope;
28
+ return typeof scope === 'string' ? scope.split(' ').filter(Boolean) : [];
29
+ }
30
+ catch {
31
+ return [];
32
+ }
33
+ },
34
+ request: async (alias, url) => {
35
+ const auth = await getClient(alias);
36
+ await auth.request({ url, timeout: 10_000 });
37
+ },
38
+ };
39
+ export async function probeApiEnablement(alias, deps = DEFAULT_DEPS) {
40
+ const results = [];
41
+ for (const spec of planProbes(deps.grantedScopes(alias))) {
42
+ try {
43
+ await deps.request(alias, spec.url);
44
+ results.push({ service: spec.service, api: spec.api, ok: true });
45
+ }
46
+ catch (error) {
47
+ const envelope = mapGoogleError(error, alias);
48
+ if (envelope.error === 'network_error') {
49
+ // One connect failure means they will all fail: abort so section 6
50
+ // reports a single WARN "Probe could not complete" with the code.
51
+ throw new Error(envelope.message, { cause: error });
52
+ }
53
+ if (envelope.error === 'api_not_enabled') {
54
+ results.push({ service: spec.service, api: spec.api, ok: false, notEnabled: true, message: envelope.message });
55
+ }
56
+ else if (spec.notFoundMeansEnabled && envelope.error === 'not_found') {
57
+ results.push({ service: spec.service, api: spec.api, ok: true });
58
+ }
59
+ else {
60
+ results.push({ service: spec.service, api: spec.api, ok: false, message: envelope.error });
61
+ }
62
+ }
63
+ }
64
+ return results;
65
+ }
package/dist/doctor.js CHANGED
@@ -6,6 +6,7 @@ import { deriveAccountHealth } from './tools/accounts-tool.js';
6
6
  import { peekMasterKeyProvenance, deleteMasterKeyMaterial } from './master-key.js';
7
7
  import { hasToken } from './token-store.js';
8
8
  import { configDir } from './config-file.js';
9
+ import { probeApiEnablement } from './api-probe.js';
9
10
  const MIN_NODE_MAJOR = 22;
10
11
  const DEFAULT_DEPS = {
11
12
  nodeVersion: process.versions.node,
@@ -23,6 +24,7 @@ const DEFAULT_DEPS = {
23
24
  masterKeyProvenance: () => peekMasterKeyProvenance(),
24
25
  anyTokensExist: (aliases) => aliases.some((a) => hasToken(a)),
25
26
  fileExists: fs.existsSync,
27
+ probeApi: (alias) => probeApiEnablement(alias),
26
28
  };
27
29
  /** Console deep-link to enable one API (section-6 hint, error taxonomy B10). */
28
30
  export function apiEnableLink(api) {
@@ -163,6 +165,9 @@ async function sectionApiEnablement(deps, aliases) {
163
165
  // Network / transient: WARN with the target, never crash the report.
164
166
  return { id: 6, title: 'API enablement', verdict: 'warn', lines: [`Probe could not complete: ${e?.message ?? e}`] };
165
167
  }
168
+ if (results.length === 0) {
169
+ return { id: 6, title: 'API enablement', verdict: 'unknown', lines: [`No probeable service scopes granted on "${healthy}".`] };
170
+ }
166
171
  const disabled = results.filter((r) => r.notEnabled);
167
172
  const lines = results.map((r) => `${r.service}: ${r.ok ? 'enabled' : r.notEnabled ? 'NOT ENABLED' : `unknown (${r.message ?? 'error'})`}`);
168
173
  if (disabled.length > 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-google-multi",
3
- "version": "6.0.0-alpha.2",
3
+ "version": "6.0.0-alpha.3",
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",