mcp-google-multi 6.0.0-alpha.4 → 6.0.0-alpha.5

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.
@@ -17,3 +17,4 @@ export declare const BUNDLE_ALIASES: Record<string, string>;
17
17
  export declare function resolveBundleAliases(bundles: string[]): string[];
18
18
  /** Closest catalog key for E_UNKNOWN_BUNDLE remediation (edit distance <= 2). */
19
19
  export declare function closestBundle(name: string): string | undefined;
20
+ export declare function editDistance(a: string, b: string): number;
@@ -169,7 +169,7 @@ export function closestBundle(name) {
169
169
  }
170
170
  return best;
171
171
  }
172
- function editDistance(a, b) {
172
+ export function editDistance(a, b) {
173
173
  const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
174
174
  for (let j = 1; j <= b.length; j++)
175
175
  dp[0][j] = j;
@@ -1,4 +1,5 @@
1
1
  import type { ToolRegistry } from '../registry.js';
2
+ export declare function isTextualMime(mimeType: string): boolean;
2
3
  export declare const DRIVE_QUERY_HINT: string;
3
4
  export declare function normalizeDriveQuery(raw: string): string;
4
5
  export declare function isDriveInvalidQuery(error: any): boolean;
@@ -25,6 +25,29 @@ const GOOGLE_WORKSPACE_TYPES = new Set([
25
25
  'application/vnd.google-apps.presentation',
26
26
  'application/vnd.google-apps.drawing',
27
27
  ]);
28
+ // drive_read inlines only textual content. Beyond text/*, RFC 6839 structured-
29
+ // syntax suffixes (+json/+xml/...) and a few bare application/* types are text
30
+ // in practice — image/svg+xml was the motivating false "binary" refusal.
31
+ const TEXTUAL_EXACT = new Set([
32
+ 'application/json',
33
+ 'application/xml',
34
+ 'application/javascript',
35
+ 'application/x-ndjson',
36
+ 'application/yaml',
37
+ 'application/x-yaml',
38
+ 'application/sql',
39
+ 'application/x-sh',
40
+ 'application/csv',
41
+ ]);
42
+ export function isTextualMime(mimeType) {
43
+ const bare = mimeType.split(';')[0].trim().toLowerCase();
44
+ if (bare.startsWith('text/'))
45
+ return true;
46
+ if (/\+(json|xml|yaml|toml|csv)$/.test(bare))
47
+ return true;
48
+ return TEXTUAL_EXACT.has(bare);
49
+ }
50
+ const BINARY_READ_HINT = 'Binary content cannot be inlined. Use drive_download to save the file to disk, or drive_export for Google Workspace files.';
28
51
  // Comment/Reply fields list — Drive API requires explicit `fields` on every call.
29
52
  const COMMENT_BASE_FIELDS = 'id,kind,content,htmlContent,createdTime,modifiedTime,resolved,anchor,author,deleted,quotedFileContent';
30
53
  const REPLY_SUBFIELDS = 'id,content,action,createdTime,modifiedTime,author,deleted';
@@ -126,7 +149,7 @@ export function registerDriveTools(server) {
126
149
  });
127
150
  server.registerTool('drive_read', {
128
151
  _meta: { 'anthropic/maxResultSizeChars': 100_000 },
129
- description: 'Read the content of a Google Drive file (returns up to maxChars characters per call; non-Google-native files over 2MB return too_large)',
152
+ description: 'Read the content of a Google Drive file: Workspace docs and textual types (text/*, JSON/XML/SVG and similar) inline; other binaries return error:binary (returns up to maxChars characters per call; non-Google-native files over 2MB return too_large)',
130
153
  inputSchema: {
131
154
  account: accountEnum.describe('Google account alias'),
132
155
  fileId: z.string().describe('Google Drive file ID'),
@@ -179,6 +202,7 @@ export function registerDriveTools(server) {
179
202
  name,
180
203
  mimeType,
181
204
  error: 'binary',
205
+ hint: BINARY_READ_HINT,
182
206
  webViewLink,
183
207
  }, null, 2),
184
208
  }],
@@ -199,7 +223,7 @@ export function registerDriveTools(server) {
199
223
  }],
200
224
  };
201
225
  }
202
- if (mimeType?.startsWith('text/')) {
226
+ if (mimeType && isTextualMime(mimeType)) {
203
227
  const downloaded = await drive.files.get({ fileId, alt: 'media', supportsAllDrives: true }, { responseType: 'text' });
204
228
  return respond(String(downloaded.data));
205
229
  }
@@ -211,6 +235,7 @@ export function registerDriveTools(server) {
211
235
  name,
212
236
  mimeType,
213
237
  error: 'binary',
238
+ hint: BINARY_READ_HINT,
214
239
  webViewLink,
215
240
  }, null, 2),
