google-tools-mcp 1.1.2 → 1.1.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 CHANGED
@@ -1,23 +1,26 @@
1
1
  # google-tools-mcp
2
2
 
3
- A unified MCP server for Google Workspace Drive, Docs, Sheets, Gmail, Calendar, and Forms — with **153 tools** across 9 categories.
3
+ The **easiest way** to connect your AI agent to Google Workspace.
4
4
 
5
- All tools are loaded at startup so they're immediately available to your AI agent. No discovery step needed.
5
+ **153 tools** for Drive, Docs, Sheets, Gmail, Calendar, and Forms all in one package. One install, one auth, and you're done.
6
6
 
7
- ## Why This Exists
7
+ ```bash
8
+ claude mcp add -s user google -- npx -y google-tools-mcp
9
+ ```
8
10
 
9
- Most Google MCP servers split functionality across separate packages. This server combines everything into one — single auth token, single process, single config.
11
+ ## Why google-tools-mcp?
10
12
 
11
- ## Features
13
+ - **One command to install.** No cloning repos, no building from source, no Docker. Just `npx -y google-tools-mcp` and it works.
14
+ - **One login for everything.** A single OAuth flow gives you Drive, Docs, Sheets, Gmail, Calendar, and Forms. No juggling multiple tokens or servers.
15
+ - **Auth that stays out of your way.** No browser popup until your first tool call. After that, your token is saved and you won't be asked again.
16
+ - **Read anything in your Drive.** PDFs, Word docs (.docx), spreadsheets — your AI agent can read them directly. No extra setup.
17
+ - **153 tools, zero config.** Every tool is available the moment the server starts. Send emails, create docs, manage calendar events, build forms — it's all there.
18
+ - **Switch between Google accounts.** Set a profile name and keep work and personal accounts completely separate.
19
+ - **No telemetry. No tracking. Fully open source.**
12
20
 
13
- - **153 tools** across 9 categories, all available immediately
14
- - **Single auth token** — one OAuth flow covers Drive, Docs, Sheets, Gmail, Calendar, and Forms
15
- - **Lazy-loading auth** — no browser popup until your first tool call
16
- - **No lazy tool loading** — all 153 tools are registered eagerly at startup since most MCP clients (including Claude Code) don't support `notifications/tools/list_changed`
17
- - **Multi-profile support** — separate tokens per Google account
18
- - **No telemetry**
21
+ ## Quick Start
19
22
 
20
- ## Getting Started
23
+ You can be up and running in under 5 minutes.
21
24
 
22
25
  ### Step 1: Create Google OAuth Credentials
23
26
 
package/dist/auth.js CHANGED
@@ -52,6 +52,8 @@ const SCOPES = [
52
52
  'https://www.googleapis.com/auth/forms.body',
53
53
  'https://www.googleapis.com/auth/forms.body.readonly',
54
54
  'https://www.googleapis.com/auth/forms.responses.readonly',
55
+ // Slides
56
+ 'https://www.googleapis.com/auth/presentations',
55
57
  ];
56
58
 
57
59
  // ---------------------------------------------------------------------------
package/dist/clients.js CHANGED
@@ -13,6 +13,7 @@ let googleScript = null;
13
13
  let gmailClient = null;
14
14
  let calendarClient = null;
15
15
  let formsClient = null;
16
+ let slidesClient = null;
16
17
 
