mcp-google-multi 6.0.0-alpha.55 → 6.0.0-alpha.56
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/accounts.js +3 -3
- package/dist/auth.js +2 -2
- package/dist/index.js +2 -17
- package/dist/mcp-token.d.ts +3 -0
- package/dist/migrate-config.js +2 -2
- package/dist/oauth-as.d.ts +29 -11
- package/dist/oauth-as.js +32 -7
- package/dist/scope-catalog.d.ts +3 -0
- package/dist/scope-catalog.js +6 -1
- package/dist/tools/account-wizard.d.ts +1 -1
- package/dist/tools/account-wizard.js +2 -2
- package/package.json +3 -1
package/dist/accounts.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import { z } from 'zod';
|
|
4
4
|
import { loadEnvFiles } from './env-load.js';
|
|
5
5
|
import { ALIAS_RE, CONFIG_VERSION, configDir, configFilePath, failStartup, isReservedAlias, loadConfigFile, mutateConfigFile } from './config-file.js';
|
|
6
|
-
import {
|
|
6
|
+
import { closestBundle, isKnownBundle, resolveBundleAliases } from './scope-catalog.js';
|
|
7
7
|
const envLoad = loadEnvFiles();
|
|
8
8
|
const defaultTokenDir = path.join(configDir(), 'tokens');
|
|
9
9
|
const tokenDir = process.env.TOKEN_STORE_PATH
|
|
@@ -95,7 +95,7 @@ opts = {}) {
|
|
|
95
95
|
if (bundle === 'admin') {
|
|
96
96
|
fail('E_UNKNOWN_BUNDLE', '"admin" is not a global bundle: grant it per account via GOOGLE_ADMIN_ACCOUNTS or an "admin: true" scope profile.');
|
|
97
97
|
}
|
|
98
|
-
if (!(bundle
|
|
98
|
+
if (!isKnownBundle(bundle)) {
|
|
99
99
|
const hint = closestBundle(bundle);
|
|
100
100
|
fail('E_UNKNOWN_BUNDLE', `unknown bundle "${bundle}" in GOOGLE_OPTIONAL_SCOPES${hint ? ` — did you mean "${hint}"?` : ''}`);
|
|
101
101
|
}
|
|
@@ -152,7 +152,7 @@ opts = {}) {
|
|
|
152
152
|
for (const [name, profile] of Object.entries(config?.scopeProfiles ?? {})) {
|
|
153
153
|
const bundles = resolveBundleAliases(profile.bundles);
|
|
154
154
|
for (const bundle of bundles) {
|
|
155
|
-
if (!(bundle
|
|
155
|
+
if (!isKnownBundle(bundle)) {
|
|
156
156
|
const hint = closestBundle(bundle);
|
|
157
157
|
fail('E_UNKNOWN_BUNDLE', `unknown bundle "${bundle}" in scope profile "${name}"${hint ? ` — did you mean "${hint}"?` : ''}`);
|
|
158
158
|
}
|
package/dist/auth.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
2
|
import { openUrl } from './open-url.js';
|
|
3
3
|
import { ACCOUNTS, getAccountSet } from './accounts.js';
|
|
4
|
-
import { ADMIN_SCOPES, BUNDLE_CATALOG, closestBundle, resolveBundleAliases } from './scope-catalog.js';
|
|
4
|
+
import { ADMIN_SCOPES, BUNDLE_CATALOG, closestBundle, isKnownBundle, resolveBundleAliases } from './scope-catalog.js';
|
|
5
5
|
import { resolveMasterKey } from './master-key.js';
|
|
6
6
|
import { writeToken } from './token-store.js';
|
|
7
7
|
import { buildConsentClient, openLoopbackConsent, TESTING_MODE_WARNING } from './oauth-consent.js';
|
|
@@ -48,7 +48,7 @@ function legacyGlobalProfile() {
|
|
|
48
48
|
if (bundle === 'admin') {
|
|
49
49
|
throw new Error('E_UNKNOWN_BUNDLE: "admin" is not a global bundle: grant it per account via GOOGLE_ADMIN_ACCOUNTS or an "admin: true" scope profile.');
|
|
50
50
|
}
|
|
51
|
-
if (!(bundle
|
|
51
|
+
if (!isKnownBundle(bundle)) {
|
|
52
52
|
const hint = closestBundle(bundle);
|
|
53
53
|
throw new Error(`E_UNKNOWN_BUNDLE: unknown bundle "${bundle}" in GOOGLE_OPTIONAL_SCOPES${hint ? ` — did you mean "${hint}"?` : ''}`);
|
|
54
54
|
}
|
package/dist/index.js
CHANGED
|
@@ -246,7 +246,7 @@ async function main() {
|
|
|
246
246
|
registry.installListHandler();
|
|
247
247
|
registerSetupPrompt(httpServer);
|
|
248
248
|
// B13: mount the OAuth 2.1 AS (legs A + B) + the Bearer authenticator.
|
|
249
|
-
const { buildAuthServer } = await import('./oauth-as.js');
|
|
249
|
+
const { buildAuthServer, verifiedEmailFromIdToken } = await import('./oauth-as.js');
|
|
250
250
|
const { jwtSecretFrom } = await import('./mcp-token.js');
|
|
251
251
|
const { resolveJwtKey, resolveMasterKeyForDispatch } = await import('./master-key.js');
|
|
252
252
|
const { OAuth2Client } = await import('googleapis-common');
|
|
@@ -255,20 +255,6 @@ async function main() {
|
|
|
255
255
|
const { configDir } = await import('./config-file.js');
|
|
256
256
|
const { getAccountSet } = await import('./accounts.js');
|
|
257
257
|
const googleClient = () => new OAuth2Client(process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, `${httpCfg.publicUrl}/callback`);
|
|
258
|
-
// Only trust the id_token email when Google marks it verified (#8).
|
|
259
|
-
const verifiedEmailFromIdToken = (idToken) => {
|
|
260
|
-
if (!idToken)
|
|
261
|
-
return undefined;
|
|
262
|
-
try {
|
|
263
|
-
const [, payload] = idToken.split('.');
|
|
264
|
-
const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf-8'));
|
|
265
|
-
const verified = claims.email_verified === true || claims.email_verified === 'true';
|
|
266
|
-
return verified ? claims.email : undefined;
|
|
267
|
-
}
|
|
268
|
-
catch {
|
|
269
|
-
return undefined;
|
|
270
|
-
}
|
|
271
|
-
};
|
|
272
258
|
const authServer = buildAuthServer({
|
|
273
259
|
base: httpCfg.publicUrl,
|
|
274
260
|
resourceUri: httpCfg.resourceUri,
|
|
@@ -309,8 +295,7 @@ async function main() {
|
|
|
309
295
|
const { setHttpReauthBase } = await import('./reauth-hint.js');
|
|
310
296
|
setHttpReauthBase(httpCfg.publicUrl);
|
|
311
297
|
// Wizard consent over HTTP: hand out the clientless AS link instead of
|
|
312
|
-
// binding a loopback listener
|
|
313
|
-
// its own signed-mint context here).
|
|
298
|
+
// binding a loopback listener.
|
|
314
299
|
const { setWizardHttpConsent } = await import('./tools/account-wizard.js');
|
|
315
300
|
setWizardHttpConsent({
|
|
316
301
|
mintConsentUrl: (alias) => `${httpCfg.publicUrl}/authorize?flow=alias_reauth&alias=${encodeURIComponent(alias)}`,
|
package/dist/mcp-token.d.ts
CHANGED
|
@@ -32,6 +32,9 @@ export interface StatePayload {
|
|
|
32
32
|
tenantId?: string;
|
|
33
33
|
/** alias_add only: scope bundles chosen when the link was minted. */
|
|
34
34
|
bundles?: string[];
|
|
35
|
+
/** alias_add only: an opaque value the minting caller chose, signed like
|
|
36
|
+
* tenantId and handed back to its binder (e.g. a server-side link record). */
|
|
37
|
+
nonce?: string;
|
|
35
38
|
}
|
|
36
39
|
export interface CodePayload {
|
|
37
40
|
redirect_uri: string;
|
package/dist/migrate-config.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CONFIG_VERSION, configFilePath, loadConfigFile, mutateConfigFile } from './config-file.js';
|
|
2
|
-
import {
|
|
2
|
+
import { closestBundle, isKnownBundle, resolveBundleAliases } from './scope-catalog.js';
|
|
3
3
|
function parseCsv(value) {
|
|
4
4
|
return (value ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
5
5
|
}
|
|
@@ -47,7 +47,7 @@ export function runMigrateConfig(env = process.env) {
|
|
|
47
47
|
process.exitCode = 1;
|
|
48
48
|
return;
|
|
49
49
|
}
|
|
50
|
-
if (!(bundle
|
|
50
|
+
if (!isKnownBundle(bundle)) {
|
|
51
51
|
const hint = closestBundle(bundle);
|
|
52
52
|
console.error(`E_UNKNOWN_BUNDLE: unknown bundle "${bundle}" in GOOGLE_OPTIONAL_SCOPES${hint ? ` — did you mean "${hint}"?` : ''}. Nothing written.`);
|
|
53
53
|
process.exitCode = 1;
|
package/dist/oauth-as.d.ts
CHANGED
|
@@ -22,6 +22,27 @@ export interface AuthServerConfig {
|
|
|
22
22
|
* cross-tenant re-auth vector. Server-minted signed flows are unaffected. */
|
|
23
23
|
legacyAliasReauth?: boolean;
|
|
24
24
|
}
|
|
25
|
+
/** What /callback hands the alias_add binder once Google returned a verified
|
|
26
|
+
* identity for a server-minted link. */
|
|
27
|
+
export interface TenantAliasBind {
|
|
28
|
+
tenantId: string;
|
|
29
|
+
alias: string;
|
|
30
|
+
email: string;
|
|
31
|
+
bundles?: string[];
|
|
32
|
+
nonce?: string;
|
|
33
|
+
tokens: Record<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
/** A binder's deliberate refusal (policy, not failure): /callback answers 403
|
|
36
|
+
* with this slug and message. The message reaches an unauthenticated browser. */
|
|
37
|
+
export interface AliasBindRefusal {
|
|
38
|
+
refused: {
|
|
39
|
+
slug: string;
|
|
40
|
+
message: string;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Only trust the id_token email when Google marks it verified (#8). The token
|
|
44
|
+
* comes straight from Google's token endpoint, so its claims are read unsigned. */
|
|
45
|
+
export declare function verifiedEmailFromIdToken(idToken?: string | null): string | undefined;
|
|
25
46
|
export interface AuthServerDeps {
|
|
26
47
|
fetchCimd?: (clientId: string) => Promise<Record<string, unknown>>;
|
|
27
48
|
/** Exchange a Google auth code at `${base}/callback` for tokens (+ owner email). */
|
|
@@ -34,6 +55,7 @@ export interface AuthServerDeps {
|
|
|
34
55
|
flow: StatePayload['flow'];
|
|
35
56
|
alias?: string;
|
|
36
57
|
state: string;
|
|
58
|
+
bundles?: string[];
|
|
37
59
|
}) => string;
|
|
38
60
|
writeToken?: (alias: string, tokens: Record<string, unknown>) => void;
|
|
39
61
|
registeredClients?: Map<string, {
|
|
@@ -52,16 +74,11 @@ export interface AuthServerDeps {
|
|
|
52
74
|
resolveSubject?: (email: string) => {
|
|
53
75
|
sub: string;
|
|
54
76
|
} | null;
|
|
55
|
-
/** alias_add completion: persist the config row
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
alias: string;
|
|
61
|
-
email: string;
|
|
62
|
-
bundles?: string[];
|
|
63
|
-
tokens: Record<string, unknown>;
|
|
64
|
-
}) => void;
|
|
77
|
+
/** alias_add completion: persist the new alias's config row and tokens under
|
|
78
|
+
* its tenant. /callback awaits it only after a successful Google exchange.
|
|
79
|
+
* A returned refusal answers 403 with its slug; a throw or rejection answers
|
|
80
|
+
* 500 E_ALIAS_ADD_FAILED. */
|
|
81
|
+
bindTenantAlias?: (bind: TenantAliasBind) => void | AliasBindRefusal | Promise<void | AliasBindRefusal>;
|
|
65
82
|
}
|
|
66
83
|
export interface AuthServer {
|
|
67
84
|
routes: Record<string, RouteHandler>;
|
|
@@ -69,12 +86,13 @@ export interface AuthServer {
|
|
|
69
86
|
/** Mint a signed, single-use, TTL-bound clientless flow URL
|
|
70
87
|
* (`${base}/authorize?flow=...&state=<signed>`). The tenantId/bundles ride
|
|
71
88
|
* INSIDE the signed state, so nothing at /authorize or /callback trusts a
|
|
72
|
-
* caller-supplied identity. Inert unless something calls it
|
|
89
|
+
* caller-supplied identity. Inert unless something calls it. */
|
|
73
90
|
mintFlowState: (opts: {
|
|
74
91
|
flow: 'alias_add' | 'alias_reauth';
|
|
75
92
|
alias: string;
|
|
76
93
|
tenantId?: string;
|
|
77
94
|
bundles?: string[];
|
|
95
|
+
nonce?: string;
|
|
78
96
|
}) => Promise<string>;
|
|
79
97
|
}
|
|
80
98
|
/** C6: exact redirect_uri match, with RFC 8252 §7.3 port-agnostic loopback and
|
package/dist/oauth-as.js
CHANGED
|
@@ -17,6 +17,21 @@ const CLAUDE_AI_FIXED_CALLBACK = 'https://claude.ai/api/mcp/auth_callback';
|
|
|
17
17
|
export const DCR_MAX_CLIENTS = 1000;
|
|
18
18
|
export const DCR_MAX_REDIRECT_URIS = 10;
|
|
19
19
|
const DCR_MAX_URI_LEN = 2048;
|
|
20
|
+
/** Only trust the id_token email when Google marks it verified (#8). The token
|
|
21
|
+
* comes straight from Google's token endpoint, so its claims are read unsigned. */
|
|
22
|
+
export function verifiedEmailFromIdToken(idToken) {
|
|
23
|
+
if (!idToken)
|
|
24
|
+
return undefined;
|
|
25
|
+
try {
|
|
26
|
+
const [, payload] = idToken.split('.');
|
|
27
|
+
const claims = JSON.parse(Buffer.from(payload, 'base64url').toString('utf-8'));
|
|
28
|
+
const verified = claims.email_verified === true || claims.email_verified === 'true';
|
|
29
|
+
return verified ? claims.email : undefined;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
20
35
|
// --- helpers ----------------------------------------------------------------
|
|
21
36
|
function json(res, status, body, extraHeaders = {}) {
|
|
22
37
|
res.writeHead(status, { 'Content-Type': 'application/json', ...extraHeaders });
|
|
@@ -203,7 +218,7 @@ export function buildAuthServer(config, deps = {}) {
|
|
|
203
218
|
async function toGoogle(res, sp) {
|
|
204
219
|
const state = await signState(sp, base, secret, nowSec());
|
|
205
220
|
const authUrl = deps.buildGoogleAuthUrl
|
|
206
|
-
? deps.buildGoogleAuthUrl({ flow: sp.flow, alias: sp.alias, state })
|
|
221
|
+
? deps.buildGoogleAuthUrl({ flow: sp.flow, alias: sp.alias, state, bundles: sp.bundles })
|
|
207
222
|
: `https://accounts.google.com/o/oauth2/v2/auth?state=${encodeURIComponent(state)}`;
|
|
208
223
|
return redirect(res, authUrl);
|
|
209
224
|
}
|
|
@@ -356,16 +371,26 @@ export function buildAuthServer(config, deps = {}) {
|
|
|
356
371
|
const boundEmail = (exchanged.email ?? '').toLowerCase();
|
|
357
372
|
if (!boundEmail)
|
|
358
373
|
return errorPage(res, 403, 'access_denied', 'Google did not return an email for the signed-in account');
|
|
374
|
+
let refusal;
|
|
359
375
|
try {
|
|
360
|
-
deps.bindTenantAlias({ tenantId: st.tenantId, alias: st.alias, email: boundEmail, bundles: st.bundles, tokens: exchanged.tokens });
|
|
376
|
+
const out = await deps.bindTenantAlias({ tenantId: st.tenantId, alias: st.alias, email: boundEmail, bundles: st.bundles, nonce: st.nonce, tokens: exchanged.tokens });
|
|
377
|
+
refusal = out?.refused;
|
|
361
378
|
}
|
|
362
379
|
catch (e) {
|
|
363
|
-
|
|
380
|
+
// Any rejection reason, even none, must still reach this answer.
|
|
381
|
+
log(`alias_add bind failed for "${st.alias}": ${e instanceof Error ? e.message : typeof e === 'string' ? e : 'non-Error rejection'}`);
|
|
364
382
|
return errorPage(res, 500, 'E_ALIAS_ADD_FAILED', 'the account could not be saved; try the link again or ask for a fresh one');
|
|
365
383
|
}
|
|
384
|
+
if (refusal) {
|
|
385
|
+
// The slug lands in a text/plain body; keep it a bare identifier.
|
|
386
|
+
const slug = typeof refusal.slug === 'string' && /^[A-Za-z0-9_]+$/.test(refusal.slug) ? refusal.slug : 'access_denied';
|
|
387
|
+
const message = typeof refusal.message === 'string' && refusal.message ? refusal.message : 'the account could not be linked';
|
|
388
|
+
log(`alias_add refused for "${st.alias}": ${slug}`);
|
|
389
|
+
return errorPage(res, 403, slug, message);
|
|
390
|
+
}
|
|
366
391
|
log(`callback ok flow=alias_add alias=${st.alias}`);
|
|
367
392
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
368
|
-
res.end(`<!doctype html><meta charset=utf-8><p>Connected "${st.alias}". You can close this window and retry your request.</p>`);
|
|
393
|
+
res.end(`<!doctype html><meta charset=utf-8><p>Connected "${escapeHtml(st.alias)}". You can close this window and retry your request.</p>`);
|
|
369
394
|
return true;
|
|
370
395
|
}
|
|
371
396
|
if (st.flow === 'alias_reauth') {
|
|
@@ -385,7 +410,7 @@ export function buildAuthServer(config, deps = {}) {
|
|
|
385
410
|
deps.writeToken?.(st.alias, exchanged.tokens);
|
|
386
411
|
log(`callback ok flow=alias_reauth alias=${st.alias}`);
|
|
387
412
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
388
|
-
res.end(`<!doctype html><meta charset=utf-8><p>Re-authenticated "${st.alias}". You can close this window.</p>`);
|
|
413
|
+
res.end(`<!doctype html><meta charset=utf-8><p>Re-authenticated "${escapeHtml(st.alias)}". You can close this window.</p>`);
|
|
389
414
|
return true;
|
|
390
415
|
}
|
|
391
416
|
// owner_gate: resolve the verified email to its subject (default = the
|
|
@@ -503,8 +528,8 @@ export function buildAuthServer(config, deps = {}) {
|
|
|
503
528
|
return { ok: false, status: 401, headers: { 'WWW-Authenticate': wwwAuth }, body: JSON.stringify({ error: 'E_MCP_TOKEN_INVALID', message: 'invalid or expired token' }) };
|
|
504
529
|
}
|
|
505
530
|
};
|
|
506
|
-
const mintFlowState = async ({ flow, alias, tenantId, bundles }) => {
|
|
507
|
-
const state = await signState({ flow, client_id: '', redirect_uri: '', code_challenge: '', resource, alias, tenantId, bundles }, base, secret, nowSec());
|
|
531
|
+
const mintFlowState = async ({ flow, alias, tenantId, bundles, nonce }) => {
|
|
532
|
+
const state = await signState({ flow, client_id: '', redirect_uri: '', code_challenge: '', resource, alias, tenantId, bundles, nonce }, base, secret, nowSec());
|
|
508
533
|
return `${base}/authorize?flow=${flow}&state=${encodeURIComponent(state)}`;
|
|
509
534
|
};
|
|
510
535
|
return { routes, authenticate, mintFlowState };
|
package/dist/scope-catalog.d.ts
CHANGED
|
@@ -14,6 +14,9 @@ export declare const BUNDLE_CATALOG: Record<string, BundleCatalogEntry>;
|
|
|
14
14
|
/** One-way soft-rename map (UD5): empty at 6.0.0; the mechanism ships now so a
|
|
15
15
|
* future rename is a deprecation warning, never a breaking config change. */
|
|
16
16
|
export declare const BUNDLE_ALIASES: Record<string, string>;
|
|
17
|
+
/** Bundle names are user input: an Object.prototype member ("constructor",
|
|
18
|
+
* "toString") passes a plain `in` or index lookup, so membership is own-key only. */
|
|
19
|
+
export declare function isKnownBundle(name: string): boolean;
|
|
17
20
|
export declare function resolveBundleAliases(bundles: string[]): string[];
|
|
18
21
|
/** Closest catalog key for E_UNKNOWN_BUNDLE remediation (edit distance <= 2). */
|
|
19
22
|
export declare function closestBundle(name: string): string | undefined;
|
package/dist/scope-catalog.js
CHANGED
|
@@ -160,9 +160,14 @@ export const BUNDLE_CATALOG = {
|
|
|
160
160
|
* future rename is a deprecation warning, never a breaking config change. */
|
|
161
161
|
export const BUNDLE_ALIASES = {};
|
|
162
162
|
const warnedAliases = new Set();
|
|
163
|
+
/** Bundle names are user input: an Object.prototype member ("constructor",
|
|
164
|
+
* "toString") passes a plain `in` or index lookup, so membership is own-key only. */
|
|
165
|
+
export function isKnownBundle(name) {
|
|
166
|
+
return Object.hasOwn(BUNDLE_CATALOG, name);
|
|
167
|
+
}
|
|
163
168
|
export function resolveBundleAliases(bundles) {
|
|
164
169
|
return bundles.map((name) => {
|
|
165
|
-
const target = BUNDLE_ALIASES[name];
|
|
170
|
+
const target = Object.hasOwn(BUNDLE_ALIASES, name) ? BUNDLE_ALIASES[name] : undefined;
|
|
166
171
|
if (!target)
|
|
167
172
|
return name;
|
|
168
173
|
if (!warnedAliases.has(name)) {
|
|
@@ -57,7 +57,7 @@ export declare function scopeGrantDiff(requested: string[], grantedScope: string
|
|
|
57
57
|
* present, runConsent NEVER binds a server-side loopback listener — the
|
|
58
58
|
* user's browser cannot reach the daemon's loopback — and instead hands out
|
|
59
59
|
* a clientless AS consent URL. The injector decides the flow (single-owner =
|
|
60
|
-
* the legacy alias_reauth link
|
|
60
|
+
* the legacy alias_reauth link). */
|
|
61
61
|
export interface WizardHttpConsent {
|
|
62
62
|
mintConsentUrl: (alias: string) => Promise<string> | string;
|
|
63
63
|
}
|
|
@@ -3,7 +3,7 @@ import { getAccountSet, invalidateAccountSet } from '../accounts.js';
|
|
|
3
3
|
import { ALIAS_RE, mutateConfigFile } from '../config-file.js';
|
|
4
4
|
import { writeToken } from '../token-store.js';
|
|
5
5
|
import { resolveScopesForAccount } from '../auth.js';
|
|
6
|
-
import { BUNDLE_CATALOG, closestBundle, resolveBundleAliases } from '../scope-catalog.js';
|
|
6
|
+
import { BUNDLE_CATALOG, closestBundle, isKnownBundle, resolveBundleAliases } from '../scope-catalog.js';
|
|
7
7
|
import { openUrl } from '../open-url.js';
|
|
8
8
|
import { coerceBoolean } from './_coerce.js';
|
|
9
9
|
import { safeMessage, stringifyEnvelope } from './_errors.js';
|
|
@@ -58,7 +58,7 @@ export function validateAddForm(input, existingAliases) {
|
|
|
58
58
|
if (b === 'admin') {
|
|
59
59
|
return { ok: false, slug: 'E_UNKNOWN_BUNDLE', message: '"admin" is not a scope bundle.', hint: 'Use the admin checkbox instead, or pass "admin": true.', alias };
|
|
60
60
|
}
|
|
61
|
-
if (!(b
|
|
61
|
+
if (!isKnownBundle(b)) {
|
|
62
62
|
const closest = closestBundle(b);
|
|
63
63
|
const known = Object.keys(BUNDLE_CATALOG).filter((n) => n !== 'admin').join(', ');
|
|
64
64
|
return {
|
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.56",
|
|
4
4
|
"mcpName": "io.github.bakissation/mcp-google-multi",
|
|
5
5
|
"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.",
|
|
6
6
|
"type": "module",
|
|
@@ -28,6 +28,8 @@
|
|
|
28
28
|
"./tenant-purge": "./dist/tenant-purge.js",
|
|
29
29
|
"./doctor": "./dist/doctor.js",
|
|
30
30
|
"./write-control": "./dist/write-control.js",
|
|
31
|
+
"./scope-catalog": "./dist/scope-catalog.js",
|
|
32
|
+
"./auth": "./dist/auth.js",
|
|
31
33
|
"./package.json": "./package.json"
|
|
32
34
|
},
|
|
33
35
|
"files": [
|