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.
@@ -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";
@@ -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
- const op = tool.name.includes('_') ? tool.name.slice(tool.name.indexOf('_') + 1) : tool.name;
50
- return [`${tool.service}:${tool.cud}`, `${tool.service}:${op}`];
52
+ return [`${tool.service}:${tool.cud}`, `${tool.service}:${opOf(tool)}`];
51
53
  }
52
- function matchesAny(tool, globs) {
54
+ function firstMatch(tool, globs) {
53
55
  if (globs.length === 0)
54
- return false;
56
+ return undefined;
55
57
  const cands = candidates(tool);
56
- return globs.some((g) => {
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: `Enable via GOOGLE_PROFILE=safe-writes|full-writes, or GOOGLE_WRITE_ALLOW="${tool.service}:*".`,
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.34",
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",