mcp-google-multi 6.0.0-alpha.41 → 6.0.0-alpha.43
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/auth.d.ts +3 -2
- package/dist/auth.js +6 -8
- package/dist/client.d.ts +23 -2
- package/dist/client.js +0 -0
- package/dist/identity.js +14 -5
- package/dist/index.js +2 -2
- package/dist/registry-transfer.d.ts +5 -0
- package/dist/registry-transfer.js +25 -1
- package/dist/services.d.ts +6 -2
- package/dist/services.js +6 -6
- package/dist/token-store.d.ts +17 -0
- package/dist/token-store.js +0 -0
- package/package.json +1 -1
package/dist/auth.d.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
+
import type { AccountSet } from './accounts.js';
|
|
1
2
|
import { ADMIN_SCOPES } from './scope-catalog.js';
|
|
2
3
|
export declare const BASE_SCOPES: string[];
|
|
3
4
|
export declare const OPTIONAL_SCOPE_BUNDLES: Record<string, string[]>;
|
|
4
5
|
export { ADMIN_SCOPES };
|
|
5
6
|
/** Union of every account's resolved bundles: a service registers if ANY
|
|
6
7
|
* account can authorize it; per-account authz happens at call time (BR2). */
|
|
7
|
-
export declare function getOptionalBundles(): string[];
|
|
8
|
+
export declare function getOptionalBundles(set?: AccountSet): string[];
|
|
8
9
|
/** Aliases granted ADMIN_SCOPES: per-account admin flag (env
|
|
9
10
|
* GOOGLE_ADMIN_ACCOUNTS overrides config.json at resolve) OR the account's
|
|
10
11
|
* scope profile carrying admin (boolean or "admin" bundle) — equivalent forms. */
|
|
11
|
-
export declare function getAdminAccounts(): string[];
|
|
12
|
+
export declare function getAdminAccounts(set?: AccountSet): string[];
|
|
12
13
|
/** Scopes are fixed at consent time: changing an account's profile (or the
|
|
13
14
|
* legacy env) changes its consent set and requires re-running auth. Evaluated
|
|
14
15
|
* per account: `work` can carry admin + gmail_settings while `personal` is
|
package/dist/auth.js
CHANGED
|
@@ -55,11 +55,10 @@ function legacyGlobalProfile() {
|
|
|
55
55
|
}
|
|
56
56
|
return { bundles };
|
|
57
57
|
}
|
|
58
|
-
function profileForAccount(alias) {
|
|
58
|
+
function profileForAccount(alias, set = getAccountSet()) {
|
|
59
59
|
const legacy = legacyGlobalProfile();
|
|
60
60
|
if (legacy)
|
|
61
61
|
return legacy;
|
|
62
|
-
const set = getAccountSet();
|
|
63
62
|
const name = set.configs[alias]?.scopeProfile ?? 'base';
|
|
64
63
|
// hasOwn: a profile named like an Object.prototype member must never
|
|
65
64
|
// resolve to the inherited function.
|
|
@@ -67,14 +66,13 @@ function profileForAccount(alias) {
|
|
|
67
66
|
}
|
|
68
67
|
/** Union of every account's resolved bundles: a service registers if ANY
|
|
69
68
|
* account can authorize it; per-account authz happens at call time (BR2). */
|
|
70
|
-
export function getOptionalBundles() {
|
|
69
|
+
export function getOptionalBundles(set = getAccountSet()) {
|
|
71
70
|
const legacy = legacyGlobalProfile();
|
|
72
71
|
if (legacy)
|
|
73
72
|
return legacy.bundles.filter(b => b !== 'admin');
|
|
74
73
|
const union = new Set();
|
|
75
|
-
const set = getAccountSet();
|
|
76
74
|
for (const alias of set.aliases) {
|
|
77
|
-
for (const b of profileForAccount(alias).bundles) {
|
|
75
|
+
for (const b of profileForAccount(alias, set).bundles) {
|
|
78
76
|
if (b !== 'admin')
|
|
79
77
|
union.add(b);
|
|
80
78
|
}
|
|
@@ -84,12 +82,12 @@ export function getOptionalBundles() {
|
|
|
84
82
|
/** Aliases granted ADMIN_SCOPES: per-account admin flag (env
|
|
85
83
|
* GOOGLE_ADMIN_ACCOUNTS overrides config.json at resolve) OR the account's
|
|
86
84
|
* scope profile carrying admin (boolean or "admin" bundle) — equivalent forms. */
|
|
87
|
-
export function getAdminAccounts() {
|
|
88
|
-
const { aliases, configs } =
|
|
85
|
+
export function getAdminAccounts(set = getAccountSet()) {
|
|
86
|
+
const { aliases, configs } = set;
|
|
89
87
|
return aliases.filter((a) => {
|
|
90
88
|
if (configs[a].admin === true)
|
|
91
89
|
return true;
|
|
92
|
-
const p = profileForAccount(a);
|
|
90
|
+
const p = profileForAccount(a, set);
|
|
93
91
|
return p.admin === true || p.bundles.includes('admin');
|
|
94
92
|
});
|
|
95
93
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
1
|
import { OAuth2Client } from 'googleapis-common';
|
|
2
|
-
import type { Account } from './accounts.js';
|
|
3
|
-
|
|
2
|
+
import type { Account, AccountSet } from './accounts.js';
|
|
3
|
+
import type { TokenData } from './types.js';
|
|
4
|
+
export interface GetClientDeps {
|
|
5
|
+
/** Live account view — read at call time, never a captured snapshot. */
|
|
6
|
+
accounts: () => AccountSet;
|
|
7
|
+
readToken: (alias: string) => TokenData | null;
|
|
8
|
+
updateToken: (alias: string, updates: object) => void;
|
|
9
|
+
/** Cross-process staleness probe (single-owner global registry only). */
|
|
10
|
+
refreshIfStale?: () => void;
|
|
11
|
+
}
|
|
12
|
+
/** Test-only visibility into the dedupe map. */
|
|
13
|
+
export declare function __inflightSizeForTest(): number;
|
|
14
|
+
/**
|
|
15
|
+
* Per-context client factory: `contextKey` namespaces the single-flight map
|
|
16
|
+
* (one lane per tenant), `deps` carry that context's account view and token
|
|
17
|
+
* custody. The free core builds exactly one, bound to the global registry.
|
|
18
|
+
*/
|
|
19
|
+
export declare function makeGetClient(contextKey: string, deps: GetClientDeps): (account: Account) => Promise<OAuth2Client>;
|
|
20
|
+
/** The single-owner client, bound to the global registry + token store. The
|
|
21
|
+
* operator surfaces that never carry a tenant (api-probe, executor and
|
|
22
|
+
* escape-hatch `deps.getClientFn ?? getClient` defaults, stdio dispatch)
|
|
23
|
+
* resolve through this. */
|
|
24
|
+
export declare const getClient: (account: Account) => Promise<OAuth2Client>;
|
package/dist/client.js
CHANGED
|
Binary file
|
package/dist/identity.js
CHANGED
|
@@ -1,8 +1,12 @@
|
|
|
1
|
-
import { getAccountSet } from './accounts.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
1
|
+
import { getAccountSet, getTokenDir, refreshAccountSetIfStale } from './accounts.js';
|
|
2
|
+
import { makeGetClient } from './client.js';
|
|
3
|
+
import { createTokenStore } from './token-store.js';
|
|
4
4
|
import { resolvePolicy } from './write-control.js';
|
|
5
5
|
export function buildIdentityContext(env = process.env, opts = {}) {
|
|
6
|
+
// Real closures over the single-owner state (the SAME factories EE
|
|
7
|
+
// instantiates per tenant), bound here to the global registry + token dir —
|
|
8
|
+
// behavior-identical to the module-level functions for the free core.
|
|
9
|
+
const tokenStore = createTokenStore(getTokenDir());
|
|
6
10
|
return {
|
|
7
11
|
subject: 'owner',
|
|
8
12
|
// Live getter: the seam must always see the current registry, never a
|
|
@@ -13,7 +17,12 @@ export function buildIdentityContext(env = process.env, opts = {}) {
|
|
|
13
17
|
// The dispatch transport rides into the resolved policy as a reserved seam
|
|
14
18
|
// (cc-write-control B14); it does not change any write-control verdict.
|
|
15
19
|
policy: resolvePolicy(env, { transport: opts.transport }),
|
|
16
|
-
getClient,
|
|
17
|
-
|
|
20
|
+
getClient: makeGetClient('owner', {
|
|
21
|
+
accounts: getAccountSet,
|
|
22
|
+
readToken: tokenStore.readToken,
|
|
23
|
+
updateToken: tokenStore.updateToken,
|
|
24
|
+
refreshIfStale: refreshAccountSetIfStale,
|
|
25
|
+
}),
|
|
26
|
+
tokenStore,
|
|
18
27
|
};
|
|
19
28
|
}
|
package/dist/index.js
CHANGED
|
@@ -58,7 +58,7 @@ function buildRegistry(server, ctx, mode, metrics = null) {
|
|
|
58
58
|
for (const svc of SERVICES) {
|
|
59
59
|
if (!toolsetEnabled(toolsets, svc.name))
|
|
60
60
|
continue;
|
|
61
|
-
if (svc.enabled && !svc.enabled()) {
|
|
61
|
+
if (svc.enabled && !svc.enabled(ctx.accounts)) {
|
|
62
62
|
if (toolsets !== 'all') {
|
|
63
63
|
const hint = svc.name === 'admin' ? 'set admin on an account/profile (or GOOGLE_ADMIN_ACCOUNTS)' : `add "${svc.name}" to an account's scope profile (or legacy GOOGLE_OPTIONAL_SCOPES)`;
|
|
64
64
|
process.stderr.write(`GOOGLE_TOOLSETS: "${svc.name}" requested but not enabled — ${hint}\n`);
|
|
@@ -72,7 +72,7 @@ function buildRegistry(server, ctx, mode, metrics = null) {
|
|
|
72
72
|
continue;
|
|
73
73
|
const curated = SERVICES.find((s) => s.name === gen.name);
|
|
74
74
|
const gate = curated?.enabled ?? GENERATED_GATES[gen.name]?.enabled;
|
|
75
|
-
if (gate && !gate()) {
|
|
75
|
+
if (gate && !gate(ctx.accounts)) {
|
|
76
76
|
if (!curated && toolsets !== 'all') {
|
|
77
77
|
process.stderr.write(`GOOGLE_TOOLSETS: "${gen.name}" requested but not enabled — ${GENERATED_GATES[gen.name].hint}\n`);
|
|
78
78
|
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** D11 (refuse-only for v1): export/import operate on the FLAT single-owner
|
|
2
|
+
* store. A tenant-namespaced layout means the bundle would either miss every
|
|
3
|
+
* tenant's tokens or exfiltrate all of them in one file — refuse instead.
|
|
4
|
+
* Returns the offending dir, or null when the store is flat. */
|
|
5
|
+
export declare function refuseIfMultiTenant(dir?: string): string | null;
|
|
1
6
|
export interface TransferBundle {
|
|
2
7
|
manifest: {
|
|
3
8
|
v: 1;
|
|
@@ -9,8 +9,22 @@ import * as readline from 'node:readline';
|
|
|
9
9
|
import path from 'node:path';
|
|
10
10
|
import { encryptToken, decryptToken } from './token-store.js';
|
|
11
11
|
import { atomicWriteFileSync, atomicWriteWithLock } from './fs-atomic.js';
|
|
12
|
-
import { ALIAS_RE, configFilePath, CONFIG_VERSION } from './config-file.js';
|
|
12
|
+
import { ALIAS_RE, configFilePath, CONFIG_VERSION, tenantsDir } from './config-file.js';
|
|
13
13
|
import { getTokenDir } from './accounts.js';
|
|
14
|
+
/** D11 (refuse-only for v1): export/import operate on the FLAT single-owner
|
|
15
|
+
* store. A tenant-namespaced layout means the bundle would either miss every
|
|
16
|
+
* tenant's tokens or exfiltrate all of them in one file — refuse instead.
|
|
17
|
+
* Returns the offending dir, or null when the store is flat. */
|
|
18
|
+
export function refuseIfMultiTenant(dir = tenantsDir()) {
|
|
19
|
+
try {
|
|
20
|
+
if (readdirSync(dir).length > 0)
|
|
21
|
+
return dir;
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// absent or unreadable tenants/ = flat store
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
14
28
|
function resolveDeps(deps) {
|
|
15
29
|
return {
|
|
16
30
|
configPath: deps.configPath ?? configFilePath(),
|
|
@@ -152,6 +166,11 @@ async function resolvePassphrase(prompt) {
|
|
|
152
166
|
}
|
|
153
167
|
}
|
|
154
168
|
export async function runExportCli(argv) {
|
|
169
|
+
const mtDir = refuseIfMultiTenant();
|
|
170
|
+
if (mtDir) {
|
|
171
|
+
console.error(`E_MULTI_TENANT_EXPORT_REFUSED: tenant-namespaced token stores exist under ${mtDir}; account export only operates on a flat single-owner store.`);
|
|
172
|
+
return 2;
|
|
173
|
+
}
|
|
155
174
|
const out = argFlag(argv, '--out');
|
|
156
175
|
if (!out) {
|
|
157
176
|
console.error('Usage: mcp-google-multi account export --out <bundle.enc>');
|
|
@@ -169,6 +188,11 @@ export async function runExportCli(argv) {
|
|
|
169
188
|
return 0;
|
|
170
189
|
}
|
|
171
190
|
export async function runImportCli(argv) {
|
|
191
|
+
const mtDir = refuseIfMultiTenant();
|
|
192
|
+
if (mtDir) {
|
|
193
|
+
console.error(`E_MULTI_TENANT_EXPORT_REFUSED: tenant-namespaced token stores exist under ${mtDir}; account import only operates on a flat single-owner store.`);
|
|
194
|
+
return 2;
|
|
195
|
+
}
|
|
172
196
|
const file = argv[argv.indexOf('import') + 1];
|
|
173
197
|
if (!file || file.startsWith('--')) {
|
|
174
198
|
console.error('Usage: mcp-google-multi account import <bundle.enc> [--replace]');
|
package/dist/services.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import type { AccountSet } from './accounts.js';
|
|
1
2
|
import type { ToolRegistry } from './registry.js';
|
|
2
3
|
export interface ServiceEntry {
|
|
3
4
|
name: string;
|
|
4
5
|
register: (registry: ToolRegistry) => void;
|
|
5
|
-
|
|
6
|
+
/** Absent = always on. Gates evaluate against the PASSED account set (the
|
|
7
|
+
* calling tenant's view); omitted set = the global registry, today's
|
|
8
|
+
* behavior. */
|
|
9
|
+
enabled?: (set?: AccountSet) => boolean;
|
|
6
10
|
}
|
|
7
11
|
export declare const SERVICES: ServiceEntry[];
|
|
8
12
|
export declare const GENERATED_GATES: Record<string, {
|
|
9
|
-
enabled: () => boolean;
|
|
13
|
+
enabled: (set?: AccountSet) => boolean;
|
|
10
14
|
hint: string;
|
|
11
15
|
}>;
|
|
12
16
|
/**
|
package/dist/services.js
CHANGED
|
@@ -25,16 +25,16 @@ export const SERVICES = [
|
|
|
25
25
|
{ name: 'searchconsole', register: registerSearchConsoleTools },
|
|
26
26
|
{ name: 'tasks', register: registerTasksTools },
|
|
27
27
|
{ name: 'meet', register: registerMeetTools },
|
|
28
|
-
{ name: 'slides', register: registerSlidesTools, enabled: () => new Set(getOptionalBundles()).has('slides') },
|
|
29
|
-
{ name: 'forms', register: registerFormsTools, enabled: () => new Set(getOptionalBundles()).has('forms') },
|
|
30
|
-
{ name: 'chat', register: registerChatTools, enabled: () => new Set(getOptionalBundles()).has('chat') },
|
|
31
|
-
{ name: 'analytics', register: registerAnalyticsTools, enabled: () => { const b = new Set(getOptionalBundles()); return b.has('analytics') || b.has('analytics_write'); } },
|
|
32
|
-
{ name: 'admin', register: registerAdminTools, enabled: () => getAdminAccounts().length > 0 },
|
|
28
|
+
{ name: 'slides', register: registerSlidesTools, enabled: (set) => new Set(getOptionalBundles(set)).has('slides') },
|
|
29
|
+
{ name: 'forms', register: registerFormsTools, enabled: (set) => new Set(getOptionalBundles(set)).has('forms') },
|
|
30
|
+
{ name: 'chat', register: registerChatTools, enabled: (set) => new Set(getOptionalBundles(set)).has('chat') },
|
|
31
|
+
{ name: 'analytics', register: registerAnalyticsTools, enabled: (set) => { const b = new Set(getOptionalBundles(set)); return b.has('analytics') || b.has('analytics_write'); } },
|
|
32
|
+
{ name: 'admin', register: registerAdminTools, enabled: (set) => getAdminAccounts(set).length > 0 },
|
|
33
33
|
];
|
|
34
34
|
// Generated-only services with opt-in scopes; admin/forms/chat/analytics reuse their curated gate in buildRegistry,
|
|
35
35
|
// and workspaceevents is deliberately absent — no dedicated scope (subscriptions use resource scopes).
|
|
36
36
|
const bundleGate = (name) => ({
|
|
37
|
-
enabled: () => new Set(getOptionalBundles()).has(name),
|
|
37
|
+
enabled: (set) => new Set(getOptionalBundles(set)).has(name),
|
|
38
38
|
hint: `add "${name}" to an account's scope profile (or legacy GOOGLE_OPTIONAL_SCOPES)`,
|
|
39
39
|
});
|
|
40
40
|
export const GENERATED_GATES = {
|
package/dist/token-store.d.ts
CHANGED
|
@@ -7,3 +7,20 @@ export declare function readToken(alias: string): TokenData | null;
|
|
|
7
7
|
export declare function writeToken(alias: string, data: object): void;
|
|
8
8
|
export declare function updateToken(alias: string, updates: object): void;
|
|
9
9
|
export declare function hasToken(alias: string): boolean;
|
|
10
|
+
export interface TokenStore {
|
|
11
|
+
readToken(alias: string): TokenData | null;
|
|
12
|
+
writeToken(alias: string, data: object): void;
|
|
13
|
+
updateToken(alias: string, updates: object): void;
|
|
14
|
+
hasToken(alias: string): boolean;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Context-scoped token store: `.enc` files live under `dir`, ciphertext keyed
|
|
18
|
+
* by `tenantId` when present (HKDF v2 envelope; omitted = the single-owner v1
|
|
19
|
+
* envelope). Same lock + atomic-write + error contract as the module-level
|
|
20
|
+
* functions: readToken returns null ONLY when the file is missing and throws
|
|
21
|
+
* on a decrypt failure.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createTokenStore(dir: string, opts?: {
|
|
24
|
+
tenantId?: string;
|
|
25
|
+
resolveKey?: () => string;
|
|
26
|
+
}): TokenStore;
|
package/dist/token-store.js
CHANGED
|
Binary file
|
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.43",
|
|
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",
|