gogcli-mcp 2.8.0 → 2.18.0
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.js +20218 -19700
- package/dist/lib.js +15845 -23331
- package/manifest.json +33 -1
- package/package.json +7 -6
- package/server.json +2 -2
- package/src/connector-auth.ts +46 -0
- package/src/connector-runtime.ts +177 -0
- package/src/index.ts +7 -5
- package/src/lib.ts +8 -7
- package/src/runner.ts +225 -30
- package/src/server.ts +21 -25
- package/src/tools/api.ts +65 -0
- package/src/tools/auth.ts +112 -15
- package/src/tools/calendar.ts +24 -9
- package/src/tools/docs.ts +28 -3
- package/src/tools/drive.ts +124 -1
- package/src/tools/gmail.ts +7 -3
- package/src/tools/sheets.ts +6 -3
- package/src/tools/slides.ts +9 -4
- package/src/tools/tasks.ts +3 -1
- package/src/tools/utils.ts +163 -27
- package/src/worker.ts +99 -0
- package/tests/connector-auth.test.ts +28 -0
- package/tests/connector-runtime.test.ts +474 -0
- package/tests/runner-file-args-failure.test.ts +94 -0
- package/tests/runner-file-args.test.ts +232 -0
- package/tests/runner.test.ts +221 -13
- package/tests/server.test.ts +28 -28
- package/tests/tools/api.test.ts +107 -0
- package/tests/tools/auth.test.ts +187 -31
- package/tests/tools/calendar.test.ts +115 -52
- package/tests/tools/classroom.test.ts +77 -77
- package/tests/tools/contacts.test.ts +24 -24
- package/tests/tools/docs.test.ts +84 -46
- package/tests/tools/drive.test.ts +226 -55
- package/tests/tools/gmail.test.ts +61 -28
- package/tests/tools/sheets.test.ts +81 -70
- package/tests/tools/slides.test.ts +56 -36
- package/tests/tools/tasks.test.ts +33 -33
- package/tests/tools/utils.test.ts +116 -2
- package/tests/worker.test.ts +142 -0
- package/tsconfig.json +4 -1
- package/vitest.config.ts +33 -2
- package/tests/helpers/test-harness.ts +0 -27
package/src/tools/calendar.ts
CHANGED
|
@@ -13,9 +13,11 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
13
13
|
today: z.boolean().optional().describe('Only show today\'s events'),
|
|
14
14
|
query: z.string().optional().describe('Free text search within events'),
|
|
15
15
|
all: z.boolean().optional().describe('Fetch events from all calendars'),
|
|
16
|
+
eventTypes: z.array(z.enum(['default', 'birthday', 'focus-time', 'from-gmail', 'out-of-office', 'working-location'])).optional().describe('Filter to specific event types (repeatable)'),
|
|
17
|
+
timezone: z.string().optional().describe('Display timezone for event times (IANA name, e.g. America/New_York, or "local" for the system timezone). Default: each event\'s timezone, then its calendar\'s timezone.'),
|
|
16
18
|
account: accountParam,
|
|
17
19
|
},
|
|
18
|
-
}, async ({ calendarId, from, to, today, query, all, account }) => {
|
|
20
|
+
}, async ({ calendarId, from, to, today, query, all, eventTypes, timezone, account }) => {
|
|
19
21
|
const args = ['calendar', 'events'];
|
|
20
22
|
if (calendarId) args.push(calendarId);
|
|
21
23
|
if (from) args.push(`--from=${from}`);
|
|
@@ -23,6 +25,8 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
23
25
|
if (today) args.push('--today');
|
|
24
26
|
if (query) args.push(`--query=${query}`);
|
|
25
27
|
if (all) args.push('--all');
|
|
28
|
+
if (eventTypes) for (const t of eventTypes) args.push(`--event-types=${t}`);
|
|
29
|
+
if (timezone) args.push(`--timezone=${timezone}`);
|
|
26
30
|
return runOrDiagnose(args, { account });
|
|
27
31
|
});
|
|
28
32
|
|
|
@@ -32,10 +36,13 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
32
36
|
inputSchema: {
|
|
33
37
|
calendarId: z.string().describe('Calendar ID'),
|
|
34
38
|
eventId: z.string().describe('Event ID'),
|
|
39
|
+
timezone: z.string().optional().describe('Display timezone for event times (IANA name, e.g. America/New_York, or "local" for the system timezone). Default: the event\'s timezone, then its calendar\'s timezone.'),
|
|
35
40
|
account: accountParam,
|
|
36
41
|
},
|
|
37
|
-
}, async ({ calendarId, eventId, account }) => {
|
|
38
|
-
|
|
42
|
+
}, async ({ calendarId, eventId, timezone, account }) => {
|
|
43
|
+
const args = ['calendar', 'event', calendarId, eventId];
|
|
44
|
+
if (timezone) args.push(`--timezone=${timezone}`);
|
|
45
|
+
return runOrDiagnose(args, { account });
|
|
39
46
|
});
|
|
40
47
|
|
|
41
48
|
server.registerTool('gog_calendar_create', {
|
|
@@ -48,23 +55,25 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
48
55
|
to: z.string().describe('End time (RFC3339 or date for all-day events)'),
|
|
49
56
|
description: z.string().optional().describe('Event description'),
|
|
50
57
|
location: z.string().optional().describe('Event location'),
|
|
51
|
-
attendees: z.string().optional().describe('Attendee emails, comma-separated'),
|
|
58
|
+
attendees: z.string().optional().describe('Attendee emails, comma-separated. Per-attendee modifiers (gog >= 0.31.1 for ;resource): ;optional, ;resource (e.g. meeting rooms), ;comment=TEXT'),
|
|
52
59
|
allDay: z.boolean().optional().describe('All-day event (use date-only in from/to)'),
|
|
60
|
+
timezone: z.string().optional().describe('IANA timezone metadata applied to from/to (e.g. America/New_York). Sets both start and end timezone unless start/end timezone are overridden.'),
|
|
53
61
|
withZoom: z.boolean().optional().describe('Create a Zoom video conference for this event (requires Zoom S2S OAuth setup)'),
|
|
54
62
|
account: accountParam,
|
|
55
63
|
},
|
|
56
|
-
}, async ({ calendarId, summary, from, to, description, location, attendees, allDay, withZoom, account }) => {
|
|
64
|
+
}, async ({ calendarId, summary, from, to, description, location, attendees, allDay, timezone, withZoom, account }) => {
|
|
57
65
|
const args = ['calendar', 'create', calendarId, `--summary=${summary}`, `--from=${from}`, `--to=${to}`];
|
|
58
66
|
if (description) args.push(`--description=${description}`);
|
|
59
67
|
if (location) args.push(`--location=${location}`);
|
|
60
68
|
if (attendees) args.push(`--attendees=${attendees}`);
|
|
61
69
|
if (allDay) args.push('--all-day');
|
|
70
|
+
if (timezone) args.push(`--timezone=${timezone}`);
|
|
62
71
|
if (withZoom) args.push('--with-zoom');
|
|
63
72
|
return runOrDiagnose(args, { account });
|
|
64
73
|
});
|
|
65
74
|
|
|
66
75
|
server.registerTool('gog_calendar_update', {
|
|
67
|
-
description: 'Update an existing calendar event. Zoom: withZoom adds a Zoom meeting, regenerateZoom replaces the existing one, removeZoom strips it (
|
|
76
|
+
description: 'Update an existing calendar event. Zoom: withZoom adds a Zoom meeting, regenerateZoom replaces the existing one, removeZoom strips it. removeMeet clears the event\'s Google Meet conference data (e.g. before attaching another provider). Conference flags are independent — use one per call.',
|
|
68
77
|
annotations: { destructiveHint: false },
|
|
69
78
|
inputSchema: {
|
|
70
79
|
calendarId: z.string().describe('Calendar ID'),
|
|
@@ -74,14 +83,16 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
74
83
|
to: z.string().optional().describe('New end time (RFC3339)'),
|
|
75
84
|
description: z.string().optional().describe('New description'),
|
|
76
85
|
location: z.string().optional().describe('New location'),
|
|
77
|
-
attendees: z.string().optional().describe('New attendee emails, comma-separated (replaces existing)'),
|
|
86
|
+
attendees: z.string().optional().describe('New attendee emails, comma-separated (replaces ALL existing; set empty to clear). Per-attendee modifiers: ;optional, ;resource, ;comment=TEXT'),
|
|
87
|
+
addAttendees: z.string().optional().describe('Attendee emails to add, comma-separated (preserves existing attendees). Per-attendee modifiers: ;optional, ;resource, ;comment=TEXT'),
|
|
78
88
|
attachments: z.array(z.string()).optional().describe('File attachment URLs (e.g. Drive links). Replaces ALL existing attachments; pass a single empty string to clear them.'),
|
|
79
89
|
withZoom: z.boolean().optional().describe('Create a Zoom video conference for this event'),
|
|
80
90
|
regenerateZoom: z.boolean().optional().describe('Replace the event\'s existing Zoom video conference'),
|
|
81
91
|
removeZoom: z.boolean().optional().describe('Remove the event\'s Zoom video conference'),
|
|
92
|
+
removeMeet: z.boolean().optional().describe('Remove the event\'s Google Meet video conference (clears conference data only)'),
|
|
82
93
|
account: accountParam,
|
|
83
94
|
},
|
|
84
|
-
}, async ({ calendarId, eventId, summary, from, to, description, location, attendees, attachments, withZoom, regenerateZoom, removeZoom, account }) => {
|
|
95
|
+
}, async ({ calendarId, eventId, summary, from, to, description, location, attendees, addAttendees, attachments, withZoom, regenerateZoom, removeZoom, removeMeet, account }) => {
|
|
85
96
|
const args = ['calendar', 'update', calendarId, eventId];
|
|
86
97
|
if (summary !== undefined) args.push(`--summary=${summary}`);
|
|
87
98
|
if (from !== undefined) args.push(`--from=${from}`);
|
|
@@ -89,10 +100,12 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
89
100
|
if (description !== undefined) args.push(`--description=${description}`);
|
|
90
101
|
if (location !== undefined) args.push(`--location=${location}`);
|
|
91
102
|
if (attendees !== undefined) args.push(`--attendees=${attendees}`);
|
|
103
|
+
if (addAttendees) args.push(`--add-attendee=${addAttendees}`);
|
|
92
104
|
if (attachments) for (const a of attachments) args.push(`--attachment=${a}`);
|
|
93
105
|
if (withZoom) args.push('--with-zoom');
|
|
94
106
|
if (regenerateZoom) args.push('--regenerate-zoom');
|
|
95
107
|
if (removeZoom) args.push('--remove-zoom');
|
|
108
|
+
if (removeMeet) args.push('--remove-meet');
|
|
96
109
|
return runOrDiagnose(args, { account });
|
|
97
110
|
});
|
|
98
111
|
|
|
@@ -105,7 +118,9 @@ export function registerCalendarTools(server: McpServer): void {
|
|
|
105
118
|
account: accountParam,
|
|
106
119
|
},
|
|
107
120
|
}, async ({ calendarId, eventId, account }) => {
|
|
108
|
-
|
|
121
|
+
// gog gates this delete behind a confirmation; the runner injects
|
|
122
|
+
// --no-input, so without --force it refuses at runtime.
|
|
123
|
+
return runOrDiagnose(['calendar', 'delete', calendarId, eventId, '--force'], { account });
|
|
109
124
|
});
|
|
110
125
|
|
|
111
126
|
server.registerTool('gog_calendar_respond', {
|
package/src/tools/docs.ts
CHANGED
|
@@ -19,10 +19,13 @@ export function registerDocsTools(server: McpServer): void {
|
|
|
19
19
|
annotations: { readOnlyHint: true },
|
|
20
20
|
inputSchema: {
|
|
21
21
|
docId: z.string().describe('Doc ID (from the URL)'),
|
|
22
|
+
chips: z.boolean().optional().describe('Render Google Docs smart chips (people, dates, rich links) inline in the text output'),
|
|
22
23
|
account: accountParam,
|
|
23
24
|
},
|
|
24
|
-
}, async ({ docId, account }) => {
|
|
25
|
-
|
|
25
|
+
}, async ({ docId, chips, account }) => {
|
|
26
|
+
const args = ['docs', 'cat', docId];
|
|
27
|
+
if (chips) args.push('--chips');
|
|
28
|
+
return runOrDiagnose(args, { account });
|
|
26
29
|
});
|
|
27
30
|
|
|
28
31
|
server.registerTool('gog_docs_create', {
|
|
@@ -43,13 +46,35 @@ export function registerDocsTools(server: McpServer): void {
|
|
|
43
46
|
text: z.string().describe('Text content to write'),
|
|
44
47
|
append: z.boolean().optional().describe('Append to existing content instead of replacing (default: false)'),
|
|
45
48
|
checkOrphans: z.boolean().optional().describe('Block the write (exit code 11) if it would orphan an open comment — i.e. remove the text the comment is anchored to. Recommended for replacement writes on commented docs.'),
|
|
49
|
+
bullets: z.boolean().optional().describe('Write the paragraphs as a bulleted list with the default disc preset'),
|
|
50
|
+
bulletPreset: z.string().optional().describe('Write a bulleted list with a specific Google Docs bullet glyph preset (e.g. BULLET_DISC_CIRCLE_SQUARE)'),
|
|
51
|
+
ordered: z.boolean().optional().describe('Write the paragraphs as a numbered list with the default decimal preset'),
|
|
52
|
+
noBullets: z.boolean().optional().describe('Remove bullets or numbering from the written paragraphs'),
|
|
53
|
+
indentStart: z.number().optional().describe('Paragraph start indentation in points'),
|
|
54
|
+
indentEnd: z.number().optional().describe('Paragraph end indentation in points'),
|
|
55
|
+
indentFirstLine: z.number().optional().describe('Paragraph first-line indentation in points'),
|
|
56
|
+
spaceAbove: z.number().optional().describe('Space above the paragraph in points'),
|
|
57
|
+
spaceBelow: z.number().optional().describe('Space below the paragraph in points'),
|
|
58
|
+
keepLinesTogether: z.boolean().optional().describe('Keep all lines of the paragraph on one page/column (true) or clear that setting (false)'),
|
|
59
|
+
keepWithNext: z.boolean().optional().describe('Keep the paragraph with the next paragraph (true) or clear that setting (false)'),
|
|
46
60
|
batch: z.string().optional().describe('Append this mutation to a persisted batch (from gog_batch_begin in gogcli-mcp-docs) instead of applying it — nothing changes in the doc until the batch is submitted.'),
|
|
47
61
|
account: accountParam,
|
|
48
62
|
},
|
|
49
|
-
}, async ({ docId, text, append, checkOrphans, batch, account }) => {
|
|
63
|
+
}, async ({ docId, text, append, checkOrphans, bullets, bulletPreset, ordered, noBullets, indentStart, indentEnd, indentFirstLine, spaceAbove, spaceBelow, keepLinesTogether, keepWithNext, batch, account }) => {
|
|
50
64
|
const args = ['docs', 'write', docId, `--text=${text}`];
|
|
51
65
|
if (append) args.push('--append');
|
|
52
66
|
if (checkOrphans) args.push('--check-orphans');
|
|
67
|
+
if (bullets) args.push('--bullets');
|
|
68
|
+
if (bulletPreset) args.push(`--bullet-preset=${bulletPreset}`);
|
|
69
|
+
if (ordered) args.push('--ordered');
|
|
70
|
+
if (noBullets) args.push('--no-bullets');
|
|
71
|
+
if (indentStart !== undefined) args.push(`--indent-start=${indentStart}`);
|
|
72
|
+
if (indentEnd !== undefined) args.push(`--indent-end=${indentEnd}`);
|
|
73
|
+
if (indentFirstLine !== undefined) args.push(`--indent-first-line=${indentFirstLine}`);
|
|
74
|
+
if (spaceAbove !== undefined) args.push(`--space-above=${spaceAbove}`);
|
|
75
|
+
if (spaceBelow !== undefined) args.push(`--space-below=${spaceBelow}`);
|
|
76
|
+
if (keepLinesTogether !== undefined) args.push(keepLinesTogether ? '--keep-lines-together' : '--no-keep-lines-together');
|
|
77
|
+
if (keepWithNext !== undefined) args.push(keepWithNext ? '--keep-with-next' : '--no-keep-with-next');
|
|
53
78
|
if (batch) args.push(`--batch=${batch}`);
|
|
54
79
|
return runOrDiagnose(args, { account });
|
|
55
80
|
});
|
package/src/tools/drive.ts
CHANGED
|
@@ -1,6 +1,22 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
|
+
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
|
|
2
3
|
import { z } from 'zod';
|
|
3
|
-
import {
|
|
4
|
+
import { rawTextResult } from '@chrischall/mcp-utils';
|
|
5
|
+
import { run, runBinary } from '../runner.js';
|
|
6
|
+
import { accountParam, diagnose, runOrDiagnose, registerRunTool } from './utils.js';
|
|
7
|
+
|
|
8
|
+
// A native Google Doc exports to text directly; anything else (PDF, image,
|
|
9
|
+
// docx, …) is first copied WITH conversion to this type, which makes Drive run
|
|
10
|
+
// OCR, before exporting.
|
|
11
|
+
const GOOGLE_DOC_MIME = 'application/vnd.google-apps.document';
|
|
12
|
+
|
|
13
|
+
// Parse `gog drive get` JSON into { name, mimeType }. gog nests the payload
|
|
14
|
+
// under `file`; fall back to the top level if that ever changes.
|
|
15
|
+
function fileMeta(raw: string): { name?: string; mimeType?: string } {
|
|
16
|
+
const parsed = JSON.parse(raw) as { file?: { name?: string; mimeType?: string }; name?: string; mimeType?: string };
|
|
17
|
+
const f = parsed.file ?? parsed;
|
|
18
|
+
return { name: f.name, mimeType: f.mimeType };
|
|
19
|
+
}
|
|
4
20
|
|
|
5
21
|
export function registerDriveTools(server: McpServer): void {
|
|
6
22
|
server.registerTool('gog_drive_ls', {
|
|
@@ -92,6 +108,9 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
92
108
|
}, async ({ fileId, permanent, account }) => {
|
|
93
109
|
const args = ['drive', 'delete', fileId];
|
|
94
110
|
if (permanent) args.push('--permanent');
|
|
111
|
+
// gog gates drive delete behind a confirmation; the runner injects
|
|
112
|
+
// --no-input, so without --force it refuses at runtime.
|
|
113
|
+
args.push('--force');
|
|
95
114
|
return runOrDiagnose(args, { account });
|
|
96
115
|
});
|
|
97
116
|
|
|
@@ -111,8 +130,112 @@ export function registerDriveTools(server: McpServer): void {
|
|
|
111
130
|
if (email) args.push(`--email=${email}`);
|
|
112
131
|
if (domain) args.push(`--domain=${domain}`);
|
|
113
132
|
if (role) args.push(`--role=${role}`);
|
|
133
|
+
// gog gates public sharing behind a confirmation; the runner injects
|
|
134
|
+
// --no-input, so without --force it refuses at runtime.
|
|
135
|
+
if (to === 'anyone') args.push('--force');
|
|
114
136
|
return runOrDiagnose(args, { account });
|
|
115
137
|
});
|
|
116
138
|
|
|
139
|
+
server.registerTool('gog_drive_extract_text', {
|
|
140
|
+
description:
|
|
141
|
+
'Extract readable TEXT from a Drive file — PDF, image, docx, or a native Google Doc — and return ' +
|
|
142
|
+
'it to you directly (closing the webViewLink dead end). Accepts a Drive file id, including the id ' +
|
|
143
|
+
'from gog_gmail_attachment\'s deliveredVia:"drive" response. For non-Docs it copies the file to a ' +
|
|
144
|
+
'temporary Google Doc so Drive OCRs it (works for scanned PDFs too), exports plain text, then ' +
|
|
145
|
+
'deletes the temp Doc. Operates entirely via the Drive API within the existing drive scope — no ' +
|
|
146
|
+
'host filesystem, no scope widening. For a large file, page through with offset/maxChars.',
|
|
147
|
+
// Creates and deletes a temporary Doc for non-native files, so not read-only.
|
|
148
|
+
annotations: { destructiveHint: false },
|
|
149
|
+
inputSchema: {
|
|
150
|
+
fileId: z.string().describe('Drive file ID (e.g. the id returned by gog_gmail_attachment)'),
|
|
151
|
+
ocrLanguage: z.string().optional().describe(
|
|
152
|
+
'BCP-47 language hint for OCR of scanned/image PDFs (e.g. "en", "fr"). Optional.',
|
|
153
|
+
),
|
|
154
|
+
offset: z.number().int().nonnegative().optional().describe('Character offset to start from (default: 0)'),
|
|
155
|
+
maxChars: z.number().int().positive().optional().describe('Max characters to return (default: all from offset)'),
|
|
156
|
+
account: accountParam,
|
|
157
|
+
},
|
|
158
|
+
}, async ({ fileId, ocrLanguage, offset = 0, maxChars, account }) => {
|
|
159
|
+
let tempDocId: string | undefined;
|
|
160
|
+
try {
|
|
161
|
+
const { name, mimeType } = fileMeta(await run(['drive', 'get', fileId], { account }));
|
|
162
|
+
|
|
163
|
+
// Native Docs export straight to text; everything else is OCR-converted first.
|
|
164
|
+
let sourceId = fileId;
|
|
165
|
+
if (mimeType !== GOOGLE_DOC_MIME) {
|
|
166
|
+
const params = JSON.stringify({ fileId, ...(ocrLanguage ? { ocrLanguage } : {}) });
|
|
167
|
+
const body = JSON.stringify({ name: `gogcli-ocr-${fileId}`, mimeType: GOOGLE_DOC_MIME });
|
|
168
|
+
const copied = JSON.parse(await run(
|
|
169
|
+
['api', 'call', 'drive', 'v3', 'files.copy', '--allow-write', '--force',
|
|
170
|
+
`--params=${params}`, `--body=${body}`],
|
|
171
|
+
{ account },
|
|
172
|
+
)) as { id?: string; result?: { id?: string } };
|
|
173
|
+
tempDocId = copied.id ?? copied.result?.id;
|
|
174
|
+
if (!tempDocId) throw new Error('OCR conversion did not return a document id');
|
|
175
|
+
sourceId = tempDocId;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const exportParams = JSON.stringify({ fileId: sourceId, mimeType: 'text/plain' });
|
|
179
|
+
const raw = await run(['api', 'call', 'drive', 'v3', 'files.export', `--params=${exportParams}`], { account });
|
|
180
|
+
|
|
181
|
+
const full = raw.replace(/^/, ''); // drop the export's leading BOM
|
|
182
|
+
const start = Math.min(offset, full.length);
|
|
183
|
+
const text = maxChars !== undefined ? full.slice(start, start + maxChars) : full.slice(start);
|
|
184
|
+
return rawTextResult(JSON.stringify({
|
|
185
|
+
fileId,
|
|
186
|
+
name,
|
|
187
|
+
mimeType,
|
|
188
|
+
extractedVia: mimeType === GOOGLE_DOC_MIME ? 'native-export' : 'ocr-convert',
|
|
189
|
+
totalChars: full.length,
|
|
190
|
+
offset: start,
|
|
191
|
+
returnedChars: text.length,
|
|
192
|
+
truncated: start + text.length < full.length,
|
|
193
|
+
text,
|
|
194
|
+
}, null, 2));
|
|
195
|
+
} catch (err) {
|
|
196
|
+
return diagnose(err);
|
|
197
|
+
} finally {
|
|
198
|
+
// Always remove the temp Doc — on success, and on a mid-extraction failure.
|
|
199
|
+
if (tempDocId) {
|
|
200
|
+
await run(['drive', 'delete', tempDocId, '--permanent', '--force'], { account }).catch(() => {});
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
server.registerTool('gog_drive_read_bytes', {
|
|
206
|
+
description:
|
|
207
|
+
'Fetch a Drive file\'s raw bytes and return them base64-encoded as an embedded resource — the ' +
|
|
208
|
+
'generic fallback for callers that want the file itself (to parse locally) rather than extracted ' +
|
|
209
|
+
'text. For readable text from a PDF, prefer gog_drive_extract_text. NOTE: only works on the local ' +
|
|
210
|
+
'stdio server; over the hosted connector the transport is text-only and this returns a clear error ' +
|
|
211
|
+
'(use gog_drive_extract_text there).',
|
|
212
|
+
annotations: { readOnlyHint: true },
|
|
213
|
+
inputSchema: {
|
|
214
|
+
fileId: z.string().describe('Drive file ID'),
|
|
215
|
+
account: accountParam,
|
|
216
|
+
},
|
|
217
|
+
}, async ({ fileId, account }): Promise<CallToolResult> => {
|
|
218
|
+
try {
|
|
219
|
+
const { name, mimeType } = fileMeta(await run(['drive', 'get', fileId], { account }));
|
|
220
|
+
const params = JSON.stringify({ fileId, alt: 'media' });
|
|
221
|
+
const blob = await runBinary(['api', 'call', 'drive', 'v3', 'files.get', `--params=${params}`], { account });
|
|
222
|
+
return {
|
|
223
|
+
content: [
|
|
224
|
+
{ type: 'text', text: `${name ?? fileId} (${mimeType ?? 'application/octet-stream'}) — ${Buffer.from(blob, 'base64').length} bytes, base64 resource below.` },
|
|
225
|
+
{
|
|
226
|
+
type: 'resource',
|
|
227
|
+
resource: {
|
|
228
|
+
uri: `gogdrive://${fileId}/${encodeURIComponent(name ?? 'file')}`,
|
|
229
|
+
mimeType: mimeType ?? 'application/octet-stream',
|
|
230
|
+
blob,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
],
|
|
234
|
+
};
|
|
235
|
+
} catch (err) {
|
|
236
|
+
return diagnose(err);
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
|
|
117
240
|
registerRunTool(server, { service: 'drive', examples: '"copy", "upload", "download", "permissions"' });
|
|
118
241
|
}
|
package/src/tools/gmail.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
|
-
import { accountParam, runOrDiagnose, registerRunTool } from './utils.js';
|
|
3
|
+
import { accountParam, runOrDiagnose, registerRunTool, payloadArg } from './utils.js';
|
|
4
|
+
import type { GogArg } from '../runner.js';
|
|
4
5
|
|
|
5
6
|
export function registerGmailTools(server: McpServer): void {
|
|
6
7
|
server.registerTool('gog_gmail_search', {
|
|
@@ -39,7 +40,7 @@ export function registerGmailTools(server: McpServer): void {
|
|
|
39
40
|
inputSchema: {
|
|
40
41
|
to: z.string().describe('Recipient(s), comma-separated'),
|
|
41
42
|
subject: z.string().describe('Subject line'),
|
|
42
|
-
body: z.string().describe('Email body (plain text)'),
|
|
43
|
+
body: z.string().describe('Email body (plain text). Any size — a large body is written to a temp file on the gog server rather than inlined into the command line. Note gog strips trailing newlines from a file-delivered body.'),
|
|
43
44
|
cc: z.string().optional().describe('CC recipients, comma-separated'),
|
|
44
45
|
bcc: z.string().optional().describe('BCC recipients, comma-separated'),
|
|
45
46
|
replyToMessageId: z.string().optional().describe('Message ID to reply to'),
|
|
@@ -48,7 +49,10 @@ export function registerGmailTools(server: McpServer): void {
|
|
|
48
49
|
account: accountParam,
|
|
49
50
|
},
|
|
50
51
|
}, async ({ to, subject, body, cc, bcc, replyToMessageId, threadId, attach, account }) => {
|
|
51
|
-
|
|
52
|
+
// A long body cannot ride in argv: the hosted runner caps a single arg and
|
|
53
|
+
// Linux caps MAX_ARG_STRLEN at 128 KiB. payloadArg swaps it for --body-file
|
|
54
|
+
// past the shared threshold; the executor materializes the temp file.
|
|
55
|
+
const args: GogArg[] = ['gmail', 'send', `--to=${to}`, `--subject=${subject}`, payloadArg('body', 'body-file', body)];
|
|
52
56
|
if (cc) args.push(`--cc=${cc}`);
|
|
53
57
|
if (bcc) args.push(`--bcc=${bcc}`);
|
|
54
58
|
if (replyToMessageId) args.push(`--reply-to-message-id=${replyToMessageId}`);
|
package/src/tools/sheets.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { run } from '../runner.js';
|
|
4
|
-
import {
|
|
4
|
+
import { rawTextResult } from '@chrischall/mcp-utils';
|
|
5
|
+
import { accountParam, runOrDiagnose, registerRunTool, diagnose } from './utils.js';
|
|
5
6
|
import { expandAnchorRange, countNonEmptyCells } from './sheets-a1.js';
|
|
6
7
|
|
|
7
8
|
// Cell value type: matches what gog sheets --values-json accepts (passed
|
|
@@ -42,9 +43,10 @@ export function registerSheetsTools(server: McpServer): void {
|
|
|
42
43
|
values: z.array(z.array(cellValueParam)).describe('2D array of values (rows of columns). Cells may be string/number/boolean/null; strings starting with "=" are formulas.'),
|
|
43
44
|
dry_run: dryRunParam,
|
|
44
45
|
fail_if_not_empty: failIfNotEmptyParam,
|
|
46
|
+
fail_on_formula_error: z.boolean().optional().describe('After writing, read the range back and fail if any cell holds a Sheets formula error (#REF!, #DIV/0!, etc.).'),
|
|
45
47
|
account: accountParam,
|
|
46
48
|
},
|
|
47
|
-
}, async ({ spreadsheetId, range, values, account, dry_run, fail_if_not_empty }) => {
|
|
49
|
+
}, async ({ spreadsheetId, range, values, account, dry_run, fail_if_not_empty, fail_on_formula_error }) => {
|
|
48
50
|
const cols = values.reduce((max, row) => Math.max(max, row.length), 0);
|
|
49
51
|
if (fail_if_not_empty && values.length > 0 && cols > 0) {
|
|
50
52
|
const readRange = expandAnchorRange(range, values.length, cols);
|
|
@@ -62,7 +64,7 @@ export function registerSheetsTools(server: McpServer): void {
|
|
|
62
64
|
const detail = occupied < 0
|
|
63
65
|
? 'could not be verified as empty'
|
|
64
66
|
: `already contains data in ${occupied} cell(s)`;
|
|
65
|
-
return
|
|
67
|
+
return rawTextResult(
|
|
66
68
|
`Write aborted (fail_if_not_empty): target range ${readRange} ${detail}. ` +
|
|
67
69
|
'Re-run without fail_if_not_empty to overwrite, or clear it first with gog_sheets_clear.',
|
|
68
70
|
);
|
|
@@ -70,6 +72,7 @@ export function registerSheetsTools(server: McpServer): void {
|
|
|
70
72
|
}
|
|
71
73
|
const args = ['sheets', 'update', spreadsheetId, range, `--values-json=${JSON.stringify(values)}`];
|
|
72
74
|
if (dry_run) args.push('--dry-run');
|
|
75
|
+
if (fail_on_formula_error) args.push('--fail-on-formula-error');
|
|
73
76
|
return runOrDiagnose(args, { account });
|
|
74
77
|
});
|
|
75
78
|
|
package/src/tools/slides.ts
CHANGED
|
@@ -10,12 +10,14 @@ export function registerSlidesTools(server: McpServer): void {
|
|
|
10
10
|
presentationId: z.string().describe('Presentation ID'),
|
|
11
11
|
out: z.string().optional().describe('Output file path'),
|
|
12
12
|
format: z.enum(['pdf', 'pptx']).optional().describe('Export format (default: pptx)'),
|
|
13
|
+
overwrite: z.boolean().optional().describe('Overwrite the output file if it already exists (gog refuses otherwise)'),
|
|
13
14
|
account: accountParam,
|
|
14
15
|
},
|
|
15
|
-
}, async ({ presentationId, out, format, account }) => {
|
|
16
|
+
}, async ({ presentationId, out, format, overwrite, account }) => {
|
|
16
17
|
const args = ['slides', 'export', presentationId];
|
|
17
18
|
if (out) args.push(`--out=${out}`);
|
|
18
19
|
if (format) args.push(`--format=${format}`);
|
|
20
|
+
if (overwrite) args.push('--overwrite');
|
|
19
21
|
return runOrDiagnose(args, { account });
|
|
20
22
|
});
|
|
21
23
|
|
|
@@ -71,15 +73,18 @@ export function registerSlidesTools(server: McpServer): void {
|
|
|
71
73
|
});
|
|
72
74
|
|
|
73
75
|
server.registerTool('gog_slides_read_slide', {
|
|
74
|
-
description: 'Read the content of a slide (text, shapes, speaker notes).',
|
|
76
|
+
description: 'Read the content of a slide (text, shapes, speaker notes). Set detail=true to also include normalized element geometry, styled text runs, paragraphs, table-cell content, and image source URLs.',
|
|
75
77
|
annotations: { readOnlyHint: true },
|
|
76
78
|
inputSchema: {
|
|
77
79
|
presentationId: z.string().describe('Presentation ID'),
|
|
78
80
|
slideId: z.string().describe('Slide ID to read'),
|
|
81
|
+
detail: z.boolean().optional().describe('Include normalized element geometry, styled text runs, paragraphs, table-cell content, and image source URLs'),
|
|
79
82
|
account: accountParam,
|
|
80
83
|
},
|
|
81
|
-
}, async ({ presentationId, slideId, account }) => {
|
|
82
|
-
|
|
84
|
+
}, async ({ presentationId, slideId, detail, account }) => {
|
|
85
|
+
const args = ['slides', 'read-slide', presentationId, slideId];
|
|
86
|
+
if (detail) args.push('--detail');
|
|
87
|
+
return runOrDiagnose(args, { account });
|
|
83
88
|
});
|
|
84
89
|
|
|
85
90
|
registerRunTool(server, { service: 'slides', examples: '"add-slide", "delete-slide", "update-notes"' });
|
package/src/tools/tasks.ts
CHANGED
|
@@ -74,7 +74,9 @@ export function registerTasksTools(server: McpServer): void {
|
|
|
74
74
|
account: accountParam,
|
|
75
75
|
},
|
|
76
76
|
}, async ({ tasklistId, taskId, account }) => {
|
|
77
|
-
|
|
77
|
+
// gog gates this delete behind a confirmation; the runner injects
|
|
78
|
+
// --no-input, so without --force it refuses at runtime.
|
|
79
|
+
return runOrDiagnose(['tasks', 'delete', tasklistId, taskId, '--force'], { account });
|
|
78
80
|
});
|
|
79
81
|
|
|
80
82
|
registerRunTool(server, { service: 'tasks', examples: '"update", "undo", "clear"' });
|