mcp-google-multi 4.5.0-alpha.2 → 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 +32 -17
- package/dist/registry.d.ts +2 -1
- package/dist/registry.js +11 -4
- 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
|
@@ -19,25 +19,11 @@ import { registerChatTools } from './tools/chat.js';
|
|
|
19
19
|
import { registerAdminTools, registerAlertCenterTools } from './tools/admin.js';
|
|
20
20
|
import { getOptionalBundles, getAdminAccounts } from './auth.js';
|
|
21
21
|
import { ToolRegistry } from './registry.js';
|
|
22
|
+
import { resolvePolicy, isAllowed, describePolicy } from './write-control.js';
|
|
22
23
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
23
24
|
const pkg = JSON.parse(readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf-8'));
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
const { runAuthFlow } = await import('./auth.js');
|
|
27
|
-
await runAuthFlow(process.argv);
|
|
28
|
-
return;
|
|
29
|
-
}
|
|
30
|
-
if (process.argv.includes('migrate-tokens')) {
|
|
31
|
-
const { runMigrateTokens } = await import('./migrate-tokens.js');
|
|
32
|
-
runMigrateTokens();
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
35
|
-
// MCP server mode — no console.log (stdio is the MCP channel)
|
|
36
|
-
const server = new McpServer({
|
|
37
|
-
name: 'mcp-google-multi',
|
|
38
|
-
version: pkg.version,
|
|
39
|
-
});
|
|
40
|
-
const registry = new ToolRegistry(server);
|
|
25
|
+
function buildRegistry(server, policy) {
|
|
26
|
+
const registry = new ToolRegistry(server, policy);
|
|
41
27
|
registerGmailTools(registry);
|
|
42
28
|
registerDriveTools(registry);
|
|
43
29
|
registerCalendarTools(registry);
|
|
@@ -56,6 +42,35 @@ async function main() {
|
|
|
56
42
|
registerAlertCenterTools(registry);
|
|
57
43
|
if (getAdminAccounts().length > 0)
|
|
58
44
|
registerAdminTools(registry);
|
|
45
|
+
return registry;
|
|
46
|
+
}
|
|
47
|
+
async function main() {
|
|
48
|
+
if (process.argv.includes('auth')) {
|
|
49
|
+
const { runAuthFlow } = await import('./auth.js');
|
|
50
|
+
await runAuthFlow(process.argv);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (process.argv.includes('migrate-tokens')) {
|
|
54
|
+
const { runMigrateTokens } = await import('./migrate-tokens.js');
|
|
55
|
+
runMigrateTokens();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
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();
|
|
69
|
+
const server = new McpServer({
|
|
70
|
+
name: 'mcp-google-multi',
|
|
71
|
+
version: pkg.version,
|
|
72
|
+
});
|
|
73
|
+
buildRegistry(server, policy);
|
|
59
74
|
const transport = new StdioServerTransport();
|
|
60
75
|
await server.connect(transport);
|
|
61
76
|
}
|
package/dist/registry.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import { type Policy } from './write-control.js';
|
|
2
3
|
export type Cud = 'read' | 'create' | 'update' | 'delete';
|
|
3
4
|
export interface ToolEntry {
|
|
4
5
|
name: string;
|
|
@@ -9,5 +10,5 @@ export declare function inferCud(name: string): Cud;
|
|
|
9
10
|
export declare class ToolRegistry {
|
|
10
11
|
readonly tools: ToolEntry[];
|
|
11
12
|
readonly registerTool: McpServer['registerTool'];
|
|
12
|
-
constructor(server: McpServer);
|
|
13
|
+
constructor(server: McpServer, policy: Policy);
|
|
13
14
|
}
|
package/dist/registry.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isAllowed, writeDisabledResult } from './write-control.js';
|
|
1
2
|
const CUD_OVERRIDES = {
|
|
2
3
|
drive_untrash: 'update',
|
|
3
4
|
};
|
|
@@ -19,11 +20,17 @@ export function inferCud(name) {
|
|
|
19
20
|
export class ToolRegistry {
|
|
20
21
|
tools = [];
|
|
21
22
|
registerTool;
|
|
22
|
-
constructor(server) {
|
|
23
|
-
this.registerTool = ((name,
|
|
23
|
+
constructor(server, policy) {
|
|
24
|
+
this.registerTool = ((name, config, handler) => {
|
|
24
25
|
const service = name.includes('_') ? name.slice(0, name.indexOf('_')) : name;
|
|
25
|
-
|
|
26
|
-
|
|
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);
|
|
27
34
|
});
|
|
28
35
|
}
|
|
29
36
|
}
|
|
@@ -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