17
18
  async function ensureAuth() {
18
19
  if (authClient) return;
@@ -44,6 +45,7 @@ async function reauthorize() {
44
45
  gmailClient = null;
45
46
  calendarClient = null;
46
47
  formsClient = null;
48
+ slidesClient = null;
47
49
  authClient = await authorize();
48
50
  logger.info('Re-authorization successful.');
49
51
  }
@@ -94,6 +96,14 @@ export async function initializeFormsClient() {
94
96
  return { authClient, formsClient };
95
97
  }
96
98
 
99
+ // --- Slides client ---
100
+ export async function initializeSlidesClient() {
101
+ if (slidesClient) return { authClient, slidesClient };
102
+ await ensureAuth();
103
+ if (!slidesClient) slidesClient = google.slides({ version: 'v1', auth: authClient });
104
+ return { authClient, slidesClient };
105
+ }
106
+
97
107
  // --- Get auth client without triggering init (for diagnostics) ---
98
108
  export function getAuthClientIfReady() {
99
109
  return authClient;
@@ -109,6 +119,7 @@ export function resetClients() {
109
119
  gmailClient = null;
110
120
  calendarClient = null;
111
121
  formsClient = null;
122
+ slidesClient = null;
112
123
  }
113
124
 
114
125
  /**
@@ -177,3 +188,9 @@ export async function getFormsClient() {
177
188
  if (!forms) throw new UserError('Google Forms client is not initialized.');
178
189
  return forms;
179
190
  }
191
+
192
+ export async function getSlidesClient() {
193
+ const { slidesClient: slides } = await initializeSlidesClient();
194
+ if (!slides) throw new UserError('Google Slides client is not initialized.');
195
+ return slides;
196
+ }
@@ -172,6 +172,12 @@ const CATEGORIES = {
172
172
  registerFormsTools(server);
173
173
  },
174
174
  },
175
+ slides: {
176
+ async loader(server) {
177
+ const { registerSlidesTools } = await import('./slides/index.js');
178
+ registerSlidesTools(server);
179
+ },
180
+ },
175
181
  };
176
182
 
177
183
  // ---------------------------------------------------------------------------
@@ -0,0 +1,104 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSlidesClient, getDriveClient } from '../../clients.js';
4
+
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'createPresentation',
8
+ description:
9
+ 'Create a new Google Slides presentation with initial slides. Each slide has a title and content body using the TITLE_AND_BODY layout.',
10
+ parameters: z.object({
11
+ name: z.string().describe('Presentation name'),
12
+ slides: z
13
+ .array(
14
+ z.object({
15
+ title: z.string().describe('Slide title'),
16
+ content: z.string().describe('Slide body content'),
17
+ }),
18
+ )
19
+ .min(1)
20
+ .describe('Array of slide objects with title and content'),
21
+ parentFolderId: z.string().optional().describe('Parent Drive folder ID (defaults to root)'),
22
+ }),
23
+ execute: async (args, { log }) => {
24
+ const slides = await getSlidesClient();
25
+ const drive = await getDriveClient();
26
+ log.info(`Creating presentation: ${args.name}`);
27
+
28
+ try {
29
+ // Create the presentation
30
+ const presentation = await slides.presentations.create({
31
+ requestBody: { title: args.name },
32
+ });
33
+ const presentationId = presentation.data.presentationId;
34
+
35
+ // Move to folder if specified
36
+ if (args.parentFolderId) {
37
+ await drive.files.update({
38
+ fileId: presentationId,
39
+ addParents: args.parentFolderId,
40
+ removeParents: 'root',
41
+ supportsAllDrives: true,
42
+ });
43
+ }
44
+
45
+ // Create each slide and populate content
46
+ for (const slide of args.slides) {
47
+ const slideObjectId = `slide_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
48
+ await slides.presentations.batchUpdate({
49
+ presentationId,
50
+ requestBody: {
51
+ requests: [
52
+ {
53
+ createSlide: {
54
+ objectId: slideObjectId,
55
+ slideLayoutReference: { predefinedLayout: 'TITLE_AND_BODY' },
56
+ },
57
+ },
58
+ ],
59
+ },
60
+ });
61
+
62
+ // Get the slide to find placeholder IDs
63
+ const slidePage = await slides.presentations.pages.get({
64
+ presentationId,
65
+ pageObjectId: slideObjectId,
66
+ });
67
+
68
+ let titleId = '';
69
+ let bodyId = '';
70
+ for (const el of slidePage.data.pageElements || []) {
71
+ if (el.shape?.placeholder?.type === 'TITLE') titleId = el.objectId;
72
+ else if (el.shape?.placeholder?.type === 'BODY') bodyId = el.objectId;
73
+ }
74
+
75
+ const insertRequests = [];
76
+ if (titleId) insertRequests.push({ insertText: { objectId: titleId, text: slide.title, insertionIndex: 0 } });
77
+ if (bodyId) insertRequests.push({ insertText: { objectId: bodyId, text: slide.content, insertionIndex: 0 } });
78
+
79
+ if (insertRequests.length > 0) {
80
+ await slides.presentations.batchUpdate({
81
+ presentationId,
82
+ requestBody: { requests: insertRequests },
83
+ });
84
+ }
85
+ }
86
+
87
+ return JSON.stringify(
88
+ {
89
+ presentationId,
90
+ title: args.name,
91
+ link: `https://docs.google.com/presentation/d/${presentationId}`,
92
+ slidesCreated: args.slides.length,
93
+ },
94
+ null,
95
+ 2,
96
+ );
97
+ } catch (error) {
98
+ log.error(`Error creating presentation: ${error.message || error}`);
99
+ if (error.code === 401) throw new UserError('Authentication failed. Try logging out and re-authenticating.');
100
+ throw new UserError(`Failed to create presentation: ${error.message || 'Unknown error'}`);
101
+ }
102
+ },
103
+ });
104
+ }
@@ -0,0 +1,92 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSlidesClient } from '../../clients.js';
4
+
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'createSlidesShape',
8
+ description:
9
+ 'Create a shape on a slide in Google Slides. Position and size are in EMU (914400 EMU = 1 inch).',
10
+ parameters: z.object({
11
+ presentationId: z.string().describe('Presentation ID'),
12
+ pageObjectId: z.string().describe('Slide page object ID'),
13
+ shapeType: z
14
+ .enum(['RECTANGLE', 'ELLIPSE', 'DIAMOND', 'TRIANGLE', 'STAR', 'ROUND_RECTANGLE', 'ARROW'])
15
+ .describe('Shape type'),
16
+ x: z.number().describe('X position in EMU'),
17
+ y: z.number().describe('Y position in EMU'),
18
+ width: z.number().describe('Width in EMU'),
19
+ height: z.number().describe('Height in EMU'),
20
+ backgroundColor: z
21
+ .object({
22
+ red: z.number().min(0).max(1).optional(),
23
+ green: z.number().min(0).max(1).optional(),
24
+ blue: z.number().min(0).max(1).optional(),
25
+ alpha: z.number().min(0).max(1).optional(),
26
+ })
27
+ .optional()
28
+ .describe('Fill color (RGBA 0-1)'),
29
+ }),
30
+ execute: async (args, { log }) => {
31
+ const slidesApi = await getSlidesClient();
32
+ const elementId = `shape_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
33
+
34
+ const requests = [
35
+ {
36
+ createShape: {
37
+ objectId: elementId,
38
+ shapeType: args.shapeType,
39
+ elementProperties: {
40
+ pageObjectId: args.pageObjectId,
41
+ size: {
42
+ width: { magnitude: args.width, unit: 'EMU' },
43
+ height: { magnitude: args.height, unit: 'EMU' },
44
+ },
45
+ transform: {
46
+ scaleX: 1,
47
+ scaleY: 1,
48
+ translateX: args.x,
49
+ translateY: args.y,
50
+ unit: 'EMU',
51
+ },
52
+ },
53
+ },
54
+ },
55
+ ];
56
+
57
+ if (args.backgroundColor) {
58
+ requests.push({
59
+ updateShapeProperties: {
60
+ objectId: elementId,
61
+ shapeProperties: {
62
+ shapeBackgroundFill: {
63
+ solidFill: {
64
+ color: {
65
+ rgbColor: {
66
+ red: args.backgroundColor.red || 0,
67
+ green: args.backgroundColor.green || 0,
68
+ blue: args.backgroundColor.blue || 0,
69
+ },
70
+ },
71
+ alpha: args.backgroundColor.alpha ?? 1,
72
+ },
73
+ },
74
+ },
75
+ fields: 'shapeBackgroundFill',
76
+ },
77
+ });
78
+ }
79
+
80
+ try {
81
+ await slidesApi.presentations.batchUpdate({
82
+ presentationId: args.presentationId,
83
+ requestBody: { requests },
84
+ });
85
+ return JSON.stringify({ success: true, objectId: elementId, message: `Created ${args.shapeType} shape: ${elementId}` });
86
+ } catch (error) {
87
+ log.error(`Error creating shape: ${error.message || error}`);
88
+ throw new UserError(`Failed to create shape: ${error.message || 'Unknown error'}`);
89
+ }
90
+ },
91
+ });
92
+ }
@@ -0,0 +1,82 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSlidesClient } from '../../clients.js';
4
+
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'createSlidesTextBox',
8
+ description:
9
+ 'Create a text box on a slide in Google Slides. Position and size are in EMU (English Metric Units: 914400 EMU = 1 inch).',
10
+ parameters: z.object({
11
+ presentationId: z.string().describe('Presentation ID'),
12
+ pageObjectId: z.string().describe('Slide page object ID'),
13
+ text: z.string().describe('Text content'),
14
+ x: z.number().describe('X position in EMU'),
15
+ y: z.number().describe('Y position in EMU'),
16
+ width: z.number().describe('Width in EMU'),
17
+ height: z.number().describe('Height in EMU'),
18
+ fontSize: z.number().optional().describe('Font size in points'),
19
+ bold: z.boolean().optional().describe('Make text bold'),
20
+ italic: z.boolean().optional().describe('Make text italic'),
21
+ }),
22
+ execute: async (args, { log }) => {
23
+ const slidesApi = await getSlidesClient();
24
+ const elementId = `textBox_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
25
+
26
+ const requests = [
27
+ {
28
+ createShape: {
29
+ objectId: elementId,
30
+ shapeType: 'TEXT_BOX',
31
+ elementProperties: {
32
+ pageObjectId: args.pageObjectId,
33
+ size: {
34
+ width: { magnitude: args.width, unit: 'EMU' },
35
+ height: { magnitude: args.height, unit: 'EMU' },
36
+ },
37
+ transform: {
38
+ scaleX: 1,
39
+ scaleY: 1,
40
+ translateX: args.x,
41
+ translateY: args.y,
42
+ unit: 'EMU',
43
+ },
44
+ },
45
+ },
46
+ },
47
+ {
48
+ insertText: { objectId: elementId, text: args.text, insertionIndex: 0 },
49
+ },
50
+ ];
51
+
52
+ // Optional formatting
53
+ const textStyle = {};
54
+ const fields = [];
55
+ if (args.fontSize) { textStyle.fontSize = { magnitude: args.fontSize, unit: 'PT' }; fields.push('fontSize'); }
56
+ if (args.bold !== undefined) { textStyle.bold = args.bold; fields.push('bold'); }
57
+ if (args.italic !== undefined) { textStyle.italic = args.italic; fields.push('italic'); }
58
+
59
+ if (fields.length > 0) {
60
+ requests.push({
61
+ updateTextStyle: {
62
+ objectId: elementId,
63
+ style: textStyle,
64
+ fields: fields.join(','),
65
+ textRange: { type: 'ALL' },
66
+ },
67
+ });
68
+ }
69
+
70
+ try {
71
+ await slidesApi.presentations.batchUpdate({
72
+ presentationId: args.presentationId,
73
+ requestBody: { requests },
74
+ });
75
+ return JSON.stringify({ success: true, objectId: elementId, message: `Created text box: ${elementId}` });
76
+ } catch (error) {
77
+ log.error(`Error creating text box: ${error.message || error}`);
78
+ throw new UserError(`Failed to create text box: ${error.message || 'Unknown error'}`);
79
+ }
80
+ },
81
+ });
82
+ }
@@ -0,0 +1,30 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSlidesClient } from '../../clients.js';
4
+
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'deleteSlide',
8
+ description: 'Delete a slide from a Google Slides presentation.',
9
+ parameters: z.object({
10
+ presentationId: z.string().describe('Presentation ID'),
11
+ slideObjectId: z.string().describe('Object ID of the slide to delete'),
12
+ }),
13
+ execute: async (args, { log }) => {
14
+ const slidesApi = await getSlidesClient();
15
+
16
+ try {
17
+ await slidesApi.presentations.batchUpdate({
18
+ presentationId: args.presentationId,
19
+ requestBody: {
20
+ requests: [{ deleteObject: { objectId: args.slideObjectId } }],
21
+ },
22
+ });
23
+ return JSON.stringify({ success: true, message: `Deleted slide ${args.slideObjectId}` });
24
+ } catch (error) {
25
+ log.error(`Error deleting slide: ${error.message || error}`);
26
+ throw new UserError(`Failed to delete slide: ${error.message || 'Unknown error'}`);
27
+ }
28
+ },
29
+ });
30
+ }
@@ -0,0 +1,37 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSlidesClient } from '../../clients.js';
4
+
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'duplicateSlide',
8
+ description: 'Duplicate a slide in a Google Slides presentation.',
9
+ parameters: z.object({
10
+ presentationId: z.string().describe('Presentation ID'),
11
+ slideObjectId: z.string().describe('Object ID of the slide to duplicate'),
12
+ }),
13
+ execute: async (args, { log }) => {
14
+ const slidesApi = await getSlidesClient();
15
+
16
+ try {
17
+ const response = await slidesApi.presentations.batchUpdate({
18
+ presentationId: args.presentationId,
19
+ requestBody: {
20
+ requests: [{ duplicateObject: { objectId: args.slideObjectId } }],
21
+ },
22
+ });
23
+
24
+ const newId = response.data.replies?.[0]?.duplicateObject?.objectId;
25
+ return JSON.stringify({
26
+ success: true,
27
+ originalId: args.slideObjectId,
28
+ duplicateId: newId || null,
29
+ message: `Duplicated slide ${args.slideObjectId}${newId ? ` -> ${newId}` : ''}`,
30
+ });
31
+ } catch (error) {
32
+ log.error(`Error duplicating slide: ${error.message || error}`);
33
+ throw new UserError(`Failed to duplicate slide: ${error.message || 'Unknown error'}`);
34
+ }
35
+ },
36
+ });
37
+ }
@@ -0,0 +1,42 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSlidesClient } from '../../clients.js';
4
+
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'exportSlideThumbnail',
8
+ description: 'Export a slide as a thumbnail image URL (PNG or JPEG) from a Google Slides presentation.',
9
+ parameters: z.object({
10
+ presentationId: z.string().describe('Presentation ID'),
11
+ slideObjectId: z.string().describe('Slide object ID'),
12
+ mimeType: z.enum(['PNG', 'JPEG']).optional().default('PNG').describe('Image format'),
13
+ size: z.enum(['SMALL', 'MEDIUM', 'LARGE']).optional().default('LARGE').describe('Thumbnail size'),
14
+ }),
15
+ execute: async (args, { log }) => {
16
+ const slidesApi = await getSlidesClient();
17
+
18
+ try {
19
+ const response = await slidesApi.presentations.pages.getThumbnail({
20
+ presentationId: args.presentationId,
21
+ pageObjectId: args.slideObjectId,
22
+ 'thumbnailProperties.mimeType': args.mimeType,
23
+ 'thumbnailProperties.thumbnailSize': args.size,
24
+ });
25
+
26
+ const url = response.data?.contentUrl;
27
+ if (!url) throw new UserError('No thumbnail URL returned by Google Slides API.');
28
+
29
+ return JSON.stringify({
30
+ slideObjectId: args.slideObjectId,
31
+ mimeType: args.mimeType,
32
+ size: args.size,
33
+ thumbnailUrl: url,
34
+ });
35
+ } catch (error) {
36
+ if (error instanceof UserError) throw error;
37
+ log.error(`Error exporting thumbnail: ${error.message || error}`);
38
+ throw new UserError(`Failed to export thumbnail: ${error.message || 'Unknown error'}`);
39
+ }
40
+ },
41
+ });
42
+ }
@@ -0,0 +1,72 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSlidesClient } from '../../clients.js';
4
+
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'formatSlidesParagraph',
8
+ description:
9
+ 'Apply paragraph formatting (alignment, line spacing, bullets) to a text element in Google Slides.',
10
+ parameters: z.object({
11
+ presentationId: z.string().describe('Presentation ID'),
12
+ objectId: z.string().describe('Object ID of the text element'),
13
+ alignment: z.enum(['START', 'CENTER', 'END', 'JUSTIFIED']).optional().describe('Text alignment'),
14
+ lineSpacing: z.number().optional().describe('Line spacing multiplier (e.g. 1.5)'),
15
+ bulletStyle: z
16
+ .enum(['NONE', 'DISC', 'ARROW', 'SQUARE', 'DIAMOND', 'STAR', 'NUMBERED'])
17
+ .optional()
18
+ .describe('Bullet style'),
19
+ }),
20
+ execute: async (args, { log }) => {
21
+ const slidesApi = await getSlidesClient();
22
+ const requests = [];
23
+
24
+ if (args.alignment) {
25
+ requests.push({
26
+ updateParagraphStyle: {
27
+ objectId: args.objectId,
28
+ style: { alignment: args.alignment },
29
+ fields: 'alignment',
30
+ },
31
+ });
32
+ }
33
+
34
+ if (args.lineSpacing !== undefined) {
35
+ requests.push({
36
+ updateParagraphStyle: {
37
+ objectId: args.objectId,
38
+ style: { lineSpacing: args.lineSpacing },
39
+ fields: 'lineSpacing',
40
+ },
41
+ });
42
+ }
43
+
44
+ if (args.bulletStyle) {
45
+ if (args.bulletStyle === 'NONE') {
46
+ requests.push({ deleteParagraphBullets: { objectId: args.objectId } });
47
+ } else if (args.bulletStyle === 'NUMBERED') {
48
+ requests.push({
49
+ createParagraphBullets: { objectId: args.objectId, bulletPreset: 'NUMBERED_DIGIT_ALPHA_ROMAN' },
50
+ });
51
+ } else {
52
+ requests.push({
53
+ createParagraphBullets: { objectId: args.objectId, bulletPreset: `BULLET_${args.bulletStyle}_CIRCLE_SQUARE` },
54
+ });
55
+ }
56
+ }
57
+
58
+ if (requests.length === 0) throw new UserError('No formatting options specified');
59
+
60
+ try {
61
+ await slidesApi.presentations.batchUpdate({
62
+ presentationId: args.presentationId,
63
+ requestBody: { requests },
64
+ });
65
+ return JSON.stringify({ success: true, message: `Applied paragraph formatting to ${args.objectId}` });
66
+ } catch (error) {
67
+ log.error(`Error formatting paragraph: ${error.message || error}`);
68
+ throw new UserError(`Failed to format paragraph: ${error.message || 'Unknown error'}`);
69
+ }
70
+ },
71
+ });
72
+ }
@@ -0,0 +1,84 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSlidesClient } from '../../clients.js';
4
+
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'formatSlidesText',
8
+ description:
9
+ 'Apply text formatting (bold, italic, underline, font size, color, etc.) to a text element in Google Slides. Use getPresentation to find element objectIds first.',
10
+ parameters: z.object({
11
+ presentationId: z.string().describe('Presentation ID'),
12
+ objectId: z.string().describe('Object ID of the text element'),
13
+ startIndex: z.number().min(0).optional().describe('Start character index (0-based). Omit with endIndex to format all text.'),
14
+ endIndex: z.number().min(0).optional().describe('End character index (0-based)'),
15
+ bold: z.boolean().optional().describe('Make text bold'),
16
+ italic: z.boolean().optional().describe('Make text italic'),
17
+ underline: z.boolean().optional().describe('Underline text'),
18
+ strikethrough: z.boolean().optional().describe('Strikethrough text'),
19
+ fontSize: z.number().optional().describe('Font size in points'),
20
+ fontFamily: z.string().optional().describe('Font family name (e.g. "Arial", "Roboto")'),
21
+ foregroundColor: z
22
+ .object({
23
+ red: z.number().min(0).max(1).optional(),
24
+ green: z.number().min(0).max(1).optional(),
25
+ blue: z.number().min(0).max(1).optional(),
26
+ })
27
+ .optional()
28
+ .describe('Text color as RGB values (0-1)'),
29
+ }),
30
+ execute: async (args, { log }) => {
31
+ const slidesApi = await getSlidesClient();
32
+
33
+ const textStyle = {};
34
+ const fields = [];
35
+
36
+ if (args.bold !== undefined) { textStyle.bold = args.bold; fields.push('bold'); }
37
+ if (args.italic !== undefined) { textStyle.italic = args.italic; fields.push('italic'); }
38
+ if (args.underline !== undefined) { textStyle.underline = args.underline; fields.push('underline'); }
39
+ if (args.strikethrough !== undefined) { textStyle.strikethrough = args.strikethrough; fields.push('strikethrough'); }
40
+ if (args.fontSize !== undefined) {
41
+ textStyle.fontSize = { magnitude: args.fontSize, unit: 'PT' };
42
+ fields.push('fontSize');
43
+ }
44
+ if (args.fontFamily !== undefined) { textStyle.fontFamily = args.fontFamily; fields.push('fontFamily'); }
45
+ if (args.foregroundColor) {
46
+ textStyle.foregroundColor = {
47
+ opaqueColor: {
48
+ rgbColor: {
49
+ red: args.foregroundColor.red || 0,
50
+ green: args.foregroundColor.green || 0,
51
+ blue: args.foregroundColor.blue || 0,
52
+ },
53
+ },
54
+ };
55
+ fields.push('foregroundColor');
56
+ }
57
+
58
+ if (fields.length === 0) throw new UserError('No formatting options specified');
59
+
60
+ const request = {
61
+ updateTextStyle: {
62
+ objectId: args.objectId,
63
+ style: textStyle,
64
+ fields: fields.join(','),
65
+ textRange:
66
+ args.startIndex !== undefined && args.endIndex !== undefined
67
+ ? { type: 'FIXED_RANGE', startIndex: args.startIndex, endIndex: args.endIndex }
68
+ : { type: 'ALL' },
69
+ },
70
+ };
71
+
72
+ try {
73
+ await slidesApi.presentations.batchUpdate({
74
+ presentationId: args.presentationId,
75
+ requestBody: { requests: [request] },
76
+ });
77
+ return JSON.stringify({ success: true, message: `Applied text formatting to ${args.objectId}` });
78
+ } catch (error) {
79
+ log.error(`Error formatting text: ${error.message || error}`);
80
+ throw new UserError(`Failed to format text: ${error.message || 'Unknown error'}`);
81
+ }
82
+ },
83
+ });
84
+ }
@@ -0,0 +1,87 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSlidesClient } from '../../clients.js';
4
+
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'getPresentation',
8
+ description:
9
+ 'Get the content of a Google Slides presentation with element IDs for formatting. Optionally retrieve a single slide by index.',
10
+ parameters: z.object({
11
+ presentationId: z.string().describe('Presentation ID'),
12
+ slideIndex: z.number().min(0).optional().describe('Specific slide index (0-based). Omit to get all slides.'),
13
+ }),
14
+ execute: async (args, { log }) => {
15
+ const slidesApi = await getSlidesClient();
16
+ log.info(`Reading presentation: ${args.presentationId}`);
17
+
18
+ try {
19
+ const presentation = await slidesApi.presentations.get({
20
+ presentationId: args.presentationId,
21
+ });
22
+
23
+ if (!presentation.data.slides) {
24
+ throw new UserError('No slides found in presentation');
25
+ }
26
+
27
+ const allSlides = presentation.data.slides;
28
+ const slides =
29
+ args.slideIndex !== undefined ? [allSlides[args.slideIndex]] : allSlides;
30
+
31
+ const result = {
32
+ title: presentation.data.title,
33
+ presentationId: args.presentationId,
34
+ totalSlides: allSlides.length,
35
+ slides: slides.map((slide, idx) => {
36
+ if (!slide?.objectId) return null;
37
+ const slideInfo = {
38
+ index: args.slideIndex ?? idx,
39
+ objectId: slide.objectId,
40
+ elements: [],
41
+ };
42
+
43
+ for (const el of slide.pageElements || []) {
44
+ if (!el.objectId) continue;
45
+ if (el.shape?.text) {
46
+ let text = '';
47
+ for (const te of el.shape.text.textElements || []) {
48
+ if (te.textRun?.content) text += te.textRun.content;
49
+ }
50
+ slideInfo.elements.push({
51
+ type: 'text',
52
+ objectId: el.objectId,
53
+ placeholderType: el.shape.placeholder?.type || null,
54
+ text: text.trim(),
55
+ });
56
+ } else if (el.shape) {
57
+ slideInfo.elements.push({
58
+ type: 'shape',
59
+ objectId: el.objectId,
60
+ shapeType: el.shape.shapeType || 'Unknown',
61
+ });
62
+ } else if (el.image) {
63
+ slideInfo.elements.push({ type: 'image', objectId: el.objectId });
64
+ } else if (el.video) {
65
+ slideInfo.elements.push({ type: 'video', objectId: el.objectId });
66
+ } else if (el.table) {
67
+ slideInfo.elements.push({
68
+ type: 'table',
69
+ objectId: el.objectId,
70
+ rows: el.table.rows,
71
+ columns: el.table.columns,
72
+ });
73
+ }
74
+ }
75
+ return slideInfo;
76
+ }).filter(Boolean),
77
+ };
78
+
79
+ return JSON.stringify(result, null, 2);
80
+ } catch (error) {
81
+ if (error instanceof UserError) throw error;
82
+ log.error(`Error reading presentation: ${error.message || error}`);
83
+ throw new UserError(`Failed to read presentation: ${error.message || 'Unknown error'}`);
84
+ }
85
+ },
86
+ });
87
+ }
@@ -0,0 +1,33 @@
1
+ import { register as createPresentation } from './createPresentation.js';
2
+ import { register as updatePresentation } from './updatePresentation.js';
3
+ import { register as getPresentation } from './getPresentation.js';
4
+ import { register as formatText } from './formatText.js';
5
+ import { register as formatParagraph } from './formatParagraph.js';
6
+ import { register as styleShape } from './styleShape.js';
7
+ import { register as setBackground } from './setBackground.js';
8
+ import { register as createTextBox } from './createTextBox.js';
9
+ import { register as createShape } from './createShape.js';
10
+ import { register as speakerNotes } from './speakerNotes.js';
11
+ import { register as deleteSlide } from './deleteSlide.js';
12
+ import { register as duplicateSlide } from './duplicateSlide.js';
13
+ import { register as reorderSlides } from './reorderSlides.js';
14
+ import { register as replaceAllText } from './replaceAllText.js';
15
+ import { register as exportThumbnail } from './exportThumbnail.js';
16
+
17
+ export function registerSlidesTools(server) {
18
+ createPresentation(server);
19
+ updatePresentation(server);
20
+ getPresentation(server);
21
+ formatText(server);
22
+ formatParagraph(server);
23
+ styleShape(server);
24
+ setBackground(server);
25
+ createTextBox(server);
26
+ createShape(server);
27
+ speakerNotes(server);
28
+ deleteSlide(server);
29
+ duplicateSlide(server);
30
+ reorderSlides(server);
31
+ replaceAllText(server);
32
+ exportThumbnail(server);
33
+ }
@@ -0,0 +1,41 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSlidesClient } from '../../clients.js';
4
+
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'reorderSlides',
8
+ description: 'Reorder one or more slides in a Google Slides presentation by moving them to a target index.',
9
+ parameters: z.object({
10
+ presentationId: z.string().describe('Presentation ID'),
11
+ slideObjectIds: z.array(z.string()).min(1).describe('Array of slide object IDs to move'),
12
+ insertionIndex: z.number().int().min(0).describe('Target insertion index (0-based)'),
13
+ }),
14
+ execute: async (args, { log }) => {
15
+ const slidesApi = await getSlidesClient();
16
+
17
+ try {
18
+ await slidesApi.presentations.batchUpdate({
19
+ presentationId: args.presentationId,
20
+ requestBody: {
21
+ requests: [
22
+ {
23
+ updateSlidesPosition: {
24
+ slideObjectIds: args.slideObjectIds,
25
+ insertionIndex: args.insertionIndex,
26
+ },
27
+ },
28
+ ],
29
+ },
30
+ });
31
+ return JSON.stringify({
32
+ success: true,
33
+ message: `Reordered ${args.slideObjectIds.length} slide(s) to index ${args.insertionIndex}`,
34
+ });
35
+ } catch (error) {
36
+ log.error(`Error reordering slides: ${error.message || error}`);
37
+ throw new UserError(`Failed to reorder slides: ${error.message || 'Unknown error'}`);
38
+ }
39
+ },
40
+ });
41
+ }
@@ -0,0 +1,46 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSlidesClient } from '../../clients.js';
4
+
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'replaceAllTextInSlides',
8
+ description:
9
+ 'Find and replace all matching text across all slides in a Google Slides presentation.',
10
+ parameters: z.object({
11
+ presentationId: z.string().describe('Presentation ID'),
12
+ containsText: z.string().describe('Text to search for'),
13
+ replaceText: z.string().describe('Replacement text'),
14
+ matchCase: z.boolean().optional().default(false).describe('Case-sensitive match'),
15
+ }),
16
+ execute: async (args, { log }) => {
17
+ const slidesApi = await getSlidesClient();
18
+
19
+ try {
20
+ const response = await slidesApi.presentations.batchUpdate({
21
+ presentationId: args.presentationId,
22
+ requestBody: {
23
+ requests: [
24
+ {
25
+ replaceAllText: {
26
+ containsText: { text: args.containsText, matchCase: args.matchCase },
27
+ replaceText: args.replaceText,
28
+ },
29
+ },
30
+ ],
31
+ },
32
+ });
33
+
34
+ const count = response.data.replies?.[0]?.replaceAllText?.occurrencesChanged ?? 0;
35
+ return JSON.stringify({
36
+ success: true,
37
+ occurrencesChanged: count,
38
+ message: `Replaced ${count} occurrence(s) of "${args.containsText}"`,
39
+ });
40
+ } catch (error) {
41
+ log.error(`Error replacing text: ${error.message || error}`);
42
+ throw new UserError(`Failed to replace text: ${error.message || 'Unknown error'}`);
43
+ }
44
+ },
45
+ });
46
+ }
@@ -0,0 +1,58 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSlidesClient } from '../../clients.js';
4
+
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'setSlidesBackground',
8
+ description:
9
+ 'Set the background color for one or more slides in a Google Slides presentation.',
10
+ parameters: z.object({
11
+ presentationId: z.string().describe('Presentation ID'),
12
+ pageObjectIds: z.array(z.string()).min(1).describe('Array of slide page object IDs to update'),
13
+ backgroundColor: z
14
+ .object({
15
+ red: z.number().min(0).max(1).optional(),
16
+ green: z.number().min(0).max(1).optional(),
17
+ blue: z.number().min(0).max(1).optional(),
18
+ alpha: z.number().min(0).max(1).optional(),
19
+ })
20
+ .describe('Background color (RGBA 0-1)'),
21
+ }),
22
+ execute: async (args, { log }) => {
23
+ const slidesApi = await getSlidesClient();
24
+
25
+ const requests = args.pageObjectIds.map((pageObjectId) => ({
26
+ updatePageProperties: {
27
+ objectId: pageObjectId,
28
+ pageProperties: {
29
+ pageBackgroundFill: {
30
+ solidFill: {
31
+ color: {
32
+ rgbColor: {
33
+ red: args.backgroundColor.red || 0,
34
+ green: args.backgroundColor.green || 0,
35
+ blue: args.backgroundColor.blue || 0,
36
+ },
37
+ },
38
+ alpha: args.backgroundColor.alpha ?? 1,
39
+ },
40
+ },
41
+ },
42
+ fields: 'pageBackgroundFill',
43
+ },
44
+ }));
45
+
46
+ try {
47
+ await slidesApi.presentations.batchUpdate({
48
+ presentationId: args.presentationId,
49
+ requestBody: { requests },
50
+ });
51
+ return JSON.stringify({ success: true, message: `Set background for ${args.pageObjectIds.length} slide(s)` });
52
+ } catch (error) {
53
+ log.error(`Error setting background: ${error.message || error}`);
54
+ throw new UserError(`Failed to set background: ${error.message || 'Unknown error'}`);
55
+ }
56
+ },
57
+ });
58
+ }
@@ -0,0 +1,102 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSlidesClient } from '../../clients.js';
4
+
5
+ export function register(server) {
6
+ // --- Get speaker notes ---
7
+ server.addTool({
8
+ name: 'getSpeakerNotes',
9
+ description: 'Get the speaker notes from a specific slide in a Google Slides presentation.',
10
+ parameters: z.object({
11
+ presentationId: z.string().describe('Presentation ID'),
12
+ slideIndex: z.number().min(0).describe('Slide index (0-based)'),
13
+ }),
14
+ execute: async (args, { log }) => {
15
+ const slidesApi = await getSlidesClient();
16
+
17
+ try {
18
+ const presentation = await slidesApi.presentations.get({ presentationId: args.presentationId });
19
+ const allSlides = presentation.data.slides || [];
20
+
21
+ if (args.slideIndex >= allSlides.length) {
22
+ throw new UserError(`Slide index ${args.slideIndex} out of range (presentation has ${allSlides.length} slides)`);
23
+ }
24
+
25
+ const slide = allSlides[args.slideIndex];
26
+ const notesObjectId = slide.slideProperties?.notesPage?.notesProperties?.speakerNotesObjectId;
27
+
28
+ if (!notesObjectId) {
29
+ return JSON.stringify({ slideIndex: args.slideIndex, notes: '' });
30
+ }
31
+
32
+ const notesPage = slide.slideProperties?.notesPage;
33
+ const notesElement = notesPage?.pageElements?.find((el) => el.objectId === notesObjectId);
34
+
35
+ let notesText = '';
36
+ if (notesElement?.shape?.text?.textElements) {
37
+ for (const te of notesElement.shape.text.textElements) {
38
+ if (te.textRun?.content) notesText += te.textRun.content;
39
+ }
40
+ }
41
+
42
+ return JSON.stringify({ slideIndex: args.slideIndex, notes: notesText.trim() });
43
+ } catch (error) {
44
+ if (error instanceof UserError) throw error;
45
+ log.error(`Error getting speaker notes: ${error.message || error}`);
46
+ throw new UserError(`Failed to get speaker notes: ${error.message || 'Unknown error'}`);
47
+ }
48
+ },
49
+ });
50
+
51
+ // --- Update speaker notes ---
52
+ server.addTool({
53
+ name: 'updateSpeakerNotes',
54
+ description: 'Update the speaker notes for a specific slide in a Google Slides presentation.',
55
+ parameters: z.object({
56
+ presentationId: z.string().describe('Presentation ID'),
57
+ slideIndex: z.number().min(0).describe('Slide index (0-based)'),
58
+ notes: z.string().describe('Speaker notes content'),
59
+ }),
60
+ execute: async (args, { log }) => {
61
+ const slidesApi = await getSlidesClient();
62
+
63
+ try {
64
+ const presentation = await slidesApi.presentations.get({ presentationId: args.presentationId });
65
+ const allSlides = presentation.data.slides || [];
66
+
67
+ if (args.slideIndex >= allSlides.length) {
68
+ throw new UserError(`Slide index ${args.slideIndex} out of range (presentation has ${allSlides.length} slides)`);
69
+ }
70
+
71
+ const slide = allSlides[args.slideIndex];
72
+ const notesObjectId = slide.slideProperties?.notesPage?.notesProperties?.speakerNotesObjectId;
73
+
74
+ if (!notesObjectId) {
75
+ throw new UserError('This slide does not have a speaker notes object.');
76
+ }
77
+
78
+ // Check if there's existing text to delete first
79
+ const notesPage = slide.slideProperties?.notesPage;
80
+ const notesElement = notesPage?.pageElements?.find((el) => el.objectId === notesObjectId);
81
+ const hasExistingText = notesElement?.shape?.text?.textElements?.some((el) => el.textRun?.content);
82
+
83
+ const requests = [];
84
+ if (hasExistingText) {
85
+ requests.push({ deleteText: { objectId: notesObjectId, textRange: { type: 'ALL' } } });
86
+ }
87
+ requests.push({ insertText: { objectId: notesObjectId, text: args.notes, insertionIndex: 0 } });
88
+
89
+ await slidesApi.presentations.batchUpdate({
90
+ presentationId: args.presentationId,
91
+ requestBody: { requests },
92
+ });
93
+
94
+ return JSON.stringify({ success: true, message: `Updated speaker notes for slide ${args.slideIndex}` });
95
+ } catch (error) {
96
+ if (error instanceof UserError) throw error;
97
+ log.error(`Error updating speaker notes: ${error.message || error}`);
98
+ throw new UserError(`Failed to update speaker notes: ${error.message || 'Unknown error'}`);
99
+ }
100
+ },
101
+ });
102
+ }
@@ -0,0 +1,111 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSlidesClient } from '../../clients.js';
4
+
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'styleSlidesShape',
8
+ description:
9
+ 'Style a shape in Google Slides: set background color, outline color/weight/dash style.',
10
+ parameters: z.object({
11
+ presentationId: z.string().describe('Presentation ID'),
12
+ objectId: z.string().describe('Shape object ID'),
13
+ backgroundColor: z
14
+ .object({
15
+ red: z.number().min(0).max(1).optional(),
16
+ green: z.number().min(0).max(1).optional(),
17
+ blue: z.number().min(0).max(1).optional(),
18
+ alpha: z.number().min(0).max(1).optional(),
19
+ })
20
+ .optional()
21
+ .describe('Fill color (RGBA 0-1)'),
22
+ outlineColor: z
23
+ .object({
24
+ red: z.number().min(0).max(1).optional(),
25
+ green: z.number().min(0).max(1).optional(),
26
+ blue: z.number().min(0).max(1).optional(),
27
+ })
28
+ .optional()
29
+ .describe('Outline color (RGB 0-1)'),
30
+ outlineWeight: z.number().optional().describe('Outline thickness in points'),
31
+ outlineDashStyle: z
32
+ .enum(['SOLID', 'DOT', 'DASH', 'DASH_DOT', 'LONG_DASH', 'LONG_DASH_DOT'])
33
+ .optional()
34
+ .describe('Outline dash style'),
35
+ }),
36
+ execute: async (args, { log }) => {
37
+ const slidesApi = await getSlidesClient();
38
+ const shapeProperties = {};
39
+ const fields = [];
40
+
41
+ if (args.backgroundColor) {
42
+ shapeProperties.shapeBackgroundFill = {
43
+ solidFill: {
44
+ color: {
45
+ rgbColor: {
46
+ red: args.backgroundColor.red || 0,
47
+ green: args.backgroundColor.green || 0,
48
+ blue: args.backgroundColor.blue || 0,
49
+ },
50
+ },
51
+ alpha: args.backgroundColor.alpha ?? 1,
52
+ },
53
+ };
54
+ fields.push('shapeBackgroundFill');
55
+ }
56
+
57
+ const outline = {};
58
+ let hasOutline = false;
59
+
60
+ if (args.outlineColor) {
61
+ outline.outlineFill = {
62
+ solidFill: {
63
+ color: {
64
+ rgbColor: {
65
+ red: args.outlineColor.red || 0,
66
+ green: args.outlineColor.green || 0,
67
+ blue: args.outlineColor.blue || 0,
68
+ },
69
+ },
70
+ },
71
+ };
72
+ hasOutline = true;
73
+ }
74
+ if (args.outlineWeight !== undefined) {
75
+ outline.weight = { magnitude: args.outlineWeight, unit: 'PT' };
76
+ hasOutline = true;
77
+ }
78
+ if (args.outlineDashStyle) {
79
+ outline.dashStyle = args.outlineDashStyle;
80
+ hasOutline = true;
81
+ }
82
+ if (hasOutline) {
83
+ shapeProperties.outline = outline;
84
+ fields.push('outline');
85
+ }
86
+
87
+ if (fields.length === 0) throw new UserError('No styling options specified');
88
+
89
+ try {
90
+ await slidesApi.presentations.batchUpdate({
91
+ presentationId: args.presentationId,
92
+ requestBody: {
93
+ requests: [
94
+ {
95
+ updateShapeProperties: {
96
+ objectId: args.objectId,
97
+ shapeProperties,
98
+ fields: fields.join(','),
99
+ },
100
+ },
101
+ ],
102
+ },
103
+ });
104
+ return JSON.stringify({ success: true, message: `Applied styling to shape ${args.objectId}` });
105
+ } catch (error) {
106
+ log.error(`Error styling shape: ${error.message || error}`);
107
+ throw new UserError(`Failed to style shape: ${error.message || 'Unknown error'}`);
108
+ }
109
+ },
110
+ });
111
+ }
@@ -0,0 +1,128 @@
1
+ import { UserError } from 'fastmcp';
2
+ import { z } from 'zod';
3
+ import { getSlidesClient } from '../../clients.js';
4
+
5
+ export function register(server) {
6
+ server.addTool({
7
+ name: 'updatePresentation',
8
+ description:
9
+ 'Replace all slides in an existing Google Slides presentation with new content. Deletes existing slides and creates new ones with the provided title/content pairs.',
10
+ parameters: z.object({
11
+ presentationId: z.string().describe('Presentation ID'),
12
+ slides: z
13
+ .array(
14
+ z.object({
15
+ title: z.string().describe('Slide title'),
16
+ content: z.string().describe('Slide body content'),
17
+ }),
18
+ )
19
+ .min(1)
20
+ .describe('Array of slide objects to replace existing slides'),
21
+ }),
22
+ execute: async (args, { log }) => {
23
+ const slidesApi = await getSlidesClient();
24
+ log.info(`Updating presentation: ${args.presentationId}`);
25
+
26
+ try {
27
+ const currentPresentation = await slidesApi.presentations.get({
28
+ presentationId: args.presentationId,
29
+ });
30
+
31
+ if (!currentPresentation.data.slides) {
32
+ throw new UserError('No slides found in presentation');
33
+ }
34
+
35
+ // Delete all slides except the first, then clear and rewrite the first
36
+ const slideIdsToDelete = currentPresentation.data.slides
37
+ .slice(1)
38
+ .map((s) => s.objectId)
39
+ .filter(Boolean);
40
+
41
+ const requests = [];
42
+
43
+ // Delete extra slides
44
+ for (const id of slideIdsToDelete) {
45
+ requests.push({ deleteObject: { objectId: id } });
46
+ }
47
+
48
+ // Clear text from the first slide's text elements
49
+ const firstSlide = currentPresentation.data.slides[0];
50
+ if (firstSlide?.pageElements) {
51
+ for (const el of firstSlide.pageElements) {
52
+ if (el.objectId && el.shape?.text) {
53
+ requests.push({ deleteText: { objectId: el.objectId, textRange: { type: 'ALL' } } });
54
+ }
55
+ }
56
+ }
57
+
58
+ // Insert new content into first slide
59
+ const firstContent = args.slides[0];
60
+ if (firstSlide?.pageElements) {
61
+ let titleId, bodyId;
62
+ for (const el of firstSlide.pageElements) {
63
+ const pt = el.shape?.placeholder?.type;
64
+ if (pt === 'TITLE' || pt === 'CENTERED_TITLE') titleId = el.objectId;
65
+ else if (pt === 'BODY' || pt === 'SUBTITLE') bodyId = el.objectId;
66
+ }
67
+ if (titleId) requests.push({ insertText: { objectId: titleId, text: firstContent.title, insertionIndex: 0 } });
68
+ if (bodyId) requests.push({ insertText: { objectId: bodyId, text: firstContent.content, insertionIndex: 0 } });
69
+ }
70
+
71
+ // Create additional slides
72
+ for (let i = 1; i < args.slides.length; i++) {
73
+ requests.push({
74
+ createSlide: {
75
+ objectId: `slide_${Date.now()}_${i}`,
76
+ slideLayoutReference: { predefinedLayout: 'TITLE_AND_BODY' },
77
+ },
78
+ });
79
+ }
80
+
81
+ await slidesApi.presentations.batchUpdate({
82
+ presentationId: args.presentationId,
83
+ requestBody: { requests },
84
+ });
85
+
86
+ // Populate new slides (index 1+)
87
+ if (args.slides.length > 1) {
88
+ const updated = await slidesApi.presentations.get({ presentationId: args.presentationId });
89
+ const contentRequests = [];
90
+ for (let i = 1; i < args.slides.length && updated.data.slides; i++) {
91
+ const slide = args.slides[i];
92
+ const pageSlide = updated.data.slides[i];
93
+ if (!pageSlide?.pageElements) continue;
94
+ for (const el of pageSlide.pageElements) {
95
+ if (!el.objectId) continue;
96
+ const pt = el.shape?.placeholder?.type;
97
+ if (pt === 'TITLE' || pt === 'CENTERED_TITLE') {
98
+ contentRequests.push({ insertText: { objectId: el.objectId, text: slide.title, insertionIndex: 0 } });
99
+ } else if (pt === 'BODY' || pt === 'SUBTITLE') {
100
+ contentRequests.push({ insertText: { objectId: el.objectId, text: slide.content, insertionIndex: 0 } });
101
+ }
102
+ }
103
+ }
104
+ if (contentRequests.length > 0) {
105
+ await slidesApi.presentations.batchUpdate({
106
+ presentationId: args.presentationId,
107
+ requestBody: { requests: contentRequests },
108
+ });
109
+ }
110
+ }
111
+
112
+ return JSON.stringify(
113
+ {
114
+ presentationId: args.presentationId,
115
+ slidesUpdated: args.slides.length,
116
+ link: `https://docs.google.com/presentation/d/${args.presentationId}`,
117
+ },
118
+ null,
119
+ 2,
120
+ );
121
+ } catch (error) {
122
+ if (error instanceof UserError) throw error;
123
+ log.error(`Error updating presentation: ${error.message || error}`);
124
+ throw new UserError(`Failed to update presentation: ${error.message || 'Unknown error'}`);
125
+ }
126
+ },
127
+ });
128
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "google-tools-mcp",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "The easiest MCP server for Google Workspace — Drive, Docs, Sheets, Gmail, Calendar, and Forms. 153 tools with one-click browser auth. Read Word docs, PDFs, and spreadsheets straight from Drive.",
5
5
  "type": "module",
6
6
  "bin": {