216
241
  }],
@@ -2,9 +2,12 @@ import type { ToolRegistry } from '../registry.js';
2
2
  import { type Policy } from '../write-control.js';
3
3
  import { getClient } from '../client.js';
4
4
  import { type Toolsets } from '../toolsets.js';
5
- import { type DiscoveryDeps } from '../discovery-client.js';
5
+ import { type DiscoveryDeps, type DiscoveryMethod } from '../discovery-client.js';
6
6
  export interface EscapeDeps extends DiscoveryDeps {
7
7
  getClientFn?: typeof getClient;
8
8
  toolsets?: Toolsets;
9
9
  }
10
+ /** Up to three closest known ids for the unknown_method did-you-mean hint;
11
+ * bounded distance so unrelated ids never masquerade as suggestions. */
12
+ export declare function nearestMethodIds(methodId: string, index: DiscoveryMethod[]): string[];
10
13
  export declare function registerEscapeTools(registry: ToolRegistry, policy: Policy, deps?: EscapeDeps): void;
@@ -4,6 +4,7 @@ import { accountAliasSchema } from '../accounts.js';
4
4
  import { getClient } from '../client.js';
5
5
  import { coerceJson } from './_coerce.js';
6
6
  import { getToolsets, toolsetEnabled } from '../toolsets.js';
7
+ import { editDistance } from '../scope-catalog.js';
7
8
  import { executeApiMethod, jsonResult } from '../executor.js';
8
9
  import { WORKSPACE_APIS, cudFromMethod, loadMethodIndex, searchMethods, } from '../discovery-client.js';
9
10
  const accountEnum = accountAliasSchema.optional();
@@ -29,6 +30,17 @@ const SERVICE_FOR_ALIAS = {
29
30
  admin_datatransfer: 'admin',
30
31
  groupssettings: 'groupssettings',
31
32
  };
33
+ /** Up to three closest known ids for the unknown_method did-you-mean hint;
34
+ * bounded distance so unrelated ids never masquerade as suggestions. */
35
+ export function nearestMethodIds(methodId, index) {
36
+ const maxDist = Math.max(3, Math.floor(methodId.length / 3));
37
+ return index
38
+ .map((m) => ({ id: m.id, d: editDistance(methodId.toLowerCase(), m.id.toLowerCase()) }))
39
+ .filter((x) => x.d <= maxDist)
40
+ .sort((a, b) => a.d - b.d)
41
+ .slice(0, 3)
42
+ .map((x) => x.id);
43
+ }
32
44
  function describeMethod(m) {
33
45
  return {
34
46
  api: m.api,
@@ -127,12 +139,25 @@ export function registerEscapeTools(registry, policy, deps = {}) {
127
139
  catch (err) {
128
140
  return jsonResult({ error: 'discovery_unavailable', message: err.message, retriable: true, account }, true);
129
141
  }
130
- const method = index.find((m) => m.id === methodId);
142
+ let method = index.find((m) => m.id === methodId);
143
+ if (!method) {
144
+ // Some discovery docs keep a legacy id prefix (the searchconsole doc's
145
+ // methods are webmasters.*): when the caller prefixed with our api
146
+ // alias, retry under the doc's own prefix before failing.
147
+ const docPrefix = index[0]?.id.split('.')[0];
148
+ const [head, ...rest] = String(methodId).split('.');
149
+ if (docPrefix && head === api && head !== docPrefix && rest.length > 0) {
150
+ const swapped = [docPrefix, ...rest].join('.');
151
+ method = index.find((m) => m.id === swapped);
152
+ }
153
+ }
131
154
  if (!method) {
155
+ const near = nearestMethodIds(String(methodId), index);
132
156
  return jsonResult({
133
157
  error: 'unknown_method',
134
158
  message: `No method "${methodId}" in ${api}.`,
135
- hint: `Use google_api_search({query: "...", api: "${api}"}) to find the right method id.`,
159
+ hint: `${near.length ? `Did you mean: ${near.join(', ')}? ` : ''}` +
160
+ `Use google_api_search({query: "...", api: "${api}"}) to find the right method id.`,
136
161
  retriable: false,
137
162
  account,
138
163
  }, true);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-google-multi",
3
- "version": "6.0.0-alpha.4",
3
+ "version": "6.0.0-alpha.5",
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",