gogcli-mcp 1.0.10 → 2.0.2

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.
@@ -8,11 +8,19 @@ export function registerDriveTools(server: McpServer): void {
8
8
  annotations: { readOnlyHint: true },
9
9
  inputSchema: {
10
10
  folderId: z.string().optional().describe('Folder ID to list (default: root)'),
11
+ max: z.number().optional().describe('Max results (default: 20)'),
12
+ page: z.string().optional().describe('Page token for pagination'),
13
+ query: z.string().optional().describe('Drive query filter (e.g. "name contains \'budget\'")'),
14
+ allDrives: z.boolean().optional().describe('Include shared drives (default: true). Set false for My Drive only.'),
11
15
  account: accountParam,
12
16
  },
13
- }, async ({ folderId, account }) => {
17
+ }, async ({ folderId, max, page, query, allDrives, account }) => {
14
18
  const args = ['drive', 'ls'];
15
- if (folderId) args.push(folderId);
19
+ if (folderId) args.push(`--parent=${folderId}`);
20
+ if (max !== undefined) args.push(`--max=${max}`);
21
+ if (page) args.push(`--page=${page}`);
22
+ if (query) args.push(`--query=${query}`);
23
+ if (allDrives === false) args.push('--no-all-drives');
16
24
  return runOrDiagnose(args, { account });
17
25
  });
18
26
 
@@ -74,14 +82,17 @@ export function registerDriveTools(server: McpServer): void {
74
82
  });
75
83
 
76
84
  server.registerTool('gog_drive_delete', {
77
- description: 'Move a Google Drive file to trash.',
85
+ description: 'Move a Google Drive file to trash, or permanently delete it with permanent=true (irreversible).',
78
86
  annotations: { destructiveHint: true },
79
87
  inputSchema: {
80
- fileId: z.string().describe('File ID to trash'),
88
+ fileId: z.string().describe('File ID to delete'),
89
+ permanent: z.boolean().optional().describe('Permanently delete instead of moving to trash (irreversible)'),
81
90
  account: accountParam,
82
91
  },
83
- }, async ({ fileId, account }) => {
84
- return runOrDiagnose(['drive', 'delete', fileId], { account });
92
+ }, async ({ fileId, permanent, account }) => {
93
+ const args = ['drive', 'delete', fileId];
94
+ if (permanent) args.push('--permanent');
95
+ return runOrDiagnose(args, { account });
85
96
  });
86
97
 
