mcp-google-multi 5.2.0-alpha.6 → 5.2.0-beta.1
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.js +9 -1
- package/dist/executor.js +3 -0
- package/dist/index.js +14 -1
- package/dist/tools/drive.js +2 -2
- package/dist/tools/gmail.js +1 -1
- package/dist/tools/google-api.js +3 -2
- package/dist/write-control.js +5 -0
- package/package.json +1 -1
package/dist/auth.js
CHANGED
|
@@ -111,10 +111,15 @@ export function resolveScopesForAccount(alias) {
|
|
|
111
111
|
export async function runAuthFlow(args) {
|
|
112
112
|
const accountIdx = args.indexOf('--account');
|
|
113
113
|
if (accountIdx === -1 || !args[accountIdx + 1]) {
|
|
114
|
-
console.error('Usage:
|
|
114
|
+
console.error('Usage: mcp-google-multi auth --account <alias>');
|
|
115
115
|
console.error(`Valid aliases: ${ACCOUNTS.join(', ')}`);
|
|
116
116
|
process.exit(1);
|
|
117
117
|
}
|
|
118
|
+
if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) {
|
|
119
|
+
console.error('GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET are not set. Create an OAuth Desktop client ' +
|
|
120
|
+
'(see docs/google-cloud-setup.md) and add both to your environment before authenticating.');
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
118
123
|
const alias = args[accountIdx + 1];
|
|
119
124
|
if (!ACCOUNTS.includes(alias)) {
|
|
120
125
|
console.error(`Unknown account "${alias}". Valid aliases: ${ACCOUNTS.join(', ')}`);
|
|
@@ -178,6 +183,7 @@ export async function runAuthFlow(args) {
|
|
|
178
183
|
res.end('<h2>Authentication successful!</h2><p>You can close this tab.</p>');
|
|
179
184
|
server.destroy();
|
|
180
185
|
console.log(`Token saved (encrypted) for ${alias}.`);
|
|
186
|
+
console.log('Next: authenticate your other aliases, then verify with: mcp-google-multi config check');
|
|
181
187
|
resolve();
|
|
182
188
|
}
|
|
183
189
|
}
|
|
@@ -190,6 +196,8 @@ export async function runAuthFlow(args) {
|
|
|
190
196
|
})
|
|
191
197
|
// Bind to loopback only — never expose the OAuth callback to the local network.
|
|
192
198
|
.listen(4242, '127.0.0.1', () => {
|
|
199
|
+
// Always print the URL: `open` silently no-ops on headless/SSH sessions.
|
|
200
|
+
console.log(`Opening your browser to authorize "${alias}". If nothing opens, visit:\n${authorizeUrl}`);
|
|
193
201
|
open(authorizeUrl, { wait: false }).then((cp) => cp.unref());
|
|
194
202
|
});
|
|
195
203
|
destroyer(server);
|
package/dist/executor.js
CHANGED
|
@@ -27,6 +27,7 @@ export async function executeApiMethod(method, args, deps = {}) {
|
|
|
27
27
|
message: 'Binary media download (alt=media) returns no usable JSON through this tool.',
|
|
28
28
|
hint: 'Use drive_download / drive_export for file content.',
|
|
29
29
|
retriable: false,
|
|
30
|
+
account: args.account,
|
|
30
31
|
}, true);
|
|
31
32
|
}
|
|
32
33
|
let url;
|
|
@@ -39,6 +40,7 @@ export async function executeApiMethod(method, args, deps = {}) {
|
|
|
39
40
|
message: err.message,
|
|
40
41
|
hint: `Required params for ${method.id}: ${method.requiredParams.join(', ') || '(none)'}`,
|
|
41
42
|
retriable: false,
|
|
43
|
+
account: args.account,
|
|
42
44
|
}, true);
|
|
43
45
|
}
|
|
44
46
|
if (!isGoogleApiUrl(url)) {
|
|
@@ -47,6 +49,7 @@ export async function executeApiMethod(method, args, deps = {}) {
|
|
|
47
49
|
message: `Refusing to send credentials to a non-googleapis.com host for "${method.id}".`,
|
|
48
50
|
hint: 'The discovery cache may be corrupt; delete DISCOVERY_CACHE_PATH and retry.',
|
|
49
51
|
retriable: false,
|
|
52
|
+
account: args.account,
|
|
50
53
|
}, true);
|
|
51
54
|
}
|
|
52
55
|
try {
|
package/dist/index.js
CHANGED
|
@@ -81,7 +81,20 @@ async function main() {
|
|
|
81
81
|
const counts = registry.visibleCount();
|
|
82
82
|
console.log(`Write-control: ${describePolicy(policy)}`);
|
|
83
83
|
console.log(`CUD tools enabled: ${cud.length - disabled.length}/${cud.length}`);
|
|
84
|
-
|
|
84
|
+
// At full-coverage scale, a flat name dump is unreadable — summarize per
|
|
85
|
+
// service unless the list is short.
|
|
86
|
+
let disabledLine = '(none)';
|
|
87
|
+
if (disabled.length > 0 && disabled.length <= 20) {
|
|
88
|
+
disabledLine = disabled.map((t) => t.name).join(', ');
|
|
89
|
+
}
|
|
90
|
+
else if (disabled.length > 20) {
|
|
91
|
+
const perService = new Map();
|
|
92
|
+
for (const t of disabled)
|
|
93
|
+
perService.set(t.service, (perService.get(t.service) ?? 0) + 1);
|
|
94
|
+
const summary = [...perService.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([s, n]) => `${s} ${n}`).join(', ');
|
|
95
|
+
disabledLine = `${disabled.length} write tools (${summary}) — enable via GOOGLE_PROFILE / GOOGLE_WRITE_ALLOW`;
|
|
96
|
+
}
|
|
97
|
+
console.log(`Disabled: ${disabledLine}`);
|
|
85
98
|
console.log(`Services: ${registry.services().join(', ')}`);
|
|
86
99
|
console.log(`Tool surface: ${counts.eager} eager (discover + escape hatch), ${counts.hidden} deferred until discovery`);
|
|
87
100
|
console.log(`Escape hatch: google_api_call CUD verdicts follow profile=${policy.profile} and your allow/deny globs`);
|
package/dist/tools/drive.js
CHANGED
|
@@ -246,7 +246,7 @@ export function registerDriveTools(server) {
|
|
|
246
246
|
const dest = path.join(savePath, path.basename(filename));
|
|
247
247
|
const res = await drive.files.get({ fileId, alt: 'media', supportsAllDrives: true }, { responseType: 'stream' });
|
|
248
248
|
// pipeline destroys both streams on source/sink error; raw .pipe leaks the partial file.
|
|
249
|
-
await pipeline(res.data, fs.createWriteStream(dest));
|
|
249
|
+
await pipeline(res.data, fs.createWriteStream(dest, { mode: 0o600 }));
|
|
250
250
|
const { size } = fs.statSync(dest);
|
|
251
251
|
return {
|
|
252
252
|
content: [{ type: 'text', text: JSON.stringify({ savedPath: dest, bytes: size }, null, 2) }],
|
|
@@ -272,7 +272,7 @@ export function registerDriveTools(server) {
|
|
|
272
272
|
const dest = path.join(savePath, path.basename(filename));
|
|
273
273
|
const res = await drive.files.export({ fileId, mimeType: exportMime }, { responseType: 'stream' });
|
|
274
274
|
// pipeline destroys both streams on source/sink error; raw .pipe leaks the partial file.
|
|
275
|
-
await pipeline(res.data, fs.createWriteStream(dest));
|
|
275
|
+
await pipeline(res.data, fs.createWriteStream(dest, { mode: 0o600 }));
|
|
276
276
|
const { size } = fs.statSync(dest);
|
|
277
277
|
return {
|
|
278
278
|
content: [{ type: 'text', text: JSON.stringify({ savedPath: dest, bytes: size }, null, 2) }],
|
package/dist/tools/gmail.js
CHANGED
|
@@ -324,7 +324,7 @@ export function registerGmailTools(server) {
|
|
|
324
324
|
const buffer = Buffer.from(data, 'base64url');
|
|
325
325
|
// Strip path components so callers can't escape savePath via "../".
|
|
326
326
|
const fullPath = path.join(savePath, path.basename(filename));
|
|
327
|
-
await fs.promises.writeFile(fullPath, buffer);
|
|
327
|
+
await fs.promises.writeFile(fullPath, buffer, { mode: 0o600 });
|
|
328
328
|
return {
|
|
329
329
|
content: [{ type: 'text', text: `Saved to ${fullPath} (${buffer.length} bytes)` }],
|
|
330
330
|
};
|
package/dist/tools/google-api.js
CHANGED
|
@@ -112,7 +112,7 @@ export function registerEscapeTools(registry, policy, deps = {}) {
|
|
|
112
112
|
annotations: { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
|
|
113
113
|
}, async ({ account, api, methodId, pathParams, queryParams, body }) => {
|
|
114
114
|
if (!WORKSPACE_APIS[api]) {
|
|
115
|
-
return jsonResult({ error: 'unknown_api', message: `Unknown api "${api}".`, hint: `Known APIs: ${apiList}`, retriable: false }, true);
|
|
115
|
+
return jsonResult({ error: 'unknown_api', message: `Unknown api "${api}".`, hint: `Known APIs: ${apiList}`, retriable: false, account }, true);
|
|
116
116
|
}
|
|
117
117
|
if (!apiEnabled(api))
|
|
118
118
|
return toolsetDisabled(api);
|
|
@@ -121,7 +121,7 @@ export function registerEscapeTools(registry, policy, deps = {}) {
|
|
|
121
121
|
index = await loadMethodIndex(api, deps);
|
|
122
122
|
}
|
|
123
123
|
catch (err) {
|
|
124
|
-
return jsonResult({ error: 'discovery_unavailable', message: err.message, retriable: true }, true);
|
|
124
|
+
return jsonResult({ error: 'discovery_unavailable', message: err.message, retriable: true, account }, true);
|
|
125
125
|
}
|
|
126
126
|
const method = index.find((m) => m.id === methodId);
|
|
127
127
|
if (!method) {
|
|
@@ -130,6 +130,7 @@ export function registerEscapeTools(registry, policy, deps = {}) {
|
|
|
130
130
|
message: `No method "${methodId}" in ${api}.`,
|
|
131
131
|
hint: `Use google_api_search({query: "...", api: "${api}"}) to find the right method id.`,
|
|
132
132
|
retriable: false,
|
|
133
|
+
account,
|
|
133
134
|
}, true);
|
|
134
135
|
}
|
|
135
136
|
const cud = cudFromMethod(method);
|
package/dist/write-control.js
CHANGED
|
@@ -4,6 +4,11 @@ function parseGlobs(value) {
|
|
|
4
4
|
}
|
|
5
5
|
export function resolvePolicy(env = process.env) {
|
|
6
6
|
const raw = (env.GOOGLE_PROFILE ?? 'read-only').trim();
|
|
7
|
+
if (raw && !PROFILES.includes(raw)) {
|
|
8
|
+
// Fail-closed to read-only, but say so — a typo'd profile otherwise looks
|
|
9
|
+
// like every write tool silently breaking.
|
|
10
|
+
process.stderr.write(`GOOGLE_PROFILE="${raw}" is not valid (${PROFILES.join(' | ')}); using read-only\n`);
|
|
11
|
+
}
|
|
7
12
|
return {
|
|
8
13
|
profile: PROFILES.includes(raw) ? raw : 'read-only',
|
|
9
14
|
readOnly: /^(1|true|yes)$/i.test(env.GOOGLE_READ_ONLY ?? ''),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-google-multi",
|
|
3
|
-
"version": "5.2.0-
|
|
3
|
+
"version": "5.2.0-beta.1",
|
|
4
4
|
"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.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|