Skip to main content
Glama
README.md
# google-slides-mcp

A `uvx`-runnable [MCP](https://modelcontextprotocol.io) server for **low-level**
interaction with the Google Slides API.

It is built to be more capable than typical Slides MCPs. In addition to reading
presentations and slides as structured objects, it can:

- **Find existing presentations in your Drive** — native Google Slides *and*
  PowerPoint (`.pptx`) files — by name, content, or recency ("the last deck I
  created"), and **import a `.pptx` as a native Slides deck** so every other
  tool works on it.
- **Reuse a "model template" / showcase deck with full fidelity.** Copy a styled
  showcase deck, duplicate the example slides you want (e.g. a 3‑column layout),
  fill them with content, and prune the rest — leaving a result *functionally
  identical to hand‑editing the showcase* (masters, layouts, theme and colors are
  all preserved).
- **Edit element bounding boxes** in points (position/size) without doing affine
  matrix math, plus raw transform / z‑order / grouping control.
- **Render slides to PNG** and **diff two renders** so you can verify output is
  pixel‑perfect.
- **Issue raw `batchUpdate` requests** for anything the convenience tools don't
  cover.

Every tool makes a **bounded number of API calls** and returns **trimmed output**,
so an LLM driving it has a predictable, finite cost.

---

## Why the template workflow works this way

The Google Slides API has **no native cross‑presentation slide copy**.
`duplicateObject` only works *within* one presentation, and recreating a slide's
elements by hand in another deck loses fidelity (placeholder inheritance, master
styling and theme colors are dropped).

The high‑fidelity pattern this server implements is:

1. **`copy_presentation`** → Drive `files.copy` clones the *entire* showcase deck,
   preserving masters, layouts, theme and color scheme.
2. **`catalog_slides`** → summarize the example slides so you can pick one per section.
3. **`duplicate_slide`** → copy a chosen example slide *within* the new deck (full fidelity).
4. **`replace_all_text`** / **`set_element_box`** → fill content and adjust layout.
5. **`delete_objects`** → prune the original showcase example slides.
6. **`render_page`** / **`diff_pages`** → verify the result.

---

## 1. Google Cloud Console setup (one time)

1. Go to the [Google Cloud Console](https://console.cloud.google.com/) and **create
   a project** (or select an existing one).
2. **Enable the APIs**: *APIs & Services → Library* → enable both
   **Google Slides API** and **Google Drive API**.
3. **Configure the OAuth consent screen**: *APIs & Services → OAuth consent screen*.
   - User type: **External** (or **Internal** if you're in a Workspace org).
   - Fill in app name / support email.
   - Add the scopes `.../auth/presentations` and `.../auth/drive`.
   - Under **Test users**, add the Google account you'll authorize with.
   - ⚠️ While the app is in **Testing** status, refresh tokens expire after **7
     days**. Re-run the auth command when that happens, or publish the app.
4. **Create credentials**: *APIs & Services → Credentials → Create credentials →
   OAuth client ID*.
   - Application type: **Desktop app**.
   - Download the JSON — this is your **`client_secret.json`**.

> Scope note: this server requests the broad `auth/drive` scope so it can
> `files.copy` *any* template you own by ID. If you only ever copy decks the app
> itself created, you can narrow this via `GOOGLE_SLIDES_SCOPES` (see below), but
> the showcase workflow on existing decks needs `auth/drive`. Google requires the
> fully‑qualified scope URLs (e.g. `https://www.googleapis.com/auth/drive`); the
> short names `presentations` / `drive` are accepted and expanded for you.

## 2. Install & first‑time login

Auth uses a standard **interactive OAuth 2.0** installed‑app flow built to roll
out to a team: you distribute **one** Desktop OAuth client JSON, and **each person
logs in once in a browser** to cache their own personal refresh token. There is no
shared, single‑user token.

You can authorize either way:

**Option A — let the server do it (zero extra commands).** Just configure the MCP
server (next section) with your client secret. The **first launch** opens a browser
for consent automatically and caches the token; every launch after that is silent.

**Option B — log in explicitly up front** (recommended for headless/server hosts,
or to authorize before wiring up your MCP client):

```bash
GOOGLE_CLIENT_SECRET=/absolute/path/to/client_secret.json \
  uvx --from git+https://github.com/justparent/google-slides-mcp google-slides-mcp-auth
```

Either way the token is written to `~/.config/google-slides-mcp/token.json`
(override with `GOOGLE_TOKEN_PATH` / `TOKEN_PATH`). Once published to PyPI you'll be
able to drop `--from …` and just run `uvx google-slides-mcp-auth`.

> If you're deploying somewhere without a browser, set
> `GOOGLE_SLIDES_NO_BROWSER_AUTH=1` so the server never tries to open one, and use
> Option B to authorize ahead of time.

## 3. Configure your MCP client

Add the server to Claude Desktop / Claude Code (`mcpServers` config):

```json
{
  "mcpServers": {
    "google-slides": {
      "command": "uvx",
      "args": [
        "--from", "git+https://github.com/justparent/google-slides-mcp",
        "google-slides-mcp"
      ],
      "env": {
        "GOOGLE_CLIENT_SECRET": "/absolute/path/to/client_secret.json",
        "GOOGLE_TOKEN_PATH": "~/.config/google-slides-mcp/token.json"
      }
    }
  }
}
```

After PyPI publication: `"args": ["google-slides-mcp"]`.

### Environment variables

| Variable                                       | Default                                   | Purpose                                                   |
| ---------------------------------------------- | ----------------------------------------- | --------------------------------------------------------- |
| `GOOGLE_CLIENT_SECRET` (alias `CREDENTIALS_PATH`) | `client_secret.json`                   | Path to the Desktop OAuth client JSON.                    |
| `GOOGLE_TOKEN_PATH` (alias `TOKEN_PATH`)       | `~/.config/google-slides-mcp/token.json`  | Where this user's cached token is stored (must be writable). |
| `GOOGLE_SLIDES_SCOPES`                         | `presentations,drive`                     | Comma‑separated scope override (advanced). Short names or full `https://www.googleapis.com/auth/...` URLs; short names are expanded. |
| `GOOGLE_SLIDES_NO_BROWSER_AUTH`                | unset                                     | Set to `1` to disable the automatic browser flow on first launch (headless). |

> The `GOOGLE_*` names and the shorter aliases (`CREDENTIALS_PATH` / `TOKEN_PATH`)
> are interchangeable; if both are set, the `GOOGLE_*` name wins.

---

## Tools

**Find / import (Drive discovery)**

| Tool | Purpose | API calls |
| ---- | ------- | --------- |
| `search_presentations(name_contains?, full_text_contains?, file_type?, order_by?, max_results?, owned_by_me?, created_after?, modified_after?, page_token?)` | Find existing Google Slides **and** PowerPoint files in Drive. | 1 |
| `import_presentation(file_id, title?, parent_folder_id?)` | Convert a Drive `.pptx`/`.ppt` into a native Slides deck (original untouched). | 1 |

> The Slides API has no listing endpoint, so discovery goes through Drive.
> `search_presentations` covers native decks and PowerPoint files; results flag
> `directlyEditable` so you know when an `import_presentation` conversion is
> needed first. Recipes: *last deck I created* →
> `order_by="createdTime desc", owned_by_me=true, max_results=1`; *find the
> corporate template* → `name_contains="corporate template"`.

**Core read**

| Tool | Purpose | API calls |
| ---- | ------- | --------- |
| `create_presentation(title)` | Create an empty deck. | 1 |
| `get_presentation(presentation_id, include_masters?, raw?)` | Bounded structured overview of a deck. | 1 |
| `get_page(presentation_id, page_id, raw?)` | Bounded summary of one slide. | 1 |
| `list_slides(presentation_id)` | Minimal index/id/layout list. | 1 |

**Raw write**

| Tool | Purpose | API calls |
| ---- | ------- | --------- |
| `batch_update(presentation_id, requests)` | Apply raw Slides `batchUpdate` requests atomically. | 1 |

**Element / transform / bounding box**

| Tool | Purpose | API calls |
| ---- | ------- | --------- |
| `set_element_box(presentation_id, element_id, x_pt, y_pt, width_pt?, height_pt?)` | Place/size a box in points. | ≤2 |
| `update_transform(presentation_id, element_id, transform, mode?)` | Apply a raw AffineTransform. | 1 |
| `set_z_order(presentation_id, element_ids, operation)` | Reorder front/back stacking. | 1 |
| `group_elements` / `ungroup_elements` | Group/ungroup elements. | 1 |

**Text**

| Tool | Purpose | API calls |
| ---- | ------- | --------- |
| `replace_all_text(presentation_id, mappings, page_ids?, match_case?)` | Fill placeholder tokens. | 1 |
| `insert_text(presentation_id, element_id, text, index?)` | Insert text into a shape/cell. | 1 |
| `set_element_text(presentation_id, element_id, text)` | Replace **all** of one element's text. | 1 |

**Template / showcase reuse**

| Tool | Purpose | API calls |
| ---- | ------- | --------- |
| `copy_presentation(source_id, title, parent_folder_id?)` | Clone a deck preserving full styling. | 1 |
| `catalog_slides(presentation_id)` | Per‑slide descriptor (incl. `isSkipped`). | 1 |
| `duplicate_slide(presentation_id, page_id, insertion_index?)` | Full‑fidelity in‑deck slide copy. | 1 |
| `delete_objects(presentation_id, object_ids)` | Delete slides/elements (prune). | 1 |
| `reorder_slides(presentation_id, slide_ids, insertion_index)` | Move slides. | 1 |

**Iteration / palette**

| Tool | Purpose | API calls |
| ---- | ------- | --------- |
| `park_slides(presentation_id, slide_ids)` | Hide slides (skip) to keep them as a clone source. | 1 |
| `unpark_slides(presentation_id, slide_ids)` | Unhide parked slides. | 1 |
| `prune_parked_slides(presentation_id)` | Delete **all** parked slides — final cleanup. | ≤2 |

**Rendering / verification**

| Tool | Purpose | API calls |
| ---- | ------- | --------- |
| `render_page(presentation_id, page_id, size?)` | Render a slide to PNG. | 1 (expensive) |
| `diff_pages(presentation_id, page_a, page_b, size?)` | Render two slides + pixel diff. | 2 (expensive) |

> `render_page`/`diff_pages` use the thumbnail endpoint, an *expensive* quota
> operation (300/min per project, 60/min per user). Rendering is per‑page by
> design — there is no whole‑deck render.

---

## Example: build a deck from a showcase template

```
copy_presentation(source_id="<showcase_id>", title="Q3 Review")      → new deck id
catalog_slides(presentation_id="<new_id>")                           → pick example slides
duplicate_slide(presentation_id="<new_id>", page_id="<3col_example>", insertion_index=1)
replace_all_text(presentation_id="<new_id>", mappings={"{{title}}": "Results"})
delete_objects(presentation_id="<new_id>", object_ids=["<original_examples>..."])
render_page(presentation_id="<new_id>", page_id="<new_slide>")       → verify visually
diff_pages(presentation_id="<new_id>", page_a="<new_slide>", page_b="<showcase_example>")
```

---

## Example: port your latest deck into the corporate template

```
search_presentations(order_by="createdTime desc", owned_by_me=true, max_results=1)
                                                                      → the source deck (or .pptx)
import_presentation(file_id="<source_id>")                            → only if the source is a .pptx
search_presentations(name_contains="corporate template", file_type="google_slides")
                                                                      → the template deck
copy_presentation(source_id="<template_id>", title="Q3 Review (rebranded)")
catalog_slides(presentation_id="<source_id>")                         → read the source content
catalog_slides(presentation_id="<new_id>")                            → pick matching template layouts
duplicate_slide / set_element_text / replace_all_text                 → rebuild each slide on-brand
render_page(presentation_id="<new_id>", page_id="<slide>")            → verify
```

---

## Iterative / palette workflow

You do **not** have to assemble the deck in one shot. Every tool is an independent
call, so you can keep adding slides across many turns ("great, now add another
three‑column slide that says xyz").

The key idea is a **palette**: because the Slides API can only duplicate slides
*within* one deck, keep the showcase example slides present in the working deck as
reusable sources, and duplicate from them on demand. `park_slides` hides them
(skips them in present mode) so they don't clutter the in‑progress deck, and
`prune_parked_slides` removes them all at the very end.

```
# Once, at the start of a project:
copy_presentation(source_id="<showcase_id>", title="Q3 Review")   → working deck (palette embedded)
park_slides(presentation_id="<new_id>", slide_ids=[<all example ids>])   → hide the palette

# Repeat any number of times, across separate turns:
catalog_slides(presentation_id="<new_id>")                        → find the 3‑column example id (isSkipped=true)
duplicate_slide(presentation_id="<new_id>", page_id="<3col_example>")    → new live slide
set_element_text(presentation_id="<new_id>", element_id="<col1>", text="xyz")
# ...or replace_all_text(..., page_ids=["<new_slide>"]) if the example uses {{tokens}}
render_page(presentation_id="<new_id>", page_id="<new_slide>")    → verify

# Once, at the end (the skill's responsibility):
prune_parked_slides(presentation_id="<new_id>")                   → delete the palette
```

> **Why park instead of delete?** `park_slides` keeps every layout available as a
> clone source for later turns. If you `delete_objects` an example early, a layout
> you never instantiated is gone from the deck (there is no cross‑deck copy).

### Fallback: recreating a layout

If a needed layout was already pruned, you can rebuild a slide from the deck's
`layouts` (visible via `get_presentation(..., include_masters=True)`) using
`createSlide` with a `slideLayoutReference`, plus `createShape`/`createImage` via
`batch_update`. This reproduces a *layout's* placeholders faithfully, but
recreating an arbitrary *slide's* elements this way **is not guaranteed
pixel‑perfect** — theme colors, master styling and placeholder inheritance can be
lost. Prefer keeping the palette parked.

---

## Development

```bash
uv venv && uv pip install -e ".[dev]"
uv run pytest                 # unit + smoke tests (no network/credentials)
uv build                      # build sdist + wheel
```

A ready-to-use Claude *skill* describing the canonical workflow lives at
[`example_skill/SKILL.md`](example_skill/SKILL.md).

The codebase is intentionally small and modular so it can back that skill:

| Module | Responsibility |
| ------ | -------------- |
| `auth.py` | OAuth installed‑app flow, token cache, login CLI. |
| `client.py` | Builds Slides/Drive services; bounded 429/5xx backoff. |
| `units.py` | EMU/PT conversion, transform & bounding‑box math. |
| `views.py` | Trims verbose API responses into bounded summaries. |
| `ids.py` | Collision‑safe object‑ID generation. |
| `template.py` | Copy / catalog / duplicate / prune helpers. |
| `search.py` | Drive search over Slides/PPTX files; PPTX → Slides import. |
| `render.py` | Thumbnail rendering + Pillow image diff. |
| `server.py` | FastMCP server wiring the tools together. |

## Troubleshooting

- **"No cached Google credentials …"** — run `google-slides-mcp-auth` first.
- **Token stopped working after ~7 days** — your OAuth app is in *Testing* status;
  re-run the auth command or publish the consent screen.
- **`insufficient scopes` / can't copy a template** — ensure the `auth/drive` scope
  is granted (re-run auth after enabling it on the consent screen).
- **"Access blocked: Authorization Error" / `Error 400: invalid_scope`
  (`Some requested scopes were invalid`)** — the scopes sent weren't
  fully‑qualified URLs. Use the full `https://www.googleapis.com/auth/...` form (or
  the short names `presentations` / `drive`, which are now expanded for you) in
  `GOOGLE_SLIDES_SCOPES`.

## License

MIT — see [LICENSE](LICENSE).

TDQS

A3.7/5.0

Scored across 23 tools

Disambiguation5/5

Each tool targets a specific, clearly distinct operation – from slide duplication to text replacement to element grouping – with no overlapping purposes. The only potential ambiguity (batch_update vs. individual tools) is resolved by batch_update being explicitly the low-level catch-all.

Naming Consistency5/5

All 23 tools follow a consistent verb_noun snake_case pattern (e.g., duplicate_slide, replace_all_text, park_slides). No mixing of camelCase or other conventions, making the naming predictable and easy to remember.

Tool Count4/5

23 tools is on the higher end but appropriate for a comprehensive Google Slides wrapper that covers presentation, slide, element, text, and rendering operations. Slightly above the ideal 3-15 range but each tool serves a distinct purpose.

Completeness4/5

The tool set covers core CRUD and common workflows like template reuse (copy, duplicate, park, prune, replace). Some operations (e.g., shape/image creation) are only accessible via batch_update, which introduces a minor gap for direct high-level tools.

Maintenance

ActivityStale
ResponsivenessNo issues