mcp-google-multi 4.3.0 → 4.5.0-alpha.1
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/README.md +35 -4
- package/dist/accounts.d.ts +1 -0
- package/dist/accounts.js +4 -1
- package/dist/auth.js +7 -6
- package/dist/client.js +8 -19
- package/dist/index.js +5 -0
- package/dist/migrate-tokens.d.ts +1 -0
- package/dist/migrate-tokens.js +22 -0
- package/dist/token-store.d.ts +7 -0
- package/dist/token-store.js +66 -0
- package/dist/tools/_coerce.d.ts +4 -0
- package/dist/tools/_coerce.js +36 -0
- package/dist/tools/_errors.d.ts +8 -1
- package/dist/tools/_errors.js +58 -31
- package/dist/tools/admin.js +5 -4
- package/dist/tools/calendar.js +5 -4
- package/dist/tools/chat.js +2 -1
- package/dist/tools/docs.js +15 -14
- package/dist/tools/drive.js +14 -13
- package/dist/tools/gmail-mime.d.ts +9 -0
- package/dist/tools/gmail-mime.js +35 -0
- package/dist/tools/gmail.js +44 -21
- package/dist/tools/searchconsole.js +4 -3
- package/dist/tools/sheets.js +38 -37
- package/dist/tools/tasks.js +5 -4
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A local [MCP](https://modelcontextprotocol.io) server that gives Claude Code (and any MCP client) access to **Gmail**, **Google Drive**, **Google Calendar**, **Google Sheets**, **Google Docs**, **Google Contacts**, **Google Search Console**, **Google Tasks**, **Google Meet**, and optionally **Google Forms**, **Google Chat**, and **Google Workspace Admin** APIs across multiple Google accounts simultaneously.
|
|
4
4
|
|
|
5
|
+
[](https://www.npmjs.com/package/mcp-google-multi)
|
|
6
|
+
|
|
5
7
|
## Features
|
|
6
8
|
|
|
7
9
|
- **Multi-account** — manage any number of Google accounts from a single server
|
|
@@ -20,9 +22,9 @@ A local [MCP](https://modelcontextprotocol.io) server that gives Claude Code (an
|
|
|
20
22
|
| `gmail_search` | Search messages with Gmail query syntax |
|
|
21
23
|
| `gmail_read` | Read a full message by ID |
|
|
22
24
|
| `gmail_read_thread` | Read all messages in a thread |
|
|
23
|
-
| `gmail_send` | Send an email |
|
|
25
|
+
| `gmail_send` | Send an email (optional `htmlBody` for multipart/alternative) |
|
|
24
26
|
| `gmail_download_attachment` | Download an email attachment to local disk |
|
|
25
|
-
| `gmail_create_draft` | Create a draft |
|
|
27
|
+
| `gmail_create_draft` | Create a draft (optional `htmlBody` for multipart/alternative) |
|
|
26
28
|
| `gmail_modify_labels` | Add/remove labels on a message (star, archive, mark read, etc.) |
|
|
27
29
|
| `gmail_trash` | Move a message to Trash (recoverable) |
|
|
28
30
|
| `gmail_delete` | Permanently delete a message (irreversible) |
|
|
@@ -249,14 +251,25 @@ Enable with `GOOGLE_OPTIONAL_SCOPES=alertcenter` in `.env`.
|
|
|
249
251
|
|
|
250
252
|
### 2. Install & Configure
|
|
251
253
|
|
|
254
|
+
**Option A — from npm (no clone):**
|
|
255
|
+
|
|
256
|
+
```bash
|
|
257
|
+
npm install -g mcp-google-multi # installs the `mcp-google-multi` CLI
|
|
258
|
+
# …or run on demand without installing: npx mcp-google-multi
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
Published with channel dist-tags — `@latest` (stable), `@dev` (alpha), `@staging` (beta); e.g. `npm i -g mcp-google-multi@dev` for the latest alpha.
|
|
262
|
+
|
|
263
|
+
**Option B — from source:**
|
|
264
|
+
|
|
252
265
|
```bash
|
|
253
266
|
git clone https://github.com/bakissation/mcp-google-multi.git
|
|
254
267
|
cd mcp-google-multi
|
|
255
|
-
npm install
|
|
268
|
+
npm install && npm run build
|
|
256
269
|
cp .env.example .env
|
|
257
270
|
```
|
|
258
271
|
|
|
259
|
-
|
|
272
|
+
Create a `.env` (from source, `.env.example` is the template; from npm, make one in the directory you'll run the server from) with:
|
|
260
273
|
|
|
261
274
|
```env
|
|
262
275
|
GOOGLE_CLIENT_ID=your_client_id_here
|
|
@@ -310,6 +323,24 @@ Or add it manually to your Claude Code MCP config:
|
|
|
310
323
|
}
|
|
311
324
|
```
|
|
312
325
|
|
|
326
|
+
If you installed from npm, point at the package instead and pass config via `env` (authenticate first with `mcp-google-multi auth --account <alias>`):
|
|
327
|
+
|
|
328
|
+
```json
|
|
329
|
+
{
|
|
330
|
+
"mcpServers": {
|
|
331
|
+
"google-multi": {
|
|
332
|
+
"command": "npx",
|
|
333
|
+
"args": ["-y", "mcp-google-multi"],
|
|
334
|
+
"env": {
|
|
335
|
+
"GOOGLE_CLIENT_ID": "your_client_id_here",
|
|
336
|
+
"GOOGLE_CLIENT_SECRET": "your_client_secret_here",
|
|
337
|
+
"GOOGLE_ACCOUNTS": "work:you@company.com,personal:you@gmail.com"
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
313
344
|
## Migrating from v3
|
|
314
345
|
|
|
315
346
|
v4.0.0 grows the base OAuth scope set (Tasks and Meet are now included by default). Existing v3 tokens lack those scopes and the corresponding tools will 403.
|
package/dist/accounts.d.ts
CHANGED
package/dist/accounts.js
CHANGED
|
@@ -3,7 +3,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
5
5
|
dotenv.config({ path: path.resolve(__dirname, '..', '.env') });
|
|
6
|
-
const tokenDir =
|
|
6
|
+
const tokenDir = process.env.TOKEN_STORE_PATH
|
|
7
|
+
? path.resolve(process.env.TOKEN_STORE_PATH)
|
|
8
|
+
: path.resolve(__dirname, '..', 'tokens');
|
|
7
9
|
/**
|
|
8
10
|
* Parse accounts from the GOOGLE_ACCOUNTS env var.
|
|
9
11
|
* Format: "alias1:email1,alias2:email2,..."
|
|
@@ -39,6 +41,7 @@ function parseAccounts() {
|
|
|
39
41
|
configs[alias] = {
|
|
40
42
|
email,
|
|
41
43
|
tokenPath: path.join(tokenDir, alias, 'token.json'),
|
|
44
|
+
encPath: path.join(tokenDir, `${alias}.enc`),
|
|
42
45
|
};
|
|
43
46
|
}
|
|
44
47
|
if (aliases.length === 0) {
|
package/dist/auth.js
CHANGED
|
@@ -2,11 +2,10 @@ import { google } from 'googleapis';
|
|
|
2
2
|
import http from 'node:http';
|
|
3
3
|
import { URL } from 'node:url';
|
|
4
4
|
import { randomBytes } from 'node:crypto';
|
|
5
|
-
import fs from 'node:fs/promises';
|
|
6
|
-
import path from 'node:path';
|
|
7
5
|
import open from 'open';
|
|
8
6
|
import destroyer from 'server-destroy';
|
|
9
7
|
import { ACCOUNTS, ACCOUNT_CONFIG } from './accounts.js';
|
|
8
|
+
import { writeToken } from './token-store.js';
|
|
10
9
|
// ─── Scope tiers ────────────────────────────────────────────────────────
|
|
11
10
|
//
|
|
12
11
|
// BASE: always granted. Existing v3 surface + Tasks + Meet (added in v4.0.0).
|
|
@@ -98,6 +97,10 @@ export async function runAuthFlow(args) {
|
|
|
98
97
|
}
|
|
99
98
|
const config = ACCOUNT_CONFIG[alias];
|
|
100
99
|
const scopes = resolveScopesForAccount(alias);
|
|
100
|
+
if (!process.env.MASTER_KEY) {
|
|
101
|
+
console.error('MASTER_KEY is not set. Generate one (openssl rand -base64 32) and add it to .env before authenticating.');
|
|
102
|
+
process.exit(1);
|
|
103
|
+
}
|
|
101
104
|
const oauth2Client = new google.auth.OAuth2(process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, 'http://localhost:4242/oauth2callback');
|
|
102
105
|
// CSRF protection for the OAuth callback (RFC 6749 §10.12).
|
|
103
106
|
const expectedState = randomBytes(32).toString('hex');
|
|
@@ -145,13 +148,11 @@ export async function runAuthFlow(args) {
|
|
|
145
148
|
return;
|
|
146
149
|
}
|
|
147
150
|
const { tokens } = await oauth2Client.getToken(code);
|
|
148
|
-
|
|
149
|
-
// 0o600: tokens grant full account access; keep them user-only.
|
|
150
|
-
await fs.writeFile(config.tokenPath, JSON.stringify(tokens, null, 2), { mode: 0o600 });
|
|
151
|
+
writeToken(alias, tokens);
|
|
151
152
|
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
152
153
|
res.end('<h2>Authentication successful!</h2><p>You can close this tab.</p>');
|
|
153
154
|
server.destroy();
|
|
154
|
-
console.log(`Token saved
|
|
155
|
+
console.log(`Token saved (encrypted) for ${alias}.`);
|
|
155
156
|
resolve();
|
|
156
157
|
}
|
|
157
158
|
}
|
package/dist/client.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { google } from 'googleapis';
|
|
2
|
-
import fs from 'node:fs/promises';
|
|
3
2
|
import { ACCOUNT_CONFIG } from './accounts.js';
|
|
3
|
+
import { readToken, writeToken } from './token-store.js';
|
|
4
4
|
export async function getClient(account) {
|
|
5
5
|
const config = ACCOUNT_CONFIG[account];
|
|
6
6
|
if (!process.env.GOOGLE_CLIENT_ID || !process.env.GOOGLE_CLIENT_SECRET) {
|
|
@@ -8,26 +8,15 @@ export async function getClient(account) {
|
|
|
8
8
|
'Check that .env exists in the project root or pass them as env vars.');
|
|
9
9
|
}
|
|
10
10
|
const oauth2Client = new google.auth.OAuth2(process.env.GOOGLE_CLIENT_ID, process.env.GOOGLE_CLIENT_SECRET, 'http://localhost:4242/oauth2callback');
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
}
|
|
16
|
-
catch {
|
|
17
|
-
throw new Error(`No token file found for account "${account}" at ${config.tokenPath}. ` +
|
|
18
|
-
`Run: node dist/index.js auth --account ${account}`);
|
|
11
|
+
const tokenData = readToken(account);
|
|
12
|
+
if (!tokenData) {
|
|
13
|
+
throw new Error(`No token found for account "${account}" (${config.email}). ` +
|
|
14
|
+
`Run: npx mcp-google-multi auth --account ${account}`);
|
|
19
15
|
}
|
|
20
16
|
oauth2Client.setCredentials(tokenData);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const existing = JSON.parse(await fs.readFile(config.tokenPath, 'utf-8'));
|
|
25
|
-
const merged = { ...existing, ...tokens };
|
|
26
|
-
await fs.writeFile(config.tokenPath, JSON.stringify(merged, null, 2), { mode: 0o600 });
|
|
27
|
-
}
|
|
28
|
-
catch {
|
|
29
|
-
await fs.writeFile(config.tokenPath, JSON.stringify(tokens, null, 2), { mode: 0o600 });
|
|
30
|
-
}
|
|
17
|
+
oauth2Client.on('tokens', (tokens) => {
|
|
18
|
+
const existing = readToken(account) ?? {};
|
|
19
|
+
writeToken(account, { ...existing, ...tokens });
|
|
31
20
|
});
|
|
32
21
|
return oauth2Client;
|
|
33
22
|
}
|
package/dist/index.js
CHANGED
|
@@ -26,6 +26,11 @@ async function main() {
|
|
|
26
26
|
await runAuthFlow(process.argv);
|
|
27
27
|
return;
|
|
28
28
|
}
|
|
29
|
+
if (process.argv.includes('migrate-tokens')) {
|
|
30
|
+
const { runMigrateTokens } = await import('./migrate-tokens.js');
|
|
31
|
+
runMigrateTokens();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
29
34
|
// MCP server mode — no console.log (stdio is the MCP channel)
|
|
30
35
|
const server = new McpServer({
|
|
31
36
|
name: 'mcp-google-multi',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runMigrateTokens(): void;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { ACCOUNTS, ACCOUNT_CONFIG } from './accounts.js';
|
|
3
|
+
import { writeToken, hasToken } from './token-store.js';
|
|
4
|
+
export function runMigrateTokens() {
|
|
5
|
+
let migrated = 0;
|
|
6
|
+
let skipped = 0;
|
|
7
|
+
for (const alias of ACCOUNTS) {
|
|
8
|
+
const plain = ACCOUNT_CONFIG[alias].tokenPath;
|
|
9
|
+
if (!fs.existsSync(plain))
|
|
10
|
+
continue;
|
|
11
|
+
if (hasToken(alias)) {
|
|
12
|
+
console.log(`• ${alias}: encrypted token already exists, skipping`);
|
|
13
|
+
skipped++;
|
|
14
|
+
continue;
|
|
15
|
+
}
|
|
16
|
+
writeToken(alias, JSON.parse(fs.readFileSync(plain, 'utf8')));
|
|
17
|
+
console.log(`✓ ${alias}: migrated ${plain} → encrypted store`);
|
|
18
|
+
migrated++;
|
|
19
|
+
}
|
|
20
|
+
console.log(`Done. ${migrated} migrated, ${skipped} skipped. ` +
|
|
21
|
+
'Delete the plaintext tokens/<alias>/token.json once verified.');
|
|
22
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { TokenData } from './types.js';
|
|
2
|
+
export declare function deriveKey(masterKey: string): Buffer;
|
|
3
|
+
export declare function encryptToken(data: object, masterKey: string): string;
|
|
4
|
+
export declare function decryptToken(fileContents: string, masterKey: string): TokenData;
|
|
5
|
+
export declare function readToken(alias: string): TokenData | null;
|
|
6
|
+
export declare function writeToken(alias: string, data: object): void;
|
|
7
|
+
export declare function hasToken(alias: string): boolean;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, createHash, randomBytes } from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { ACCOUNT_CONFIG } from './accounts.js';
|
|
5
|
+
const ENC_VERSION = 1;
|
|
6
|
+
const ALGO = 'aes-256-gcm';
|
|
7
|
+
export function deriveKey(masterKey) {
|
|
8
|
+
if (!masterKey) {
|
|
9
|
+
throw new Error('MASTER_KEY is required to encrypt/decrypt tokens. Generate one with: openssl rand -base64 32');
|
|
10
|
+
}
|
|
11
|
+
const raw = Buffer.from(masterKey, 'base64');
|
|
12
|
+
if (raw.length === 32)
|
|
13
|
+
return raw;
|
|
14
|
+
return createHash('sha256').update(masterKey, 'utf8').digest();
|
|
15
|
+
}
|
|
16
|
+
export function encryptToken(data, masterKey) {
|
|
17
|
+
const key = deriveKey(masterKey);
|
|
18
|
+
const iv = randomBytes(12);
|
|
19
|
+
const cipher = createCipheriv(ALGO, key, iv);
|
|
20
|
+
const ciphertext = Buffer.concat([
|
|
21
|
+
cipher.update(Buffer.from(JSON.stringify(data), 'utf8')),
|
|
22
|
+
cipher.final(),
|
|
23
|
+
]);
|
|
24
|
+
const file = {
|
|
25
|
+
v: ENC_VERSION,
|
|
26
|
+
iv: iv.toString('base64'),
|
|
27
|
+
tag: cipher.getAuthTag().toString('base64'),
|
|
28
|
+
data: ciphertext.toString('base64'),
|
|
29
|
+
};
|
|
30
|
+
return JSON.stringify(file);
|
|
31
|
+
}
|
|
32
|
+
export function decryptToken(fileContents, masterKey) {
|
|
33
|
+
const key = deriveKey(masterKey);
|
|
34
|
+
const file = JSON.parse(fileContents);
|
|
35
|
+
if (file.v !== ENC_VERSION) {
|
|
36
|
+
throw new Error(`Unsupported token file version: ${file.v}`);
|
|
37
|
+
}
|
|
38
|
+
const decipher = createDecipheriv(ALGO, key, Buffer.from(file.iv, 'base64'));
|
|
39
|
+
decipher.setAuthTag(Buffer.from(file.tag, 'base64'));
|
|
40
|
+
const plaintext = Buffer.concat([
|
|
41
|
+
decipher.update(Buffer.from(file.data, 'base64')),
|
|
42
|
+
decipher.final(),
|
|
43
|
+
]);
|
|
44
|
+
return JSON.parse(plaintext.toString('utf8'));
|
|
45
|
+
}
|
|
46
|
+
function masterKey() {
|
|
47
|
+
return process.env.MASTER_KEY ?? '';
|
|
48
|
+
}
|
|
49
|
+
export function readToken(alias) {
|
|
50
|
+
let contents;
|
|
51
|
+
try {
|
|
52
|
+
contents = fs.readFileSync(ACCOUNT_CONFIG[alias].encPath, 'utf8');
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
return decryptToken(contents, masterKey());
|
|
58
|
+
}
|
|
59
|
+
export function writeToken(alias, data) {
|
|
60
|
+
const p = ACCOUNT_CONFIG[alias].encPath;
|
|
61
|
+
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
62
|
+
fs.writeFileSync(p, encryptToken(data, masterKey()), { mode: 0o600 });
|
|
63
|
+
}
|
|
64
|
+
export function hasToken(alias) {
|
|
65
|
+
return fs.existsSync(ACCOUNT_CONFIG[alias].encPath);
|
|
66
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare function coerceArray<T extends z.ZodTypeAny>(element: T): z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodArray<T>>;
|
|
3
|
+
export declare function coerceJson<T extends z.ZodTypeAny>(schema: T): z.ZodPipe<z.ZodTransform<unknown, unknown>, T>;
|
|
4
|
+
export declare const coerceBoolean: z.ZodPipe<z.ZodTransform<unknown, unknown>, z.ZodBoolean>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
function parseJsonLoose(s) {
|
|
3
|
+
try {
|
|
4
|
+
return JSON.parse(s);
|
|
5
|
+
}
|
|
6
|
+
catch {
|
|
7
|
+
return s;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function coerceArray(element) {
|
|
11
|
+
return z.preprocess((val) => {
|
|
12
|
+
if (typeof val !== 'string')
|
|
13
|
+
return val;
|
|
14
|
+
const t = val.trim();
|
|
15
|
+
if (t === '')
|
|
16
|
+
return [];
|
|
17
|
+
if (t.startsWith('['))
|
|
18
|
+
return parseJsonLoose(t);
|
|
19
|
+
return t.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
|
|
20
|
+
}, z.array(element));
|
|
21
|
+
}
|
|
22
|
+
export function coerceJson(schema) {
|
|
23
|
+
return z.preprocess((val) => (typeof val === 'string' ? parseJsonLoose(val) : val), schema);
|
|
24
|
+
}
|
|
25
|
+
export const coerceBoolean = z.preprocess((val) => {
|
|
26
|
+
if (typeof val === 'boolean')
|
|
27
|
+
return val;
|
|
28
|
+
if (typeof val === 'string') {
|
|
29
|
+
const t = val.trim().toLowerCase();
|
|
30
|
+
if (['true', '1', 'yes', 'y'].includes(t))
|
|
31
|
+
return true;
|
|
32
|
+
if (['false', '0', 'no', 'n'].includes(t))
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
return val;
|
|
36
|
+
}, z.boolean());
|
package/dist/tools/_errors.d.ts
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
import type { Account } from '../accounts.js';
|
|
2
|
-
|
|
2
|
+
export interface ErrorEnvelope {
|
|
3
|
+
error: string;
|
|
4
|
+
message: string;
|
|
5
|
+
hint?: string;
|
|
6
|
+
retriable: boolean;
|
|
7
|
+
account: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function mapGoogleError(error: any, account: Account, forbiddenHint?: string): ErrorEnvelope;
|
|
3
10
|
export declare function handleGoogleApiError(error: any, account: Account, forbiddenHint?: string): {
|
|
4
11
|
content: {
|
|
5
12
|
type: "text";
|
package/dist/tools/_errors.js
CHANGED
|
@@ -1,42 +1,69 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
1
|
+
function statusOf(error) {
|
|
2
|
+
const c = error?.code ?? error?.status ?? error?.response?.status;
|
|
3
|
+
const n = typeof c === 'string' ? Number(c) : c;
|
|
4
|
+
return Number.isFinite(n) ? n : undefined;
|
|
5
|
+
}
|
|
6
|
+
function reasonOf(error) {
|
|
7
|
+
return (error?.errors?.[0]?.reason ??
|
|
8
|
+
error?.response?.data?.error?.errors?.[0]?.reason ??
|
|
9
|
+
error?.response?.data?.error?.status);
|
|
10
|
+
}
|
|
11
|
+
function messageOf(error) {
|
|
12
|
+
return error?.response?.data?.error?.message ?? error?.message ?? String(error);
|
|
13
|
+
}
|
|
14
|
+
export function mapGoogleError(error, account, forbiddenHint) {
|
|
15
|
+
const status = statusOf(error);
|
|
16
|
+
const reason = reasonOf(error);
|
|
17
|
+
const message = messageOf(error);
|
|
18
|
+
if (status === 401) {
|
|
4
19
|
return {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
20
|
+
error: 'auth_required',
|
|
21
|
+
message: `Authentication failed for account "${account}".`,
|
|
22
|
+
hint: `Run: npx mcp-google-multi auth --account ${account}`,
|
|
23
|
+
retriable: false,
|
|
24
|
+
account,
|
|
10
25
|
};
|
|
11
26
|
}
|
|
12
|
-
if (
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
if (status === 403) {
|
|
28
|
+
const scopeIssue = reason === 'insufficientPermissions' ||
|
|
29
|
+
reason === 'ACCESS_TOKEN_SCOPE_INSUFFICIENT' ||
|
|
30
|
+
/insufficient.*scope/i.test(message);
|
|
31
|
+
if (scopeIssue) {
|
|
32
|
+
return {
|
|
33
|
+
error: 'insufficient_scope',
|
|
34
|
+
message,
|
|
35
|
+
hint: forbiddenHint ?? `Re-auth "${account}" with the scope this operation needs.`,
|
|
36
|
+
retriable: false,
|
|
37
|
+
account,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return { error: 'forbidden', message, hint: forbiddenHint, retriable: false, account };
|
|
24
41
|
}
|
|
25
|
-
if (
|
|
26
|
-
|
|
42
|
+
if (status === 400 && /invalid[_ ]scope/i.test(message)) {
|
|
43
|
+
return { error: 'invalid_scope', message, retriable: false, account };
|
|
44
|
+
}
|
|
45
|
+
if (status === 404) {
|
|
46
|
+
return { error: 'not_found', message, retriable: false, account };
|
|
47
|
+
}
|
|
48
|
+
if (status === 429) {
|
|
49
|
+
const retryAfter = error?.response?.headers?.['retry-after'];
|
|
27
50
|
return {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
51
|
+
error: 'rate_limited',
|
|
52
|
+
message,
|
|
53
|
+
hint: retryAfter ? `Retry after ${retryAfter}s.` : 'Back off and retry.',
|
|
54
|
+
retriable: true,
|
|
55
|
+
account,
|
|
33
56
|
};
|
|
34
57
|
}
|
|
58
|
+
if (status !== undefined && status >= 500) {
|
|
59
|
+
return { error: 'upstream_error', message, retriable: true, account };
|
|
60
|
+
}
|
|
61
|
+
return { error: 'upstream_error', message, retriable: false, account };
|
|
62
|
+
}
|
|
63
|
+
export function handleGoogleApiError(error, account, forbiddenHint) {
|
|
64
|
+
const envelope = mapGoogleError(error, account, forbiddenHint);
|
|
35
65
|
return {
|
|
36
|
-
content: [{
|
|
37
|
-
type: 'text',
|
|
38
|
-
text: JSON.stringify({ error: error.message ?? String(error), code: error.code }),
|
|
39
|
-
}],
|
|
66
|
+
content: [{ type: 'text', text: JSON.stringify(envelope) }],
|
|
40
67
|
isError: true,
|
|
41
68
|
};
|
|
42
69
|
}
|
package/dist/tools/admin.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { coerceBoolean } from './_coerce.js';
|
|
2
3
|
import { google } from 'googleapis';
|
|
3
4
|
import { ACCOUNTS } from '../accounts.js';
|
|
4
5
|
import { getClient } from '../client.js';
|
|
@@ -75,7 +76,7 @@ export function registerAdminTools(server) {
|
|
|
75
76
|
maxResults: z.number().min(1).max(500).optional(),
|
|
76
77
|
pageToken: z.string().optional(),
|
|
77
78
|
orderBy: z.enum(['email', 'familyName', 'givenName']).optional(),
|
|
78
|
-
showDeleted:
|
|
79
|
+
showDeleted: coerceBoolean.optional(),
|
|
79
80
|
projection: z.enum(['basic', 'custom', 'full']).optional(),
|
|
80
81
|
},
|
|
81
82
|
}, async ({ account, customer, domain, query, maxResults, pageToken, orderBy, showDeleted, projection }) => {
|
|
@@ -130,9 +131,9 @@ export function registerAdminTools(server) {
|
|
|
130
131
|
userKey: z.string().describe('User email or ID'),
|
|
131
132
|
givenName: z.string().optional(),
|
|
132
133
|
familyName: z.string().optional(),
|
|
133
|
-
suspended:
|
|
134
|
+
suspended: coerceBoolean.optional(),
|
|
134
135
|
password: z.string().optional(),
|
|
135
|
-
changePasswordAtNextLogin:
|
|
136
|
+
changePasswordAtNextLogin: coerceBoolean.optional(),
|
|
136
137
|
orgUnitPath: z.string().optional(),
|
|
137
138
|
},
|
|
138
139
|
}, async ({ account, userKey, givenName, familyName, suspended, password, changePasswordAtNextLogin, orgUnitPath }) => {
|
|
@@ -217,7 +218,7 @@ export function registerAdminTools(server) {
|
|
|
217
218
|
account: accountEnum.describe('Google account alias (must be a Workspace admin)'),
|
|
218
219
|
groupKey: z.string().describe('Group email or ID'),
|
|
219
220
|
roles: z.string().optional().describe('Comma-separated roles to include (OWNER, MANAGER, MEMBER)'),
|
|
220
|
-
includeDerivedMembership:
|
|
221
|
+
includeDerivedMembership: coerceBoolean.optional(),
|
|
221
222
|
maxResults: z.number().min(1).max(200).optional(),
|
|
222
223
|
pageToken: z.string().optional(),
|
|
223
224
|
},
|
package/dist/tools/calendar.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { coerceArray, coerceBoolean } from './_coerce.js';
|
|
2
3
|
import { google } from 'googleapis';
|
|
3
4
|
import { ACCOUNTS } from '../accounts.js';
|
|
4
5
|
import { getClient } from '../client.js';
|
|
@@ -111,7 +112,7 @@ export function registerCalendarTools(server) {
|
|
|
111
112
|
.describe('Comma-separated email addresses of attendees'),
|
|
112
113
|
calendarId: z.string().default('primary').optional()
|
|
113
114
|
.describe('Calendar ID (default: primary)'),
|
|
114
|
-
allDay:
|
|
115
|
+
allDay: coerceBoolean.default(false).optional()
|
|
115
116
|
.describe('If true, start/end are dates (YYYY-MM-DD) not datetimes'),
|
|
116
117
|
},
|
|
117
118
|
}, async ({ account, summary, start, end, description, location, attendees, calendarId, allDay }) => {
|
|
@@ -240,7 +241,7 @@ export function registerCalendarTools(server) {
|
|
|
240
241
|
calendarId: z.string().default('primary').optional()
|
|
241
242
|
.describe('Calendar ID (default: primary)'),
|
|
242
243
|
text: z.string().describe('Natural language event, e.g. "Lunch with Farouk Thursday 1pm at Le Boulanger"'),
|
|
243
|
-
sendNotifications:
|
|
244
|
+
sendNotifications: coerceBoolean.optional().describe('Send notifications to attendees (default: false)'),
|
|
244
245
|
},
|
|
245
246
|
}, async ({ account, calendarId, text, sendNotifications }) => {
|
|
246
247
|
try {
|
|
@@ -266,7 +267,7 @@ export function registerCalendarTools(server) {
|
|
|
266
267
|
calendarId: z.string().describe('Source calendar ID'),
|
|
267
268
|
eventId: z.string().describe('Event ID to move'),
|
|
268
269
|
destinationCalendarId: z.string().describe('Destination calendar ID'),
|
|
269
|
-
sendNotifications:
|
|
270
|
+
sendNotifications: coerceBoolean.optional().describe('Send notifications (default: false)'),
|
|
270
271
|
},
|
|
271
272
|
}, async ({ account, calendarId, eventId, destinationCalendarId, sendNotifications }) => {
|
|
272
273
|
try {
|
|
@@ -322,7 +323,7 @@ export function registerCalendarTools(server) {
|
|
|
322
323
|
description: 'Check free/busy times for one or more calendars within a time window. Returns only busy blocks, not event details.',
|
|
323
324
|
inputSchema: {
|
|
324
325
|
account: accountEnum.describe('Google account alias'),
|
|
325
|
-
calendarIds:
|
|
326
|
+
calendarIds: coerceArray(z.string()).describe('Calendar IDs to check, e.g. ["primary", "user@example.com"]'),
|
|
326
327
|
timeMin: z.string().describe('ISO 8601 start of window'),
|
|
327
328
|
timeMax: z.string().describe('ISO 8601 end of window'),
|
|
328
329
|
timeZone: z.string().optional().describe('Timezone (default: UTC)'),
|
package/dist/tools/chat.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { coerceJson } from './_coerce.js';
|
|
2
3
|
import { google } from 'googleapis';
|
|
3
4
|
import { ACCOUNTS } from '../accounts.js';
|
|
4
5
|
import { getClient } from '../client.js';
|
|
@@ -55,7 +56,7 @@ export function registerChatTools(server) {
|
|
|
55
56
|
account: accountEnum.describe('Google account alias'),
|
|
56
57
|
parent: z.string().describe('Space resource name, format: spaces/{space}'),
|
|
57
58
|
text: z.string().optional().describe('Plain message text'),
|
|
58
|
-
cardsV2: z.array(z.record(z.string(), z.any())).optional().describe('Optional Card v2 payloads'),
|
|
59
|
+
cardsV2: coerceJson(z.array(z.record(z.string(), z.any()))).optional().describe('Optional Card v2 payloads'),
|
|
59
60
|
threadKey: z.string().optional().describe('Thread key to group messages'),
|
|
60
61
|
messageReplyOption: z.enum(['MESSAGE_REPLY_OPTION_UNSPECIFIED', 'REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD', 'REPLY_MESSAGE_OR_FAIL']).optional(),
|
|
61
62
|
},
|
package/dist/tools/docs.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { coerceBoolean, coerceJson } from './_coerce.js';
|
|
2
3
|
import { google } from 'googleapis';
|
|
3
4
|
import { ACCOUNTS } from '../accounts.js';
|
|
4
5
|
import { getClient } from '../client.js';
|
|
@@ -56,7 +57,7 @@ export function registerDocsTools(server) {
|
|
|
56
57
|
inputSchema: {
|
|
57
58
|
account: accountEnum.describe('Google account alias'),
|
|
58
59
|
documentId: z.string().describe('Google Docs document ID'),
|
|
59
|
-
includeTabsContent:
|
|
60
|
+
includeTabsContent: coerceBoolean.optional()
|
|
60
61
|
.describe('Include full content for every tab (default: false — first tab only)'),
|
|
61
62
|
suggestionsViewMode: z.enum([
|
|
62
63
|
'DEFAULT_FOR_CURRENT_ACCESS',
|
|
@@ -154,7 +155,7 @@ export function registerDocsTools(server) {
|
|
|
154
155
|
documentId: z.string().describe('Google Docs document ID'),
|
|
155
156
|
findText: z.string().describe('Text to search for'),
|
|
156
157
|
replaceText: z.string().describe('Replacement text'),
|
|
157
|
-
matchCase:
|
|
158
|
+
matchCase: coerceBoolean.default(true).optional()
|
|
158
159
|
.describe('Case-sensitive match (default: true)'),
|
|
159
160
|
},
|
|
160
161
|
}, async ({ account, documentId, findText, replaceText, matchCase }) => {
|
|
@@ -224,9 +225,9 @@ export function registerDocsTools(server) {
|
|
|
224
225
|
documentId: z.string().describe('Google Docs document ID'),
|
|
225
226
|
startIndex: z.number().min(1).describe('Start index (inclusive, 1-based)'),
|
|
226
227
|
endIndex: z.number().min(2).describe('End index (exclusive)'),
|
|
227
|
-
bold:
|
|
228
|
-
italic:
|
|
229
|
-
underline:
|
|
228
|
+
bold: coerceBoolean.optional().describe('Set bold'),
|
|
229
|
+
italic: coerceBoolean.optional().describe('Set italic'),
|
|
230
|
+
underline: coerceBoolean.optional().describe('Set underline'),
|
|
230
231
|
fontSize: z.number().min(1).optional().describe('Font size in points'),
|
|
231
232
|
fontFamily: z.string().optional().describe('Font family name (e.g. "Arial", "Times New Roman")'),
|
|
232
233
|
},
|
|
@@ -443,9 +444,9 @@ export function registerDocsTools(server) {
|
|
|
443
444
|
indentEnd: z.number().optional().describe('End indent in points'),
|
|
444
445
|
indentFirstLine: z.number().optional().describe('First-line indent in points'),
|
|
445
446
|
direction: z.enum(['LEFT_TO_RIGHT', 'RIGHT_TO_LEFT']).optional(),
|
|
446
|
-
keepLinesTogether:
|
|
447
|
-
keepWithNext:
|
|
448
|
-
avoidWidowAndOrphan:
|
|
447
|
+
keepLinesTogether: coerceBoolean.optional(),
|
|
448
|
+
keepWithNext: coerceBoolean.optional(),
|
|
449
|
+
avoidWidowAndOrphan: coerceBoolean.optional(),
|
|
449
450
|
},
|
|
450
451
|
}, async ({ account, documentId, startIndex, endIndex, ...style }) => {
|
|
451
452
|
try {
|
|
@@ -489,9 +490,9 @@ export function registerDocsTools(server) {
|
|
|
489
490
|
marginHeader: z.number().optional(),
|
|
490
491
|
marginFooter: z.number().optional(),
|
|
491
492
|
pageNumberStart: z.number().optional(),
|
|
492
|
-
useFirstPageHeaderFooter:
|
|
493
|
-
useEvenPageHeaderFooter:
|
|
494
|
-
useCustomHeaderFooterMargins:
|
|
493
|
+
useFirstPageHeaderFooter: coerceBoolean.optional(),
|
|
494
|
+
useEvenPageHeaderFooter: coerceBoolean.optional(),
|
|
495
|
+
useCustomHeaderFooterMargins: coerceBoolean.optional(),
|
|
495
496
|
},
|
|
496
497
|
}, async ({ account, documentId, ...style }) => {
|
|
497
498
|
try {
|
|
@@ -807,8 +808,8 @@ export function registerDocsTools(server) {
|
|
|
807
808
|
tableStartIndex: z.number().min(1).describe('Start index of the table in the document'),
|
|
808
809
|
rowIndex: z.number().min(0).describe('Target row (0-based)'),
|
|
809
810
|
columnIndex: z.number().min(0).describe('Target column (0-based)'),
|
|
810
|
-
insertBelow:
|
|
811
|
-
insertRight:
|
|
811
|
+
insertBelow: coerceBoolean.optional().describe('insertRow only: insert below the target row (default: false)'),
|
|
812
|
+
insertRight: coerceBoolean.optional().describe('insertColumn only: insert right of the target column (default: false)'),
|
|
812
813
|
rowSpan: z.number().min(1).optional().describe('mergeCells only: how many rows the merged cell spans (default 1)'),
|
|
813
814
|
columnSpan: z.number().min(1).optional().describe('mergeCells/unmergeCells: column span (default 1)'),
|
|
814
815
|
},
|
|
@@ -981,7 +982,7 @@ export function registerDocsTools(server) {
|
|
|
981
982
|
inputSchema: {
|
|
982
983
|
account: accountEnum.describe('Google account alias'),
|
|
983
984
|
documentId: z.string().describe('Google Docs document ID'),
|
|
984
|
-
requests: z.array(z.record(z.string(), z.any())).describe('Array of Request objects, each with one request-type key'),
|
|
985
|
+
requests: coerceJson(z.array(z.record(z.string(), z.any()))).describe('Array of Request objects, each with one request-type key'),
|
|
985
986
|
writeControl: z.object({
|
|
986
987
|
requiredRevisionId: z.string().optional(),
|
|
987
988
|
targetRevisionId: z.string().optional(),
|
package/dist/tools/drive.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { coerceArray, coerceBoolean } from './_coerce.js';
|
|
2
3
|
import { google } from 'googleapis';
|
|
3
4
|
import { ACCOUNTS } from '../accounts.js';
|
|
4
5
|
import { getClient } from '../client.js';
|
|
@@ -505,9 +506,9 @@ export function registerDriveTools(server) {
|
|
|
505
506
|
role: z.enum(['reader', 'commenter', 'writer', 'fileOrganizer', 'organizer', 'owner']).describe('Permission role'),
|
|
506
507
|
emailAddress: z.string().optional().describe('Required when type is "user" or "group"'),
|
|
507
508
|
domain: z.string().optional().describe('Required when type is "domain"'),
|
|
508
|
-
sendNotification:
|
|
509
|
+
sendNotification: coerceBoolean.optional().describe('Send notification email (default: true)'),
|
|
509
510
|
emailMessage: z.string().optional().describe('Custom message in notification email'),
|
|
510
|
-
transferOwnership:
|
|
511
|
+
transferOwnership: coerceBoolean.optional().describe('Transfer ownership to the recipient. Requires role="owner". Recipient must accept ownership.'),
|
|
511
512
|
expirationTime: z.string().optional().describe('RFC 3339 timestamp when access expires. Only valid for role="reader" or "commenter".'),
|
|
512
513
|
},
|
|
513
514
|
}, async ({ account, fileId, type, role, emailAddress, domain, sendNotification, emailMessage, transferOwnership, expirationTime }) => {
|
|
@@ -567,9 +568,9 @@ export function registerDriveTools(server) {
|
|
|
567
568
|
.describe('New role'),
|
|
568
569
|
expirationTime: z.string().optional()
|
|
569
570
|
.describe('New RFC 3339 expiration timestamp. Only valid for role "reader" or "commenter".'),
|
|
570
|
-
removeExpiration:
|
|
571
|
+
removeExpiration: coerceBoolean.optional()
|
|
571
572
|
.describe('Clear the existing expirationTime'),
|
|
572
|
-
transferOwnership:
|
|
573
|
+
transferOwnership: coerceBoolean.optional()
|
|
573
574
|
.describe('Promote to owner. Requires role="owner".'),
|
|
574
575
|
},
|
|
575
576
|
}, async ({ account, fileId, permissionId, role, expirationTime, removeExpiration, transferOwnership }) => {
|
|
@@ -658,7 +659,7 @@ export function registerDriveTools(server) {
|
|
|
658
659
|
inputSchema: {
|
|
659
660
|
account: accountEnum.describe('Google account alias'),
|
|
660
661
|
fileId: z.string().describe('Google Drive file ID'),
|
|
661
|
-
includeDeleted:
|
|
662
|
+
includeDeleted: coerceBoolean.optional().describe('Include deleted comments (default: false)'),
|
|
662
663
|
pageSize: z.number().min(1).max(100).optional().describe('Max comments per page (default: 20)'),
|
|
663
664
|
pageToken: z.string().optional().describe('Token from a previous page'),
|
|
664
665
|
startModifiedTime: z.string().optional().describe('Only return comments modified after this RFC 3339 timestamp'),
|
|
@@ -689,7 +690,7 @@ export function registerDriveTools(server) {
|
|
|
689
690
|
account: accountEnum.describe('Google account alias'),
|
|
690
691
|
fileId: z.string().describe('Google Drive file ID'),
|
|
691
692
|
commentId: z.string().describe('Comment ID'),
|
|
692
|
-
includeDeleted:
|
|
693
|
+
includeDeleted: coerceBoolean.optional(),
|
|
693
694
|
},
|
|
694
695
|
}, async ({ account, fileId, commentId, includeDeleted }) => {
|
|
695
696
|
try {
|
|
@@ -793,7 +794,7 @@ export function registerDriveTools(server) {
|
|
|
793
794
|
account: accountEnum.describe('Google account alias'),
|
|
794
795
|
fileId: z.string().describe('Google Drive file ID'),
|
|
795
796
|
commentId: z.string().describe('Parent comment ID'),
|
|
796
|
-
includeDeleted:
|
|
797
|
+
includeDeleted: coerceBoolean.optional(),
|
|
797
798
|
pageSize: z.number().min(1).max(100).optional(),
|
|
798
799
|
pageToken: z.string().optional(),
|
|
799
800
|
},
|
|
@@ -899,10 +900,10 @@ export function registerDriveTools(server) {
|
|
|
899
900
|
account: accountEnum.describe('Google account alias'),
|
|
900
901
|
fileId: z.string().describe('Google Drive file ID'),
|
|
901
902
|
revisionId: z.string().describe('Revision ID'),
|
|
902
|
-
keepForever:
|
|
903
|
-
published:
|
|
904
|
-
publishAuto:
|
|
905
|
-
publishedOutsideDomain:
|
|
903
|
+
keepForever: coerceBoolean.optional().describe('Pin this revision indefinitely'),
|
|
904
|
+
published: coerceBoolean.optional().describe('Toggle published state (Docs only)'),
|
|
905
|
+
publishAuto: coerceBoolean.optional().describe('Auto-publish subsequent revisions'),
|
|
906
|
+
publishedOutsideDomain: coerceBoolean.optional().describe('Allow publish outside domain'),
|
|
906
907
|
},
|
|
907
908
|
}, async ({ account, fileId, revisionId, keepForever, published, publishAuto, publishedOutsideDomain }) => {
|
|
908
909
|
try {
|
|
@@ -984,10 +985,10 @@ export function registerDriveTools(server) {
|
|
|
984
985
|
fileId: z.string().describe('Google Drive file ID'),
|
|
985
986
|
proposalId: z.string().describe('Access proposal ID'),
|
|
986
987
|
action: z.enum(['ACCEPT', 'DENY']).describe('Whether to accept or deny the proposal'),
|
|
987
|
-
role:
|
|
988
|
+
role: coerceArray(z.enum(['reader', 'commenter', 'writer', 'fileOrganizer'])).optional()
|
|
988
989
|
.describe('Required when action is ACCEPT'),
|
|
989
990
|
view: z.string().optional().describe('Optional view, e.g. "published"'),
|
|
990
|
-
sendNotification:
|
|
991
|
+
sendNotification: coerceBoolean.optional()
|
|
991
992
|
.describe('Email the requester about the resolution'),
|
|
992
993
|
},
|
|
993
994
|
}, async ({ account, fileId, proposalId, action, role, view, sendNotification }) => {
|
|
@@ -14,3 +14,12 @@ export declare function encodeAddressHeader(value: string): string;
|
|
|
14
14
|
* Normalize bare `\n` or `\r` to CRLF.
|
|
15
15
|
*/
|
|
16
16
|
export declare function normalizeBodyLineEndings(body: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Build a multipart/alternative body so HTML-capable clients render the rich
|
|
19
|
+
* version and plain clients fall back. Returns the header value AND the body.
|
|
20
|
+
* Caller composes the full message: headers (including this Content-Type) + CRLF + body.
|
|
21
|
+
*/
|
|
22
|
+
export declare function buildMultipartAlternative(plainBody: string, htmlBody: string): {
|
|
23
|
+
contentType: string;
|
|
24
|
+
body: string;
|
|
25
|
+
};
|
package/dist/tools/gmail-mime.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from 'node:crypto';
|
|
1
2
|
/**
|
|
2
3
|
* RFC 2047 encoded-word (`=?utf-8?B?...?=`) for non-ASCII header text.
|
|
3
4
|
* Long values are split into multiple <=75-char encoded-words separated
|
|
@@ -62,3 +63,37 @@ export function encodeAddressHeader(value) {
|
|
|
62
63
|
export function normalizeBodyLineEndings(body) {
|
|
63
64
|
return body.replace(/\r\n|\r|\n/g, '\r\n');
|
|
64
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* RFC 2046 §5.1.1 boundary token: 1-70 chars from a restricted set, no trailing space.
|
|
68
|
+
* randomBytes hex output is only [0-9a-f], all of which are bcharsnospace.
|
|
69
|
+
*/
|
|
70
|
+
function generateMimeBoundary() {
|
|
71
|
+
// 5-char prefix + 32 hex chars = 37 chars, well under the 70-char limit.
|
|
72
|
+
return `=_gm_${randomBytes(16).toString('hex')}`;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Build a multipart/alternative body so HTML-capable clients render the rich
|
|
76
|
+
* version and plain clients fall back. Returns the header value AND the body.
|
|
77
|
+
* Caller composes the full message: headers (including this Content-Type) + CRLF + body.
|
|
78
|
+
*/
|
|
79
|
+
export function buildMultipartAlternative(plainBody, htmlBody) {
|
|
80
|
+
const boundary = generateMimeBoundary();
|
|
81
|
+
const parts = [
|
|
82
|
+
`--${boundary}`,
|
|
83
|
+
'Content-Type: text/plain; charset="UTF-8"',
|
|
84
|
+
'Content-Transfer-Encoding: 8bit',
|
|
85
|
+
'',
|
|
86
|
+
normalizeBodyLineEndings(plainBody),
|
|
87
|
+
`--${boundary}`,
|
|
88
|
+
'Content-Type: text/html; charset="UTF-8"',
|
|
89
|
+
'Content-Transfer-Encoding: 8bit',
|
|
90
|
+
'',
|
|
91
|
+
normalizeBodyLineEndings(htmlBody),
|
|
92
|
+
`--${boundary}--`,
|
|
93
|
+
'',
|
|
94
|
+
];
|
|
95
|
+
return {
|
|
96
|
+
contentType: `multipart/alternative; boundary="${boundary}"`,
|
|
97
|
+
body: parts.join('\r\n'),
|
|
98
|
+
};
|
|
99
|
+
}
|
package/dist/tools/gmail.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { coerceArray, coerceBoolean } from './_coerce.js';
|
|
2
3
|
import { google } from 'googleapis';
|
|
3
4
|
import { ACCOUNTS } from '../accounts.js';
|
|
4
5
|
import { getClient } from '../client.js';
|
|
5
6
|
import { handleGoogleApiError } from './_errors.js';
|
|
6
|
-
import { encodeAddressHeader, encodeHeaderValue, normalizeBodyLineEndings } from './gmail-mime.js';
|
|
7
|
+
import { buildMultipartAlternative, encodeAddressHeader, encodeHeaderValue, normalizeBodyLineEndings } from './gmail-mime.js';
|
|
7
8
|
import * as path from 'path';
|
|
8
9
|
import * as fs from 'fs';
|
|
9
10
|
const accountEnum = z.enum(ACCOUNTS);
|
|
@@ -163,14 +164,16 @@ export function registerGmailTools(server) {
|
|
|
163
164
|
account: accountEnum.describe('Google account alias'),
|
|
164
165
|
to: z.string().describe('Recipient(s), comma-separated'),
|
|
165
166
|
subject: z.string().describe('Email subject'),
|
|
166
|
-
body: z.string().describe('Plain text body'),
|
|
167
|
+
body: z.string().describe('Plain text body (always required; also used as fallback when htmlBody is set)'),
|
|
168
|
+
htmlBody: z.string().optional()
|
|
169
|
+
.describe('Optional HTML body. When set, sends multipart/alternative so HTML-capable clients render the rich version. Use bare tags only: <p>, <a>, <br>, <strong>, <em>, <ul><li>.'),
|
|
167
170
|
cc: z.string().optional().describe('CC recipients, comma-separated'),
|
|
168
171
|
replyToMessageId: z.string().optional()
|
|
169
172
|
.describe('Message ID to reply to (sets In-Reply-To and References headers)'),
|
|
170
173
|
replyToThreadId: z.string().optional()
|
|
171
174
|
.describe('Thread ID to send the message in'),
|
|
172
175
|
},
|
|
173
|
-
}, async ({ account, to, subject, body, cc, replyToMessageId, replyToThreadId }) => {
|
|
176
|
+
}, async ({ account, to, subject, body, htmlBody, cc, replyToMessageId, replyToThreadId }) => {
|
|
174
177
|
try {
|
|
175
178
|
const auth = await getClient(account);
|
|
176
179
|
const gmail = google.gmail({ version: 'v1', auth });
|
|
@@ -180,16 +183,25 @@ export function registerGmailTools(server) {
|
|
|
180
183
|
`To: ${encodeAddressHeader(to)}`,
|
|
181
184
|
`Subject: ${encodeHeaderValue(subject)}`,
|
|
182
185
|
'MIME-Version: 1.0',
|
|
183
|
-
'Content-Type: text/plain; charset="UTF-8"',
|
|
184
|
-
'Content-Transfer-Encoding: 8bit',
|
|
185
186
|
];
|
|
187
|
+
let bodyText;
|
|
188
|
+
if (htmlBody) {
|
|
189
|
+
const { contentType, body: mp } = buildMultipartAlternative(body, htmlBody);
|
|
190
|
+
headers.push(`Content-Type: ${contentType}`);
|
|
191
|
+
bodyText = mp;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
headers.push('Content-Type: text/plain; charset="UTF-8"');
|
|
195
|
+
headers.push('Content-Transfer-Encoding: 8bit');
|
|
196
|
+
bodyText = normalizeBodyLineEndings(body);
|
|
197
|
+
}
|
|
186
198
|
if (cc)
|
|
187
199
|
headers.push(`Cc: ${encodeAddressHeader(cc)}`);
|
|
188
200
|
if (replyToMessageId) {
|
|
189
201
|
headers.push(`In-Reply-To: ${replyToMessageId}`);
|
|
190
202
|
headers.push(`References: ${replyToMessageId}`);
|
|
191
203
|
}
|
|
192
|
-
const rawMessage = [...headers, '',
|
|
204
|
+
const rawMessage = [...headers, '', bodyText].join('\r\n');
|
|
193
205
|
const encoded = Buffer.from(rawMessage, 'utf-8').toString('base64url');
|
|
194
206
|
const sendParams = {
|
|
195
207
|
userId: 'me',
|
|
@@ -249,12 +261,14 @@ export function registerGmailTools(server) {
|
|
|
249
261
|
account: accountEnum.describe('Google account alias'),
|
|
250
262
|
to: z.string().describe('Recipient(s), comma-separated'),
|
|
251
263
|
subject: z.string().describe('Email subject'),
|
|
252
|
-
body: z.string().describe('Plain text body'),
|
|
264
|
+
body: z.string().describe('Plain text body (always required; also used as fallback when htmlBody is set)'),
|
|
265
|
+
htmlBody: z.string().optional()
|
|
266
|
+
.describe('Optional HTML body. When set, drafts as multipart/alternative so HTML-capable clients render the rich version. Use bare tags only: <p>, <a>, <br>, <strong>, <em>, <ul><li>.'),
|
|
253
267
|
cc: z.string().optional().describe('CC recipients, comma-separated'),
|
|
254
268
|
replyToThreadId: z.string().optional()
|
|
255
269
|
.describe('Thread ID to associate the draft with'),
|
|
256
270
|
},
|
|
257
|
-
}, async ({ account, to, subject, body, cc, replyToThreadId }) => {
|
|
271
|
+
}, async ({ account, to, subject, body, htmlBody, cc, replyToThreadId }) => {
|
|
258
272
|
try {
|
|
259
273
|
const auth = await getClient(account);
|
|
260
274
|
const gmail = google.gmail({ version: 'v1', auth });
|
|
@@ -264,12 +278,21 @@ export function registerGmailTools(server) {
|
|
|
264
278
|
`To: ${encodeAddressHeader(to)}`,
|
|
265
279
|
`Subject: ${encodeHeaderValue(subject)}`,
|
|
266
280
|
'MIME-Version: 1.0',
|
|
267
|
-
'Content-Type: text/plain; charset="UTF-8"',
|
|
268
|
-
'Content-Transfer-Encoding: 8bit',
|
|
269
281
|
];
|
|
282
|
+
let bodyText;
|
|
283
|
+
if (htmlBody) {
|
|
284
|
+
const { contentType, body: mp } = buildMultipartAlternative(body, htmlBody);
|
|
285
|
+
headers.push(`Content-Type: ${contentType}`);
|
|
286
|
+
bodyText = mp;
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
headers.push('Content-Type: text/plain; charset="UTF-8"');
|
|
290
|
+
headers.push('Content-Transfer-Encoding: 8bit');
|
|
291
|
+
bodyText = normalizeBodyLineEndings(body);
|
|
292
|
+
}
|
|
270
293
|
if (cc)
|
|
271
294
|
headers.push(`Cc: ${encodeAddressHeader(cc)}`);
|
|
272
|
-
const rawMessage = [...headers, '',
|
|
295
|
+
const rawMessage = [...headers, '', bodyText].join('\r\n');
|
|
273
296
|
const encoded = Buffer.from(rawMessage, 'utf-8').toString('base64url');
|
|
274
297
|
const draftParams = {
|
|
275
298
|
userId: 'me',
|
|
@@ -297,8 +320,8 @@ export function registerGmailTools(server) {
|
|
|
297
320
|
inputSchema: {
|
|
298
321
|
account: accountEnum.describe('Google account alias'),
|
|
299
322
|
messageId: z.string().describe('Gmail message ID'),
|
|
300
|
-
addLabelIds:
|
|
301
|
-
removeLabelIds:
|
|
323
|
+
addLabelIds: coerceArray(z.string()).optional().describe('Label IDs to add'),
|
|
324
|
+
removeLabelIds: coerceArray(z.string()).optional().describe('Label IDs to remove'),
|
|
302
325
|
},
|
|
303
326
|
}, async ({ account, messageId, addLabelIds, removeLabelIds }) => {
|
|
304
327
|
try {
|
|
@@ -362,9 +385,9 @@ export function registerGmailTools(server) {
|
|
|
362
385
|
description: 'Add/remove labels across up to 1000 Gmail messages at once. Useful for bulk archiving, marking as read, etc.',
|
|
363
386
|
inputSchema: {
|
|
364
387
|
account: accountEnum.describe('Google account alias'),
|
|
365
|
-
messageIds:
|
|
366
|
-
addLabelIds:
|
|
367
|
-
removeLabelIds:
|
|
388
|
+
messageIds: coerceArray(z.string()).describe('Message IDs (up to 1000)'),
|
|
389
|
+
addLabelIds: coerceArray(z.string()).optional().describe('Label IDs to add'),
|
|
390
|
+
removeLabelIds: coerceArray(z.string()).optional().describe('Label IDs to remove'),
|
|
368
391
|
},
|
|
369
392
|
}, async ({ account, messageIds, addLabelIds, removeLabelIds }) => {
|
|
370
393
|
try {
|
|
@@ -390,7 +413,7 @@ export function registerGmailTools(server) {
|
|
|
390
413
|
description: 'Permanently delete multiple Gmail messages. Irreversible.',
|
|
391
414
|
inputSchema: {
|
|
392
415
|
account: accountEnum.describe('Google account alias'),
|
|
393
|
-
messageIds:
|
|
416
|
+
messageIds: coerceArray(z.string()).describe('Message IDs (up to 1000)'),
|
|
394
417
|
},
|
|
395
418
|
}, async ({ account, messageIds }) => {
|
|
396
419
|
try {
|
|
@@ -570,7 +593,7 @@ export function registerGmailTools(server) {
|
|
|
570
593
|
startHistoryId: z.string().describe('History ID from a previous gmail_get_profile or gmail_read response'),
|
|
571
594
|
maxResults: z.number().min(1).max(500).default(100).optional()
|
|
572
595
|
.describe('Max results to return (default: 100)'),
|
|
573
|
-
historyTypes:
|
|
596
|
+
historyTypes: coerceArray(z.enum(['messageAdded', 'messageDeleted', 'labelAdded', 'labelRemoved'])).optional()
|
|
574
597
|
.describe('Filter by history event types'),
|
|
575
598
|
},
|
|
576
599
|
}, async ({ account, startHistoryId, maxResults, historyTypes }) => {
|
|
@@ -613,13 +636,13 @@ export function registerGmailTools(server) {
|
|
|
613
636
|
description: 'Enable or disable Gmail vacation responder with a custom message',
|
|
614
637
|
inputSchema: {
|
|
615
638
|
account: accountEnum.describe('Google account alias'),
|
|
616
|
-
enableAutoReply:
|
|
639
|
+
enableAutoReply: coerceBoolean.describe('Whether to enable the vacation responder'),
|
|
617
640
|
responseSubject: z.string().optional().describe('Subject line for auto-reply'),
|
|
618
641
|
responseBodyPlainText: z.string().optional().describe('Plain text body for auto-reply'),
|
|
619
642
|
startTime: z.string().optional().describe('Start time as Unix timestamp in ms'),
|
|
620
643
|
endTime: z.string().optional().describe('End time as Unix timestamp in ms'),
|
|
621
|
-
restrictToContacts:
|
|
622
|
-
restrictToDomain:
|
|
644
|
+
restrictToContacts: coerceBoolean.optional().describe('Only reply to contacts (default: false)'),
|
|
645
|
+
restrictToDomain: coerceBoolean.optional().describe('Only reply to same domain (default: false)'),
|
|
623
646
|
},
|
|
624
647
|
}, async ({ account, enableAutoReply, responseSubject, responseBodyPlainText, startTime, endTime, restrictToContacts, restrictToDomain }) => {
|
|
625
648
|
try {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { coerceArray, coerceJson } from './_coerce.js';
|
|
2
3
|
import { google } from 'googleapis';
|
|
3
4
|
import { ACCOUNTS } from '../accounts.js';
|
|
4
5
|
import { getClient } from '../client.js';
|
|
@@ -169,18 +170,18 @@ export function registerSearchConsoleTools(server) {
|
|
|
169
170
|
siteUrl: z.string().describe('Site URL (e.g. "https://example.com/" or "sc-domain:example.com")'),
|
|
170
171
|
startDate: z.string().describe('Start date (YYYY-MM-DD). Data is available starting ~3 days ago.'),
|
|
171
172
|
endDate: z.string().describe('End date (YYYY-MM-DD)'),
|
|
172
|
-
dimensions:
|
|
173
|
+
dimensions: coerceArray(z.enum(['query', 'page', 'country', 'device', 'searchAppearance', 'date'])).optional()
|
|
173
174
|
.describe('Dimensions to group by. Common: ["query", "page"], ["date"], ["query", "date"]'),
|
|
174
175
|
type: z.enum(['web', 'image', 'video', 'news', 'discover', 'googleNews']).optional()
|
|
175
176
|
.describe('Search type filter (default: web)'),
|
|
176
|
-
dimensionFilterGroups: z.array(z.object({
|
|
177
|
+
dimensionFilterGroups: coerceJson(z.array(z.object({
|
|
177
178
|
groupType: z.enum(['and']).optional(),
|
|
178
179
|
filters: z.array(z.object({
|
|
179
180
|
dimension: z.enum(['query', 'page', 'country', 'device', 'searchAppearance']),
|
|
180
181
|
operator: z.enum(['contains', 'equals', 'notContains', 'notEquals', 'includingRegex', 'excludingRegex']),
|
|
181
182
|
expression: z.string(),
|
|
182
183
|
})),
|
|
183
|
-
})).optional()
|
|
184
|
+
}))).optional()
|
|
184
185
|
.describe('Filters to narrow results. Example: filter by page containing "/blog/" or query containing "keyword"'),
|
|
185
186
|
rowLimit: z.number().min(1).max(25000).optional()
|
|
186
187
|
.describe('Max rows to return (default: 1000, max: 25000)'),
|
package/dist/tools/sheets.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { coerceArray, coerceBoolean, coerceJson } from './_coerce.js';
|
|
2
3
|
import { google } from 'googleapis';
|
|
3
4
|
import { ACCOUNTS } from '../accounts.js';
|
|
4
5
|
import { getClient } from '../client.js';
|
|
@@ -10,7 +11,7 @@ export function registerSheetsTools(server) {
|
|
|
10
11
|
inputSchema: {
|
|
11
12
|
account: accountEnum.describe('Google account alias'),
|
|
12
13
|
title: z.string().describe('Spreadsheet title'),
|
|
13
|
-
sheetTitles:
|
|
14
|
+
sheetTitles: coerceArray(z.string()).optional()
|
|
14
15
|
.describe('Optional tab/sheet names (default: one sheet named "Sheet1")'),
|
|
15
16
|
},
|
|
16
17
|
}, async ({ account, title, sheetTitles }) => {
|
|
@@ -108,7 +109,7 @@ export function registerSheetsTools(server) {
|
|
|
108
109
|
account: accountEnum.describe('Google account alias'),
|
|
109
110
|
spreadsheetId: z.string().describe('Spreadsheet ID'),
|
|
110
111
|
range: z.string().describe('A1 notation target range, e.g. "Sheet1!A1:C3"'),
|
|
111
|
-
values: z.array(z.array(z.any())).describe('2D array of values (rows x columns)'),
|
|
112
|
+
values: coerceJson(z.array(z.array(z.any()))).describe('2D array of values (rows x columns)'),
|
|
112
113
|
valueInputOption: z.enum(['RAW', 'USER_ENTERED']).default('USER_ENTERED').optional()
|
|
113
114
|
.describe('How to interpret values: RAW (literal) or USER_ENTERED (formulas/dates parsed). Default: USER_ENTERED'),
|
|
114
115
|
},
|
|
@@ -141,7 +142,7 @@ export function registerSheetsTools(server) {
|
|
|
141
142
|
account: accountEnum.describe('Google account alias'),
|
|
142
143
|
spreadsheetId: z.string().describe('Spreadsheet ID'),
|
|
143
144
|
range: z.string().describe('A1 notation range to search for the table (e.g. "Sheet1")'),
|
|
144
|
-
values: z.array(z.array(z.any())).describe('2D array of rows to append'),
|
|
145
|
+
values: coerceJson(z.array(z.array(z.any()))).describe('2D array of rows to append'),
|
|
145
146
|
valueInputOption: z.enum(['RAW', 'USER_ENTERED']).default('USER_ENTERED').optional()
|
|
146
147
|
.describe('How to interpret values. Default: USER_ENTERED'),
|
|
147
148
|
},
|
|
@@ -200,7 +201,7 @@ export function registerSheetsTools(server) {
|
|
|
200
201
|
inputSchema: {
|
|
201
202
|
account: accountEnum.describe('Google account alias'),
|
|
202
203
|
spreadsheetId: z.string().describe('Spreadsheet ID'),
|
|
203
|
-
ranges:
|
|
204
|
+
ranges: coerceArray(z.string()).describe('Array of A1 notation ranges'),
|
|
204
205
|
valueRenderOption: z.enum(['FORMATTED_VALUE', 'UNFORMATTED_VALUE', 'FORMULA'])
|
|
205
206
|
.default('FORMATTED_VALUE').optional()
|
|
206
207
|
.describe('How to render values. Default: FORMATTED_VALUE'),
|
|
@@ -231,10 +232,10 @@ export function registerSheetsTools(server) {
|
|
|
231
232
|
inputSchema: {
|
|
232
233
|
account: accountEnum.describe('Google account alias'),
|
|
233
234
|
spreadsheetId: z.string().describe('Spreadsheet ID'),
|
|
234
|
-
data: z.array(z.object({
|
|
235
|
+
data: coerceJson(z.array(z.object({
|
|
235
236
|
range: z.string().describe('A1 notation range'),
|
|
236
|
-
values: z.array(z.array(z.any())).describe('2D array of values'),
|
|
237
|
-
})).describe('Array of { range, values } objects to write'),
|
|
237
|
+
values: coerceJson(z.array(z.array(z.any()))).describe('2D array of values'),
|
|
238
|
+
}))).describe('Array of { range, values } objects to write'),
|
|
238
239
|
valueInputOption: z.enum(['RAW', 'USER_ENTERED']).default('USER_ENTERED').optional()
|
|
239
240
|
.describe('How to interpret values. Default: USER_ENTERED'),
|
|
240
241
|
},
|
|
@@ -373,7 +374,7 @@ export function registerSheetsTools(server) {
|
|
|
373
374
|
sheetId: z.number().describe('Sheet ID'),
|
|
374
375
|
title: z.string().optional().describe('New tab title'),
|
|
375
376
|
index: z.number().optional().describe('New position among tabs'),
|
|
376
|
-
hidden:
|
|
377
|
+
hidden: coerceBoolean.optional().describe('Hide the tab from the UI'),
|
|
377
378
|
tabColor: rgbColorSchema.optional().describe('Tab color (RGB 0..1)'),
|
|
378
379
|
frozenRowCount: z.number().min(0).optional().describe('Number of frozen header rows'),
|
|
379
380
|
frozenColumnCount: z.number().min(0).optional().describe('Number of frozen header columns'),
|
|
@@ -460,10 +461,10 @@ export function registerSheetsTools(server) {
|
|
|
460
461
|
backgroundColor: rgbColorSchema.optional().describe('Cell background (RGB 0..1)'),
|
|
461
462
|
textFormat: z.object({
|
|
462
463
|
foregroundColor: rgbColorSchema.optional(),
|
|
463
|
-
bold:
|
|
464
|
-
italic:
|
|
465
|
-
underline:
|
|
466
|
-
strikethrough:
|
|
464
|
+
bold: coerceBoolean.optional(),
|
|
465
|
+
italic: coerceBoolean.optional(),
|
|
466
|
+
underline: coerceBoolean.optional(),
|
|
467
|
+
strikethrough: coerceBoolean.optional(),
|
|
467
468
|
fontFamily: z.string().optional(),
|
|
468
469
|
fontSize: z.number().min(1).optional(),
|
|
469
470
|
}).optional(),
|
|
@@ -609,19 +610,19 @@ export function registerSheetsTools(server) {
|
|
|
609
610
|
inputSchema: {
|
|
610
611
|
account: accountEnum.describe('Google account alias'),
|
|
611
612
|
spreadsheetId: z.string().describe('Spreadsheet ID'),
|
|
612
|
-
ranges: z.array(gridRangeSchema).describe('Ranges the rule applies to'),
|
|
613
|
+
ranges: coerceJson(z.array(gridRangeSchema)).describe('Ranges the rule applies to'),
|
|
613
614
|
index: z.number().min(0).optional().describe('Where in the rule order to insert (0 = highest priority)'),
|
|
614
615
|
booleanRule: z.object({
|
|
615
616
|
conditionType: z.string().describe(BOOLEAN_CONDITION_TYPE_DESC),
|
|
616
|
-
conditionValues:
|
|
617
|
+
conditionValues: coerceArray(z.string()).optional().describe('Values for the condition (e.g. ["100"] for NUMBER_GREATER, or ["=A1>10"] for CUSTOM_FORMULA)'),
|
|
617
618
|
format: z.object({
|
|
618
619
|
backgroundColor: rgbColorSchema.optional(),
|
|
619
620
|
textFormat: z.object({
|
|
620
621
|
foregroundColor: rgbColorSchema.optional(),
|
|
621
|
-
bold:
|
|
622
|
-
italic:
|
|
623
|
-
strikethrough:
|
|
624
|
-
underline:
|
|
622
|
+
bold: coerceBoolean.optional(),
|
|
623
|
+
italic: coerceBoolean.optional(),
|
|
624
|
+
strikethrough: coerceBoolean.optional(),
|
|
625
|
+
underline: coerceBoolean.optional(),
|
|
625
626
|
}).optional(),
|
|
626
627
|
}).describe('Format to apply when condition is true. Conditional formatting only supports bold/italic/strikethrough/underline/foregroundColor/backgroundColor.'),
|
|
627
628
|
}).optional(),
|
|
@@ -699,7 +700,7 @@ export function registerSheetsTools(server) {
|
|
|
699
700
|
account: accountEnum.describe('Google account alias'),
|
|
700
701
|
spreadsheetId: z.string().describe('Spreadsheet ID'),
|
|
701
702
|
range: gridRangeSchema,
|
|
702
|
-
sortSpecs: z.array(sortSpecSchema).describe('One spec per sort column (in priority order)'),
|
|
703
|
+
sortSpecs: coerceJson(z.array(sortSpecSchema)).describe('One spec per sort column (in priority order)'),
|
|
703
704
|
},
|
|
704
705
|
}, async ({ account, spreadsheetId, range, sortSpecs }) => {
|
|
705
706
|
try {
|
|
@@ -725,13 +726,13 @@ export function registerSheetsTools(server) {
|
|
|
725
726
|
account: accountEnum.describe('Google account alias'),
|
|
726
727
|
spreadsheetId: z.string().describe('Spreadsheet ID'),
|
|
727
728
|
range: gridRangeSchema,
|
|
728
|
-
sortSpecs: z.array(sortSpecSchema).optional(),
|
|
729
|
-
filterSpecs: z.array(z.object({
|
|
729
|
+
sortSpecs: coerceJson(z.array(sortSpecSchema)).optional(),
|
|
730
|
+
filterSpecs: coerceJson(z.array(z.object({
|
|
730
731
|
columnIndex: z.number().min(0),
|
|
731
|
-
hiddenValues:
|
|
732
|
+
hiddenValues: coerceArray(z.string()).optional().describe('Values to hide in this column'),
|
|
732
733
|
conditionType: z.string().optional().describe(BOOLEAN_CONDITION_TYPE_DESC),
|
|
733
|
-
conditionValues:
|
|
734
|
-
})).optional(),
|
|
734
|
+
conditionValues: coerceArray(z.string()).optional(),
|
|
735
|
+
}))).optional(),
|
|
735
736
|
},
|
|
736
737
|
}, async ({ account, spreadsheetId, range, sortSpecs, filterSpecs }) => {
|
|
737
738
|
try {
|
|
@@ -801,10 +802,10 @@ export function registerSheetsTools(server) {
|
|
|
801
802
|
scope: z.enum(['allSheets', 'sheet', 'range']).describe('Search scope'),
|
|
802
803
|
sheetId: z.number().optional().describe('Required when scope=sheet'),
|
|
803
804
|
range: gridRangeSchema.optional().describe('Required when scope=range'),
|
|
804
|
-
matchCase:
|
|
805
|
-
matchEntireCell:
|
|
806
|
-
searchByRegex:
|
|
807
|
-
includeFormulas:
|
|
805
|
+
matchCase: coerceBoolean.optional(),
|
|
806
|
+
matchEntireCell: coerceBoolean.optional(),
|
|
807
|
+
searchByRegex: coerceBoolean.optional(),
|
|
808
|
+
includeFormulas: coerceBoolean.optional(),
|
|
808
809
|
},
|
|
809
810
|
}, async ({ account, spreadsheetId, find, replacement, scope, sheetId, range, matchCase, matchEntireCell, searchByRegex, includeFormulas }) => {
|
|
810
811
|
try {
|
|
@@ -892,7 +893,7 @@ export function registerSheetsTools(server) {
|
|
|
892
893
|
dimension: z.enum(['ROWS', 'COLUMNS']),
|
|
893
894
|
startIndex: z.number().min(0).describe('0-based, inclusive'),
|
|
894
895
|
endIndex: z.number().min(1).describe('0-based, exclusive'),
|
|
895
|
-
inheritFromBefore:
|
|
896
|
+
inheritFromBefore: coerceBoolean.optional(),
|
|
896
897
|
},
|
|
897
898
|
}, async ({ account, spreadsheetId, sheetId, dimension, startIndex, endIndex, inheritFromBefore }) => {
|
|
898
899
|
try {
|
|
@@ -957,10 +958,10 @@ export function registerSheetsTools(server) {
|
|
|
957
958
|
spreadsheetId: z.string().describe('Spreadsheet ID'),
|
|
958
959
|
range: gridRangeSchema,
|
|
959
960
|
conditionType: z.string().describe(BOOLEAN_CONDITION_TYPE_DESC),
|
|
960
|
-
conditionValues:
|
|
961
|
+
conditionValues: coerceArray(z.string()).optional().describe('Values per condition type (list items for ONE_OF_LIST, range A1 for ONE_OF_RANGE, etc.)'),
|
|
961
962
|
inputMessage: z.string().optional().describe('Tooltip shown when cell is selected'),
|
|
962
|
-
strict:
|
|
963
|
-
showCustomUi:
|
|
963
|
+
strict: coerceBoolean.optional().describe('Reject invalid input (default: false = warning only)'),
|
|
964
|
+
showCustomUi: coerceBoolean.optional().describe('Show dropdown UI for list-type validations'),
|
|
964
965
|
},
|
|
965
966
|
}, async ({ account, spreadsheetId, range, conditionType, conditionValues, inputMessage, strict, showCustomUi }) => {
|
|
966
967
|
try {
|
|
@@ -1048,7 +1049,7 @@ export function registerSheetsTools(server) {
|
|
|
1048
1049
|
inputSchema: {
|
|
1049
1050
|
account: accountEnum.describe('Google account alias'),
|
|
1050
1051
|
spreadsheetId: z.string().describe('Spreadsheet ID'),
|
|
1051
|
-
ranges:
|
|
1052
|
+
ranges: coerceArray(z.string()).describe('Array of A1 notation ranges'),
|
|
1052
1053
|
},
|
|
1053
1054
|
}, async ({ account, spreadsheetId, ranges }) => {
|
|
1054
1055
|
try {
|
|
@@ -1073,10 +1074,10 @@ export function registerSheetsTools(server) {
|
|
|
1073
1074
|
inputSchema: {
|
|
1074
1075
|
account: accountEnum.describe('Google account alias'),
|
|
1075
1076
|
spreadsheetId: z.string().describe('Spreadsheet ID'),
|
|
1076
|
-
requests: z.array(z.record(z.string(), z.any())).describe('Array of Request objects. Each object has exactly one key (the request type) like {repeatCell: {...}}, {addChart: {...}}, {updateBanding: {...}}, etc.'),
|
|
1077
|
-
includeSpreadsheetInResponse:
|
|
1078
|
-
responseRanges:
|
|
1079
|
-
responseIncludeGridData:
|
|
1077
|
+
requests: coerceJson(z.array(z.record(z.string(), z.any()))).describe('Array of Request objects. Each object has exactly one key (the request type) like {repeatCell: {...}}, {addChart: {...}}, {updateBanding: {...}}, etc.'),
|
|
1078
|
+
includeSpreadsheetInResponse: coerceBoolean.optional(),
|
|
1079
|
+
responseRanges: coerceArray(z.string()).optional(),
|
|
1080
|
+
responseIncludeGridData: coerceBoolean.optional(),
|
|
1080
1081
|
},
|
|
1081
1082
|
}, async ({ account, spreadsheetId, requests, includeSpreadsheetInResponse, responseRanges, responseIncludeGridData }) => {
|
|
1082
1083
|
try {
|
package/dist/tools/tasks.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
import { coerceBoolean } from './_coerce.js';
|
|
2
3
|
import { google } from 'googleapis';
|
|
3
4
|
import { ACCOUNTS } from '../accounts.js';
|
|
4
5
|
import { getClient } from '../client.js';
|
|
@@ -119,10 +120,10 @@ export function registerTasksTools(server) {
|
|
|
119
120
|
tasklistId: z.string().describe('Tasklist ID'),
|
|
120
121
|
maxResults: z.number().min(1).max(100).optional(),
|
|
121
122
|
pageToken: z.string().optional(),
|
|
122
|
-
showCompleted:
|
|
123
|
-
showDeleted:
|
|
124
|
-
showHidden:
|
|
125
|
-
showAssigned:
|
|
123
|
+
showCompleted: coerceBoolean.optional().describe('Include completed tasks (default: true)'),
|
|
124
|
+
showDeleted: coerceBoolean.optional(),
|
|
125
|
+
showHidden: coerceBoolean.optional(),
|
|
126
|
+
showAssigned: coerceBoolean.optional(),
|
|
126
127
|
completedMax: z.string().optional().describe('RFC 3339 upper bound on completion date'),
|
|
127
128
|
completedMin: z.string().optional(),
|
|
128
129
|
dueMax: z.string().optional(),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-google-multi",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0-alpha.1",
|
|
4
4
|
"description": "MCP server for Gmail, Google Drive, Calendar, Sheets, Docs, and Contacts with multi-account support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"prepublishOnly": "npm run build",
|
|
45
45
|
"watch": "tsc --watch",
|
|
46
46
|
"auth": "node dist/index.js auth --account",
|
|
47
|
+
"migrate-tokens": "node dist/index.js migrate-tokens",
|
|
47
48
|
"lint": "eslint .",
|
|
48
49
|
"typecheck": "tsc --noEmit",
|
|
49
50
|
"test": "vitest run",
|
|
@@ -67,6 +68,6 @@
|
|
|
67
68
|
"semantic-release": "^25.0.3",
|
|
68
69
|
"typescript": "^6.0.3",
|
|
69
70
|
"typescript-eslint": "^8.59.1",
|
|
70
|
-
"vitest": "^
|
|
71
|
+
"vitest": "^3.2.4"
|
|
71
72
|
}
|
|
72
73
|
}
|