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
|
@@ -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,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "google-tools-mcp",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.1.3",
|
|
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": {
|
|
7
7
|
"google-tools-mcp": "dist/index.js"
|
|
@@ -11,16 +11,55 @@
|
|
|
11
11
|
],
|
|
12
12
|
"keywords": [
|
|
13
13
|
"mcp",
|
|
14
|
+
"mcp-server",
|
|
15
|
+
"model-context-protocol",
|
|
14
16
|
"google",
|
|
17
|
+
"google-workspace",
|
|
18
|
+
"google-drive",
|
|
19
|
+
"google-docs",
|
|
20
|
+
"google-sheets",
|
|
21
|
+
"google-calendar",
|
|
22
|
+
"google-forms",
|
|
15
23
|
"gdrive",
|
|
16
24
|
"gmail",
|
|
25
|
+
"email",
|
|
26
|
+
"send-email",
|
|
27
|
+
"read-email",
|
|
28
|
+
"email-drafts",
|
|
29
|
+
"email-labels",
|
|
30
|
+
"email-filters",
|
|
17
31
|
"sheets",
|
|
18
|
-
"
|
|
32
|
+
"spreadsheet",
|
|
33
|
+
"docs",
|
|
34
|
+
"calendar",
|
|
35
|
+
"forms",
|
|
36
|
+
"drive",
|
|
37
|
+
"pdf",
|
|
38
|
+
"read-pdf",
|
|
39
|
+
"word",
|
|
40
|
+
"docx",
|
|
41
|
+
"read-docx",
|
|
42
|
+
"file-reader",
|
|
43
|
+
"oauth",
|
|
44
|
+
"easy-auth",
|
|
45
|
+
"browser-auth",
|
|
46
|
+
"claude",
|
|
47
|
+
"ai",
|
|
48
|
+
"llm",
|
|
49
|
+
"ai-tools"
|
|
19
50
|
],
|
|
20
51
|
"scripts": {
|
|
21
52
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
22
53
|
"test:ci": "node --experimental-vm-modules node_modules/jest/bin/jest.js --ci --coverage"
|
|
23
54
|
},
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "https://github.com/karthikcsq/google-tools-mcp.git"
|
|
58
|
+
},
|
|
59
|
+
"homepage": "https://github.com/karthikcsq/google-tools-mcp#readme",
|
|
60
|
+
"bugs": {
|
|
61
|
+
"url": "https://github.com/karthikcsq/google-tools-mcp/issues"
|
|
62
|
+
},
|
|
24
63
|
"license": "MIT",
|
|
25
64
|
"dependencies": {
|
|
26
65
|
"fastmcp": "^3.24.0",
|