google-tools-mcp 1.1.1 → 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
+ }