mcp-google-multi 4.5.0-alpha.1 → 4.5.0-alpha.3
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/index.js +36 -19
- package/dist/registry.d.ts +14 -0
- package/dist/registry.js +36 -0
- package/dist/tools/admin.d.ts +3 -3
- package/dist/tools/calendar.d.ts +2 -2
- package/dist/tools/chat.d.ts +2 -2
- package/dist/tools/contacts.d.ts +2 -2
- package/dist/tools/docs.d.ts +2 -2
- package/dist/tools/drive.d.ts +2 -2
- package/dist/tools/forms.d.ts +2 -2
- package/dist/tools/gmail.d.ts +2 -2
- package/dist/tools/meet.d.ts +2 -2
- package/dist/tools/searchconsole.d.ts +2 -2
- package/dist/tools/sheets.d.ts +2 -2
- package/dist/tools/tasks.d.ts +2 -2
- package/dist/write-control.d.ts +24 -0
- package/dist/write-control.js +63 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18,8 +18,32 @@ import { registerFormsTools } from './tools/forms.js';
|
|
|
18
18
|
import { registerChatTools } from './tools/chat.js';
|
|
19
19
|
import { registerAdminTools, registerAlertCenterTools } from './tools/admin.js';
|
|
20
20
|
import { getOptionalBundles, getAdminAccounts } from './auth.js';
|
|
21
|
+
import { ToolRegistry } from './registry.js';
|
|
22
|
+
import { resolvePolicy, isAllowed, describePolicy } from './write-control.js';
|
|
21
23
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
22
24
|
const pkg = JSON.parse(readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf-8'));
|
|
25
|
+
function buildRegistry(server, policy) {
|
|
26
|
+
const registry = new ToolRegistry(server, policy);
|
|
27
|
+
registerGmailTools(registry);
|
|
28
|
+
registerDriveTools(registry);
|
|
29
|
+
registerCalendarTools(registry);
|
|
30
|
+
registerSheetsTools(registry);
|
|
31
|
+
registerDocsTools(registry);
|
|
32
|
+
registerContactsTools(registry);
|
|
33
|
+
registerSearchConsoleTools(registry);
|
|
34
|
+
registerTasksTools(registry);
|
|
35
|
+
registerMeetTools(registry);
|
|
36
|
+
const optional = new Set(getOptionalBundles());
|
|
37
|
+
if (optional.has('forms'))
|
|
38
|
+
registerFormsTools(registry);
|
|
39
|
+
if (optional.has('chat'))
|
|
40
|
+
registerChatTools(registry);
|
|
41
|
+
if (optional.has('alertcenter'))
|
|
42
|
+
registerAlertCenterTools(registry);
|
|
43
|
+
if (getAdminAccounts().length > 0)
|
|
44
|
+
registerAdminTools(registry);
|
|
45
|
+
return registry;
|
|
46
|
+
}
|
|
23
47
|
async function main() {
|
|
24
48
|
if (process.argv.includes('auth')) {
|
|
25
49
|
const { runAuthFlow } = await import('./auth.js');
|
|
@@ -31,29 +55,22 @@ async function main() {
|
|
|
31
55
|
runMigrateTokens();
|
|
32
56
|
return;
|
|
33
57
|
}
|
|
34
|
-
|
|
58
|
+
if (process.argv.includes('config') && process.argv.includes('check')) {
|
|
59
|
+
const policy = resolvePolicy();
|
|
60
|
+
const registry = buildRegistry(new McpServer({ name: 'mcp-google-multi', version: pkg.version }), policy);
|
|
61
|
+
const cud = registry.tools.filter((t) => t.cud !== 'read');
|
|
62
|
+
const disabled = cud.filter((t) => !isAllowed(t, policy));
|
|
63
|
+
console.log(`Write-control: ${describePolicy(policy)}`);
|
|
64
|
+
console.log(`CUD tools enabled: ${cud.length - disabled.length}/${cud.length}`);
|
|
65
|
+
console.log(`Disabled: ${disabled.map((t) => t.name).join(', ') || '(none)'}`);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const policy = resolvePolicy();
|
|
35
69
|
const server = new McpServer({
|
|
36
70
|
name: 'mcp-google-multi',
|
|
37
71
|
version: pkg.version,
|
|
38
72
|
});
|
|
39
|
-
|
|
40
|
-
registerDriveTools(server);
|
|
41
|
-
registerCalendarTools(server);
|
|
42
|
-
registerSheetsTools(server);
|
|
43
|
-
registerDocsTools(server);
|
|
44
|
-
registerContactsTools(server);
|
|
45
|
-
registerSearchConsoleTools(server);
|
|
46
|
-
registerTasksTools(server);
|
|
47
|
-
registerMeetTools(server);
|
|
48
|
-
const optional = new Set(getOptionalBundles());
|
|
49
|
-
if (optional.has('forms'))
|
|
50
|
-
registerFormsTools(server);
|
|
51
|
-
if (optional.has('chat'))
|
|
52
|
-
registerChatTools(server);
|
|
53
|
-
if (optional.has('alertcenter'))
|
|
54
|
-
registerAlertCenterTools(server);
|
|
55
|
-
if (getAdminAccounts().length > 0)
|
|
56
|
-
registerAdminTools(server);
|
|
73
|
+
buildRegistry(server, policy);
|
|
57
74
|
const transport = new StdioServerTransport();
|
|
58
75
|
await server.connect(transport);
|
|
59
76
|
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { type Policy } from './write-control.js';
|
|
3
|
+
export type Cud = 'read' | 'create' | 'update' | 'delete';
|
|
4
|
+
export interface ToolEntry {
|
|
5
|
+
name: string;
|
|
6
|
+
service: string;
|
|
7
|
+
cud: Cud;
|
|
8
|
+
}
|
|
9
|
+
export declare function inferCud(name: string): Cud;
|
|
10
|
+
export declare class ToolRegistry {
|
|
11
|
+
readonly tools: ToolEntry[];
|
|
12
|
+
readonly registerTool: McpServer['registerTool'];
|
|
13
|
+
constructor(server: McpServer, policy: Policy);
|
|
14
|
+
}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isAllowed, writeDisabledResult } from './write-control.js';
|
|
2
|
+
const CUD_OVERRIDES = {
|
|
3
|
+
drive_untrash: 'update',
|
|
4
|
+
};
|
|
5
|
+
const DELETE_VERB = /(^|_)(delete|remove|trash|clear|empty)(_|$)/;
|
|
6
|
+
const CREATE_VERB = /(^|_)(create|add|insert|send|upload|copy|import|append|submit|duplicate|share|quick)(_|$)/;
|
|
7
|
+
const UPDATE_VERB = /(^|_)(update|patch|modify|set|move|write|format|merge|unmerge|sort|replace|resize|publish|resolve)(_|$)/;
|
|
8
|
+
export function inferCud(name) {
|
|
9
|
+
const override = CUD_OVERRIDES[name];
|
|
10
|
+
if (override)
|
|
11
|
+
return override;
|
|
12
|
+
if (DELETE_VERB.test(name))
|
|
13
|
+
return 'delete';
|
|
14
|
+
if (CREATE_VERB.test(name))
|
|
15
|
+
return 'create';
|
|
16
|
+
if (UPDATE_VERB.test(name))
|
|
17
|
+
return 'update';
|
|
18
|
+
return 'read';
|
|
19
|
+
}
|
|
20
|
+
export class ToolRegistry {
|
|
21
|
+
tools = [];
|
|
22
|
+
registerTool;
|
|
23
|
+
constructor(server, policy) {
|
|
24
|
+
this.registerTool = ((name, config, handler) => {
|
|
25
|
+
const service = name.includes('_') ? name.slice(0, name.indexOf('_')) : name;
|
|
26
|
+
const cud = inferCud(name);
|
|
27
|
+
this.tools.push({ name, service, cud });
|
|
28
|
+
const guarded = cud === 'read'
|
|
29
|
+
? handler
|
|
30
|
+
: (...args) => isAllowed({ name, service, cud }, policy)
|
|
31
|
+
? handler(...args)
|
|
32
|
+
: writeDisabledResult({ name, service, cud }, policy);
|
|
33
|
+
return server.registerTool(name, config, guarded);
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
package/dist/tools/admin.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ToolRegistry } from '../registry.js';
|
|
2
2
|
/**
|
|
3
3
|
* Admin SDK tools require Workspace super-admin (or delegated admin) privileges on the target account.
|
|
4
4
|
* Personal `@gmail.com` accounts will 403 on every endpoint.
|
|
@@ -6,7 +6,7 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
|
6
6
|
* Writes are gated behind GOOGLE_ALLOW_ADMIN_WRITES=true to prevent accidents on small orgs
|
|
7
7
|
* (a stray users.update on a 3-person Workspace is a bad day).
|
|
8
8
|
*/
|
|
9
|
-
export declare function registerAdminTools(server:
|
|
9
|
+
export declare function registerAdminTools(server: ToolRegistry): void;
|
|
10
10
|
/**
|
|
11
11
|
* Alert Center tools. Gated behind the `alertcenter` optional bundle, NOT the
|
|
12
12
|
* admin bundle: the apps.alerts scope cannot be granted via this server's
|
|
@@ -14,4 +14,4 @@ export declare function registerAdminTools(server: McpServer): void;
|
|
|
14
14
|
* domain-wide delegation. Registered separately so a missing/ungrantable
|
|
15
15
|
* apps.alerts scope never blocks the working Admin SDK admin tools.
|
|
16
16
|
*/
|
|
17
|
-
export declare function registerAlertCenterTools(server:
|
|
17
|
+
export declare function registerAlertCenterTools(server: ToolRegistry): void;
|
package/dist/tools/calendar.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function registerCalendarTools(server:
|
|
1
|
+
import type { ToolRegistry } from '../registry.js';
|
|
2
|
+
export declare function registerCalendarTools(server: ToolRegistry): void;
|
package/dist/tools/chat.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function registerChatTools(server:
|
|
1
|
+
import type { ToolRegistry } from '../registry.js';
|
|
2
|
+
export declare function registerChatTools(server: ToolRegistry): void;
|
package/dist/tools/contacts.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function registerContactsTools(server:
|
|
1
|
+
import type { ToolRegistry } from '../registry.js';
|
|
2
|
+
export declare function registerContactsTools(server: ToolRegistry): void;
|
package/dist/tools/docs.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function registerDocsTools(server:
|
|
1
|
+
import type { ToolRegistry } from '../registry.js';
|
|
2
|
+
export declare function registerDocsTools(server: ToolRegistry): void;
|
|
3
3
|
export declare function buildParagraphStyle(input: {
|
|
4
4
|
namedStyleType?: string;
|
|
5
5
|
alignment?: string;
|
package/dist/tools/drive.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function registerDriveTools(server:
|
|
1
|
+
import type { ToolRegistry } from '../registry.js';
|
|
2
|
+
export declare function registerDriveTools(server: ToolRegistry): void;
|
package/dist/tools/forms.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function registerFormsTools(server:
|
|
1
|
+
import type { ToolRegistry } from '../registry.js';
|
|
2
|
+
export declare function registerFormsTools(server: ToolRegistry): void;
|
package/dist/tools/gmail.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function registerGmailTools(server:
|
|
1
|
+
import type { ToolRegistry } from '../registry.js';
|
|
2
|
+
export declare function registerGmailTools(server: ToolRegistry): void;
|
package/dist/tools/meet.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function registerMeetTools(server:
|
|
1
|
+
import type { ToolRegistry } from '../registry.js';
|
|
2
|
+
export declare function registerMeetTools(server: ToolRegistry): void;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function registerSearchConsoleTools(server:
|
|
1
|
+
import type { ToolRegistry } from '../registry.js';
|
|
2
|
+
export declare function registerSearchConsoleTools(server: ToolRegistry): void;
|
package/dist/tools/sheets.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function registerSheetsTools(server:
|
|
1
|
+
import type { ToolRegistry } from '../registry.js';
|
|
2
|
+
export declare function registerSheetsTools(server: ToolRegistry): void;
|
|
3
3
|
export declare function buildCellFormat(input: {
|
|
4
4
|
backgroundColor?: {
|
|
5
5
|
red?: number;
|
package/dist/tools/tasks.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function registerTasksTools(server:
|
|
1
|
+
import type { ToolRegistry } from '../registry.js';
|
|
2
|
+
export declare function registerTasksTools(server: ToolRegistry): void;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Cud } from './registry.js';
|
|
2
|
+
export type Profile = 'read-only' | 'safe-writes' | 'full-writes';
|
|
3
|
+
export interface Policy {
|
|
4
|
+
profile: Profile;
|
|
5
|
+
readOnly: boolean;
|
|
6
|
+
allow: string[];
|
|
7
|
+
deny: string[];
|
|
8
|
+
}
|
|
9
|
+
interface ToolRef {
|
|
10
|
+
name: string;
|
|
11
|
+
service: string;
|
|
12
|
+
cud: Cud;
|
|
13
|
+
}
|
|
14
|
+
export declare function resolvePolicy(env?: NodeJS.ProcessEnv): Policy;
|
|
15
|
+
export declare function isAllowed(tool: ToolRef, policy: Policy): boolean;
|
|
16
|
+
export declare function writeDisabledResult(tool: ToolRef, policy: Policy): {
|
|
17
|
+
content: {
|
|
18
|
+
type: "text";
|
|
19
|
+
text: string;
|
|
20
|
+
}[];
|
|
21
|
+
isError: true;
|
|
22
|
+
};
|
|
23
|
+
export declare function describePolicy(policy: Policy): string;
|
|
24
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const PROFILES = ['read-only', 'safe-writes', 'full-writes'];
|
|
2
|
+
function parseGlobs(value) {
|
|
3
|
+
return (value ?? '').split(',').map((s) => s.trim()).filter(Boolean);
|
|
4
|
+
}
|
|
5
|
+
export function resolvePolicy(env = process.env) {
|
|
6
|
+
const raw = (env.GOOGLE_PROFILE ?? 'read-only').trim();
|
|
7
|
+
return {
|
|
8
|
+
profile: PROFILES.includes(raw) ? raw : 'read-only',
|
|
9
|
+
readOnly: /^(1|true|yes)$/i.test(env.GOOGLE_READ_ONLY ?? ''),
|
|
10
|
+
allow: parseGlobs(env.GOOGLE_WRITE_ALLOW),
|
|
11
|
+
deny: parseGlobs(env.GOOGLE_WRITE_DENY),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function globToRegExp(glob) {
|
|
15
|
+
const escaped = glob.trim().replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
16
|
+
return new RegExp(`^${escaped}$`);
|
|
17
|
+
}
|
|
18
|
+
function candidates(tool) {
|
|
19
|
+
const op = tool.name.includes('_') ? tool.name.slice(tool.name.indexOf('_') + 1) : tool.name;
|
|
20
|
+
return [`${tool.service}:${tool.cud}`, `${tool.service}:${op}`];
|
|
21
|
+
}
|
|
22
|
+
function matchesAny(tool, globs) {
|
|
23
|
+
if (globs.length === 0)
|
|
24
|
+
return false;
|
|
25
|
+
const cands = candidates(tool);
|
|
26
|
+
return globs.some((g) => {
|
|
27
|
+
const re = globToRegExp(g);
|
|
28
|
+
return cands.some((c) => re.test(c));
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
function profileAllows(profile, cud) {
|
|
32
|
+
if (profile === 'full-writes')
|
|
33
|
+
return true;
|
|
34
|
+
if (profile === 'safe-writes')
|
|
35
|
+
return cud === 'create' || cud === 'update';
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
export function isAllowed(tool, policy) {
|
|
39
|
+
if (tool.cud === 'read')
|
|
40
|
+
return true;
|
|
41
|
+
if (policy.readOnly)
|
|
42
|
+
return false;
|
|
43
|
+
if (matchesAny(tool, policy.deny))
|
|
44
|
+
return false;
|
|
45
|
+
if (matchesAny(tool, policy.allow))
|
|
46
|
+
return true;
|
|
47
|
+
return profileAllows(policy.profile, tool.cud);
|
|
48
|
+
}
|
|
49
|
+
export function writeDisabledResult(tool, policy) {
|
|
50
|
+
const envelope = {
|
|
51
|
+
error: 'write_disabled',
|
|
52
|
+
message: `"${tool.name}" (${tool.cud}) is disabled by the current write-control policy (profile: ${policy.profile}${policy.readOnly ? ', GOOGLE_READ_ONLY=true' : ''}).`,
|
|
53
|
+
hint: `Enable via GOOGLE_PROFILE=safe-writes|full-writes, or GOOGLE_WRITE_ALLOW="${tool.service}:*".`,
|
|
54
|
+
retriable: false,
|
|
55
|
+
};
|
|
56
|
+
return {
|
|
57
|
+
content: [{ type: 'text', text: JSON.stringify(envelope) }],
|
|
58
|
+
isError: true,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function describePolicy(policy) {
|
|
62
|
+
return `profile=${policy.profile} readOnly=${policy.readOnly} allow=[${policy.allow.join(', ')}] deny=[${policy.deny.join(', ')}]`;
|
|
63
|
+
}
|
package/package.json
CHANGED