google-tools-mcp 1.1.2 → 1.1.4
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 +20 -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 +1 -1
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
|
// ---------------------------------------------------------------------------
|
|
@@ -164,6 +166,23 @@ async function loadSavedCredentialsIfExist() {
|
|
|
164
166
|
const tokenPath = getTokenPath();
|
|
165
167
|
const content = await fs.readFile(tokenPath, 'utf8');
|
|
166
168
|
const credentials = JSON.parse(content);
|
|
169
|
+
|
|
170
|
+
// Check that the saved token covers all current SCOPES.
|
|
171
|
+
// Tokens without a scopes field (pre-upgrade) or with missing scopes
|
|
172
|
+
// are stale — delete and force re-auth via browser automatically.
|
|
173
|
+
if (!Array.isArray(credentials.scopes)) {
|
|
174
|
+
logger.info('Saved token has no scopes record (pre-upgrade token). Re-authentication required for new scopes.');
|
|
175
|
+
try { await fs.unlink(tokenPath); } catch {}
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
const saved = new Set(credentials.scopes);
|
|
179
|
+
const missing = SCOPES.filter(s => !saved.has(s));
|
|
180
|
+
if (missing.length > 0) {
|
|
181
|
+
logger.info(`Saved token is missing scope(s): ${missing.join(', ')}. Re-authentication required.`);
|
|
182
|
+
try { await fs.unlink(tokenPath); } catch {}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
167
186
|
const { client_secret, client_id } = await loadClientSecrets();
|
|
168
187
|
const client = new google.auth.OAuth2(client_id, client_secret);
|
|
169
188
|
client.setCredentials(credentials);
|
|
@@ -183,6 +202,7 @@ async function saveCredentials(client) {
|
|
|
183
202
|
client_id,
|
|
184
203
|
client_secret,
|
|
185
204
|
refresh_token: client.credentials.refresh_token,
|
|
205
|
+
scopes: SCOPES,
|
|
186
206
|
}, null, 2);
|
|
187
207
|
await fs.writeFile(tokenPath, payload);
|
|
188
208
|
logger.info('Token stored to', tokenPath);
|
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
|
+
}
|
|
@@ -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.
|
|
3
|
+
"version": "1.1.4",
|
|
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": {
|