mcp-google-multi 6.0.0-alpha.45 → 6.0.0-alpha.47
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/compose.d.ts +5 -0
- package/dist/compose.js +67 -0
- package/dist/doctor.js +28 -2
- package/dist/index.js +3 -59
- package/dist/registry-transfer.js +40 -3
- package/package.json +19 -1
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/server";
|
|
2
|
+
import { ToolRegistry, type DiscoveryMode } from './registry.js';
|
|
3
|
+
import type { IdentityContext } from './identity.js';
|
|
4
|
+
import type { Metrics } from './usage-metrics.js';
|
|
5
|
+
export declare function buildRegistry(server: McpServer, ctx: IdentityContext, mode?: DiscoveryMode, metrics?: Metrics | null): ToolRegistry;
|
package/dist/compose.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// S1.12: registry composition, extracted from index.ts so the tenancy module
|
|
2
|
+
// (and tests) can build registries without touching the CLI entrypoint. This
|
|
3
|
+
// module must stay free of import-time side effects of its own: no argv
|
|
4
|
+
// parsing, no process.exit, no prints (accounts.js module-load resolution is
|
|
5
|
+
// the one inherited, pre-existing exception on the import chain).
|
|
6
|
+
import { GENERATED_SERVICES } from './tools/generated/index.js';
|
|
7
|
+
import { GENERATED_GATES, SERVICES } from './services.js';
|
|
8
|
+
import { ToolRegistry } from './registry.js';
|
|
9
|
+
import { registerDiscoverTools } from './discover.js';
|
|
10
|
+
import { registerEscapeTools } from './tools/google-api.js';
|
|
11
|
+
import { registerAccountTools } from './tools/accounts-tool.js';
|
|
12
|
+
import { registerDiagnoseTool } from './doctor.js';
|
|
13
|
+
import { registerAccountWizardTools } from './tools/account-wizard.js';
|
|
14
|
+
import { getToolsets, toolsetEnabled } from './toolsets.js';
|
|
15
|
+
export function buildRegistry(server, ctx, mode, metrics = null) {
|
|
16
|
+
const policy = ctx.policy;
|
|
17
|
+
// The registry's account view IS the context's: fan-out expansion,
|
|
18
|
+
// selector validation and default-account injection all follow this
|
|
19
|
+
// context, not the process global (identical for the free core's one ctx).
|
|
20
|
+
const registry = new ToolRegistry(server, policy, mode, metrics, () => ctx.accounts);
|
|
21
|
+
const toolsets = getToolsets();
|
|
22
|
+
if (toolsets !== 'all') {
|
|
23
|
+
const known = new Set([...SERVICES.map((s) => s.name), ...GENERATED_SERVICES.map((s) => s.name)]);
|
|
24
|
+
for (const requested of toolsets) {
|
|
25
|
+
if (!known.has(requested)) {
|
|
26
|
+
process.stderr.write(`GOOGLE_TOOLSETS: unknown service "${requested}" ignored\n`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
for (const svc of SERVICES) {
|
|
31
|
+
if (!toolsetEnabled(toolsets, svc.name))
|
|
32
|
+
continue;
|
|
33
|
+
if (svc.enabled && !svc.enabled(ctx.accounts)) {
|
|
34
|
+
if (toolsets !== 'all') {
|
|
35
|
+
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)`;
|
|
36
|
+
process.stderr.write(`GOOGLE_TOOLSETS: "${svc.name}" requested but not enabled — ${hint}\n`);
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
svc.register(registry);
|
|
41
|
+
}
|
|
42
|
+
for (const gen of GENERATED_SERVICES) {
|
|
43
|
+
if (!toolsetEnabled(toolsets, gen.name))
|
|
44
|
+
continue;
|
|
45
|
+
const curated = SERVICES.find((s) => s.name === gen.name);
|
|
46
|
+
const gate = curated?.enabled ?? GENERATED_GATES[gen.name]?.enabled;
|
|
47
|
+
if (gate && !gate(ctx.accounts)) {
|
|
48
|
+
if (!curated && toolsets !== 'all') {
|
|
49
|
+
process.stderr.write(`GOOGLE_TOOLSETS: "${gen.name}" requested but not enabled — ${GENERATED_GATES[gen.name].hint}\n`);
|
|
50
|
+
}
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
gen.register(registry);
|
|
54
|
+
}
|
|
55
|
+
if (registry.services().length === 0) {
|
|
56
|
+
const known = [...new Set([...SERVICES.map((s) => s.name), ...GENERATED_SERVICES.map((s) => s.name)])].sort();
|
|
57
|
+
throw new Error(`GOOGLE_TOOLSETS="${process.env.GOOGLE_TOOLSETS ?? ''}" selected no enabled services. ` +
|
|
58
|
+
`Known services: ${known.join(', ')}. ` +
|
|
59
|
+
`Note: optional services need their bundle in an account's scope profile (or legacy GOOGLE_OPTIONAL_SCOPES); admin needs an admin account/profile.`);
|
|
60
|
+
}
|
|
61
|
+
registerDiscoverTools(registry, policy);
|
|
62
|
+
registerEscapeTools(registry, policy);
|
|
63
|
+
registerAccountTools(registry);
|
|
64
|
+
registerDiagnoseTool(registry, ctx);
|
|
65
|
+
registerAccountWizardTools(registry, server);
|
|
66
|
+
return registry;
|
|
67
|
+
}
|
package/dist/doctor.js
CHANGED
|
@@ -341,9 +341,35 @@ export function renderDoctorText(report) {
|
|
|
341
341
|
out.push(`Overall: ${report.verdict.toUpperCase()}`);
|
|
342
342
|
return out.join('\n');
|
|
343
343
|
}
|
|
344
|
-
|
|
344
|
+
const EMAIL_LOCAL_CHAR = /[A-Za-z0-9._%+-]/;
|
|
345
|
+
const EMAIL_DOMAIN_CHAR = /[A-Za-z0-9.-]/;
|
|
346
|
+
/** Mask the local-part of an email so a report never carries a full address
|
|
347
|
+
* (BR8). A linear scan, not a regex: the `local* @domain` pattern backtracks
|
|
348
|
+
* polynomially on long local-charset runs without an @ (CodeQL
|
|
349
|
+
* js/polynomial-redos), and report text can embed arbitrary error strings. */
|
|
345
350
|
function maskEmails(text) {
|
|
346
|
-
|
|
351
|
+
let out = '';
|
|
352
|
+
let i = 0;
|
|
353
|
+
while (i < text.length) {
|
|
354
|
+
if (text[i] === '@') {
|
|
355
|
+
let j = i + 1;
|
|
356
|
+
while (j < text.length && EMAIL_DOMAIN_CHAR.test(text[j]))
|
|
357
|
+
j += 1;
|
|
358
|
+
let k = out.length;
|
|
359
|
+
while (k > 0 && EMAIL_LOCAL_CHAR.test(out[k - 1]))
|
|
360
|
+
k -= 1;
|
|
361
|
+
if (j > i + 1 && k < out.length) {
|
|
362
|
+
// keep the first local char, mask the rest ('*' is outside the local
|
|
363
|
+
// charset, so a masked run can never be re-masked by a later @)
|
|
364
|
+
out = `${out.slice(0, k + 1)}***${text.slice(i, j)}`;
|
|
365
|
+
i = j;
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
out += text[i];
|
|
370
|
+
i += 1;
|
|
371
|
+
}
|
|
372
|
+
return out;
|
|
347
373
|
}
|
|
348
374
|
/** Redacted, paste-ready bug report (BR8): verdicts + provenance labels + token
|
|
349
375
|
* statuses + slugs; NO token values, secrets, keys, or full email addresses. */
|
package/dist/index.js
CHANGED
|
@@ -8,17 +8,11 @@ import { fileURLToPath } from 'node:url';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { StdioServerTransport } from "@modelcontextprotocol/server/stdio";
|
|
10
10
|
import { McpServer } from "@modelcontextprotocol/server";
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import { ToolRegistry, resolveDiscoveryMode } from './registry.js';
|
|
14
|
-
import { registerDiscoverTools } from './discover.js';
|
|
15
|
-
import { registerEscapeTools } from './tools/google-api.js';
|
|
16
|
-
import { registerAccountTools } from './tools/accounts-tool.js';
|
|
17
|
-
import { registerDiagnoseTool } from './doctor.js';
|
|
18
|
-
import { registerAccountWizardTools } from './tools/account-wizard.js';
|
|
19
|
-
import { getToolsets, toolsetEnabled } from './toolsets.js';
|
|
11
|
+
import { unknownToolMessage } from './services.js';
|
|
12
|
+
import { resolveDiscoveryMode } from './registry.js';
|
|
20
13
|
import { isAllowed, describePolicy } from './write-control.js';
|
|
21
14
|
import { buildIdentityContext } from './identity.js';
|
|
15
|
+
import { buildRegistry } from './compose.js';
|
|
22
16
|
import { registerSetupPrompt } from './setup-prompt.js';
|
|
23
17
|
import { applyNetTuning } from './net-tuning.js';
|
|
24
18
|
import { argNormalizationEnabled, withArgNormalization, withValidationEnvelope } from './arg-normalize.js';
|
|
@@ -43,56 +37,6 @@ function strictArgOptions(registry, metrics) {
|
|
|
43
37
|
onDrop: metrics ? (tool, keys) => metrics.recordArgDrop(tool, keys) : undefined,
|
|
44
38
|
};
|
|
45
39
|
}
|
|
46
|
-
function buildRegistry(server, ctx, mode, metrics = null) {
|
|
47
|
-
const policy = ctx.policy;
|
|
48
|
-
const registry = new ToolRegistry(server, policy, mode, metrics);
|
|
49
|
-
const toolsets = getToolsets();
|
|
50
|
-
if (toolsets !== 'all') {
|
|
51
|
-
const known = new Set([...SERVICES.map((s) => s.name), ...GENERATED_SERVICES.map((s) => s.name)]);
|
|
52
|
-
for (const requested of toolsets) {
|
|
53
|
-
if (!known.has(requested)) {
|
|
54
|
-
process.stderr.write(`GOOGLE_TOOLSETS: unknown service "${requested}" ignored\n`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
for (const svc of SERVICES) {
|
|
59
|
-
if (!toolsetEnabled(toolsets, svc.name))
|
|
60
|
-
continue;
|
|
61
|
-
if (svc.enabled && !svc.enabled(ctx.accounts)) {
|
|
62
|
-
if (toolsets !== 'all') {
|
|
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
|
-
process.stderr.write(`GOOGLE_TOOLSETS: "${svc.name}" requested but not enabled — ${hint}\n`);
|
|
65
|
-
}
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
svc.register(registry);
|
|
69
|
-
}
|
|
70
|
-
for (const gen of GENERATED_SERVICES) {
|
|
71
|
-
if (!toolsetEnabled(toolsets, gen.name))
|
|
72
|
-
continue;
|
|
73
|
-
const curated = SERVICES.find((s) => s.name === gen.name);
|
|
74
|
-
const gate = curated?.enabled ?? GENERATED_GATES[gen.name]?.enabled;
|
|
75
|
-
if (gate && !gate(ctx.accounts)) {
|
|
76
|
-
if (!curated && toolsets !== 'all') {
|
|
77
|
-
process.stderr.write(`GOOGLE_TOOLSETS: "${gen.name}" requested but not enabled — ${GENERATED_GATES[gen.name].hint}\n`);
|
|
78
|
-
}
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
gen.register(registry);
|
|
82
|
-
}
|
|
83
|
-
if (registry.services().length === 0) {
|
|
84
|
-
const known = [...new Set([...SERVICES.map((s) => s.name), ...GENERATED_SERVICES.map((s) => s.name)])].sort();
|
|
85
|
-
throw new Error(`GOOGLE_TOOLSETS="${process.env.GOOGLE_TOOLSETS ?? ''}" selected no enabled services. ` +
|
|
86
|
-
`Known services: ${known.join(', ')}. ` +
|
|
87
|
-
`Note: optional services need their bundle in an account's scope profile (or legacy GOOGLE_OPTIONAL_SCOPES); admin needs an admin account/profile.`);
|
|
88
|
-
}
|
|
89
|
-
registerDiscoverTools(registry, policy);
|
|
90
|
-
registerEscapeTools(registry, policy);
|
|
91
|
-
registerAccountTools(registry);
|
|
92
|
-
registerDiagnoseTool(registry, ctx);
|
|
93
|
-
registerAccountWizardTools(registry, server);
|
|
94
|
-
return registry;
|
|
95
|
-
}
|
|
96
40
|
async function main() {
|
|
97
41
|
if (process.argv.includes('auth')) {
|
|
98
42
|
const { runAuthFlow } = await import('./auth.js');
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
8
8
|
import * as readline from 'node:readline';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
-
import {
|
|
10
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
|
|
11
11
|
import { atomicWriteFileSync, atomicWriteWithLock } from './fs-atomic.js';
|
|
12
12
|
import { ALIAS_RE, configFilePath, CONFIG_VERSION, tenantsDir } from './config-file.js';
|
|
13
13
|
import { getTokenDir } from './accounts.js';
|
|
@@ -61,15 +61,52 @@ export function buildTransferBundle(exportedAt, deps = {}) {
|
|
|
61
61
|
}
|
|
62
62
|
return { manifest: { v: 1, exportedAt, aliases: [...aliasSet].sort() }, config, tokens };
|
|
63
63
|
}
|
|
64
|
+
// Bundle envelope v2: a REAL passphrase KDF. The v1 envelope reused the
|
|
65
|
+
// token store's deriveKey, whose passphrase branch is one unsalted sha256 —
|
|
66
|
+
// an exported bundle could be brute-forced offline (CodeQL
|
|
67
|
+
// js/insufficient-password-hash). scrypt with a per-bundle random salt fixes
|
|
68
|
+
// that; v1 bundles (a prerelease-only format, never produced by any stable
|
|
69
|
+
// release) are refused with a re-export hint rather than kept decryptable.
|
|
70
|
+
const BUNDLE_VERSION = 2;
|
|
71
|
+
const SCRYPT_PARAMS = { N: 1 << 15, r: 8, p: 1, maxmem: 64 * 1024 * 1024 };
|
|
72
|
+
function bundleKey(passphrase, salt) {
|
|
73
|
+
return scryptSync(passphrase, salt, 32, SCRYPT_PARAMS);
|
|
74
|
+
}
|
|
64
75
|
export function encryptBundle(bundle, passphrase) {
|
|
65
|
-
|
|
76
|
+
const salt = randomBytes(16);
|
|
77
|
+
const iv = randomBytes(12);
|
|
78
|
+
const cipher = createCipheriv('aes-256-gcm', bundleKey(passphrase, salt), iv);
|
|
79
|
+
const data = Buffer.concat([cipher.update(Buffer.from(JSON.stringify(bundle), 'utf8')), cipher.final()]);
|
|
80
|
+
const file = {
|
|
81
|
+
bv: BUNDLE_VERSION,
|
|
82
|
+
salt: salt.toString('base64'),
|
|
83
|
+
iv: iv.toString('base64'),
|
|
84
|
+
tag: cipher.getAuthTag().toString('base64'),
|
|
85
|
+
data: data.toString('base64'),
|
|
86
|
+
};
|
|
87
|
+
return JSON.stringify(file);
|
|
66
88
|
}
|
|
67
89
|
export class BundleDecryptError extends Error {
|
|
68
90
|
}
|
|
69
91
|
export function decryptBundle(contents, passphrase) {
|
|
92
|
+
let file;
|
|
93
|
+
try {
|
|
94
|
+
file = JSON.parse(contents);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
throw new BundleDecryptError('The file is not a registry export bundle.');
|
|
98
|
+
}
|
|
99
|
+
if (file.v !== undefined && file.bv === undefined) {
|
|
100
|
+
throw new BundleDecryptError('This bundle uses the retired v1 prerelease format (weak passphrase derivation); re-export it from the source machine with the current version.');
|
|
101
|
+
}
|
|
102
|
+
if (file.bv !== BUNDLE_VERSION || !file.salt || !file.iv || !file.tag || !file.data) {
|
|
103
|
+
throw new BundleDecryptError('The file is not a valid registry export (bad envelope).');
|
|
104
|
+
}
|
|
70
105
|
let obj;
|
|
71
106
|
try {
|
|
72
|
-
|
|
107
|
+
const decipher = createDecipheriv('aes-256-gcm', bundleKey(passphrase, Buffer.from(file.salt, 'base64')), Buffer.from(file.iv, 'base64'));
|
|
108
|
+
decipher.setAuthTag(Buffer.from(file.tag, 'base64'));
|
|
109
|
+
obj = JSON.parse(Buffer.concat([decipher.update(Buffer.from(file.data, 'base64')), decipher.final()]).toString('utf8'));
|
|
73
110
|
}
|
|
74
111
|
catch {
|
|
75
112
|
throw new BundleDecryptError('Could not decrypt the bundle — wrong passphrase or the file is corrupt.');
|
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.47",
|
|
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",
|
|
@@ -9,6 +9,24 @@
|
|
|
9
9
|
"mcp-google-multi": "dist/index.js"
|
|
10
10
|
},
|
|
11
11
|
"main": "dist/index.js",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./dist/index.js",
|
|
14
|
+
"./identity": "./dist/identity.js",
|
|
15
|
+
"./compose": "./dist/compose.js",
|
|
16
|
+
"./registry": "./dist/registry.js",
|
|
17
|
+
"./oauth-as": "./dist/oauth-as.js",
|
|
18
|
+
"./accounts": "./dist/accounts.js",
|
|
19
|
+
"./token-store": "./dist/token-store.js",
|
|
20
|
+
"./http-transport": "./dist/http-transport.js",
|
|
21
|
+
"./http-config": "./dist/http-config.js",
|
|
22
|
+
"./client": "./dist/client.js",
|
|
23
|
+
"./config-file": "./dist/config-file.js",
|
|
24
|
+
"./mcp-token": "./dist/mcp-token.js",
|
|
25
|
+
"./boot-gates": "./dist/boot-gates.js",
|
|
26
|
+
"./tenant-purge": "./dist/tenant-purge.js",
|
|
27
|
+
"./doctor": "./dist/doctor.js",
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
12
30
|
"files": [
|
|
13
31
|
"dist"
|
|
14
32
|
],
|