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.
- package/dist/api-probe.d.ts +18 -0
- package/dist/api-probe.js +65 -0
- package/dist/doctor.js +5 -0
- package/package.json +1 -1
|
@@ -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.
|
|
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",
|