mcp-google-multi 6.0.0-alpha.34 → 6.0.0-alpha.35
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/write-control.d.ts +13 -0
- package/dist/write-control.js +50 -6
- package/package.json +1 -1
package/dist/write-control.d.ts
CHANGED
|
@@ -36,6 +36,19 @@ export declare function resolvePolicy(env?: NodeJS.ProcessEnv, opts?: {
|
|
|
36
36
|
transport?: Transport;
|
|
37
37
|
}): Policy;
|
|
38
38
|
export declare function isAllowed(tool: ToolRef, policy: Policy): boolean;
|
|
39
|
+
export type DenyReason = {
|
|
40
|
+
rule: 'read_only';
|
|
41
|
+
} | {
|
|
42
|
+
rule: 'deny_glob';
|
|
43
|
+
pattern: string;
|
|
44
|
+
} | {
|
|
45
|
+
rule: 'profile';
|
|
46
|
+
profile: Profile;
|
|
47
|
+
};
|
|
48
|
+
/** WHICH rule refused the call. `isAllowed` has a precedence order, so exactly
|
|
49
|
+
* one rule decides; naming the others sends the caller to a setting that
|
|
50
|
+
* cannot change the outcome. */
|
|
51
|
+
export declare function denyReason(tool: ToolRef, policy: Policy): DenyReason | undefined;
|
|
39
52
|
export declare function writeDisabledResult(tool: ToolRef, policy: Policy, account?: string): {
|
|
40
53
|
content: {
|
|
41
54
|
type: "text";
|
package/dist/write-control.js
CHANGED
|
@@ -45,19 +45,24 @@ function globToRegExp(glob) {
|
|
|
45
45
|
const escaped = glob.trim().replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*');
|
|
46
46
|
return new RegExp(`^${escaped}$`);
|
|
47
47
|
}
|
|
48
|
+
function opOf(tool) {
|
|
49
|
+
return tool.name.includes('_') ? tool.name.slice(tool.name.indexOf('_') + 1) : tool.name;
|
|
50
|
+
}
|
|
48
51
|
function candidates(tool) {
|
|
49
|
-
|
|
50
|
-
return [`${tool.service}:${tool.cud}`, `${tool.service}:${op}`];
|
|
52
|
+
return [`${tool.service}:${tool.cud}`, `${tool.service}:${opOf(tool)}`];
|
|
51
53
|
}
|
|
52
|
-
function
|
|
54
|
+
function firstMatch(tool, globs) {
|
|
53
55
|
if (globs.length === 0)
|
|
54
|
-
return
|
|
56
|
+
return undefined;
|
|
55
57
|
const cands = candidates(tool);
|
|
56
|
-
return globs.
|
|
58
|
+
return globs.find((g) => {
|
|
57
59
|
const re = globToRegExp(g);
|
|
58
60
|
return cands.some((c) => re.test(c));
|
|
59
61
|
});
|
|
60
62
|
}
|
|
63
|
+
function matchesAny(tool, globs) {
|
|
64
|
+
return firstMatch(tool, globs) !== undefined;
|
|
65
|
+
}
|
|
61
66
|
function profileAllows(profile, cud) {
|
|
62
67
|
if (profile === 'full-writes')
|
|
63
68
|
return true;
|
|
@@ -76,11 +81,50 @@ export function isAllowed(tool, policy) {
|
|
|
76
81
|
return true;
|
|
77
82
|
return profileAllows(policy.profile, tool.cud);
|
|
78
83
|
}
|
|
84
|
+
/** Which profiles would permit this cud at all. */
|
|
85
|
+
function profilesAllowing(cud) {
|
|
86
|
+
return PROFILES.filter((p) => profileAllows(p, cud));
|
|
87
|
+
}
|
|
88
|
+
/** WHICH rule refused the call. `isAllowed` has a precedence order, so exactly
|
|
89
|
+
* one rule decides; naming the others sends the caller to a setting that
|
|
90
|
+
* cannot change the outcome. */
|
|
91
|
+
export function denyReason(tool, policy) {
|
|
92
|
+
if (tool.cud === 'read')
|
|
93
|
+
return undefined;
|
|
94
|
+
if (policy.readOnly)
|
|
95
|
+
return { rule: 'read_only' };
|
|
96
|
+
const pattern = firstMatch(tool, policy.deny);
|
|
97
|
+
if (pattern !== undefined)
|
|
98
|
+
return { rule: 'deny_glob', pattern };
|
|
99
|
+
if (matchesAny(tool, policy.allow))
|
|
100
|
+
return undefined;
|
|
101
|
+
if (profileAllows(policy.profile, tool.cud))
|
|
102
|
+
return undefined;
|
|
103
|
+
return { rule: 'profile', profile: policy.profile };
|
|
104
|
+
}
|
|
105
|
+
function denyHint(tool, reason) {
|
|
106
|
+
if (reason.rule === 'read_only') {
|
|
107
|
+
return 'GOOGLE_READ_ONLY=true refuses every write, ahead of the profile and the allow-list, so neither of those can enable this. Unset GOOGLE_READ_ONLY to allow writes.';
|
|
108
|
+
}
|
|
109
|
+
if (reason.rule === 'deny_glob') {
|
|
110
|
+
return `GOOGLE_WRITE_DENY pattern "${reason.pattern}" matches this tool, and deny is checked before the allow-list and the profile, so neither can override it. Remove or narrow that pattern.`;
|
|
111
|
+
}
|
|
112
|
+
const usable = profilesAllowing(tool.cud).filter((p) => p !== reason.profile);
|
|
113
|
+
const profiles = usable.length > 0
|
|
114
|
+
? `Use GOOGLE_PROFILE=${usable.join(' or ')}`
|
|
115
|
+
: 'No profile permits this operation';
|
|
116
|
+
// The allow-list vocabulary is service:cud or service:op, never the tool
|
|
117
|
+
// name, so suggesting the name would hand back a pattern that never matches.
|
|
118
|
+
return `Profile "${reason.profile}" does not permit ${tool.cud}. ${profiles}, or allow just this one with GOOGLE_WRITE_ALLOW="${tool.service}:${opOf(tool)}".`;
|
|
119
|
+
}
|
|
79
120
|
export function writeDisabledResult(tool, policy, account) {
|
|
121
|
+
// Recomputed rather than passed in: every call site has already run
|
|
122
|
+
// isAllowed, and a hint that disagrees with the verdict is worse than none.
|
|
123
|
+
const reason = denyReason(tool, policy) ?? { rule: 'profile', profile: policy.profile };
|
|
80
124
|
const envelope = {
|
|
81
125
|
error: 'write_disabled',
|
|
82
126
|
message: `"${tool.name}" (${tool.cud}) is disabled by the current write-control policy (profile: ${policy.profile}${policy.readOnly ? ', GOOGLE_READ_ONLY=true' : ''}).`,
|
|
83
|
-
hint:
|
|
127
|
+
hint: denyHint(tool, reason),
|
|
84
128
|
retriable: false,
|
|
85
129
|
...(account ? { account } : {}),
|
|
86
130
|
};
|
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.35",
|
|
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",
|