mcp-google-multi 6.0.0-alpha.3 → 6.0.0-alpha.4
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/tools/_errors.js
CHANGED
|
@@ -20,6 +20,10 @@ const RETRIABLE_NET_CODES = new Set([
|
|
|
20
20
|
'EHOSTUNREACH', 'EPIPE', 'EAI_AGAIN', 'UND_ERR_CONNECT_TIMEOUT', 'UND_ERR_SOCKET',
|
|
21
21
|
]);
|
|
22
22
|
const NET_CODES = new Set([...RETRIABLE_NET_CODES, 'ENOTFOUND']);
|
|
23
|
+
// Local-filesystem syscall codes from caller-supplied paths (localPath/savePath).
|
|
24
|
+
// String codes, so they never collide with Google's numeric statuses; the
|
|
25
|
+
// network codes above are deliberately excluded.
|
|
26
|
+
const LOCAL_FS_CODES = new Set(['ENOENT', 'EACCES', 'EISDIR', 'ENOTDIR', 'EPERM', 'ELOOP', 'ENAMETOOLONG', 'ENOSPC']);
|
|
23
27
|
/** First known network code on the error or its cause chain (GaxiosError.cause
|
|
24
28
|
* -> FetchError; undici TypeError.cause -> AggregateError.errors). */
|
|
25
29
|
function netCodeOf(error) {
|
|
@@ -137,6 +141,18 @@ export function mapGoogleError(error, account, forbiddenHint, scopeContext) {
|
|
|
137
141
|
return { error: 'upstream_error', message, retriable: true, account };
|
|
138
142
|
}
|
|
139
143
|
if (status === undefined) {
|
|
144
|
+
const fsCode = typeof error?.code === 'string' && LOCAL_FS_CODES.has(error.code) ? error.code : undefined;
|
|
145
|
+
if (fsCode) {
|
|
146
|
+
const p = typeof error?.path === 'string' ? ` "${error.path}"` : '';
|
|
147
|
+
return {
|
|
148
|
+
error: 'invalid_params',
|
|
149
|
+
message: `Cannot access local path${p}: ${fsCode}`,
|
|
150
|
+
hint: 'The path must exist on the machine running this server and be accessible to it. ' +
|
|
151
|
+
'When the server runs remotely (HTTP transport), paths on your own machine are not visible to it.',
|
|
152
|
+
retriable: false,
|
|
153
|
+
account,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
140
156
|
const netCode = netCodeOf(error);
|
|
141
157
|
if (netCode) {
|
|
142
158
|
return {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
// path.basename() is a traversal guard — a caller-supplied filename must never escape savePath.
|
|
4
|
+
export function prepareLocalDest(savePath, filename) {
|
|
5
|
+
const dest = path.join(savePath, path.basename(filename));
|
|
6
|
+
fs.mkdirSync(savePath, { recursive: true });
|
|
7
|
+
return dest;
|
|
8
|
+
}
|
|
9
|
+
// fs.createReadStream() reports an unopenable path as an async 'error' EVENT;
|
|
10
|
+
// with no listener attached, that single event kills the whole process — fatal
|
|
11
|
+
// for the shared HTTP transport. Opening the fd first turns the open-failure
|
|
12
|
+
// class (ENOENT/EACCES/...) into a normal rejection the caller's try/catch can
|
|
13
|
+
// map to an error envelope.
|
|
14
|
+
export async function openLocalReadStream(localPath) {
|
|
15
|
+
const handle = await fs.promises.open(localPath, 'r');
|
|
16
|
+
// open() succeeds on a directory; fail it here rather than as an async read error.
|
|
17
|
+
if ((await handle.stat()).isDirectory()) {
|
|
18
|
+
await handle.close();
|
|
19
|
+
throw Object.assign(new Error(`EISDIR: illegal operation on a directory, read '${localPath}'`), {
|
|
20
|
+
code: 'EISDIR',
|
|
21
|
+
path: localPath,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
const stream = handle.createReadStream();
|
|
25
|
+
// Mid-read errors still reach the consumer through its own listeners; this
|
|
26
|
+
// one only closes the unhandled-'error' crash path.
|
|
27
|
+
stream.on('error', () => { });
|
|
28
|
+
return stream;
|
|
29
|
+
}
|
package/dist/tools/drive.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { ToolRegistry } from '../registry.js';
|
|
2
|
-
export declare function prepareLocalDest(savePath: string, filename: string): string;
|
|
3
2
|
export declare const DRIVE_QUERY_HINT: string;
|
|
4
3
|
export declare function normalizeDriveQuery(raw: string): string;
|
|
5
4
|
export declare function isDriveInvalidQuery(error: any): boolean;
|
package/dist/tools/drive.js
CHANGED
|
@@ -4,6 +4,7 @@ import { drive as driveClient } from '@googleapis/drive';
|
|
|
4
4
|
import { accountAliasSchema, getAccountSet } from '../accounts.js';
|
|
5
5
|
import { getClient } from '../client.js';
|
|
6
6
|
import { handleGoogleApiError } from './_errors.js';
|
|
7
|
+
import { openLocalReadStream, prepareLocalDest } from './_local-files.js';
|
|
7
8
|
import { isAllowed, writeDisabledResult } from '../write-control.js';
|
|
8
9
|
import { capText } from '../trim.js';
|
|
9
10
|
import * as fs from 'fs';
|
|
@@ -31,12 +32,6 @@ const COMMENT_FIELDS = `${COMMENT_BASE_FIELDS},replies(${REPLY_SUBFIELDS})`;
|
|
|
31
32
|
const COMMENT_LIST_FIELDS = `nextPageToken,comments(${COMMENT_BASE_FIELDS},replies(${REPLY_SUBFIELDS}))`;
|
|
32
33
|
const REPLY_FIELDS = `kind,htmlContent,${REPLY_SUBFIELDS}`;
|
|
33
34
|
const REPLY_LIST_FIELDS = `nextPageToken,replies(${REPLY_FIELDS})`;
|
|
34
|
-
// path.basename() is a traversal guard — a caller-supplied filename must never escape savePath.
|
|
35
|
-
export function prepareLocalDest(savePath, filename) {
|
|
36
|
-
const dest = path.join(savePath, path.basename(filename));
|
|
37
|
-
fs.mkdirSync(savePath, { recursive: true });
|
|
38
|
-
return dest;
|
|
39
|
-
}
|
|
40
35
|
export const DRIVE_QUERY_HINT = "Drive search syntax: a plain keyword is treated as a full-text search, but a " +
|
|
41
36
|
"structured query needs an operator, e.g. \"name contains 'report'\", " +
|
|
42
37
|
"\"mimeType = 'application/pdf'\", or \"'me' in owners\". " +
|
|
@@ -275,7 +270,7 @@ export function registerDriveTools(server) {
|
|
|
275
270
|
const auth = await getClient(account);
|
|
276
271
|
const drive = driveClient({ version: 'v3', auth });
|
|
277
272
|
const resolvedMime = mimeTypeArg ?? (mime.lookup(localPath) || 'application/octet-stream');
|
|
278
|
-
const fileStream =
|
|
273
|
+
const fileStream = await openLocalReadStream(localPath);
|
|
279
274
|
const res = await drive.files.create({
|
|
280
275
|
requestBody: {
|
|
281
276
|
name: filename,
|
|
@@ -414,7 +409,7 @@ export function registerDriveTools(server) {
|
|
|
414
409
|
if (localPathArg) {
|
|
415
410
|
params.media = {
|
|
416
411
|
mimeType: mimeTypeArg ?? (mime.lookup(localPathArg) || 'application/octet-stream'),
|
|
417
|
-
body:
|
|
412
|
+
body: await openLocalReadStream(localPathArg),
|
|
418
413
|
};
|
|
419
414
|
if (convertTo)
|
|
420
415
|
requestBody.mimeType = convertTo;
|
|
@@ -1440,7 +1435,7 @@ async function downloadAndUpload(sourceDrive, targetDrive, fileId, sourceMime, p
|
|
|
1440
1435
|
},
|
|
1441
1436
|
media: {
|
|
1442
1437
|
mimeType: plan.kind === 'native' ? plan.exportMime : (sourceMime ?? 'application/octet-stream'),
|
|
1443
|
-
body:
|
|
1438
|
+
body: await openLocalReadStream(tmp),
|
|
1444
1439
|
},
|
|
1445
1440
|
supportsAllDrives: true,
|
|
1446
1441
|
fields: 'id,name,mimeType,webViewLink',
|
package/dist/tools/gmail.js
CHANGED
|
@@ -5,6 +5,7 @@ import { accountAliasSchema } from '../accounts.js';
|
|
|
5
5
|
import { getClient } from '../client.js';
|
|
6
6
|
import { handleGoogleApiError, mapGoogleError } from './_errors.js';
|
|
7
7
|
import { buildReplyHeaders, composeRaw, renderMarkdown, htmlToMarkdown, HeaderInjectionError } from './gmail-mime.js';
|
|
8
|
+
import { prepareLocalDest } from './_local-files.js';
|
|
8
9
|
import addressparser from 'nodemailer/lib/addressparser/index.js';
|
|
9
10
|
import { lookup as lookupMime } from 'mime-types';
|
|
10
11
|
import { configDir } from '../config-file.js';
|
|
@@ -679,8 +680,7 @@ export function registerGmailTools(server) {
|
|
|
679
680
|
if (!data)
|
|
680
681
|
throw new Error('No attachment data returned');
|
|
681
682
|
const buffer = Buffer.from(data, 'base64url');
|
|
682
|
-
|
|
683
|
-
const fullPath = path.join(savePath, path.basename(filename));
|
|
683
|
+
const fullPath = prepareLocalDest(savePath, filename);
|
|
684
684
|
await fs.promises.writeFile(fullPath, buffer, { mode: 0o600 });
|
|
685
685
|
return {
|
|
686
686
|
content: [{ type: 'text', text: `Saved to ${fullPath} (${buffer.length} bytes)` }],
|
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.4",
|
|
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",
|