mcp-google-multi 6.0.0-alpha.6 → 6.0.0-alpha.7
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/doctor.d.ts +12 -0
- package/dist/doctor.js +93 -15
- package/package.json +1 -1
package/dist/doctor.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ToolRegistry } from './registry.js';
|
|
|
2
2
|
import { getAccountSet } from './accounts.js';
|
|
3
3
|
import { type AccountHealth } from './tools/accounts-tool.js';
|
|
4
4
|
import { peekMasterKeyProvenance } from './master-key.js';
|
|
5
|
+
import { type HttpConfig } from './http-config.js';
|
|
5
6
|
export type Verdict = 'ok' | 'warn' | 'fail' | 'unknown';
|
|
6
7
|
export interface DiagnosticSection {
|
|
7
8
|
id: number;
|
|
@@ -38,6 +39,17 @@ export interface DiagnosticsDeps {
|
|
|
38
39
|
/** Optional live section-6 probe; when absent the section reports `unknown`
|
|
39
40
|
* (spec: a section that cannot run is unknown, not FAIL). */
|
|
40
41
|
probeApi?: (alias: string) => Promise<ApiProbeResult[]>;
|
|
42
|
+
/** Optional live section-7 endpoint probe (PRM/AS-metadata self-fetch);
|
|
43
|
+
* when absent, section 7 stays on its offline config checks. */
|
|
44
|
+
probeHttp?: (cfg: HttpConfig) => Promise<HttpProbeResult>;
|
|
45
|
+
}
|
|
46
|
+
/** Live §7 probe outcome. `unreachable` = connection-level failure (server not
|
|
47
|
+
* running), reported as `unknown` rather than FAIL; `problem` = a real
|
|
48
|
+
* metadata fault at a reachable server. */
|
|
49
|
+
export interface HttpProbeResult {
|
|
50
|
+
ok: boolean;
|
|
51
|
+
unreachable?: boolean;
|
|
52
|
+
problem?: string;
|
|
41
53
|
}
|
|
42
54
|
/** Console deep-link to enable one API (section-6 hint, error taxonomy B10). */
|
|
43
55
|
export declare function apiEnableLink(api: string): string;
|
package/dist/doctor.js
CHANGED
|
@@ -7,6 +7,8 @@ import { peekMasterKeyProvenance, deleteMasterKeyMaterial } from './master-key.j
|
|
|
7
7
|
import { hasToken } from './token-store.js';
|
|
8
8
|
import { configDir } from './config-file.js';
|
|
9
9
|
import { probeApiEnablement } from './api-probe.js';
|
|
10
|
+
import { resolveHttpConfig, HttpConfigError } from './http-config.js';
|
|
11
|
+
import { parseOwnerEmails } from './http-transport.js';
|
|
10
12
|
const MIN_NODE_MAJOR = 22;
|
|
11
13
|
const DEFAULT_DEPS = {
|
|
12
14
|
nodeVersion: process.versions.node,
|
|
@@ -25,17 +27,43 @@ const DEFAULT_DEPS = {
|
|
|
25
27
|
anyTokensExist: (aliases) => aliases.some((a) => hasToken(a)),
|
|
26
28
|
fileExists: fs.existsSync,
|
|
27
29
|
probeApi: (alias) => probeApiEnablement(alias),
|
|
30
|
+
probeHttp: (cfg) => probeHttpEndpoints(cfg),
|
|
28
31
|
};
|
|
32
|
+
/** §7 live check: the advertised OAuth metadata must derive from MCP_PUBLIC_URL
|
|
33
|
+
* exactly — one mismatch between PRM `resource` / AS `issuer` and what clients
|
|
34
|
+
* compute from the public URL is the perpetual-401 interop bug (BR4). */
|
|
35
|
+
async function probeHttpEndpoints(cfg) {
|
|
36
|
+
try {
|
|
37
|
+
const prmRes = await fetch(`${cfg.publicUrl}/.well-known/oauth-protected-resource`, {
|
|
38
|
+
signal: AbortSignal.timeout(2000),
|
|
39
|
+
redirect: 'manual',
|
|
40
|
+
});
|
|
41
|
+
if (!prmRes.ok)
|
|
42
|
+
return { ok: false, problem: `PRM endpoint returned HTTP ${prmRes.status}` };
|
|
43
|
+
const prm = (await prmRes.json());
|
|
44
|
+
if (prm.resource !== cfg.resourceUri) {
|
|
45
|
+
return { ok: false, problem: `PRM resource "${prm.resource}" does not match the expected "${cfg.resourceUri}"` };
|
|
46
|
+
}
|
|
47
|
+
const asRes = await fetch(`${cfg.publicUrl}/.well-known/oauth-authorization-server`, {
|
|
48
|
+
signal: AbortSignal.timeout(2000),
|
|
49
|
+
redirect: 'manual',
|
|
50
|
+
});
|
|
51
|
+
if (!asRes.ok)
|
|
52
|
+
return { ok: false, problem: `AS metadata endpoint returned HTTP ${asRes.status}` };
|
|
53
|
+
const as = (await asRes.json());
|
|
54
|
+
if (as.issuer !== cfg.publicUrl) {
|
|
55
|
+
return { ok: false, problem: `AS metadata issuer "${as.issuer}" does not match the public URL "${cfg.publicUrl}"` };
|
|
56
|
+
}
|
|
57
|
+
return { ok: true };
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return { ok: false, unreachable: true };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
29
63
|
/** Console deep-link to enable one API (section-6 hint, error taxonomy B10). */
|
|
30
64
|
export function apiEnableLink(api) {
|
|
31
65
|
return `https://console.cloud.google.com/apis/library/${api}.googleapis.com`;
|
|
32
66
|
}
|
|
33
|
-
function transportsFrom(env) {
|
|
34
|
-
return (env.MCP_TRANSPORT ?? 'stdio')
|
|
35
|
-
.split(',')
|
|
36
|
-
.map((s) => s.trim().toLowerCase())
|
|
37
|
-
.filter(Boolean);
|
|
38
|
-
}
|
|
39
67
|
const LEGACY_ENV_KEYS = ['GOOGLE_ACCOUNTS', 'GOOGLE_OPTIONAL_SCOPES', 'GOOGLE_ADMIN_ACCOUNTS'];
|
|
40
68
|
function sectionRuntime(deps) {
|
|
41
69
|
const major = Number.parseInt(deps.nodeVersion.split('.')[0] ?? '0', 10);
|
|
@@ -182,15 +210,65 @@ async function sectionApiEnablement(deps, aliases) {
|
|
|
182
210
|
}
|
|
183
211
|
return { id: 6, title: 'API enablement', verdict: 'ok', lines: lines.length ? lines : ['(probed account, all enabled)'] };
|
|
184
212
|
}
|
|
185
|
-
function
|
|
186
|
-
|
|
213
|
+
async function sectionHttp(deps, aliases) {
|
|
214
|
+
const raw = (deps.env.MCP_TRANSPORT ?? '').trim().toLowerCase();
|
|
215
|
+
if (raw === '' || raw === 'stdio')
|
|
187
216
|
return null;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
217
|
+
let cfg;
|
|
218
|
+
try {
|
|
219
|
+
cfg = resolveHttpConfig(deps.env);
|
|
220
|
+
}
|
|
221
|
+
catch (err) {
|
|
222
|
+
return {
|
|
223
|
+
id: 7,
|
|
224
|
+
title: 'HTTP',
|
|
225
|
+
verdict: 'fail',
|
|
226
|
+
slug: err instanceof HttpConfigError ? err.slug : 'E_HTTP_CONFIG',
|
|
227
|
+
lines: [err.message],
|
|
228
|
+
hint: 'Fix the MCP_* variable above and re-run doctor.',
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
const lines = [`bind ${cfg.host}:${cfg.port}, public URL ${cfg.publicUrl} (resource ${cfg.resourceUri})`];
|
|
232
|
+
let verdict = 'ok';
|
|
233
|
+
let slug;
|
|
234
|
+
const hints = [];
|
|
235
|
+
const owners = parseOwnerEmails(deps.env);
|
|
236
|
+
if (owners.length === 0) {
|
|
237
|
+
verdict = 'fail';
|
|
238
|
+
slug = 'E_OWNER_EMAILS_REQUIRED';
|
|
239
|
+
lines.push('MCP_OWNER_EMAILS is empty — nobody can pass the owner gate.');
|
|
240
|
+
hints.push('Set MCP_OWNER_EMAILS to the Google email(s) allowed to authenticate.');
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
const known = new Set(aliases.map((a) => deps.accountHealth(a).email.toLowerCase()));
|
|
244
|
+
const strangers = known.size > 0 ? owners.filter((o) => !known.has(o)) : [];
|
|
245
|
+
lines.push(`owner gate: ${owners.length} email(s)${strangers.length ? `, ${strangers.length} matching no configured account` : ''}`);
|
|
246
|
+
if (strangers.length > 0) {
|
|
247
|
+
verdict = 'warn';
|
|
248
|
+
slug = 'W_OWNER_EMAIL_UNKNOWN';
|
|
249
|
+
hints.push(`Owner entry ${strangers.join(', ')} is not a configured account email. ` +
|
|
250
|
+
'If that is a misspelling of your account email, sign-in will be refused — fix MCP_OWNER_EMAILS.');
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (verdict !== 'fail' && deps.probeHttp) {
|
|
254
|
+
const probe = await deps.probeHttp(cfg);
|
|
255
|
+
if (probe.ok) {
|
|
256
|
+
lines.push('live: PRM + AS metadata verified at the public URL');
|
|
257
|
+
}
|
|
258
|
+
else if (probe.unreachable) {
|
|
259
|
+
if (verdict === 'ok')
|
|
260
|
+
verdict = 'unknown';
|
|
261
|
+
lines.push(`live: ${cfg.publicUrl} not reachable (server not running?)`);
|
|
262
|
+
hints.push('Start the server (MCP_TRANSPORT=http) and re-run doctor for the live endpoint checks.');
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
verdict = 'fail';
|
|
266
|
+
slug = 'E_HTTP_METADATA_MISMATCH';
|
|
267
|
+
lines.push(`live: ${probe.problem}`);
|
|
268
|
+
hints.push('The advertised OAuth metadata must derive from MCP_PUBLIC_URL exactly; restart the server after changing it.');
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { id: 7, title: 'HTTP', verdict, ...(slug ? { slug } : {}), lines, ...(hints.length ? { hint: hints.join('\n') } : {}) };
|
|
194
272
|
}
|
|
195
273
|
const RANK = { ok: 0, unknown: 0, warn: 1, fail: 2 };
|
|
196
274
|
/** Roll section verdicts to an overall verdict. `unknown` never worsens it. */
|
|
@@ -214,7 +292,7 @@ export async function runDiagnostics(deps = DEFAULT_DEPS) {
|
|
|
214
292
|
sections.push(tokens, scopes);
|
|
215
293
|
sections.push(await sectionApiEnablement(deps, aliases));
|
|
216
294
|
}
|
|
217
|
-
const http =
|
|
295
|
+
const http = await sectionHttp(deps, aliases);
|
|
218
296
|
if (http)
|
|
219
297
|
sections.push(http);
|
|
220
298
|
return { verdict: overallVerdict(sections), sections };
|
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.7",
|
|
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",
|