mcp-google-multi 6.0.0-alpha.37 → 6.0.0-alpha.39
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.d.ts +4 -1
- package/dist/accounts.js +10 -9
- package/dist/config-file.d.ts +11 -0
- package/dist/config-file.js +32 -0
- package/dist/doctor.js +2 -1
- package/dist/fanout.d.ts +1 -1
- package/dist/fanout.js +9 -6
- package/dist/http-transport.d.ts +1 -0
- package/dist/http-transport.js +9 -0
- package/dist/identity.d.ts +3 -1
- package/dist/oauth-as.js +5 -5
- package/dist/registry.d.ts +3 -1
- package/dist/registry.js +12 -6
- package/dist/scope-observability.js +2 -1
- package/dist/token-store.d.ts +3 -2
- package/dist/token-store.js +0 -0
- package/dist/tools/accounts-tool.js +3 -2
- package/package.json +1 -1
package/dist/accounts.d.ts
CHANGED
|
@@ -18,7 +18,10 @@ export interface AccountSet {
|
|
|
18
18
|
defaultAccount?: string;
|
|
19
19
|
defaultAccountSource?: 'env' | 'config';
|
|
20
20
|
}
|
|
21
|
-
export declare function resolveAccounts(env?: NodeJS.ProcessEnv, filePath?: string, onInvalid?: 'exit' | 'throw'
|
|
21
|
+
export declare function resolveAccounts(env?: NodeJS.ProcessEnv, filePath?: string, onInvalid?: 'exit' | 'throw', opts?: {
|
|
22
|
+
tokenDir?: string;
|
|
23
|
+
}): AccountSet;
|
|
24
|
+
export declare function fileStamp(filePath: string, version: number): string;
|
|
22
25
|
/** Live accessor: dispatch-time readers use this, never a captured snapshot. */
|
|
23
26
|
export declare function getAccountSet(): AccountSet;
|
|
24
27
|
/** Re-resolve after a config.json mutation and swap the live set. */
|
package/dist/accounts.js
CHANGED
|
@@ -15,14 +15,14 @@ export function getTokenDir() {
|
|
|
15
15
|
function parseCsv(value) {
|
|
16
16
|
return (value ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
17
17
|
}
|
|
18
|
-
function accountPaths(alias) {
|
|
18
|
+
function accountPaths(alias, dir = tokenDir) {
|
|
19
19
|
return {
|
|
20
|
-
tokenPath: path.join(
|
|
21
|
-
encPath: path.join(
|
|
20
|
+
tokenPath: path.join(dir, alias, 'token.json'),
|
|
21
|
+
encPath: path.join(dir, `${alias}.enc`),
|
|
22
22
|
};
|
|
23
23
|
}
|
|
24
24
|
/** v5 env parser, guards verbatim. Format: GOOGLE_ACCOUNTS="alias1:email1,alias2:email2". */
|
|
25
|
-
function parseEnvAccounts(raw, adminAliases) {
|
|
25
|
+
function parseEnvAccounts(raw, adminAliases, dir = tokenDir) {
|
|
26
26
|
const configs = {};
|
|
27
27
|
const aliases = [];
|
|
28
28
|
for (const entry of raw.split(',')) {
|
|
@@ -49,7 +49,7 @@ function parseEnvAccounts(raw, adminAliases) {
|
|
|
49
49
|
aliases.push(alias);
|
|
50
50
|
configs[alias] = {
|
|
51
51
|
email,
|
|
52
|
-
...accountPaths(alias),
|
|
52
|
+
...accountPaths(alias, dir),
|
|
53
53
|
admin: adminAliases.includes(alias) || undefined,
|
|
54
54
|
source: 'env',
|
|
55
55
|
};
|
|
@@ -74,7 +74,8 @@ function noAccountsMessage() {
|
|
|
74
74
|
* empty behaves as unset, mirroring GOOGLE_ACCOUNTS semantics.
|
|
75
75
|
*/
|
|
76
76
|
let warnedLegacy = false;
|
|
77
|
-
export function resolveAccounts(env = process.env, filePath = configFilePath(), onInvalid = 'exit') {
|
|
77
|
+
export function resolveAccounts(env = process.env, filePath = configFilePath(), onInvalid = 'exit', opts = {}) {
|
|
78
|
+
const dir = opts.tokenDir ?? tokenDir;
|
|
78
79
|
const adminEnv = parseCsv(env.GOOGLE_ADMIN_ACCOUNTS);
|
|
79
80
|
const rawEnv = env.GOOGLE_ACCOUNTS;
|
|
80
81
|
const fail = (slug, message) => {
|
|
@@ -102,7 +103,7 @@ export function resolveAccounts(env = process.env, filePath = configFilePath(),
|
|
|
102
103
|
}
|
|
103
104
|
}
|
|
104
105
|
if (rawEnv && rawEnv.trim() !== '') {
|
|
105
|
-
const { aliases, configs } = parseEnvAccounts(rawEnv, adminEnv);
|
|
106
|
+
const { aliases, configs } = parseEnvAccounts(rawEnv, adminEnv, dir);
|
|
106
107
|
materializeFirstRun(aliases, configs, filePath);
|
|
107
108
|
const def = resolveDefaultAccount(env, null, aliases, fail);
|
|
108
109
|
return {
|
|
@@ -167,7 +168,7 @@ export function resolveAccounts(env = process.env, filePath = configFilePath(),
|
|
|
167
168
|
aliases.push(alias);
|
|
168
169
|
configs[alias] = {
|
|
169
170
|
email: entry.email,
|
|
170
|
-
...accountPaths(alias),
|
|
171
|
+
...accountPaths(alias, dir),
|
|
171
172
|
scopeProfile: entry.scopeProfile,
|
|
172
173
|
admin: adminEnv.length > 0 ? adminEnv.includes(alias) : entry.admin,
|
|
173
174
|
source: 'config',
|
|
@@ -196,7 +197,7 @@ function resolveDefaultAccount(env, fromConfig, aliases, fail) {
|
|
|
196
197
|
}
|
|
197
198
|
return { defaultAccount: value, defaultAccountSource: fromEnv ? 'env' : 'config' };
|
|
198
199
|
}
|
|
199
|
-
function fileStamp(filePath, version) {
|
|
200
|
+
export function fileStamp(filePath, version) {
|
|
200
201
|
try {
|
|
201
202
|
return `${version}:${fs.statSync(filePath).mtimeMs}`;
|
|
202
203
|
}
|
package/dist/config-file.d.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
export declare const CONFIG_VERSION = 1;
|
|
2
2
|
export declare function configDir(env?: NodeJS.ProcessEnv): string;
|
|
3
3
|
export declare function configFilePath(env?: NodeJS.ProcessEnv): string;
|
|
4
|
+
export declare function tenantsDir(env?: NodeJS.ProcessEnv): string;
|
|
5
|
+
export declare function tenantConfigFilePath(tenantId: string, env?: NodeJS.ProcessEnv): string;
|
|
6
|
+
export declare function tenantTokenDir(tenantId: string, env?: NodeJS.ProcessEnv): string;
|
|
7
|
+
/** Create and permission-harden the tenant directory chain. mkdirSync's
|
|
8
|
+
* recursive mode only stamps the DEEPEST newly-created dir, so every level
|
|
9
|
+
* gets an explicit chmod: the shared tenants/ root (created by whichever
|
|
10
|
+
* tenant provisions first) must never stay group-readable. */
|
|
11
|
+
export declare function ensureTenantDirs(tenantId: string, env?: NodeJS.ProcessEnv): {
|
|
12
|
+
configFile: string;
|
|
13
|
+
tokenDir: string;
|
|
14
|
+
};
|
|
4
15
|
export declare const ALIAS_RE: RegExp;
|
|
5
16
|
export declare const RESERVED_ALIASES: string[];
|
|
6
17
|
export declare function isReservedAlias(name: string): boolean;
|
package/dist/config-file.js
CHANGED
|
@@ -10,6 +10,38 @@ export function configDir(env = process.env) {
|
|
|
10
10
|
export function configFilePath(env = process.env) {
|
|
11
11
|
return path.join(configDir(env), 'config.json');
|
|
12
12
|
}
|
|
13
|
+
export function tenantsDir(env = process.env) {
|
|
14
|
+
return path.join(configDir(env), 'tenants');
|
|
15
|
+
}
|
|
16
|
+
// A tenant id becomes a filesystem path segment exactly like an account
|
|
17
|
+
// alias, so it rides the same traversal guard.
|
|
18
|
+
function assertTenantId(tenantId) {
|
|
19
|
+
if (!ALIAS_RE.test(tenantId) || isReservedAlias(tenantId)) {
|
|
20
|
+
throw new Error(`E_TENANT_ID_INVALID: invalid tenant id "${tenantId}".`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function tenantConfigFilePath(tenantId, env = process.env) {
|
|
24
|
+
assertTenantId(tenantId);
|
|
25
|
+
return path.join(tenantsDir(env), tenantId, 'config.json');
|
|
26
|
+
}
|
|
27
|
+
export function tenantTokenDir(tenantId, env = process.env) {
|
|
28
|
+
assertTenantId(tenantId);
|
|
29
|
+
return path.join(tenantsDir(env), tenantId, 'tokens');
|
|
30
|
+
}
|
|
31
|
+
/** Create and permission-harden the tenant directory chain. mkdirSync's
|
|
32
|
+
* recursive mode only stamps the DEEPEST newly-created dir, so every level
|
|
33
|
+
* gets an explicit chmod: the shared tenants/ root (created by whichever
|
|
34
|
+
* tenant provisions first) must never stay group-readable. */
|
|
35
|
+
export function ensureTenantDirs(tenantId, env = process.env) {
|
|
36
|
+
const tokenDir = tenantTokenDir(tenantId, env);
|
|
37
|
+
const levels = [configDir(env), tenantsDir(env), path.join(tenantsDir(env), tenantId), tokenDir];
|
|
38
|
+
for (const dir of levels) {
|
|
39
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
40
|
+
if (process.platform !== 'win32')
|
|
41
|
+
fs.chmodSync(dir, 0o700);
|
|
42
|
+
}
|
|
43
|
+
return { configFile: tenantConfigFilePath(tenantId, env), tokenDir };
|
|
44
|
+
}
|
|
13
45
|
// Alias charset is load-bearing security, not cosmetics: the alias becomes a
|
|
14
46
|
// tokenDir path segment (encPath), so this guard blocks path traversal.
|
|
15
47
|
export const ALIAS_RE = /^[a-zA-Z0-9_-]+$/;
|
package/dist/doctor.js
CHANGED
|
@@ -7,6 +7,7 @@ import { peekMasterKeyProvenance, deleteMasterKeyMaterial } from './master-key.j
|
|
|
7
7
|
import { hasToken } from './token-store.js';
|
|
8
8
|
import { configDir, loadConfigFile } from './config-file.js';
|
|
9
9
|
import { envValueSource } from './env-load.js';
|
|
10
|
+
import { reauthHint } from './reauth-hint.js';
|
|
10
11
|
import { describeMetricsDir, resolveUsageMetrics, sourceLabel } from './usage-metrics.js';
|
|
11
12
|
import { probeApiEnablement } from './api-probe.js';
|
|
12
13
|
import { safeMessage, stringifyEnvelope } from './tools/_errors.js';
|
|
@@ -181,7 +182,7 @@ function sectionsTokensAndScopes(deps, aliases) {
|
|
|
181
182
|
if (scopeVerdict === 'ok')
|
|
182
183
|
scopeVerdict = 'warn';
|
|
183
184
|
scopeLines.push(`${alias}: ${r.callable.length} callable, ${r.requestable.length} requested-not-granted`);
|
|
184
|
-
scopeHint = `Re-auth to grant missing scopes
|
|
185
|
+
scopeHint = `Re-auth to grant missing scopes. ${reauthHint(alias)}`;
|
|
185
186
|
}
|
|
186
187
|
else {
|
|
187
188
|
scopeLines.push(`${alias}: ${r.callable.length} callable, all profile scopes granted`);
|
package/dist/fanout.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export declare const CSV_RE: RegExp;
|
|
3
|
-
export declare function fanoutAccountField(description: string): z.ZodType;
|
|
3
|
+
export declare function fanoutAccountField(description: string, aliases: readonly string[]): z.ZodType;
|
|
4
4
|
export type AccountSelector = {
|
|
5
5
|
ok: true;
|
|
6
6
|
fanout: boolean;
|
package/dist/fanout.js
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
2
|
+
import { getAccountSet, unknownAliasMessage } from './accounts.js';
|
|
3
3
|
export const CSV_RE = /^[a-zA-Z0-9_-]+(\s*,\s*[a-zA-Z0-9_-]+)+$/;
|
|
4
4
|
const FANOUT_CONCURRENCY = 5;
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
// `aliases` is required (no global default): the field is baked into a
|
|
6
|
+
// registry's inputSchema, so it must be built from THAT registry's account
|
|
7
|
+
// set or a '*' would advertise and accept another registry's aliases.
|
|
8
|
+
export function fanoutAccountField(description, aliases) {
|
|
9
|
+
const csvExample = aliases.length > 1 ? `; or a CSV subset like "${aliases.slice(0, 2).join(',')}"` : '';
|
|
7
10
|
// The union's OWN error only fires when every branch aborts at the type
|
|
8
11
|
// check (a non-string). For a string, zod surfaces the single non-aborted
|
|
9
12
|
// branch verbatim, which is the CSV regex, so a mistyped alias used to read
|
|
10
13
|
// as "Invalid string: must match pattern /^[a-zA-Z0-9_-]+(,...)/" and never
|
|
11
14
|
// named an alias. Hence the same message on BOTH.
|
|
12
|
-
const bad = () => unknownAliasMessage(
|
|
15
|
+
const bad = () => unknownAliasMessage(aliases, true);
|
|
13
16
|
return z
|
|
14
|
-
// '*' first so the tuple is statically non-empty even when
|
|
17
|
+
// '*' first so the tuple is statically non-empty even when aliases is empty
|
|
15
18
|
// (a fresh install): z.enum requires [string, ...string[]].
|
|
16
|
-
.union([z.enum(['*', ...
|
|
19
|
+
.union([z.enum(['*', ...aliases]), z.string().regex(CSV_RE, { error: bad })], { error: bad })
|
|
17
20
|
.optional()
|
|
18
21
|
.describe(`${description}; "*" = all accounts${csvExample}; omit for the default account`);
|
|
19
22
|
}
|
package/dist/http-transport.d.ts
CHANGED
package/dist/http-transport.js
CHANGED
|
@@ -149,6 +149,15 @@ export class HttpTransportHost {
|
|
|
149
149
|
this.log(`${auth.status} auth_failed path=/mcp`);
|
|
150
150
|
return;
|
|
151
151
|
}
|
|
152
|
+
// Thread the verified subject to tool handlers: the Node transport forwards
|
|
153
|
+
// req.auth verbatim as ctx.http.authInfo. Token/clientId stay empty — the
|
|
154
|
+
// bearer value must not re-enter the dispatch path via handler context.
|
|
155
|
+
req.auth = {
|
|
156
|
+
token: '',
|
|
157
|
+
clientId: '',
|
|
158
|
+
scopes: ['mcp:use'],
|
|
159
|
+
extra: { sub: auth.sub ?? 'owner' },
|
|
160
|
+
};
|
|
152
161
|
let body;
|
|
153
162
|
try {
|
|
154
163
|
body = await this.readJson(req);
|
package/dist/identity.d.ts
CHANGED
|
@@ -6,9 +6,11 @@ import { type Policy, type Transport } from './write-control.js';
|
|
|
6
6
|
* The one forward-compat seam (frozen public API): the free core builds
|
|
7
7
|
* exactly one context with subject "owner"; EE later instantiates N contexts
|
|
8
8
|
* from N registries. Nothing here may assume a global current-account.
|
|
9
|
+
* `subject` is a string (not the literal 'owner') so a context can carry a
|
|
10
|
+
* tenant id; the free core still always builds 'owner'.
|
|
9
11
|
*/
|
|
10
12
|
export interface IdentityContext {
|
|
11
|
-
subject:
|
|
13
|
+
subject: string;
|
|
12
14
|
accounts: AccountSet;
|
|
13
15
|
policy: Policy;
|
|
14
16
|
getClient: typeof getClient;
|
package/dist/oauth-as.js
CHANGED
|
@@ -385,8 +385,8 @@ export function buildAuthServer(config, deps = {}) {
|
|
|
385
385
|
if (!form.code_verifier || pkceS256(form.code_verifier) !== code.code_challenge) {
|
|
386
386
|
return json(res, 400, { error: 'invalid_grant', message: 'PKCE verification failed' });
|
|
387
387
|
}
|
|
388
|
-
const access = await signAccessToken({ base, secret, iat: nowSec(), ttlSec: accessTtl, sub:
|
|
389
|
-
const refreshToken = refresh.issue(now(),
|
|
388
|
+
const access = await signAccessToken({ base, secret, iat: nowSec(), ttlSec: accessTtl, sub: code.sub });
|
|
389
|
+
const refreshToken = refresh.issue(now(), code.sub);
|
|
390
390
|
log('token issued grant=authorization_code');
|
|
391
391
|
return json(res, 200, { access_token: access, token_type: 'Bearer', expires_in: accessTtl, refresh_token: refreshToken, scope: 'mcp:use' });
|
|
392
392
|
}
|
|
@@ -394,7 +394,7 @@ export function buildAuthServer(config, deps = {}) {
|
|
|
394
394
|
const next = refresh.rotate(form.refresh_token ?? '', now());
|
|
395
395
|
if (!next)
|
|
396
396
|
return json(res, 400, { error: 'invalid_grant', message: 'unknown or rotated refresh token' });
|
|
397
|
-
const access = await signAccessToken({ base, secret, iat: nowSec(), ttlSec: accessTtl, sub:
|
|
397
|
+
const access = await signAccessToken({ base, secret, iat: nowSec(), ttlSec: accessTtl, sub: next.sub });
|
|
398
398
|
log('token issued grant=refresh_token');
|
|
399
399
|
return json(res, 200, { access_token: access, token_type: 'Bearer', expires_in: accessTtl, refresh_token: next.token, scope: 'mcp:use' });
|
|
400
400
|
}
|
|
@@ -446,8 +446,8 @@ export function buildAuthServer(config, deps = {}) {
|
|
|
446
446
|
return { ok: false, status: 401, headers: { 'WWW-Authenticate': wwwAuth }, body: JSON.stringify({ error: 'E_MCP_TOKEN_INVALID', message: 'missing bearer token' }) };
|
|
447
447
|
}
|
|
448
448
|
try {
|
|
449
|
-
await verifyAccessToken(header.slice(7), base, secret);
|
|
450
|
-
return { ok: true };
|
|
449
|
+
const claims = await verifyAccessToken(header.slice(7), base, secret);
|
|
450
|
+
return { ok: true, sub: claims.sub };
|
|
451
451
|
}
|
|
452
452
|
catch {
|
|
453
453
|
return { ok: false, status: 401, headers: { 'WWW-Authenticate': wwwAuth }, body: JSON.stringify({ error: 'E_MCP_TOKEN_INVALID', message: 'invalid or expired token' }) };
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/server";
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { type Policy } from './write-control.js';
|
|
4
|
+
import { type AccountSet } from './accounts.js';
|
|
4
5
|
import type { ArgShape } from './arg-normalize.js';
|
|
5
6
|
import type { Metrics } from './usage-metrics.js';
|
|
6
7
|
export type Cud = 'read' | 'create' | 'update' | 'delete';
|
|
@@ -41,6 +42,7 @@ export declare function inferCud(name: string): Cud;
|
|
|
41
42
|
export declare class ToolRegistry {
|
|
42
43
|
private readonly server;
|
|
43
44
|
private readonly metrics;
|
|
45
|
+
private readonly accounts;
|
|
44
46
|
readonly tools: ToolEntry[];
|
|
45
47
|
readonly policy: Policy;
|
|
46
48
|
readonly registerTool: McpServer['registerTool'];
|
|
@@ -57,7 +59,7 @@ export declare class ToolRegistry {
|
|
|
57
59
|
/** Agent-toggled runtime overlay (discover_all / discover_reset): lifts a
|
|
58
60
|
* lazy surface to curated without touching the configured mode. */
|
|
59
61
|
private expanded;
|
|
60
|
-
constructor(server: McpServer, policy: Policy, mode?: DiscoveryMode, metrics?: Metrics | null);
|
|
62
|
+
constructor(server: McpServer, policy: Policy, mode?: DiscoveryMode, metrics?: Metrics | null, accounts?: () => AccountSet);
|
|
61
63
|
registerMeta: McpServer['registerTool'];
|
|
62
64
|
services(): string[];
|
|
63
65
|
/** Declared input-schema keys + scalar kinds for one tool (tools/call arg
|
package/dist/registry.js
CHANGED
|
@@ -91,6 +91,7 @@ export function inferCud(name) {
|
|
|
91
91
|
export class ToolRegistry {
|
|
92
92
|
server;
|
|
93
93
|
metrics;
|
|
94
|
+
accounts;
|
|
94
95
|
tools = [];
|
|
95
96
|
policy;
|
|
96
97
|
registerTool;
|
|
@@ -107,9 +108,14 @@ export class ToolRegistry {
|
|
|
107
108
|
/** Agent-toggled runtime overlay (discover_all / discover_reset): lifts a
|
|
108
109
|
* lazy surface to curated without touching the configured mode. */
|
|
109
110
|
expanded = false;
|
|
110
|
-
constructor(server, policy, mode = resolveDiscoveryMode(), metrics = null
|
|
111
|
+
constructor(server, policy, mode = resolveDiscoveryMode(), metrics = null,
|
|
112
|
+
// The registry's OWN account view: fan-out expansion, selector validation
|
|
113
|
+
// and default-account injection all read through it so a registry built
|
|
114
|
+
// over a subset never observes (or leaks) the global set.
|
|
115
|
+
accounts = getAccountSet) {
|
|
111
116
|
this.server = server;
|
|
112
117
|
this.metrics = metrics;
|
|
118
|
+
this.accounts = accounts;
|
|
113
119
|
this.policy = policy;
|
|
114
120
|
this.mode = mode;
|
|
115
121
|
this.registerTool = ((name, config, handler) => {
|
|
@@ -142,12 +148,12 @@ export class ToolRegistry {
|
|
|
142
148
|
const hasAccountField = 'account' in inputShape && !DEFAULT_ACCOUNT_EXCLUDE.has(name);
|
|
143
149
|
if (cud === 'read' && !this.registeringMeta && !FANOUT_EXCLUDE.has(name) && isAccountEnum(inputShape.account)) {
|
|
144
150
|
const description = inputShape.account.description ?? 'Google account alias';
|
|
145
|
-
inputShape = { ...inputShape, account: fanoutAccountField(description) };
|
|
151
|
+
inputShape = { ...inputShape, account: fanoutAccountField(description, this.accounts().aliases) };
|
|
146
152
|
baseHandler = async (...args) => {
|
|
147
153
|
const first = args[0];
|
|
148
|
-
const parsed = parseAccountSelector(typeof first?.account === 'string' ? first.account : '');
|
|
154
|
+
const parsed = parseAccountSelector(typeof first?.account === 'string' ? first.account : '', this.accounts().aliases);
|
|
149
155
|
if (!parsed.ok)
|
|
150
|
-
return invalidAccountsResult(parsed.invalid);
|
|
156
|
+
return invalidAccountsResult(parsed.invalid, this.accounts().aliases);
|
|
151
157
|
if (!parsed.fanout)
|
|
152
158
|
return handler({ ...first, account: parsed.aliases[0] }, ...args.slice(1));
|
|
153
159
|
return runFanout(handler, args, parsed.aliases);
|
|
@@ -188,7 +194,7 @@ export class ToolRegistry {
|
|
|
188
194
|
// heals for a client that always omits account (this branch never
|
|
189
195
|
// reaches getClient's probe). One stat, only on omission.
|
|
190
196
|
refreshAccountSetIfStale();
|
|
191
|
-
const def =
|
|
197
|
+
const def = this.accounts().defaultAccount;
|
|
192
198
|
if (!def) {
|
|
193
199
|
return {
|
|
194
200
|
content: [
|
|
@@ -197,7 +203,7 @@ export class ToolRegistry {
|
|
|
197
203
|
text: JSON.stringify({
|
|
198
204
|
error: 'E_NO_DEFAULT_ACCOUNT',
|
|
199
205
|
message: 'No "account" given and no default account is configured.',
|
|
200
|
-
hint: `Pass account explicitly (valid: ${
|
|
206
|
+
hint: `Pass account explicitly (valid: ${this.accounts().aliases.join(', ')}), or set GOOGLE_DEFAULT_ACCOUNT / "defaultAccount" in config.json.`,
|
|
201
207
|
retriable: false,
|
|
202
208
|
}),
|
|
203
209
|
},
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ADMIN_SCOPES, BUNDLE_CATALOG } from './scope-catalog.js';
|
|
2
2
|
import { BASE_SCOPES, resolveScopesForAccount } from './auth.js';
|
|
3
3
|
import { readToken } from './token-store.js';
|
|
4
|
+
import { reauthHint } from './reauth-hint.js';
|
|
4
5
|
// Reverse index scope -> bundles offering it, narrowest bundle first (fewest
|
|
5
6
|
// scopes) so the remediation names the smallest grant. BASE_SCOPES map to the
|
|
6
7
|
// pseudo-bundle "base" (outside a profile only via includesBase:false).
|
|
@@ -74,7 +75,7 @@ export function scopeHint(scope, c, alias) {
|
|
|
74
75
|
case 'requestable_not_granted':
|
|
75
76
|
return {
|
|
76
77
|
hint: `Scope ${scope} is in ${alias}'s profile but not granted (you unchecked it at consent). ` +
|
|
77
|
-
|
|
78
|
+
reauthHint(alias),
|
|
78
79
|
retriable: true,
|
|
79
80
|
};
|
|
80
81
|
case 'not_requestable':
|
package/dist/token-store.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { TokenData } from './types.js';
|
|
2
2
|
export declare function deriveKey(masterKey: string): Buffer;
|
|
3
|
-
export declare function
|
|
4
|
-
export declare function
|
|
3
|
+
export declare function deriveTenantKey(masterKey: string, tenantId: string): Buffer;
|
|
4
|
+
export declare function encryptToken(data: object, masterKey: string, tenantId?: string): string;
|
|
5
|
+
export declare function decryptToken(fileContents: string, masterKey: string, tenantId?: string): TokenData;
|
|
5
6
|
export declare function readToken(alias: string): TokenData | null;
|
|
6
7
|
export declare function writeToken(alias: string, data: object): void;
|
|
7
8
|
export declare function updateToken(alias: string, updates: object): void;
|
package/dist/token-store.js
CHANGED
|
Binary file
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { reauthHint } from '../reauth-hint.js';
|
|
1
2
|
import * as fs from 'node:fs';
|
|
2
3
|
import { getAccountSet, refreshAccountSetIfStale } from '../accounts.js';
|
|
3
4
|
import { buildScopesReport } from '../scope-observability.js';
|
|
@@ -35,7 +36,7 @@ export function deriveAccountHealth(alias, deps = DEFAULT_DEPS) {
|
|
|
35
36
|
status: 'missing',
|
|
36
37
|
hint: legacy
|
|
37
38
|
? 'Plaintext token.json found — run: npx mcp-google-multi migrate-tokens'
|
|
38
|
-
:
|
|
39
|
+
: reauthHint(alias),
|
|
39
40
|
},
|
|
40
41
|
scopes: noScopes,
|
|
41
42
|
};
|
|
@@ -69,7 +70,7 @@ export function deriveAccountHealth(alias, deps = DEFAULT_DEPS) {
|
|
|
69
70
|
status,
|
|
70
71
|
expiryDate: expiry !== undefined ? new Date(expiry).toISOString() : undefined,
|
|
71
72
|
hint: status === 'needs_reauth'
|
|
72
|
-
?
|
|
73
|
+
? reauthHint(alias)
|
|
73
74
|
: missing.length > 0
|
|
74
75
|
? 'Re-auth to grant the missing scopes'
|
|
75
76
|
: undefined,
|
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.39",
|
|
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",
|