mcp-google-multi 6.0.0-alpha.38 → 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/fanout.d.ts +1 -1
- package/dist/fanout.js +9 -6
- package/dist/identity.d.ts +3 -1
- package/dist/registry.d.ts +3 -1
- package/dist/registry.js +12 -6
- package/dist/token-store.d.ts +3 -2
- package/dist/token-store.js +0 -0
- 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/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/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/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
|
},
|
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
|
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",
|