gogcli-mcp-slides 2.0.2 → 2.0.6

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/manifest.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "manifest_version": "0.3",
4
4
  "name": "gogcli-mcp-slides",
5
5
  "display_name": "gogcli (Slides)",
6
- "version": "2.0.2",
6
+ "version": "2.0.6",
7
7
  "description": "Extended Google Slides for Claude via gogcli — auth + full Slides support (create, edit, export, templates, markdown)",
8
8
  "author": {
9
9
  "name": "Chris Hall",
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "gogcli-mcp-slides",
3
- "version": "2.0.2",
3
+ "version": "2.0.6",
4
+ "mcpName": "io.github.chrischall/gogcli-mcp-slides",
4
5
  "description": "Extended Google Slides MCP server via gogcli — auth + full Slides support",
5
6
  "author": "Claude Code (AI) <https://www.anthropic.com/claude>",
6
7
  "repository": {
@@ -24,7 +25,7 @@
24
25
  },
25
26
  "dependencies": {
26
27
  "@modelcontextprotocol/sdk": "^1.29.0",
27
- "zod": "^4.3.6"
28
+ "zod": "^4.4.3"
28
29
  },
29
30
  "license": "MIT",
30
31
  "keywords": [
package/server.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
3
+ "name": "io.github.chrischall/gogcli-mcp-slides",
4
+ "description": "Google Slides via gogcli for Claude \u2014 deck and slide authoring",
5
+ "repository": {
6
+ "url": "https://github.com/chrischall/gogcli-mcp",
7
+ "source": "github",
8
+ "subfolder": "packages/gogcli-mcp-slides"
9
+ },
10
+ "version": "2.0.3",
11
+ "packages": [
12
+ {
13
+ "registryType": "npm",
14
+ "identifier": "gogcli-mcp-slides",
15
+ "version": "2.0.3",
16
+ "transport": {
17
+ "type": "stdio"
18
+ },
19
+ "environmentVariables": [
20
+ {
21
+ "name": "GOG_ACCOUNT",
22
+ "description": "Email address of the Google account to use (matches gogcli auth)",
23
+ "isRequired": false,
24
+ "format": "string"
25
+ },
26
+ {
27
+ "name": "GOG_PATH",
28
+ "description": "Override path to the gogcli binary (auto-discovered otherwise)",
29
+ "isRequired": false,
30
+ "format": "string"
31
+ }
32
+ ]
33
+ }
34
+ ]
35
+ }
@@ -1,5 +1,113 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { z } from 'zod';
3
+ import { accountParam, runOrDiagnose } from '../../../gogcli-mcp/src/lib.js';
2
4
 
3
- export function registerExtraSlidesTools(_server: McpServer): void {
4
- /* no extras yet */
5
+ export function registerExtraSlidesTools(server: McpServer): void {
6
+ server.registerTool('gog_slides_create_from_markdown', {
7
+ description: 'Create a new Google Slides presentation from markdown content (inline or from a file).',
8
+ inputSchema: {
9
+ title: z.string().describe('Presentation title'),
10
+ content: z.string().optional().describe('Inline markdown content'),
11
+ contentFile: z.string().optional().describe('Path to a markdown file'),
12
+ parent: z.string().optional().describe('Destination folder ID'),
13
+ debug: z.boolean().optional().describe('Enable debug output'),
14
+ account: accountParam,
15
+ },
16
+ }, async ({ title, content, contentFile, parent, debug, account }) => {
17
+ const args = ['slides', 'create-from-markdown', title];
18
+ if (content) args.push(`--content=${content}`);
19
+ if (contentFile) args.push(`--content-file=${contentFile}`);
20
+ if (parent) args.push(`--parent=${parent}`);
21
+ if (debug) args.push('--debug');
22
+ return runOrDiagnose(args, { account });
23
+ });
24
+
25
+ server.registerTool('gog_slides_create_from_template', {
26
+ description: 'Create a new Google Slides presentation from a template, with optional placeholder replacements.',
27
+ inputSchema: {
28
+ templateId: z.string().describe('Template presentation ID'),
29
+ title: z.string().describe('New presentation title'),
30
+ replacements: z.record(z.string(), z.string()).optional().describe('Placeholder replacements as a key/value object (emitted as --replace=k=v for each entry)'),
31
+ replacementsFile: z.string().optional().describe('Path to a JSON file containing replacements'),
32
+ parent: z.string().optional().describe('Destination folder ID'),
33
+ exact: z.boolean().optional().describe('Require exact placeholder matches'),
34
+ account: accountParam,
35
+ },
36
+ }, async ({ templateId, title, replacements, replacementsFile, parent, exact, account }) => {
37
+ const args = ['slides', 'create-from-template', templateId, title];
38
+ if (replacements) {
39
+ for (const [k, v] of Object.entries(replacements)) {
40
+ args.push(`--replace=${k}=${v}`);
41
+ }
42
+ }
43
+ if (replacementsFile) args.push(`--replacements=${replacementsFile}`);
44
+ if (parent) args.push(`--parent=${parent}`);
45
+ if (exact) args.push('--exact');
46
+ return runOrDiagnose(args, { account });
47
+ });
48
+
49
+ server.registerTool('gog_slides_add_slide', {
50
+ description: 'Add a new slide to a presentation from a local image, with optional speaker notes.',
51
+ inputSchema: {
52
+ presentationId: z.string().describe('Presentation ID'),
53
+ image: z.string().describe('Path to the local image file'),
54
+ notes: z.string().optional().describe('Speaker notes text'),
55
+ notesFile: z.string().optional().describe('Path to a file containing speaker notes'),
56
+ before: z.string().optional().describe('Insert before this slide ID (default: append at end)'),
57
+ account: accountParam,
58
+ },
59
+ }, async ({ presentationId, image, notes, notesFile, before, account }) => {
60
+ const args = ['slides', 'add-slide', presentationId, image];
61
+ if (notes) args.push(`--notes=${notes}`);
62
+ if (notesFile) args.push(`--notes-file=${notesFile}`);
63
+ if (before) args.push(`--before=${before}`);
64
+ return runOrDiagnose(args, { account });
65
+ });
66
+
67
+ server.registerTool('gog_slides_delete_slide', {
68
+ description: 'Delete a slide from a Google Slides presentation.',
69
+ annotations: { destructiveHint: true },
70
+ inputSchema: {
71
+ presentationId: z.string().describe('Presentation ID'),
72
+ slideId: z.string().describe('Slide ID to delete'),
73
+ account: accountParam,
74
+ },
75
+ }, async ({ presentationId, slideId, account }) => {
76
+ return runOrDiagnose(['slides', 'delete-slide', presentationId, slideId], { account });
77
+ });
78
+
79
+ server.registerTool('gog_slides_update_notes', {
80
+ description: 'Update the speaker notes on a slide (inline text or from a file).',
81
+ annotations: { destructiveHint: true },
82
+ inputSchema: {
83
+ presentationId: z.string().describe('Presentation ID'),
84
+ slideId: z.string().describe('Slide ID'),
85
+ notes: z.string().optional().describe('New speaker notes text'),
86
+ notesFile: z.string().optional().describe('Path to a file containing new speaker notes'),
87
+ account: accountParam,
88
+ },
89
+ }, async ({ presentationId, slideId, notes, notesFile, account }) => {
90
+ const args = ['slides', 'update-notes', presentationId, slideId];
91
+ if (notes) args.push(`--notes=${notes}`);
92
+ if (notesFile) args.push(`--notes-file=${notesFile}`);
93
+ return runOrDiagnose(args, { account });
94
+ });
95
+
96
+ server.registerTool('gog_slides_replace_slide', {
97
+ description: 'Replace the image content of an existing slide, with optional speaker notes.',
98
+ annotations: { destructiveHint: true },
99
+ inputSchema: {
100
+ presentationId: z.string().describe('Presentation ID'),
101
+ slideId: z.string().describe('Slide ID to replace'),
102
+ image: z.string().describe('Path to the new local image file'),
103
+ notes: z.string().optional().describe('Speaker notes text'),
104
+ notesFile: z.string().optional().describe('Path to a file containing speaker notes'),
105
+ account: accountParam,
106
+ },
107
+ }, async ({ presentationId, slideId, image, notes, notesFile, account }) => {
108
+ const args = ['slides', 'replace-slide', presentationId, slideId, image];
109
+ if (notes) args.push(`--notes=${notes}`);
110
+ if (notesFile) args.push(`--notes-file=${notesFile}`);
111
+ return runOrDiagnose(args, { account });
112
+ });
5
113
  }
@@ -1,10 +1,220 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
3
2
  import { registerExtraSlidesTools } from '../../src/tools/slides-extra.js';
3
+ import * as lib from '../../../gogcli-mcp/src/lib.js';
4
+ import { setupExtrasHandlers, toText, type ToolHandler } from '../../../gogcli-mcp/tests/helpers/extras-harness.js';
4
5
 
5
- describe('registerExtraSlidesTools', () => {
6
- it('does not throw when called (no extras yet)', () => {
7
- const server = new McpServer({ name: 'test', version: '0.0.0' });
8
- expect(() => registerExtraSlidesTools(server)).not.toThrow();
6
+ vi.mock('../../../gogcli-mcp/src/lib.js', async (importOriginal) => {
7
+ const actual = await importOriginal<typeof lib>();
8
+ return {
9
+ ...actual,
10
+ runOrDiagnose: vi.fn(),
11
+ };
12
+ });
13
+
14
+ let handlers: Map<string, ToolHandler>;
15
+
16
+ beforeEach(() => {
17
+ vi.clearAllMocks();
18
+ vi.mocked(lib.runOrDiagnose).mockResolvedValue(toText('{}'));
19
+ handlers = setupExtrasHandlers(registerExtraSlidesTools);
20
+ });
21
+
22
+ describe('gog_slides_create_from_markdown', () => {
23
+ it('calls runOrDiagnose with title only', async () => {
24
+ await handlers.get('gog_slides_create_from_markdown')!({ title: 'Deck' });
25
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
26
+ ['slides', 'create-from-markdown', 'Deck'],
27
+ { account: undefined },
28
+ );
29
+ });
30
+
31
+ it('passes all optional flags', async () => {
32
+ await handlers.get('gog_slides_create_from_markdown')!({
33
+ title: 'Deck',
34
+ content: '# Slide 1',
35
+ contentFile: '/tmp/deck.md',
36
+ parent: 'folder1',
37
+ debug: true,
38
+ });
39
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
40
+ [
41
+ 'slides', 'create-from-markdown', 'Deck',
42
+ '--content=# Slide 1',
43
+ '--content-file=/tmp/deck.md',
44
+ '--parent=folder1',
45
+ '--debug',
46
+ ],
47
+ { account: undefined },
48
+ );
49
+ });
50
+
51
+ it('omits --debug when false', async () => {
52
+ await handlers.get('gog_slides_create_from_markdown')!({ title: 'Deck', debug: false });
53
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
54
+ ['slides', 'create-from-markdown', 'Deck'],
55
+ { account: undefined },
56
+ );
57
+ });
58
+ });
59
+
60
+ describe('gog_slides_create_from_template', () => {
61
+ it('calls runOrDiagnose with templateId and title only', async () => {
62
+ await handlers.get('gog_slides_create_from_template')!({ templateId: 'tpl1', title: 'Deck' });
63
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
64
+ ['slides', 'create-from-template', 'tpl1', 'Deck'],
65
+ { account: undefined },
66
+ );
67
+ });
68
+
69
+ it('passes --replace for a single replacement entry', async () => {
70
+ await handlers.get('gog_slides_create_from_template')!({
71
+ templateId: 'tpl1',
72
+ title: 'Deck',
73
+ replacements: { name: 'Alice' },
74
+ });
75
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
76
+ ['slides', 'create-from-template', 'tpl1', 'Deck', '--replace=name=Alice'],
77
+ { account: undefined },
78
+ );
79
+ });
80
+
81
+ it('passes --replace for each entry in replacements', async () => {
82
+ await handlers.get('gog_slides_create_from_template')!({
83
+ templateId: 'tpl1',
84
+ title: 'Deck',
85
+ replacements: { name: 'Alice', company: 'Acme' },
86
+ });
87
+ const call = vi.mocked(lib.runOrDiagnose).mock.calls[0]!;
88
+ expect(call[0]).toEqual(expect.arrayContaining(['--replace=name=Alice', '--replace=company=Acme']));
89
+ expect(call[1]).toEqual({ account: undefined });
90
+ });
91
+
92
+ it('passes --replacements, --parent, and --exact', async () => {
93
+ await handlers.get('gog_slides_create_from_template')!({
94
+ templateId: 'tpl1',
95
+ title: 'Deck',
96
+ replacementsFile: '/tmp/r.json',
97
+ parent: 'folder1',
98
+ exact: true,
99
+ });
100
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
101
+ [
102
+ 'slides', 'create-from-template', 'tpl1', 'Deck',
103
+ '--replacements=/tmp/r.json',
104
+ '--parent=folder1',
105
+ '--exact',
106
+ ],
107
+ { account: undefined },
108
+ );
109
+ });
110
+
111
+ it('omits --exact when false', async () => {
112
+ await handlers.get('gog_slides_create_from_template')!({
113
+ templateId: 'tpl1',
114
+ title: 'Deck',
115
+ exact: false,
116
+ });
117
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
118
+ ['slides', 'create-from-template', 'tpl1', 'Deck'],
119
+ { account: undefined },
120
+ );
121
+ });
122
+ });
123
+
124
+ describe('gog_slides_add_slide', () => {
125
+ it('calls runOrDiagnose with presentationId and image', async () => {
126
+ await handlers.get('gog_slides_add_slide')!({ presentationId: 'p1', image: '/tmp/img.png' });
127
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
128
+ ['slides', 'add-slide', 'p1', '/tmp/img.png'],
129
+ { account: undefined },
130
+ );
131
+ });
132
+
133
+ it('passes --notes, --notes-file, and --before', async () => {
134
+ await handlers.get('gog_slides_add_slide')!({
135
+ presentationId: 'p1',
136
+ image: '/tmp/img.png',
137
+ notes: 'Speaker note',
138
+ notesFile: '/tmp/notes.txt',
139
+ before: 'slide5',
140
+ });
141
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
142
+ [
143
+ 'slides', 'add-slide', 'p1', '/tmp/img.png',
144
+ '--notes=Speaker note',
145
+ '--notes-file=/tmp/notes.txt',
146
+ '--before=slide5',
147
+ ],
148
+ { account: undefined },
149
+ );
150
+ });
151
+ });
152
+
153
+ describe('gog_slides_delete_slide', () => {
154
+ it('calls runOrDiagnose with presentationId and slideId', async () => {
155
+ await handlers.get('gog_slides_delete_slide')!({ presentationId: 'p1', slideId: 's1' });
156
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
157
+ ['slides', 'delete-slide', 'p1', 's1'],
158
+ { account: undefined },
159
+ );
160
+ });
161
+ });
162
+
163
+ describe('gog_slides_update_notes', () => {
164
+ it('calls runOrDiagnose with presentationId and slideId', async () => {
165
+ await handlers.get('gog_slides_update_notes')!({ presentationId: 'p1', slideId: 's1' });
166
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
167
+ ['slides', 'update-notes', 'p1', 's1'],
168
+ { account: undefined },
169
+ );
170
+ });
171
+
172
+ it('passes --notes and --notes-file when provided', async () => {
173
+ await handlers.get('gog_slides_update_notes')!({
174
+ presentationId: 'p1',
175
+ slideId: 's1',
176
+ notes: 'speak clearly',
177
+ notesFile: '/tmp/n.txt',
178
+ });
179
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
180
+ [
181
+ 'slides', 'update-notes', 'p1', 's1',
182
+ '--notes=speak clearly',
183
+ '--notes-file=/tmp/n.txt',
184
+ ],
185
+ { account: undefined },
186
+ );
187
+ });
188
+ });
189
+
190
+ describe('gog_slides_replace_slide', () => {
191
+ it('calls runOrDiagnose with presentationId, slideId, and image', async () => {
192
+ await handlers.get('gog_slides_replace_slide')!({
193
+ presentationId: 'p1',
194
+ slideId: 's1',
195
+ image: '/tmp/img.png',
196
+ });
197
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
198
+ ['slides', 'replace-slide', 'p1', 's1', '/tmp/img.png'],
199
+ { account: undefined },
200
+ );
201
+ });
202
+
203
+ it('passes --notes and --notes-file when provided', async () => {
204
+ await handlers.get('gog_slides_replace_slide')!({
205
+ presentationId: 'p1',
206
+ slideId: 's1',
207
+ image: '/tmp/img.png',
208
+ notes: 'updated',
209
+ notesFile: '/tmp/n.txt',
210
+ });
211
+ expect(lib.runOrDiagnose).toHaveBeenCalledWith(
212
+ [
213
+ 'slides', 'replace-slide', 'p1', 's1', '/tmp/img.png',
214
+ '--notes=updated',
215
+ '--notes-file=/tmp/n.txt',
216
+ ],
217
+ { account: undefined },
218
+ );
9
219
  });
10
220
  });