mcp-google-multi 5.1.0-alpha.2 → 5.1.0-alpha.3
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 +9 -0
- package/dist/registry.d.ts +1 -0
- package/dist/registry.js +6 -1
- package/dist/tools/calendar.d.ts +20 -0
- package/dist/tools/calendar.js +23 -9
- package/dist/tools/drive.js +22 -19
- package/dist/tools/gmail.d.ts +2 -0
- package/dist/tools/gmail.js +15 -8
- package/dist/trim.d.ts +16 -0
- package/dist/trim.js +29 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -72,6 +72,7 @@ Now the only thing on disk is the **encrypted** token store. Pass the token via
|
|
|
72
72
|
| `GOOGLE_TOOLSETS` | — | `all` (default) or a CSV filter of: `gmail`, `drive`, `calendar`, `sheets`, `docs`, `contacts`, `searchconsole`, `tasks`, `meet`, `forms`, `chat`, `admin` |
|
|
73
73
|
| `TOKEN_STORE_PATH` | — | override the encrypted token dir (default: `~/.config/mcp-google-multi/tokens`) |
|
|
74
74
|
| `DISCOVERY_CACHE_PATH` | — | override the Discovery-doc cache dir (default: `~/.config/mcp-google-multi/discovery`) |
|
|
75
|
+
| `GOOGLE_TRIM` | — | `off` (or `0`/`false`/`no`) disables compact JSON serialization of tool responses |
|
|
75
76
|
|
|
76
77
|
Inspect the resolved setup any time: `mcp-google-multi config check`.
|
|
77
78
|
|
|
@@ -87,6 +88,14 @@ Two eager tools cover everything the curated set doesn't: `google_api_search` fi
|
|
|
87
88
|
|
|
88
89
|
Discovery documents are fetched from Google on first use and cached on disk for 7 days (`DISCOVERY_CACHE_PATH`, default `~/.config/mcp-google-multi/discovery`); a stale cache is used when offline.
|
|
89
90
|
|
|
91
|
+
## Lean responses by default
|
|
92
|
+
|
|
93
|
+
Tool responses are serialized compactly (no pretty-print token tax; set `GOOGLE_TRIM=off` to restore pretty JSON), and the fat readers ship sensible caps with per-call escape valves. The caps are per-call controls (`full` / `maxChars`) and are NOT affected by `GOOGLE_TRIM`:
|
|
94
|
+
|
|
95
|
+
- `drive_read` returns up to `maxChars` characters (default 100k) with `truncated`/`totalChars`/`offset` for paging — this also bounds Google Doc exports, which can reach 10MB. (Non-Google-native files over 2MB are still rejected with `too_large`, not paged.)
|
|
96
|
+
- `gmail_read` / `gmail_read_thread` cap each message body at 50k chars (`bodyTruncated` + `bodyTotalChars` flags); pass `full: true` for the whole body.
|
|
97
|
+
- `calendar_list_events` / `calendar_list_instances` trim descriptions to ~300 chars and drop empty/audit fields (`created`/`updated`) in list view; `calendar_get_event` always returns the full event.
|
|
98
|
+
|
|
90
99
|
## Write-control (deny-by-default)
|
|
91
100
|
|
|
92
101
|
Reads are never gated. **Every create/update/delete is off until you opt in** — pick a profile:
|
package/dist/registry.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export declare class ToolRegistry {
|
|
|
24
24
|
readonly registerTool: McpServer['registerTool'];
|
|
25
25
|
private readonly revealed;
|
|
26
26
|
private readonly jsonSchemaCache;
|
|
27
|
+
private readonly compactOutput;
|
|
27
28
|
private registeringMeta;
|
|
28
29
|
constructor(server: McpServer, policy: Policy);
|
|
29
30
|
registerMeta: McpServer['registerTool'];
|
package/dist/registry.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { isAllowed, writeDisabledResult } from './write-control.js';
|
|
4
|
+
import { compactResult, trimEnabled } from './trim.js';
|
|
4
5
|
const CUD_OVERRIDES = {
|
|
5
6
|
drive_untrash: 'update',
|
|
6
7
|
};
|
|
@@ -28,6 +29,7 @@ export class ToolRegistry {
|
|
|
28
29
|
registerTool;
|
|
29
30
|
revealed = new Set();
|
|
30
31
|
jsonSchemaCache = new Map();
|
|
32
|
+
compactOutput = trimEnabled();
|
|
31
33
|
registeringMeta = false;
|
|
32
34
|
constructor(server, policy) {
|
|
33
35
|
this.server = server;
|
|
@@ -54,7 +56,10 @@ export class ToolRegistry {
|
|
|
54
56
|
: (...args) => isAllowed({ name, service, cud }, policy)
|
|
55
57
|
? handler(...args)
|
|
56
58
|
: writeDisabledResult({ name, service, cud }, policy);
|
|
57
|
-
|
|
59
|
+
const finalHandler = this.compactOutput
|
|
60
|
+
? async (...args) => compactResult(await guarded(...args))
|
|
61
|
+
: guarded;
|
|
62
|
+
return server.registerTool(name, { ...config, annotations }, finalHandler);
|
|
58
63
|
});
|
|
59
64
|
}
|
|
60
65
|
registerMeta = ((name, config, handler) => {
|
package/dist/tools/calendar.d.ts
CHANGED
|
@@ -1,2 +1,22 @@
|
|
|
1
1
|
import type { ToolRegistry } from '../registry.js';
|
|
2
2
|
export declare function registerCalendarTools(server: ToolRegistry): void;
|
|
3
|
+
export declare function formatEvent(event: any, opts?: {
|
|
4
|
+
full?: boolean;
|
|
5
|
+
}): {
|
|
6
|
+
id: any;
|
|
7
|
+
summary: any;
|
|
8
|
+
description: string;
|
|
9
|
+
location: any;
|
|
10
|
+
start: any;
|
|
11
|
+
end: any;
|
|
12
|
+
status: any;
|
|
13
|
+
htmlLink: any;
|
|
14
|
+
organizer: any;
|
|
15
|
+
attendees: any;
|
|
16
|
+
recurringEventId: any;
|
|
17
|
+
hangoutLink: any;
|
|
18
|
+
created: any;
|
|
19
|
+
updated: any;
|
|
20
|
+
} | {
|
|
21
|
+
[k: string]: any;
|
|
22
|
+
};
|
package/dist/tools/calendar.js
CHANGED
|
@@ -4,6 +4,7 @@ import { google } from 'googleapis';
|
|
|
4
4
|
import { ACCOUNTS } from '../accounts.js';
|
|
5
5
|
import { getClient } from '../client.js';
|
|
6
6
|
import { handleGoogleApiError } from './_errors.js';
|
|
7
|
+
import { sliceClean } from '../trim.js';
|
|
7
8
|
const accountEnum = z.enum(ACCOUNTS);
|
|
8
9
|
export function registerCalendarTools(server) {
|
|
9
10
|
server.registerTool('calendar_list_calendars', {
|
|
@@ -33,7 +34,7 @@ export function registerCalendarTools(server) {
|
|
|
33
34
|
}
|
|
34
35
|
});
|
|
35
36
|
server.registerTool('calendar_list_events', {
|
|
36
|
-
description: 'List events from a Google Calendar',
|
|
37
|
+
description: 'List events from a Google Calendar (trimmed view: descriptions capped, created/updated and empty fields omitted — use calendar_get_event for a full event)',
|
|
37
38
|
inputSchema: {
|
|
38
39
|
account: accountEnum.describe('Google account alias'),
|
|
39
40
|
calendarId: z.string().default('primary').optional()
|
|
@@ -66,7 +67,7 @@ export function registerCalendarTools(server) {
|
|
|
66
67
|
params.timeMin = new Date().toISOString();
|
|
67
68
|
}
|
|
68
69
|
const res = await cal.events.list(params);
|
|
69
|
-
const events = (res.data.items ?? []).map(formatEvent);
|
|
70
|
+
const events = (res.data.items ?? []).map((e) => formatEvent(e, { full: false }));
|
|
70
71
|
return {
|
|
71
72
|
content: [{ type: 'text', text: JSON.stringify(events, null, 2) }],
|
|
72
73
|
};
|
|
@@ -288,7 +289,7 @@ export function registerCalendarTools(server) {
|
|
|
288
289
|
}
|
|
289
290
|
});
|
|
290
291
|
server.registerTool('calendar_list_instances', {
|
|
291
|
-
description: 'List all occurrences of a recurring calendar event',
|
|
292
|
+
description: 'List all occurrences of a recurring calendar event (trimmed view like calendar_list_events — use calendar_get_event for a full event)',
|
|
292
293
|
inputSchema: {
|
|
293
294
|
account: accountEnum.describe('Google account alias'),
|
|
294
295
|
calendarId: z.string().default('primary').optional()
|
|
@@ -310,7 +311,7 @@ export function registerCalendarTools(server) {
|
|
|
310
311
|
timeMax,
|
|
311
312
|
maxResults: maxResults ?? 25,
|
|
312
313
|
});
|
|
313
|
-
const events = (res.data.items ?? []).map(formatEvent);
|
|
314
|
+
const events = (res.data.items ?? []).map((e) => formatEvent(e, { full: false }));
|
|
314
315
|
return {
|
|
315
316
|
content: [{ type: 'text', text: JSON.stringify(events, null, 2) }],
|
|
316
317
|
};
|
|
@@ -376,11 +377,19 @@ export function registerCalendarTools(server) {
|
|
|
376
377
|
}
|
|
377
378
|
});
|
|
378
379
|
}
|
|
379
|
-
|
|
380
|
-
|
|
380
|
+
const LIST_DESCRIPTION_CAP = 300;
|
|
381
|
+
const TRUNCATION_MARKER = '… [truncated, use calendar_get_event]';
|
|
382
|
+
export function formatEvent(event, opts = {}) {
|
|
383
|
+
const full = opts.full ?? true;
|
|
384
|
+
const description = event.description ?? '';
|
|
385
|
+
// Slack margin: truncating a 301-char description to 300 + 37-char marker would GROW it.
|
|
386
|
+
const cappedDescription = full || description.length <= LIST_DESCRIPTION_CAP + TRUNCATION_MARKER.length
|
|
387
|
+
? description
|
|
388
|
+
: `${sliceClean(description, LIST_DESCRIPTION_CAP)}${TRUNCATION_MARKER}`;
|
|
389
|
+
const base = {
|
|
381
390
|
id: event.id,
|
|
382
391
|
summary: event.summary ?? '',
|
|
383
|
-
description:
|
|
392
|
+
description: cappedDescription,
|
|
384
393
|
location: event.location ?? '',
|
|
385
394
|
start: event.start?.dateTime ?? event.start?.date ?? '',
|
|
386
395
|
end: event.end?.dateTime ?? event.end?.date ?? '',
|
|
@@ -391,9 +400,14 @@ function formatEvent(event) {
|
|
|
391
400
|
email: a.email,
|
|
392
401
|
responseStatus: a.responseStatus,
|
|
393
402
|
})),
|
|
394
|
-
|
|
395
|
-
|
|
403
|
+
recurringEventId: event.recurringEventId,
|
|
404
|
+
hangoutLink: event.hangoutLink,
|
|
405
|
+
created: full ? event.created : undefined,
|
|
406
|
+
updated: full ? event.updated : undefined,
|
|
396
407
|
};
|
|
408
|
+
if (full)
|
|
409
|
+
return base;
|
|
410
|
+
return Object.fromEntries(Object.entries(base).filter(([, v]) => v !== undefined && v !== '' && !(Array.isArray(v) && v.length === 0)));
|
|
397
411
|
}
|
|
398
412
|
function handleCalendarError(error, account) {
|
|
399
413
|
return handleGoogleApiError(error, account);
|
package/dist/tools/drive.js
CHANGED
|
@@ -4,6 +4,7 @@ import { google } from 'googleapis';
|
|
|
4
4
|
import { ACCOUNTS } from '../accounts.js';
|
|
5
5
|
import { getClient } from '../client.js';
|
|
6
6
|
import { handleGoogleApiError } from './_errors.js';
|
|
7
|
+
import { capText } from '../trim.js';
|
|
7
8
|
import * as fs from 'fs';
|
|
8
9
|
import * as path from 'path';
|
|
9
10
|
import { pipeline } from 'node:stream/promises';
|
|
@@ -59,12 +60,16 @@ export function registerDriveTools(server) {
|
|
|
59
60
|
}
|
|
60
61
|
});
|
|
61
62
|
server.registerTool('drive_read', {
|
|
62
|
-
description: 'Read the content of a Google Drive file',
|
|
63
|
+
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)',
|
|
63
64
|
inputSchema: {
|
|
64
65
|
account: accountEnum.describe('Google account alias'),
|
|
65
66
|
fileId: z.string().describe('Google Drive file ID'),
|
|
67
|
+
maxChars: z.number().min(1).max(2_000_000).default(100_000).optional()
|
|
68
|
+
.describe('Max characters of content to return (default: 100000)'),
|
|
69
|
+
offset: z.number().min(0).default(0).optional()
|
|
70
|
+
.describe('Character offset to continue a truncated read (Google Docs/Sheets/Slides exports page fine; plain files >2MB are rejected, not paged)'),
|
|
66
71
|
},
|
|
67
|
-
}, async ({ account, fileId }) => {
|
|
72
|
+
}, async ({ account, fileId, maxChars, offset }) => {
|
|
68
73
|
try {
|
|
69
74
|
const auth = await getClient(account);
|
|
70
75
|
const drive = google.drive({ version: 'v3', auth });
|
|
@@ -74,11 +79,9 @@ export function registerDriveTools(server) {
|
|
|
74
79
|
supportsAllDrives: true,
|
|
75
80
|
});
|
|
76
81
|
const { name, mimeType, size, webViewLink } = meta.data;
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
mimeType: 'text/plain',
|
|
81
|
-
});
|
|
82
|
+
const respond = (raw) => {
|
|
83
|
+
const from = offset ?? 0;
|
|
84
|
+
const capped = capText(raw, maxChars ?? 100_000, from);
|
|
82
85
|
return {
|
|
83
86
|
content: [{
|
|
84
87
|
type: 'text',
|
|
@@ -86,10 +89,20 @@ export function registerDriveTools(server) {
|
|
|
86
89
|
id: fileId,
|
|
87
90
|
name,
|
|
88
91
|
mimeType,
|
|
89
|
-
content:
|
|
92
|
+
content: capped.text,
|
|
93
|
+
...(capped.truncated || from > 0
|
|
94
|
+
? { truncated: capped.truncated, totalChars: capped.totalChars, offset: from }
|
|
95
|
+
: {}),
|
|
90
96
|
}, null, 2),
|
|
91
97
|
}],
|
|
92
98
|
};
|
|
99
|
+
};
|
|
100
|
+
if (mimeType && GOOGLE_WORKSPACE_TYPES.has(mimeType)) {
|
|
101
|
+
const exported = await drive.files.export({
|
|
102
|
+
fileId,
|
|
103
|
+
mimeType: 'text/plain',
|
|
104
|
+
});
|
|
105
|
+
return respond(String(exported.data));
|
|
93
106
|
}
|
|
94
107
|
if (mimeType === 'application/pdf') {
|
|
95
108
|
return {
|
|
@@ -122,17 +135,7 @@ export function registerDriveTools(server) {
|
|
|
122
135
|
}
|
|
123
136
|
if (mimeType?.startsWith('text/')) {
|
|
124
137
|
const downloaded = await drive.files.get({ fileId, alt: 'media', supportsAllDrives: true }, { responseType: 'text' });
|
|
125
|
-
return
|
|
126
|
-
content: [{
|
|
127
|
-
type: 'text',
|
|
128
|
-
text: JSON.stringify({
|
|
129
|
-
id: fileId,
|
|
130
|
-
name,
|
|
131
|
-
mimeType,
|
|
132
|
-
content: String(downloaded.data),
|
|
133
|
-
}, null, 2),
|
|
134
|
-
}],
|
|
135
|
-
};
|
|
138
|
+
return respond(String(downloaded.data));
|
|
136
139
|
}
|
|
137
140
|
return {
|
|
138
141
|
content: [{
|
package/dist/tools/gmail.d.ts
CHANGED
package/dist/tools/gmail.js
CHANGED
|
@@ -5,6 +5,7 @@ import { ACCOUNTS } from '../accounts.js';
|
|
|
5
5
|
import { getClient } from '../client.js';
|
|
6
6
|
import { handleGoogleApiError } from './_errors.js';
|
|
7
7
|
import { buildMultipartAlternative, encodeAddressHeader, encodeHeaderValue, normalizeBodyLineEndings } from './gmail-mime.js';
|
|
8
|
+
import { sliceClean } from '../trim.js';
|
|
8
9
|
import * as path from 'path';
|
|
9
10
|
import * as fs from 'fs';
|
|
10
11
|
const accountEnum = z.enum(ACCOUNTS);
|
|
@@ -52,8 +53,11 @@ function getAttachments(payload) {
|
|
|
52
53
|
}
|
|
53
54
|
return attachments;
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
+
const BODY_CAP_CHARS = 50_000;
|
|
57
|
+
export function parseMessage(msg, bodyCap) {
|
|
56
58
|
const headers = msg.payload?.headers ?? [];
|
|
59
|
+
const body = decodeBody(msg.payload);
|
|
60
|
+
const capped = bodyCap !== undefined && body.length > bodyCap;
|
|
57
61
|
return {
|
|
58
62
|
id: msg.id ?? '',
|
|
59
63
|
threadId: msg.threadId ?? '',
|
|
@@ -62,7 +66,8 @@ function parseMessage(msg) {
|
|
|
62
66
|
to: getHeader(headers, 'To'),
|
|
63
67
|
cc: getHeader(headers, 'Cc'),
|
|
64
68
|
date: getHeader(headers, 'Date'),
|
|
65
|
-
body:
|
|
69
|
+
body: capped ? sliceClean(body, bodyCap) : body,
|
|
70
|
+
...(capped ? { bodyTruncated: true, bodyTotalChars: body.length } : {}),
|
|
66
71
|
attachments: getAttachments(msg.payload),
|
|
67
72
|
};
|
|
68
73
|
}
|
|
@@ -111,12 +116,13 @@ export function registerGmailTools(server) {
|
|
|
111
116
|
}
|
|
112
117
|
});
|
|
113
118
|
server.registerTool('gmail_read', {
|
|
114
|
-
description: 'Read a full Gmail message by ID',
|
|
119
|
+
description: 'Read a full Gmail message by ID (body capped at 50k chars unless full=true)',
|
|
115
120
|
inputSchema: {
|
|
116
121
|
account: accountEnum.describe('Google account alias'),
|
|
117
122
|
messageId: z.string().describe('Gmail message ID'),
|
|
123
|
+
full: coerceBoolean.optional().describe('Return the entire body without the character cap'),
|
|
118
124
|
},
|
|
119
|
-
}, async ({ account, messageId }) => {
|
|
125
|
+
}, async ({ account, messageId, full }) => {
|
|
120
126
|
try {
|
|
121
127
|
const auth = await getClient(account);
|
|
122
128
|
const gmail = google.gmail({ version: 'v1', auth });
|
|
@@ -125,7 +131,7 @@ export function registerGmailTools(server) {
|
|
|
125
131
|
id: messageId,
|
|
126
132
|
format: 'full',
|
|
127
133
|
});
|
|
128
|
-
const result = parseMessage(res.data);
|
|
134
|
+
const result = parseMessage(res.data, full ? undefined : BODY_CAP_CHARS);
|
|
129
135
|
return {
|
|
130
136
|
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
131
137
|
};
|
|
@@ -135,12 +141,13 @@ export function registerGmailTools(server) {
|
|
|
135
141
|
}
|
|
136
142
|
});
|
|
137
143
|
server.registerTool('gmail_read_thread', {
|
|
138
|
-
description: 'Read all messages in a Gmail thread',
|
|
144
|
+
description: 'Read all messages in a Gmail thread (bodies capped at 50k chars each unless full=true)',
|
|
139
145
|
inputSchema: {
|
|
140
146
|
account: accountEnum.describe('Google account alias'),
|
|
141
147
|
threadId: z.string().describe('Gmail thread ID'),
|
|
148
|
+
full: coerceBoolean.optional().describe('Return entire bodies without the character cap'),
|
|
142
149
|
},
|
|
143
|
-
}, async ({ account, threadId }) => {
|
|
150
|
+
}, async ({ account, threadId, full }) => {
|
|
144
151
|
try {
|
|
145
152
|
const auth = await getClient(account);
|
|
146
153
|
const gmail = google.gmail({ version: 'v1', auth });
|
|
@@ -149,7 +156,7 @@ export function registerGmailTools(server) {
|
|
|
149
156
|
id: threadId,
|
|
150
157
|
format: 'full',
|
|
151
158
|
});
|
|
152
|
-
const messages = (res.data.messages ?? []).map(parseMessage);
|
|
159
|
+
const messages = (res.data.messages ?? []).map((m) => parseMessage(m, full ? undefined : BODY_CAP_CHARS));
|
|
153
160
|
return {
|
|
154
161
|
content: [{ type: 'text', text: JSON.stringify(messages, null, 2) }],
|
|
155
162
|
};
|
package/dist/trim.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export declare function trimEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
2
|
+
export interface CappedText {
|
|
3
|
+
text: string;
|
|
4
|
+
truncated: boolean;
|
|
5
|
+
totalChars: number;
|
|
6
|
+
}
|
|
7
|
+
export declare function capText(text: string, maxChars: number, offset?: number): CappedText;
|
|
8
|
+
export declare function sliceClean(text: string, max: number): string;
|
|
9
|
+
interface ToolResult {
|
|
10
|
+
content?: {
|
|
11
|
+
type?: string;
|
|
12
|
+
text?: string;
|
|
13
|
+
}[];
|
|
14
|
+
}
|
|
15
|
+
export declare function compactResult<T extends ToolResult>(result: T): T;
|
|
16
|
+
export {};
|
package/dist/trim.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export function trimEnabled(env = process.env) {
|
|
2
|
+
return !/^(0|false|off|no)$/i.test((env.GOOGLE_TRIM ?? '').trim());
|
|
3
|
+
}
|
|
4
|
+
export function capText(text, maxChars, offset = 0) {
|
|
5
|
+
const slice = text.slice(offset, offset + maxChars);
|
|
6
|
+
return { text: slice, truncated: offset + slice.length < text.length, totalChars: text.length };
|
|
7
|
+
}
|
|
8
|
+
// For permanent (non-paging) caps: dropping a trailing lone high surrogate keeps
|
|
9
|
+
// the truncated string well-formed Unicode instead of ending in mojibake.
|
|
10
|
+
export function sliceClean(text, max) {
|
|
11
|
+
const sliced = text.slice(0, max);
|
|
12
|
+
return /[\uD800-\uDBFF]$/.test(sliced) ? sliced.slice(0, -1) : sliced;
|
|
13
|
+
}
|
|
14
|
+
// Pretty-printed JSON costs ~20-30% extra tokens; re-serialize compactly.
|
|
15
|
+
export function compactResult(result) {
|
|
16
|
+
if (!result || !Array.isArray(result.content))
|
|
17
|
+
return result;
|
|
18
|
+
for (const item of result.content) {
|
|
19
|
+
if (item?.type === 'text' && typeof item.text === 'string' && /^[\s]*[[{]/.test(item.text)) {
|
|
20
|
+
try {
|
|
21
|
+
item.text = JSON.stringify(JSON.parse(item.text));
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
// not JSON — leave untouched
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return result;
|
|
29
|
+
}
|
package/dist/types.d.ts
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-google-multi",
|
|
3
|
-
"version": "5.1.0-alpha.
|
|
3
|
+
"version": "5.1.0-alpha.3",
|
|
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",
|