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.
package/dist/scope-catalog.d.ts
CHANGED
|
@@ -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;
|
package/dist/scope-catalog.js
CHANGED
|
@@ -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;
|
package/dist/tools/drive.d.ts
CHANGED
|
@@ -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;
|
package/dist/tools/drive.js
CHANGED
|
@@ -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
|
|
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;
|
package/dist/tools/google-api.js
CHANGED
|
@@ -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
|
-
|
|
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: `
|
|
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.
|
|
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",
|