@a1-x-tech/mcp-google-slides 0.1.0

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.
@@ -0,0 +1,87 @@
1
+ import { z } from "zod";
2
+ import { DESTRUCTIVE, fail, objectIdSchema, ok, presentationIdSchema, READ_ONLY, WRITE } from "./util.js";
3
+ export function registerPresentationTools(server, client) {
4
+ server.registerTool("create_presentation", {
5
+ title: "Create a presentation",
6
+ annotations: WRITE,
7
+ description: "Creates a new Google Slides presentation and returns it (presentationId, revisionId, pageSize, the initial title slide with its object ids, layouts and masters). The API honors only the title at creation — add slides with add_slide, content with the text/shape/table/image tools. The new deck starts with one title slide and belongs to the authorized user's Drive. Note: this server cannot delete a presentation; removing the file is a Drive operation outside its scope.",
8
+ inputSchema: {
9
+ title: z.string().min(1).describe("The presentation title (also the Drive file name)."),
10
+ },
11
+ }, async ({ title }) => {
12
+ try {
13
+ return ok(await client.createPresentation(title));
14
+ }
15
+ catch (e) {
16
+ return fail(e);
17
+ }
18
+ });
19
+ server.registerTool("get_presentation", {
20
+ title: "Get a presentation",
21
+ annotations: READ_ONLY,
22
+ description: "Returns the raw presentation JSON: slides[] with every page element (shapes, text runs, tables, images), layouts[], masters[], notesMaster, pageSize (EMU: 1 pt = 12700 EMU) and revisionId. WARNING: a full deck is often hundreds of kilobytes — prefer list_slides for an inventory, and pass fields (a Google fields mask, e.g. \"slides(objectId,pageElements(objectId,shape(text)))\") to trim this response when you need raw detail.",
23
+ inputSchema: {
24
+ presentation_id: presentationIdSchema(),
25
+ fields: z
26
+ .string()
27
+ .optional()
28
+ .describe('Optional Google fields mask to trim the response, e.g. "slides(objectId),pageSize".'),
29
+ },
30
+ }, async ({ presentation_id, fields }) => {
31
+ try {
32
+ return ok(await client.getPresentation(presentation_id, fields));
33
+ }
34
+ catch (e) {
35
+ return fail(e);
36
+ }
37
+ });
38
+ server.registerTool("list_slides", {
39
+ title: "List slides (compact inventory)",
40
+ annotations: READ_ONLY,
41
+ description: "Compact inventory of the deck — the read to start from: presentationId, title, revisionId, pageSizePt, slideCount and per slide its 0-based index, objectId, layoutObjectId, speakerNotesObjectId and elements[] with each element's objectId, type (shape type / table RxC / image / video / line / group), placeholder role and visible text (truncated to 160 chars). All mutation tools address these object ids, so call this before editing; per-element geometry and full text need get_presentation or get_page.",
42
+ inputSchema: { presentation_id: presentationIdSchema() },
43
+ }, async ({ presentation_id }) => {
44
+ try {
45
+ return ok(await client.listSlides(presentation_id));
46
+ }
47
+ catch (e) {
48
+ return fail(e);
49
+ }
50
+ });
51
+ server.registerTool("get_page", {
52
+ title: "Get one page",
53
+ annotations: READ_ONLY,
54
+ description: "Returns one page in full by its object id — works for slides, layouts, masters and notes pages alike. For a slide the response includes slideProperties.notesPage (the speaker-notes page) and every pageElement with its size, transform (EMU: 1 pt = 12700 EMU) and complete text. Use it when list_slides' summary is not enough for one page but the whole deck via get_presentation would be too much.",
55
+ inputSchema: {
56
+ presentation_id: presentationIdSchema(),
57
+ page_object_id: objectIdSchema("The page's object id — a slide/layout/master/notes-page id from list_slides or get_presentation."),
58
+ },
59
+ }, async ({ presentation_id, page_object_id }) => {
60
+ try {
61
+ return ok(await client.getPage(presentation_id, page_object_id));
62
+ }
63
+ catch (e) {
64
+ return fail(e);
65
+ }
66
+ });
67
+ server.registerTool("batch_update", {
68
+ title: "Raw batchUpdate",
69
+ // Arbitrary requests include deleteObject — annotate for the worst case.
70
+ annotations: DESTRUCTIVE,
71
+ description: 'Sends raw Slides API batchUpdate requests for anything the typed tools don\'t cover: updateShapeProperties (fill, outline, shadow), updatePageProperties (slide background), updateParagraphStyle, createParagraphBullets, updateTableCellProperties, mergeTableCells, createVideo, createLine, groupObjects, updateSlideProperties and the rest of the request union. requests is the API\'s requests[] array verbatim, e.g. [{"updateShapeProperties":{"objectId":"...","shapeProperties":{...},"fields":"shapeFill"}}]. The batch is atomic: one invalid request rejects the whole batch and nothing is applied. Lengths/coordinates take {"magnitude":N,"unit":"PT"}; wire enums are UPPER_CASE.',
72
+ inputSchema: {
73
+ presentation_id: presentationIdSchema(),
74
+ requests: z
75
+ .array(z.record(z.any()))
76
+ .min(1)
77
+ .describe("The Slides API batchUpdate requests[] array, verbatim wire format."),
78
+ },
79
+ }, async ({ presentation_id, requests }) => {
80
+ try {
81
+ return ok(await client.batchUpdate(presentation_id, requests));
82
+ }
83
+ catch (e) {
84
+ return fail(e);
85
+ }
86
+ });
87
+ }
@@ -0,0 +1,30 @@
1
+ import { z } from "zod";
2
+ import { DESTRUCTIVE, fail, ok } from "./util.js";
3
+ export function registerRawTool(server, client) {
4
+ server.registerTool("raw_request", {
5
+ title: "Raw Google Slides API call",
6
+ // Full API surface incl. arbitrary batchUpdate — annotate for the worst
7
+ // case a call can do, not the average.
8
+ annotations: DESTRUCTIVE,
9
+ description: 'Escape hatch to call any Google Slides API v1 path directly, for requests even batch_update doesn\'t cover — e.g. GET "v1/presentations/<id>/pages/<pageId>" with a custom fields mask in the query string, or a POST with a body shaped differently from the typed tools. The Slides API only has GET and POST endpoints; the path is relative to https://slides.googleapis.com and may carry a query string. The Bearer token is added automatically; a path resolving to a foreign origin is rejected. Drive endpoints (comments, export) are NOT reachable here — use manage_comments / export_presentation.',
10
+ inputSchema: {
11
+ path: z
12
+ .string()
13
+ .min(1)
14
+ .describe('API path relative to https://slides.googleapis.com, e.g. "v1/presentations/<id>:batchUpdate".'),
15
+ method: z
16
+ .enum(["GET", "POST"])
17
+ .optional()
18
+ .describe("HTTP method (the Slides API uses only these two). Defaults to GET."),
19
+ body: z.record(z.any()).optional().describe("JSON request body (POST only)."),
20
+ },
21
+ }, async ({ path, method, body }) => {
22
+ try {
23
+ const m = (method ?? "GET");
24
+ return ok(await client.request(m, path, body));
25
+ }
26
+ catch (e) {
27
+ return fail(e);
28
+ }
29
+ });
30
+ }
@@ -0,0 +1,117 @@
1
+ import { z } from "zod";
2
+ import { DESTRUCTIVE, fail, indexSchema, newObjectIdSchema, objectIdSchema, ok, presentationIdSchema, READ_ONLY, WRITE, } from "./util.js";
3
+ export function registerSlideTools(server, client) {
4
+ server.registerTool("add_slide", {
5
+ title: "Add a slide",
6
+ annotations: WRITE,
7
+ description: "Adds a slide (batchUpdate createSlide) and returns the new slide's objectId in the reply. Pick the look with either predefined_layout (BLANK, TITLE, TITLE_AND_BODY, SECTION_HEADER, ...) or layout_object_id from list_layouts — not both; with neither the slide copies the layout of the current last slide. insertion_index is the 0-based position (omitted = append at the end). Placeholders inherited from the layout arrive empty — find their object ids via list_slides, then fill them with set_text.",
8
+ inputSchema: {
9
+ presentation_id: presentationIdSchema(),
10
+ insertion_index: indexSchema()
11
+ .optional()
12
+ .describe("0-based position for the new slide; omitted = append at the end."),
13
+ predefined_layout: z
14
+ .enum([
15
+ "BLANK",
16
+ "CAPTION_ONLY",
17
+ "TITLE",
18
+ "TITLE_AND_BODY",
19
+ "TITLE_AND_TWO_COLUMNS",
20
+ "TITLE_ONLY",
21
+ "SECTION_HEADER",
22
+ "SECTION_TITLE_AND_DESCRIPTION",
23
+ "ONE_COLUMN_TEXT",
24
+ "MAIN_POINT",
25
+ "BIG_NUMBER",
26
+ ])
27
+ .optional()
28
+ .describe("A predefined layout; the theme's actual layouts may support only a subset."),
29
+ layout_object_id: z
30
+ .string()
31
+ .min(1)
32
+ .optional()
33
+ .describe("A concrete layout's object id from list_layouts (alternative to predefined_layout)."),
34
+ object_id: newObjectIdSchema()
35
+ .optional()
36
+ .describe("Optional custom object id for the new slide (5-50 chars of [a-zA-Z0-9_-:])."),
37
+ },
38
+ }, async ({ presentation_id, insertion_index, predefined_layout, layout_object_id, object_id }) => {
39
+ try {
40
+ return ok(await client.addSlide({
41
+ presentationId: presentation_id,
42
+ insertionIndex: insertion_index,
43
+ predefinedLayout: predefined_layout,
44
+ layoutObjectId: layout_object_id,
45
+ objectId: object_id,
46
+ }));
47
+ }
48
+ catch (e) {
49
+ return fail(e);
50
+ }
51
+ });
52
+ server.registerTool("duplicate_slide", {
53
+ title: "Duplicate a slide",
54
+ annotations: WRITE,
55
+ description: "Duplicates a slide (batchUpdate duplicateObject) — the copy, with all its elements and speaker notes, lands immediately after the original and its new objectId comes back in the reply. Works on any page element id too (the copy lands on the same slide, slightly offset). Move the copy with move_slides afterwards if needed.",
56
+ inputSchema: {
57
+ presentation_id: presentationIdSchema(),
58
+ slide_object_id: objectIdSchema("The object id of the slide (or page element) to duplicate."),
59
+ },
60
+ }, async ({ presentation_id, slide_object_id }) => {
61
+ try {
62
+ return ok(await client.duplicateSlide(presentation_id, slide_object_id));
63
+ }
64
+ catch (e) {
65
+ return fail(e);
66
+ }
67
+ });
68
+ server.registerTool("move_slides", {
69
+ title: "Move slides",
70
+ annotations: WRITE,
71
+ description: "Moves one or more slides to a new position (batchUpdate updateSlidesPosition). slide_object_ids must appear in their current deck order with no duplicates; insertion_index is the 0-based position BEFORE the move is applied (moving a slide down: index counts with the slide still in its old place). Use list_slides to see the current order first.",
72
+ inputSchema: {
73
+ presentation_id: presentationIdSchema(),
74
+ slide_object_ids: z
75
+ .array(z.string().min(1))
76
+ .min(1)
77
+ .describe("The slides to move, listed in their current presentation order."),
78
+ insertion_index: indexSchema().describe("0-based target position, evaluated before the slides are removed from their old places."),
79
+ },
80
+ }, async ({ presentation_id, slide_object_ids, insertion_index }) => {
81
+ try {
82
+ return ok(await client.moveSlides(presentation_id, slide_object_ids, insertion_index));
83
+ }
84
+ catch (e) {
85
+ return fail(e);
86
+ }
87
+ });
88
+ server.registerTool("delete_object", {
89
+ title: "Delete a slide or element",
90
+ annotations: DESTRUCTIVE,
91
+ description: "Deletes a slide or a page element (shape, image, table, video, line, group) by object id — batchUpdate deleteObject. Deleting a slide removes all its elements and speaker notes; deleting a group removes its children. There is no undo through the API (version history lives in the Slides UI). It cannot delete the whole presentation — that is a Drive file operation outside this server.",
92
+ inputSchema: {
93
+ presentation_id: presentationIdSchema(),
94
+ object_id: objectIdSchema("The object id of the slide or page element to delete (from list_slides)."),
95
+ },
96
+ }, async ({ presentation_id, object_id }) => {
97
+ try {
98
+ return ok(await client.deleteObject(presentation_id, object_id));
99
+ }
100
+ catch (e) {
101
+ return fail(e);
102
+ }
103
+ });
104
+ server.registerTool("list_layouts", {
105
+ title: "List layouts and masters",
106
+ annotations: READ_ONLY,
107
+ description: "Lists the presentation's theme building blocks: layouts[] (objectId, name like TITLE_AND_BODY, displayName, masterObjectId), masters[] and the notesMasterObjectId. Layout object ids feed add_slide's layout_object_id; the Slides API cannot import a new theme or change the master set — themes are chosen in the UI, this tool only reads what the deck already has.",
108
+ inputSchema: { presentation_id: presentationIdSchema() },
109
+ }, async ({ presentation_id }) => {
110
+ try {
111
+ return ok(await client.listLayouts(presentation_id));
112
+ }
113
+ catch (e) {
114
+ return fail(e);
115
+ }
116
+ });
117
+ }
@@ -0,0 +1,87 @@
1
+ import { z } from "zod";
2
+ import { DESTRUCTIVE, fail, indexSchema, newObjectIdSchema, objectIdSchema, ok, pointsSchema, presentationIdSchema, WRITE, } from "./util.js";
3
+ export function registerTableTools(server, client) {
4
+ server.registerTool("create_table", {
5
+ title: "Create a table",
6
+ annotations: WRITE,
7
+ description: "Creates an empty rows x columns table on a slide (batchUpdate createTable) and returns its objectId in the reply. Fill cells with set_text / insert_text using row_index + column_index (0-based); restructure later with edit_table. Position/size are in points; omitted geometry lets the API pick a centered default. Cell styling (borders, background) goes through batch_update updateTableCellProperties.",
8
+ inputSchema: {
9
+ presentation_id: presentationIdSchema(),
10
+ page_object_id: objectIdSchema("The slide's object id to place the table on (from list_slides)."),
11
+ rows: z.number().int().min(1).max(20).describe("Row count (the API allows at most 20 rows at creation)."),
12
+ columns: z
13
+ .number()
14
+ .int()
15
+ .min(1)
16
+ .max(20)
17
+ .describe("Column count (the API allows at most 20 columns at creation)."),
18
+ x_pt: pointsSchema().optional().describe("Left edge in points (requires y_pt)."),
19
+ y_pt: pointsSchema().optional().describe("Top edge in points (requires x_pt)."),
20
+ width_pt: pointsSchema().optional().describe("Width in points (requires height_pt)."),
21
+ height_pt: pointsSchema().optional().describe("Height in points (requires width_pt)."),
22
+ object_id: newObjectIdSchema().optional().describe("Optional custom object id for the new table."),
23
+ },
24
+ }, async (args) => {
25
+ try {
26
+ return ok(await client.createTable({
27
+ presentationId: args.presentation_id,
28
+ pageObjectId: args.page_object_id,
29
+ rows: args.rows,
30
+ columns: args.columns,
31
+ xPt: args.x_pt,
32
+ yPt: args.y_pt,
33
+ widthPt: args.width_pt,
34
+ heightPt: args.height_pt,
35
+ objectId: args.object_id,
36
+ }));
37
+ }
38
+ catch (e) {
39
+ return fail(e);
40
+ }
41
+ });
42
+ server.registerTool("edit_table", {
43
+ title: "Edit table structure",
44
+ // insert_* only adds, but delete_row/delete_column remove content — the
45
+ // whole tool carries the worst case's hints.
46
+ annotations: DESTRUCTIVE,
47
+ description: "Restructures an existing table in one call. action=insert_rows needs row_index (+optional below, default true, and count 1-20); insert_columns needs column_index (+optional right, default true, and count); delete_row needs row_index and delete_column needs column_index — deletion permanently discards the cells' content, and the remaining rows/columns shift so indexes change between successive edits (re-check with list_slides). Cell TEXT is written with set_text/insert_text (row_index + column_index), not here.",
48
+ inputSchema: {
49
+ presentation_id: presentationIdSchema(),
50
+ table_object_id: objectIdSchema('The table\'s object id (type "table" in list_slides).'),
51
+ action: z
52
+ .enum(["insert_rows", "insert_columns", "delete_row", "delete_column"])
53
+ .describe("The structural edit to apply."),
54
+ row_index: indexSchema()
55
+ .optional()
56
+ .describe("Reference row, 0-based (insert_rows / delete_row)."),
57
+ column_index: indexSchema()
58
+ .optional()
59
+ .describe("Reference column, 0-based (insert_columns / delete_column)."),
60
+ count: z.number().int().min(1).max(20).optional().describe("How many rows/columns to insert (default 1)."),
61
+ below: z
62
+ .boolean()
63
+ .optional()
64
+ .describe("insert_rows: insert below the reference row (default true; false = above)."),
65
+ right: z
66
+ .boolean()
67
+ .optional()
68
+ .describe("insert_columns: insert to the right of the reference column (default true; false = left)."),
69
+ },
70
+ }, async ({ presentation_id, table_object_id, action, row_index, column_index, count, below, right }) => {
71
+ try {
72
+ return ok(await client.editTable({
73
+ presentationId: presentation_id,
74
+ tableObjectId: table_object_id,
75
+ action,
76
+ rowIndex: row_index,
77
+ columnIndex: column_index,
78
+ count,
79
+ below,
80
+ right,
81
+ }));
82
+ }
83
+ catch (e) {
84
+ return fail(e);
85
+ }
86
+ });
87
+ }
@@ -0,0 +1,129 @@
1
+ import { z } from "zod";
2
+ import { fail, hexColorSchema, indexSchema, objectIdSchema, ok, presentationIdSchema, UPDATE, WRITE, } from "./util.js";
3
+ export function registerTextTools(server, client) {
4
+ server.registerTool("insert_text", {
5
+ title: "Insert text",
6
+ annotations: WRITE,
7
+ description: "Inserts text into a shape or a table cell (batchUpdate insertText) at a 0-based character index (default 0 = the beginning). For a table cell pass row_index + column_index together. Text carries \\n for new paragraphs; the target must be a shape that can hold text or a table — images and lines cannot. To REPLACE what's already there use set_text instead; to fill an empty layout placeholder this works directly (find its object id via list_slides).",
8
+ inputSchema: {
9
+ presentation_id: presentationIdSchema(),
10
+ object_id: objectIdSchema("The shape or table object id (from list_slides)."),
11
+ text: z.string().min(1).describe("The text to insert; use \\n for paragraph breaks."),
12
+ insertion_index: indexSchema()
13
+ .optional()
14
+ .describe("0-based character index to insert at (default 0 — the start of the existing text)."),
15
+ row_index: indexSchema().optional().describe("Table cell row (0-based; requires column_index)."),
16
+ column_index: indexSchema().optional().describe("Table cell column (0-based; requires row_index)."),
17
+ },
18
+ }, async ({ presentation_id, object_id, text, insertion_index, row_index, column_index }) => {
19
+ try {
20
+ return ok(await client.insertText({
21
+ presentationId: presentation_id,
22
+ objectId: object_id,
23
+ text,
24
+ insertionIndex: insertion_index,
25
+ rowIndex: row_index,
26
+ columnIndex: column_index,
27
+ }));
28
+ }
29
+ catch (e) {
30
+ return fail(e);
31
+ }
32
+ });
33
+ server.registerTool("set_text", {
34
+ title: "Set (replace) text",
35
+ annotations: UPDATE,
36
+ description: "Replaces the ENTIRE text of a shape or table cell: one atomic batch of deleteText(ALL) + insertText. If the target has no text yet the delete is skipped automatically, so it works on empty placeholders too. An empty text just clears the object. Replacing text drops per-character styling — restyle with update_text_style afterwards if needed. For a table cell pass row_index + column_index together.",
37
+ inputSchema: {
38
+ presentation_id: presentationIdSchema(),
39
+ object_id: objectIdSchema("The shape or table object id (from list_slides)."),
40
+ text: z.string().describe("The new full text; empty string clears the object. \\n starts a new paragraph."),
41
+ row_index: indexSchema().optional().describe("Table cell row (0-based; requires column_index)."),
42
+ column_index: indexSchema().optional().describe("Table cell column (0-based; requires row_index)."),
43
+ },
44
+ }, async ({ presentation_id, object_id, text, row_index, column_index }) => {
45
+ try {
46
+ return ok(await client.setText({
47
+ presentationId: presentation_id,
48
+ objectId: object_id,
49
+ text,
50
+ rowIndex: row_index,
51
+ columnIndex: column_index,
52
+ }));
53
+ }
54
+ catch (e) {
55
+ return fail(e);
56
+ }
57
+ });
58
+ server.registerTool("replace_text", {
59
+ title: "Find & replace text",
60
+ annotations: UPDATE,
61
+ description: "Finds and replaces every occurrence of a string across the whole deck — shapes and table cells on slides, and their speaker notes (batchUpdate replaceAllText). Scope it with page_object_ids to touch only specific slides. Matching is literal (no regex), case-sensitive by default (match_case=false to ignore case). Returns occurrencesChanged in the reply — 0 means nothing matched, check the exact wording with list_slides. The classic template flow: duplicate a template deck, then replace {{placeholders}}.",
62
+ inputSchema: {
63
+ presentation_id: presentationIdSchema(),
64
+ find: z.string().min(1).describe("The literal text to find (no regex)."),
65
+ replace: z.string().describe("The replacement text (empty string deletes the matches)."),
66
+ match_case: z.boolean().optional().describe("Case-sensitive matching (default true)."),
67
+ page_object_ids: z
68
+ .array(z.string().min(1))
69
+ .optional()
70
+ .describe("Limit the replacement to these slides/pages (default: the whole presentation)."),
71
+ },
72
+ }, async ({ presentation_id, find, replace, match_case, page_object_ids }) => {
73
+ try {
74
+ return ok(await client.replaceText({
75
+ presentationId: presentation_id,
76
+ find,
77
+ replace,
78
+ matchCase: match_case,
79
+ pageObjectIds: page_object_ids,
80
+ }));
81
+ }
82
+ catch (e) {
83
+ return fail(e);
84
+ }
85
+ });
86
+ server.registerTool("update_text_style", {
87
+ title: "Style text",
88
+ annotations: UPDATE,
89
+ description: "Styles the text of a shape or table cell (batchUpdate updateTextStyle): bold, italic, underline, strikethrough, font_family (e.g. \"Roboto\"), font_size_pt, foreground_color (#RRGGBB) and link_url. Only the provided fields change (the field mask is computed automatically); at least one is required. By default the whole text is styled; pass start_index + end_index (0-based character offsets, end exclusive) to style a range — get the offsets from get_page's textElements. For a table cell add row_index + column_index.",
90
+ inputSchema: {
91
+ presentation_id: presentationIdSchema(),
92
+ object_id: objectIdSchema("The shape or table object id whose text to style."),
93
+ bold: z.boolean().optional().describe("Bold on/off."),
94
+ italic: z.boolean().optional().describe("Italic on/off."),
95
+ underline: z.boolean().optional().describe("Underline on/off."),
96
+ strikethrough: z.boolean().optional().describe("Strikethrough on/off."),
97
+ font_family: z.string().min(1).optional().describe('Font family, e.g. "Roboto" or "Playfair Display".'),
98
+ font_size_pt: z.number().positive().optional().describe("Font size in points."),
99
+ foreground_color: hexColorSchema().optional().describe("Text color as #RRGGBB."),
100
+ link_url: z.string().url().optional().describe("Turns the styled range into a hyperlink."),
101
+ start_index: indexSchema().optional().describe("Range start, 0-based character offset (requires end_index)."),
102
+ end_index: indexSchema().optional().describe("Range end, exclusive (requires start_index)."),
103
+ row_index: indexSchema().optional().describe("Table cell row (0-based; requires column_index)."),
104
+ column_index: indexSchema().optional().describe("Table cell column (0-based; requires row_index)."),
105
+ },
106
+ }, async (args) => {
107
+ try {
108
+ return ok(await client.updateTextStyle({
109
+ presentationId: args.presentation_id,
110
+ objectId: args.object_id,
111
+ bold: args.bold,
112
+ italic: args.italic,
113
+ underline: args.underline,
114
+ strikethrough: args.strikethrough,
115
+ fontFamily: args.font_family,
116
+ fontSizePt: args.font_size_pt,
117
+ foregroundColor: args.foreground_color,
118
+ linkUrl: args.link_url,
119
+ startIndex: args.start_index,
120
+ endIndex: args.end_index,
121
+ rowIndex: args.row_index,
122
+ columnIndex: args.column_index,
123
+ }));
124
+ }
125
+ catch (e) {
126
+ return fail(e);
127
+ }
128
+ });
129
+ }
@@ -0,0 +1,73 @@
1
+ import { z } from "zod";
2
+ /**
3
+ * Schema factories, not shared consts: reusing one zod object across two fields
4
+ * makes zod-to-json-schema dedupe them into a `$ref`, which some tool-schema
5
+ * consumers (OpenAI Apps review) don't dereference and flag as `any`. A fresh
6
+ * object per field keeps each one inlined with its type + pattern.
7
+ */
8
+ export const presentationIdSchema = () => z
9
+ .string()
10
+ .min(1)
11
+ .describe("The presentation id — the long id from the URL (docs.google.com/presentation/d/<presentationId>/edit) or from create_presentation output.");
12
+ /** An existing object id (slide, page element, table, ...) as returned by get_presentation/list_slides. */
13
+ export const objectIdSchema = (what) => z.string().min(1).describe(what);
14
+ /**
15
+ * A caller-chosen id for a new object. The API's contract: 5-50 chars of
16
+ * [a-zA-Z0-9_-:], not starting with '-' or ':'.
17
+ */
18
+ export const newObjectIdSchema = () => z
19
+ .string()
20
+ .regex(/^[A-Za-z0-9_][A-Za-z0-9_:-]{4,49}$/, "5-50 chars of [a-zA-Z0-9_-:], not starting with - or :");
21
+ /** A 0-based index (slide position, table row/column, text insertion point). */
22
+ export const indexSchema = () => z.number().int().min(0);
23
+ /** A length/coordinate in points (PT). 1 pt = 12700 EMU; a default slide is 720x405 pt. */
24
+ export const pointsSchema = () => z.number().finite();
25
+ /** A hex RGB color like #FF8800 (the leading # is optional). */
26
+ export const hexColorSchema = () => z.string().regex(/^#?[0-9a-fA-F]{6}$/, "Must be a hex RGB color like #FF8800");
27
+ /** Wraps a value as a compact-JSON tool result (compact: the consumer is an LLM). */
28
+ export function ok(data) {
29
+ const text = typeof data === "string" ? data : JSON.stringify(data);
30
+ return { content: [{ type: "text", text: text ?? "null" }] };
31
+ }
32
+ export function fail(err) {
33
+ let message = err instanceof Error ? err.message : String(err);
34
+ // Surface the underlying cause (e.g. the network error behind a timeout) — no
35
+ // secrets live in cause, and it makes failures far easier to diagnose.
36
+ if (err instanceof Error && err.cause instanceof Error)
37
+ message += ` (${err.cause.message})`;
38
+ return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
39
+ }
40
+ /**
41
+ * MCP tool annotations — hints the consuming client can use to gate or label a
42
+ * tool. All four hints are set explicitly on every tool: some clients (OpenAI
43
+ * Apps review) require readOnlyHint, destructiveHint and openWorldHint on each.
44
+ *
45
+ * The Slides API mixes reads and writes, so each tool picks one of four presets:
46
+ * READ_ONLY (pure reads), WRITE (creates new state; replaying duplicates it),
47
+ * UPDATE (overwrites existing fields; replaying the same update converges) and
48
+ * DESTRUCTIVE (removes existing state; replaying hits different targets).
49
+ */
50
+ export const READ_ONLY = {
51
+ readOnlyHint: true,
52
+ destructiveHint: false,
53
+ idempotentHint: true,
54
+ openWorldHint: true,
55
+ };
56
+ export const WRITE = {
57
+ readOnlyHint: false,
58
+ destructiveHint: false,
59
+ idempotentHint: false,
60
+ openWorldHint: true,
61
+ };
62
+ export const UPDATE = {
63
+ readOnlyHint: false,
64
+ destructiveHint: true,
65
+ idempotentHint: true,
66
+ openWorldHint: true,
67
+ };
68
+ export const DESTRUCTIVE = {
69
+ readOnlyHint: false,
70
+ destructiveHint: true,
71
+ idempotentHint: false,
72
+ openWorldHint: true,
73
+ };
package/dist/types.js ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * The server talks to the Google Slides API v1 (https://slides.googleapis.com,
3
+ * REST over JSON). Comments, export and local-image upload need the Google
4
+ * Drive API v3 (https://www.googleapis.com) as an internal dependency — those
5
+ * endpoints are never exposed as generic Drive tools. Auth is Google OAuth
6
+ * 2.0: a Bearer access token, minted on demand from a refresh token via
7
+ * https://oauth2.googleapis.com/token (or a static short-lived access token,
8
+ * mostly for testing).
9
+ */
10
+ /**
11
+ * Google APIs report failures as a non-2xx HTTP status with a JSON envelope
12
+ * ({ error: { code, message, status, details } }); the OAuth token endpoint
13
+ * uses { error, error_description }. The parsed body is kept alongside the
14
+ * status and a short readable message is derived.
15
+ */
16
+ export class GoogleSlidesError extends Error {
17
+ status;
18
+ body;
19
+ constructor(status, body) {
20
+ super(`HTTP ${status}: ${formatErrorBody(body)}`);
21
+ this.name = "GoogleSlidesError";
22
+ this.status = status;
23
+ this.body = body;
24
+ }
25
+ }
26
+ /** Turns a parsed Google API error body into a short, readable message. */
27
+ function formatErrorBody(body) {
28
+ if (body == null)
29
+ return "(no body)";
30
+ if (typeof body === "string")
31
+ return body.slice(0, 500);
32
+ if (typeof body !== "object")
33
+ return String(body);
34
+ const obj = body;
35
+ // OAuth token endpoint style: { error: "invalid_grant", error_description: "..." }
36
+ if (typeof obj.error === "string") {
37
+ const description = typeof obj.error_description === "string" ? `: ${obj.error_description}` : "";
38
+ return `${obj.error}${description}`.slice(0, 500);
39
+ }
40
+ // Google API envelope: { error: { code, message, status, details } }
41
+ const err = (typeof obj.error === "object" && obj.error !== null ? obj.error : obj);
42
+ if (typeof err.message === "string") {
43
+ const status = typeof err.status === "string" ? `[${err.status}] ` : "";
44
+ return `${status}${err.message}`.slice(0, 500);
45
+ }
46
+ return JSON.stringify(obj).slice(0, 500);
47
+ }
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@a1-x-tech/mcp-google-slides",
3
+ "version": "0.1.0",
4
+ "description": "MCP server for the Google Slides API — create and edit presentations, slides, text, shapes, tables and images, manage speaker notes, comments, thumbnails and export. For Claude, Cursor, Codex and other AI clients.",
5
+ "mcpName": "io.github.A1-x-Tech/mcp-google-slides",
6
+ "type": "module",
7
+ "bin": {
8
+ "mcp-google-slides": "dist/index.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "test"
13
+ ],
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "scripts": {
21
+ "build": "rm -rf dist && tsc",
22
+ "start": "node dist/index.js",
23
+ "dev": "tsx watch src/index.ts",
24
+ "typecheck": "tsc -p tsconfig.check.json",
25
+ "smoke": "node --import tsx src/smoke.ts",
26
+ "test": "npm run test:src && npm run test:dist",
27
+ "test:src": "node --import tsx --test $(find src -name '*.test.ts')",
28
+ "test:dist": "npm run build && node --test test/dist-smoke.test.js",
29
+ "prepare": "npm run build",
30
+ "prepublishOnly": "npm run typecheck && npm test"
31
+ },
32
+ "keywords": [
33
+ "mcp",
34
+ "model-context-protocol",
35
+ "google",
36
+ "google-slides",
37
+ "slides",
38
+ "presentations",
39
+ "claude",
40
+ "cursor",
41
+ "codex"
42
+ ],
43
+ "license": "MIT",
44
+ "author": "gistrec <gistrec@gmail.com>",
45
+ "repository": {
46
+ "type": "git",
47
+ "url": "git+https://github.com/A1-x-Tech/mcp-google-slides.git"
48
+ },
49
+ "homepage": "https://github.com/A1-x-Tech/mcp-google-slides#readme",
50
+ "bugs": {
51
+ "url": "https://github.com/A1-x-Tech/mcp-google-slides/issues"
52
+ },
53
+ "dependencies": {
54
+ "@modelcontextprotocol/sdk": "^1.29.0",
55
+ "zod": "^3.25.0"
56
+ },
57
+ "devDependencies": {
58
+ "@types/node": "^25.9.3",
59
+ "tsx": "^4.22.4",
60
+ "typescript": "^5.7.0"
61
+ }
62
+ }