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 +15 -12
- package/dist/auth.js +2 -0
- package/dist/clients.js +17 -0
- package/dist/tools/index.js +6 -0
- package/dist/tools/slides/createPresentation.js +104 -0
- package/dist/tools/slides/createShape.js +92 -0
- package/dist/tools/slides/createTextBox.js +82 -0
- package/dist/tools/slides/deleteSlide.js +30 -0
- package/dist/tools/slides/duplicateSlide.js +37 -0
- package/dist/tools/slides/exportThumbnail.js +42 -0
- package/dist/tools/slides/formatParagraph.js +72 -0
- package/dist/tools/slides/formatText.js +84 -0
- package/dist/tools/slides/getPresentation.js +87 -0
- package/dist/tools/slides/index.js +33 -0
- package/dist/tools/slides/reorderSlides.js +41 -0
- package/dist/tools/slides/replaceAllText.js +46 -0
- package/dist/tools/slides/setBackground.js +58 -0
- package/dist/tools/slides/speakerNotes.js +102 -0
- package/dist/tools/slides/styleShape.js +111 -0
- package/dist/tools/slides/updatePresentation.js +128 -0
- package/package.json +42 -3
package/README.md
CHANGED
|
@@ -1,23 +1,26 @@
|
|
|
1
1
|
# google-tools-mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The **easiest way** to connect your AI agent to Google Workspace.
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
|
|
7
|
+
```bash
|
|
8
|
+
claude mcp add -s user google -- npx -y google-tools-mcp
|
|
9
|
+
```
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
## Why google-tools-mcp?
|
|
10
12
|
|
|
11
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/tools/index.js
CHANGED
|
@@ -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
|
+
}
|