87
98
  server.registerTool('gog_drive_share', {
@@ -0,0 +1,203 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { accountParam, runOrDiagnose } from './utils.js';
4
+
5
+ export function registerSlidesTools(server: McpServer): void {
6
+ server.registerTool('gog_slides_export', {
7
+ description: 'Export a Google Slides presentation to a local file (pdf or pptx).',
8
+ annotations: { readOnlyHint: true },
9
+ inputSchema: {
10
+ presentationId: z.string().describe('Presentation ID'),
11
+ out: z.string().optional().describe('Output file path'),
12
+ format: z.enum(['pdf', 'pptx']).optional().describe('Export format (default: pptx)'),
13
+ account: accountParam,
14
+ },
15
+ }, async ({ presentationId, out, format, account }) => {
16
+ const args = ['slides', 'export', presentationId];
17
+ if (out) args.push(`--out=${out}`);
18
+ if (format) args.push(`--format=${format}`);
19
+ return runOrDiagnose(args, { account });
20
+ });
21
+
22
+ server.registerTool('gog_slides_info', {
23
+ description: 'Get metadata for a Google Slides presentation (title, ID, slide count, etc.).',
24
+ annotations: { readOnlyHint: true },
25
+ inputSchema: {
26
+ presentationId: z.string().describe('Presentation ID'),
27
+ account: accountParam,
28
+ },
29
+ }, async ({ presentationId, account }) => {
30
+ return runOrDiagnose(['slides', 'info', presentationId], { account });
31
+ });
32
+
33
+ server.registerTool('gog_slides_create', {
34
+ description: 'Create a new Google Slides presentation, optionally in a folder or copying from a template.',
35
+ inputSchema: {
36
+ title: z.string().describe('Presentation title'),
37
+ parent: z.string().optional().describe('Destination folder ID'),
38
+ template: z.string().optional().describe('Template presentation ID to copy from'),
39
+ account: accountParam,
40
+ },
41
+ }, async ({ title, parent, template, account }) => {
42
+ const args = ['slides', 'create', title];
43
+ if (parent) args.push(`--parent=${parent}`);
44
+ if (template) args.push(`--template=${template}`);
45
+ return runOrDiagnose(args, { account });
46
+ });
47
+
48
+ server.registerTool('gog_slides_create_from_markdown', {
49
+ description: 'Create a new Google Slides presentation from markdown content (inline or from a file).',
50
+ inputSchema: {
51
+ title: z.string().describe('Presentation title'),
52
+ content: z.string().optional().describe('Inline markdown content'),
53
+ contentFile: z.string().optional().describe('Path to a markdown file'),
54
+ parent: z.string().optional().describe('Destination folder ID'),
55
+ debug: z.boolean().optional().describe('Enable debug output'),
56
+ account: accountParam,
57
+ },
58
+ }, async ({ title, content, contentFile, parent, debug, account }) => {
59
+ const args = ['slides', 'create-from-markdown', title];
60
+ if (content) args.push(`--content=${content}`);
61
+ if (contentFile) args.push(`--content-file=${contentFile}`);
62
+ if (parent) args.push(`--parent=${parent}`);
63
+ if (debug) args.push('--debug');
64
+ return runOrDiagnose(args, { account });
65
+ });
66
+
67
+ server.registerTool('gog_slides_create_from_template', {
68
+ description: 'Create a new Google Slides presentation from a template, with optional placeholder replacements.',
69
+ inputSchema: {
70
+ templateId: z.string().describe('Template presentation ID'),
71
+ title: z.string().describe('New presentation title'),
72
+ replacements: z.record(z.string(), z.string()).optional().describe('Placeholder replacements as a key/value object (emitted as --replace=k=v for each entry)'),
73
+ replacementsFile: z.string().optional().describe('Path to a JSON file containing replacements'),
74
+ parent: z.string().optional().describe('Destination folder ID'),
75
+ exact: z.boolean().optional().describe('Require exact placeholder matches'),
76
+ account: accountParam,
77
+ },
78
+ }, async ({ templateId, title, replacements, replacementsFile, parent, exact, account }) => {
79
+ const args = ['slides', 'create-from-template', templateId, title];
80
+ if (replacements) {
81
+ for (const [k, v] of Object.entries(replacements)) {
82
+ args.push(`--replace=${k}=${v}`);
83
+ }
84
+ }
85
+ if (replacementsFile) args.push(`--replacements=${replacementsFile}`);
86
+ if (parent) args.push(`--parent=${parent}`);
87
+ if (exact) args.push('--exact');
88
+ return runOrDiagnose(args, { account });
89
+ });
90
+
91
+ server.registerTool('gog_slides_copy', {
92
+ description: 'Copy a Google Slides presentation to a new presentation with the given title.',
93
+ inputSchema: {
94
+ presentationId: z.string().describe('Presentation ID to copy'),
95
+ title: z.string().describe('Title for the new copy'),
96
+ parent: z.string().optional().describe('Destination folder ID'),
97
+ account: accountParam,
98
+ },
99
+ }, async ({ presentationId, title, parent, account }) => {
100
+ const args = ['slides', 'copy', presentationId, title];
101
+ if (parent) args.push(`--parent=${parent}`);
102
+ return runOrDiagnose(args, { account });
103
+ });
104
+
105
+ server.registerTool('gog_slides_add_slide', {
106
+ description: 'Add a new slide to a presentation from a local image, with optional speaker notes.',
107
+ inputSchema: {
108
+ presentationId: z.string().describe('Presentation ID'),
109
+ image: z.string().describe('Path to the local image file'),
110
+ notes: z.string().optional().describe('Speaker notes text'),
111
+ notesFile: z.string().optional().describe('Path to a file containing speaker notes'),
112
+ before: z.string().optional().describe('Insert before this slide ID (default: append at end)'),
113
+ account: accountParam,
114
+ },
115
+ }, async ({ presentationId, image, notes, notesFile, before, account }) => {
116
+ const args = ['slides', 'add-slide', presentationId, image];
117
+ if (notes) args.push(`--notes=${notes}`);
118
+ if (notesFile) args.push(`--notes-file=${notesFile}`);
119
+ if (before) args.push(`--before=${before}`);
120
+ return runOrDiagnose(args, { account });
121
+ });
122
+
123
+ server.registerTool('gog_slides_list_slides', {
124
+ description: 'List slides in a Google Slides presentation.',
125
+ annotations: { readOnlyHint: true },
126
+ inputSchema: {
127
+ presentationId: z.string().describe('Presentation ID'),
128
+ account: accountParam,
129
+ },
130
+ }, async ({ presentationId, account }) => {
131
+ return runOrDiagnose(['slides', 'list-slides', presentationId], { account });
132
+ });
133
+
134
+ server.registerTool('gog_slides_delete_slide', {
135
+ description: 'Delete a slide from a Google Slides presentation.',
136
+ annotations: { destructiveHint: true },
137
+ inputSchema: {
138
+ presentationId: z.string().describe('Presentation ID'),
139
+ slideId: z.string().describe('Slide ID to delete'),
140
+ account: accountParam,
141
+ },
142
+ }, async ({ presentationId, slideId, account }) => {
143
+ return runOrDiagnose(['slides', 'delete-slide', presentationId, slideId], { account });
144
+ });
145
+
146
+ server.registerTool('gog_slides_read_slide', {
147
+ description: 'Read the content of a slide (text, shapes, speaker notes).',
148
+ annotations: { readOnlyHint: true },
149
+ inputSchema: {
150
+ presentationId: z.string().describe('Presentation ID'),
151
+ slideId: z.string().describe('Slide ID to read'),
152
+ account: accountParam,
153
+ },
154
+ }, async ({ presentationId, slideId, account }) => {
155
+ return runOrDiagnose(['slides', 'read-slide', presentationId, slideId], { account });
156
+ });
157
+
158
+ server.registerTool('gog_slides_update_notes', {
159
+ description: 'Update the speaker notes on a slide (inline text or from a file).',
160
+ annotations: { destructiveHint: true },
161
+ inputSchema: {
162
+ presentationId: z.string().describe('Presentation ID'),
163
+ slideId: z.string().describe('Slide ID'),
164
+ notes: z.string().optional().describe('New speaker notes text'),
165
+ notesFile: z.string().optional().describe('Path to a file containing new speaker notes'),
166
+ account: accountParam,
167
+ },
168
+ }, async ({ presentationId, slideId, notes, notesFile, account }) => {
169
+ const args = ['slides', 'update-notes', presentationId, slideId];
170
+ if (notes) args.push(`--notes=${notes}`);
171
+ if (notesFile) args.push(`--notes-file=${notesFile}`);
172
+ return runOrDiagnose(args, { account });
173
+ });
174
+
175
+ server.registerTool('gog_slides_replace_slide', {
176
+ description: 'Replace the image content of an existing slide, with optional speaker notes.',
177
+ annotations: { destructiveHint: true },
178
+ inputSchema: {
179
+ presentationId: z.string().describe('Presentation ID'),
180
+ slideId: z.string().describe('Slide ID to replace'),
181
+ image: z.string().describe('Path to the new local image file'),
182
+ notes: z.string().optional().describe('Speaker notes text'),
183
+ notesFile: z.string().optional().describe('Path to a file containing speaker notes'),
184
+ account: accountParam,
185
+ },
186
+ }, async ({ presentationId, slideId, image, notes, notesFile, account }) => {
187
+ const args = ['slides', 'replace-slide', presentationId, slideId, image];
188
+ if (notes) args.push(`--notes=${notes}`);
189
+ if (notesFile) args.push(`--notes-file=${notesFile}`);
190
+ return runOrDiagnose(args, { account });
191
+ });
192
+
193
+ server.registerTool('gog_slides_run', {
194
+ description: 'Run any gog slides subcommand not covered by the other tools. Run `gog slides --help` for the full list of subcommands, or `gog slides <subcommand> --help` for flags on a specific subcommand.',
195
+ inputSchema: {
196
+ subcommand: z.string().describe('The gog slides subcommand to run'),
197
+ args: z.array(z.string()).describe('Additional positional args and flags'),
198
+ account: accountParam,
199
+ },
200
+ }, async ({ subcommand, args, account }) => {
201
+ return runOrDiagnose(['slides', subcommand, ...args], { account });
202
+ });
203
+ }
@@ -17,10 +17,17 @@ export function toError(err: unknown): ToolResult {
17
17
 
18
18
  const AUTH_ERROR_PATTERN = /\b(401|unauthorized|token.*(expired|revoked)|invalid_grant)\b/i;
19
19
 
20
+ const TRANSIENT_ERROR_PATTERN =
21
+ /\b429\b|\b5\d\d\b|\bquota\b|rateLimit|\bDEADLINE_EXCEEDED\b/i;
22
+
20
23
  const AUTH_HINT =
21
24
  '\n\nAuthentication may have expired. Use gog_auth_add to re-authorize the account. ' +
22
25
  'Ask the user if they would like to re-authenticate.';
23
26
 
27
+ const TRANSIENT_HINT =
28
+ '\n\nThis error is often transient. Retry the same call before trying a different approach ' +
29
+ '(do not fall back to smaller writes or row-by-row operations).';
30
+
24
31
  export async function runOrDiagnose(
25
32
  args: string[],
26
33
  options: { account?: string },
@@ -31,7 +38,8 @@ export async function runOrDiagnose(
31
38
  const base = toError(err);
32
39
  const errText = base.content[0].text;
33
40
  const isAuthError = AUTH_ERROR_PATTERN.test(errText);
34
- const hint = isAuthError ? AUTH_HINT : '';
41
+ const isTransientError = !isAuthError && TRANSIENT_ERROR_PATTERN.test(errText);
42
+ const hint = isAuthError ? AUTH_HINT : isTransientError ? TRANSIENT_HINT : '';
35
43
  try {
36
44
  const accounts = await run(['auth', 'list']);
37
45
  return toText(`${errText}\n\nConfigured accounts:\n${accounts}${hint}`);
@@ -300,6 +300,22 @@ describe('run', () => {
300
300
  vi.useRealTimers();
301
301
  });
302
302
 
303
+ it('formats 1-minute timeout as singular "minute" (not plural)', async () => {
304
+ vi.useFakeTimers();
305
+ const spawner = vi.fn(() => {
306
+ const proc = new EventEmitter() as ReturnType<Spawner>;
307
+ (proc as unknown as { stdout: EventEmitter; stderr: EventEmitter }).stdout = new EventEmitter();
308
+ (proc as unknown as { stdout: EventEmitter; stderr: EventEmitter }).stderr = new EventEmitter();
309
+ proc.kill = vi.fn();
310
+ return proc;
311
+ }) as unknown as Spawner;
312
+
313
+ const promise = run(['docs', 'cat', 'id'], { spawner, timeout: 60_000 });
314
+ vi.advanceTimersByTime(60_000);
315
+ await expect(promise).rejects.toThrow('gog timed out after 60000ms (1 minute)');
316
+ vi.useRealTimers();
317
+ });
318
+
303
319
  it('strips GOG_ACCESS_TOKEN from child environment to force refresh-token auth', async () => {
304
320
  const spawner = makeSpawner(0, '{}');
305
321
  const originalToken = process.env.GOG_ACCESS_TOKEN;
@@ -0,0 +1,43 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
3
+ import { createServer, createBaseServer, VERSION } from '../src/server.js';
4
+
5
+ describe('createServer', () => {
6
+ it('returns an McpServer with default name and version', () => {
7
+ const server = createServer();
8
+ expect(server).toBeInstanceOf(McpServer);
9
+ });
10
+
11
+ it('accepts custom name and version', () => {
12
+ const server = createServer({ name: 'custom', version: '9.9.9' });
13
+ expect(server).toBeInstanceOf(McpServer);
14
+ });
15
+
16
+ it('accepts partial options (name only)', () => {
17
+ const server = createServer({ name: 'partial' });
18
+ expect(server).toBeInstanceOf(McpServer);
19
+ });
20
+ });
21
+
22
+ describe('createBaseServer', () => {
23
+ it('returns an McpServer with all services registered', () => {
24
+ const server = createBaseServer();
25
+ expect(server).toBeInstanceOf(McpServer);
26
+ });
27
+
28
+ it('accepts custom options', () => {
29
+ const server = createBaseServer({ name: 'gogcli-all', version: '9.9.9' });
30
+ expect(server).toBeInstanceOf(McpServer);
31
+ });
32
+ });
33
+
34
+ describe('VERSION', () => {
35
+ it('is a string', () => {
36
+ expect(typeof VERSION).toBe('string');
37
+ });
38
+
39
+ it('defaults to 0.0.0 when GOGCLI_VERSION is not injected (dev/test runtime)', () => {
40
+ // At test runtime, esbuild has not injected GOGCLI_VERSION, so the fallback branch runs.
41
+ expect(VERSION).toBe('0.0.0');
42
+ });
43
+ });