slides-mcp 0.2.0__tar.gz

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.
Files changed (50) hide show
  1. slides_mcp-0.2.0/.github/workflows/release.yml +129 -0
  2. slides_mcp-0.2.0/.gitignore +34 -0
  3. slides_mcp-0.2.0/.python-version +1 -0
  4. slides_mcp-0.2.0/LICENSE +21 -0
  5. slides_mcp-0.2.0/PKG-INFO +13 -0
  6. slides_mcp-0.2.0/README.md +140 -0
  7. slides_mcp-0.2.0/pyproject.toml +50 -0
  8. slides_mcp-0.2.0/releases/v0.2.0.md +64 -0
  9. slides_mcp-0.2.0/skills/slides-mcp/SKILL.md +65 -0
  10. slides_mcp-0.2.0/skills/slides-mcp/rules/bidi-edit.md +75 -0
  11. slides_mcp-0.2.0/skills/slides-mcp/rules/escape-hatch.md +115 -0
  12. slides_mcp-0.2.0/skills/slides-mcp/rules/read-deck.md +85 -0
  13. slides_mcp-0.2.0/skills/slides-mcp/rules/theme-hygiene.md +87 -0
  14. slides_mcp-0.2.0/skills/slides-mcp/rules/workflow.md +71 -0
  15. slides_mcp-0.2.0/skills/slides-mcp/rules/write-deck.md +86 -0
  16. slides_mcp-0.2.0/src/slides_mcp/__init__.py +3 -0
  17. slides_mcp-0.2.0/src/slides_mcp/archetypes/3col_pill_cards.yaml +40 -0
  18. slides_mcp-0.2.0/src/slides_mcp/archetypes/4col_card_with_image.yaml +42 -0
  19. slides_mcp-0.2.0/src/slides_mcp/archetypes/4col_numbered_flow.yaml +47 -0
  20. slides_mcp-0.2.0/src/slides_mcp/archetypes/cover_with_hero.yaml +36 -0
  21. slides_mcp-0.2.0/src/slides_mcp/archetypes/generic_layout.yaml +29 -0
  22. slides_mcp-0.2.0/src/slides_mcp/archetypes/logo_strip.yaml +25 -0
  23. slides_mcp-0.2.0/src/slides_mcp/archetypes/table_slide.yaml +35 -0
  24. slides_mcp-0.2.0/src/slides_mcp/archetypes/text_heavy_body.yaml +21 -0
  25. slides_mcp-0.2.0/src/slides_mcp/archetypes/text_left_image_right.yaml +32 -0
  26. slides_mcp-0.2.0/src/slides_mcp/archetypes.py +77 -0
  27. slides_mcp-0.2.0/src/slides_mcp/audit.py +230 -0
  28. slides_mcp-0.2.0/src/slides_mcp/auth.py +74 -0
  29. slides_mcp-0.2.0/src/slides_mcp/bootstrap.py +54 -0
  30. slides_mcp-0.2.0/src/slides_mcp/classify.py +109 -0
  31. slides_mcp-0.2.0/src/slides_mcp/cli.py +55 -0
  32. slides_mcp-0.2.0/src/slides_mcp/diff.py +385 -0
  33. slides_mcp-0.2.0/src/slides_mcp/install_skill.py +126 -0
  34. slides_mcp-0.2.0/src/slides_mcp/normalize.py +249 -0
  35. slides_mcp-0.2.0/src/slides_mcp/projection.py +380 -0
  36. slides_mcp-0.2.0/src/slides_mcp/server.py +775 -0
  37. slides_mcp-0.2.0/src/slides_mcp/slides_api.py +172 -0
  38. slides_mcp-0.2.0/src/slides_mcp/theme.py +130 -0
  39. slides_mcp-0.2.0/src/slides_mcp/themes/example.yaml +71 -0
  40. slides_mcp-0.2.0/tests/__init__.py +0 -0
  41. slides_mcp-0.2.0/tests/fixtures/__init__.py +219 -0
  42. slides_mcp-0.2.0/tests/unit/__init__.py +0 -0
  43. slides_mcp-0.2.0/tests/unit/test_audit.py +60 -0
  44. slides_mcp-0.2.0/tests/unit/test_classify.py +43 -0
  45. slides_mcp-0.2.0/tests/unit/test_diff.py +265 -0
  46. slides_mcp-0.2.0/tests/unit/test_exec_batch_update.py +197 -0
  47. slides_mcp-0.2.0/tests/unit/test_normalize.py +54 -0
  48. slides_mcp-0.2.0/tests/unit/test_projection.py +134 -0
  49. slides_mcp-0.2.0/tests/unit/test_slides_api_parsing.py +27 -0
  50. slides_mcp-0.2.0/uv.lock +1589 -0
@@ -0,0 +1,129 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*.*.*'
7
+
8
+ permissions:
9
+ contents: write
10
+
11
+ jobs:
12
+ build:
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Install uv
18
+ uses: astral-sh/setup-uv@v3
19
+ with:
20
+ python-version: '3.12'
21
+
22
+ - name: Verify tag matches pyproject.toml version
23
+ run: |
24
+ TAG_VERSION="${GITHUB_REF_NAME#v}"
25
+ PKG_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
26
+ if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
27
+ echo "::error::Tag ($TAG_VERSION) != pyproject.toml ($PKG_VERSION). Bump pyproject.toml before tagging."
28
+ exit 1
29
+ fi
30
+
31
+ - name: Install deps
32
+ run: uv sync --frozen
33
+
34
+ - name: Run tests
35
+ run: uv run pytest tests/unit/ -q
36
+
37
+ - name: Ruff lint
38
+ run: uv run ruff check src/ tests/
39
+
40
+ - name: Build sdist + wheel
41
+ run: uv build
42
+
43
+ - name: Upload dist artifact
44
+ uses: actions/upload-artifact@v4
45
+ with:
46
+ name: dist
47
+ path: dist/
48
+
49
+ release:
50
+ runs-on: ubuntu-latest
51
+ needs: build
52
+ permissions:
53
+ contents: write
54
+ steps:
55
+ - uses: actions/checkout@v4
56
+ with:
57
+ fetch-depth: 0
58
+
59
+ - name: Download dist artifact
60
+ uses: actions/download-artifact@v4
61
+ with:
62
+ name: dist
63
+ path: dist/
64
+
65
+ - name: Generate categorized release notes from conventional commits
66
+ run: |
67
+ PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "")
68
+ TAG="${{ github.ref_name }}"
69
+ {
70
+ if [ -z "$PREV_TAG" ]; then
71
+ echo "🎉 Initial release"
72
+ echo ""
73
+ echo "**Full Changelog**: https://github.com/${{ github.repository }}/commits/${TAG}"
74
+ else
75
+ echo "## What's Changed"
76
+ echo ""
77
+ for prefix_label in "feat:✨ Features" "fix:🐛 Bug Fixes" "perf:⚡ Performance" "docs:📖 Documentation"; do
78
+ prefix="${prefix_label%%:*}"
79
+ label="${prefix_label#*:}"
80
+ items=$(git log "$PREV_TAG..$TAG" --format="- %s (%h)" --grep="^${prefix}" 2>/dev/null || true)
81
+ if [ -n "$items" ]; then
82
+ echo "### $label"
83
+ echo "$items"
84
+ echo ""
85
+ fi
86
+ done
87
+ uncategorized=$(git log "$PREV_TAG..$TAG" --format="- %s (%h)" \
88
+ --invert-grep --grep="^feat" --grep="^fix" --grep="^perf" --grep="^docs" 2>/dev/null || true)
89
+ if [ -n "$uncategorized" ]; then
90
+ echo "### 🔧 Other Changes"
91
+ echo "$uncategorized"
92
+ echo ""
93
+ fi
94
+ echo "### 📊 Diff Summary"
95
+ echo '```'
96
+ git diff --stat "$PREV_TAG..$TAG" | tail -1
97
+ echo '```'
98
+ echo ""
99
+ echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREV_TAG}...${TAG}"
100
+ fi
101
+ } > /tmp/release-body.md
102
+ cat /tmp/release-body.md
103
+
104
+ - name: Create GitHub Release
105
+ env:
106
+ GH_TOKEN: ${{ github.token }}
107
+ run: |
108
+ gh release create "${{ github.ref_name }}" \
109
+ --title "${{ github.ref_name }}" \
110
+ --notes-file /tmp/release-body.md \
111
+ dist/*
112
+
113
+ # PyPI publish via OIDC Trusted Publisher (Pending Publisher registered on pypi.org).
114
+ publish:
115
+ runs-on: ubuntu-latest
116
+ needs: [build, release]
117
+ permissions:
118
+ id-token: write
119
+ contents: read
120
+ steps:
121
+ - name: Download dist artifact
122
+ uses: actions/download-artifact@v4
123
+ with:
124
+ name: dist
125
+ path: dist/
126
+ - name: Publish to PyPI (OIDC Trusted Publisher)
127
+ uses: pypa/gh-action-pypi-publish@release/v1
128
+ with:
129
+ skip-existing: true
@@ -0,0 +1,34 @@
1
+ # Privacy-sensitive (NEVER ship)
2
+ .claude/
3
+ gsd-lite/
4
+ samples/
5
+
6
+ # Secrets
7
+ token.json
8
+ client_secret*.json
9
+ credentials*.json
10
+ *.env
11
+ .env
12
+
13
+ # Python
14
+ __pycache__/
15
+ *.py[cod]
16
+ *$py.class
17
+ .venv/
18
+ .venv-*/
19
+ *.egg-info/
20
+ dist/
21
+ build/
22
+
23
+ # Tooling
24
+ .pytest_cache/
25
+ .mypy_cache/
26
+ .ruff_cache/
27
+ .coverage
28
+ htmlcov/
29
+
30
+ # Editor
31
+ .vscode/
32
+ .idea/
33
+ *.swp
34
+ .DS_Store
@@ -0,0 +1 @@
1
+ 3.12
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The slides-mcp contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE OUTSIDE OF THE USE OR OTHER
21
+ DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,13 @@
1
+ Metadata-Version: 2.4
2
+ Name: slides-mcp
3
+ Version: 0.2.0
4
+ Summary: Google Slides MCP server with compact YAML DSL and brand-theme enforcement
5
+ License-File: LICENSE
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: google-api-python-client>=2.140
8
+ Requires-Dist: google-auth-httplib2>=0.2
9
+ Requires-Dist: google-auth-oauthlib>=1.2
10
+ Requires-Dist: google-auth>=2.34
11
+ Requires-Dist: mcp[cli]>=1.2.0
12
+ Requires-Dist: pydantic>=2.8
13
+ Requires-Dist: pyyaml>=6.0
@@ -0,0 +1,140 @@
1
+ # slides-mcp
2
+
3
+ An MCP server that lets Claude (and other agents) edit Google Slides through a compact YAML DSL. Built for multi-hundred-turn editing sessions without token bloat, with first-class brand-theme enforcement, presenter-note support, and a true bidirectional agent loop — the agent can **see** the rendered slide (native `ImageContent`) AND **move** shapes (coordinate-level writes) in the same session.
4
+
5
+ ## Why this exists
6
+
7
+ Existing Google Slides MCP servers are thin JSON passthroughs over the Slides REST API. A single text edit costs ~2,000 tokens (1K to read + 1K to write), and a rendered slide costs another round-trip. That math kills 100-turn editing sessions.
8
+
9
+ This server projects each slide into ~100–150 tokens of structured YAML, keyed to a small vocabulary of **archetypes** (layout templates) and a **theme** (palette + fonts). Writes produce the updated DSL and (for geometry changes) a rendered thumbnail in a single call. Thumbnails return as native MCP `ImageContent` — no URL-fetch round-trip.
10
+
11
+ ## Core ideas
12
+
13
+ - **Archetypes over layouts.** PowerPoint/Slides layout names are unreliable (one real deck used `CUSTOM_7_1_1_1_1_1_1_1_1_1_1_1_1_1_1_1` as the name for 11 structurally-different slides). The classifier reasons about element topology — column counts, separator lines, picture-to-text ratios — not layout strings.
14
+ - **Theme roles, not hex codes.** A slide references `palette.brand_accent`, not `#3366CC`. Change the theme once; the deck follows. Drift (hex values not in the theme) is surfaced by `audit_deck_colors` and can be accepted via `promote_to_theme`.
15
+ - **Presenter notes are first-class.** Read path preserves the full notes body on every slide (no truncation). Write-side emission is landing incrementally — current state is *detected and flagged* on diff; explicit emission is the next small task.
16
+ - **Bidi geometry.** Opt-in `include_elements=True` on `get_slide` returns `elements: [{id, at:[x,y,w,h]}]`. Editing those values produces `updatePageElementTransform` requests in `RELATIVE` mode, preserving scale and rotation on the underlying shape.
17
+ - **Privacy boundary.** Bundled theme + archetypes are generic. Your real brand theme lives in `~/.config/slides-mcp/themes/` and never enters the repo. Research artifacts, session state, and real client decks stay in local-only directories that are in `.gitignore`.
18
+
19
+ ## What the MVP does
20
+
21
+ - Read a deck outline (1 call, whole deck, ~40 tok/slide) — `get_deck_outline`
22
+ - Read one slide as compact YAML (~100–150 tok typical) — `get_slide`
23
+ - Search a deck for text substrings — `search_deck`
24
+ - Patch a slide (text edits + translation of existing elements) in one call — `patch_slide`
25
+ - Render a slide as native `ImageContent` for visual verification — `render_thumbnail`
26
+ - Audit every color and font in the deck against the active theme — `audit_deck_colors`
27
+ - Promote a drift value to the theme as a named role — `promote_to_theme`
28
+ - Copy a deck via Drive — `clone_deck`
29
+ - List themes / archetypes / deck archetype inventory — `list_themes`, `list_archetypes`, `list_deck_layouts`
30
+ - Diagnostic: `auth_status`
31
+
32
+ ## What the MVP does NOT do (yet)
33
+
34
+ By design, the current scope is tightly scoped to "edit an existing brownfield deck." The following are deliberately deferred — if you need them, watch the roadmap issues:
35
+
36
+ - **Creating slides from scratch** (no `distill_doc_to_deck`, no `create_from_markdown`)
37
+ - **Inserting new shapes or icons** via DSL (moving existing elements works; creating new ones doesn't)
38
+ - **Resizing / rotating** elements (warn-only in diff; translation-only writes in MVP)
39
+ - **Archetype swap** ("relayout this slide to 3 columns" is a Phase 2 primitive)
40
+ - **Template replacement map** (`clone_deck` copies only; per-slide text replacements are multiple calls today)
41
+ - **Writing presenter notes** (reads work; write emission is a known ~15-line follow-up)
42
+ - **Slide variant ideation** ("give me 3 alternative versions of this slide")
43
+ - **`.pptx` target** (Google Slides only in this version)
44
+
45
+ ## Status
46
+
47
+ MVP shipped. 13 MCP tools, 45 unit tests, live-verified bidi loop on real decks (read → edit → render → move → re-render, all in token budget). Still pre-v1: no public releases cut, no committed production users, tests cover the core layers but integration tests against live decks are empty. Use at your own risk and expect rough edges — especially on the text-edit side where `replaceAllText` is currently slide-scoped.
48
+
49
+ ## Installation
50
+
51
+ Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/).
52
+
53
+ ```bash
54
+ git clone https://github.com/YOUR-USER/slides-mcp.git
55
+ cd slides-mcp
56
+ uv sync
57
+ ```
58
+
59
+ ## Auth setup (one-time)
60
+
61
+ The MCP uses user-OAuth to access Google Slides. You need a Google Cloud project with the Slides + Drive APIs enabled and a desktop OAuth client.
62
+
63
+ 1. Create a Google Cloud project; enable `slides.googleapis.com` and `drive.googleapis.com`
64
+ 2. Create an OAuth 2.0 client (type: **Desktop app**), download `client_secret.json`
65
+ 3. On any machine with a browser, run:
66
+
67
+ ```bash
68
+ uv run slides-mcp-auth --client-secret /path/to/client_secret.json --out ./token.json
69
+ ```
70
+
71
+ This opens a browser, you consent, and `token.json` is written. The refresh token inside is long-lived.
72
+
73
+ 4. If your MCP server runs on a headless host (devcontainer, VPS), copy `token.json` over, e.g. `scp token.json user@devbox:~/.config/slides-mcp/token.json`. Point the server at it via `$SLIDES_MCP_TOKEN_PATH`.
74
+
75
+ ## MCP client configuration
76
+
77
+ Add to your MCP client config:
78
+
79
+ ```json
80
+ {
81
+ "mcpServers": {
82
+ "slides": {
83
+ "command": "uv",
84
+ "args": ["run", "--directory", "/path/to/slides-mcp", "slides-mcp"],
85
+ "env": {
86
+ "SLIDES_MCP_TOKEN_PATH": "/path/to/token.json",
87
+ "SLIDES_MCP_THEMES_DIR": "/path/to/your/private/themes"
88
+ }
89
+ }
90
+ }
91
+ }
92
+ ```
93
+
94
+ ## Theme setup
95
+
96
+ The bundled theme at `src/slides_mcp/themes/example.yaml` is a generic placeholder. For your real brand, drop a theme file into one of these locations (first match wins):
97
+
98
+ 1. `$SLIDES_MCP_THEMES_DIR`
99
+ 2. `$XDG_CONFIG_HOME/slides-mcp/themes` (default: `~/.config/slides-mcp/themes`)
100
+ 3. `./slides-mcp-themes` (project-local; add it to your own `.gitignore`)
101
+ 4. Bundled `example.yaml` (fallback only)
102
+
103
+ Theme files are never committed to this repo. See `src/slides_mcp/themes/example.yaml` for the schema.
104
+
105
+ ## MCP tool reference
106
+
107
+ | Tool | Purpose |
108
+ |------|---------|
109
+ | `list_themes()` | All theme files discoverable in the search paths |
110
+ | `list_archetypes()` | Archetype templates (bundled + any user overrides) |
111
+ | `list_deck_layouts(deck_url)` | Archetype inventory of a deck, with counts and slide IDs |
112
+ | `get_deck_outline(deck_url, theme?, sub_theme?)` | Compact index of all slides — one call for whole-deck reasoning |
113
+ | `get_slide(deck_url, slide_id, theme?, sub_theme?, mode?, include_elements?)` | Single slide as YAML; `mode='faithful'` preserves raw geometry; `include_elements=True` opts in to the geometry channel |
114
+ | `search_deck(deck_url, query)` | Find slides whose text contains `query` |
115
+ | `patch_slide(deck_url, slide_id, new_dsl_yaml, theme?, sub_theme?, verify?)` | Apply a DSL patch (text + element translation). Returns new YAML + auto-thumbnail when geometry changed |
116
+ | `render_thumbnail(deck_url, slide_id, size?)` | Rendered PNG as native MCP `ImageContent` |
117
+ | `render_thumbnail_url(deck_url, slide_id, size?)` | Same, but returns a URL (for non-agent callers) |
118
+ | `audit_deck_colors(deck_url, theme?, sub_theme?)` | Report colors and fonts not in the active theme, with nearest-role suggestions |
119
+ | `promote_to_theme(theme, sub_theme, role_name, kind, value)` | Add a drift value (color hex or font spec) to the user theme file under a named role |
120
+ | `clone_deck(src_url, new_title)` | Copy a deck via Drive; returns new deck ID + URL |
121
+ | `auth_status()` | Diagnostic — token path, scopes, expiry (no secrets) |
122
+
123
+ ## Architecture
124
+
125
+ A four-layer design:
126
+
127
+ 1. **Google Slides + auth** — REST wrapper with FieldMask-projected GETs + `batchUpdate`; token.json load + silent refresh
128
+ 2. **DSL projection + diff** — `pageElement` → `FlatShape` → archetype classifier (topology-based) → compact YAML; YAML diff → `batchUpdate` requests
129
+ 3. **Theme + archetype registry** — YAML-driven, user-overridable
130
+ 4. **FastMCP tool surface** — 13 tools over stdio
131
+
132
+ See `src/slides_mcp/server.py` for the tool definitions and `src/slides_mcp/projection.py` for the compression core.
133
+
134
+ ## Contributing
135
+
136
+ Early project. Issues welcome. PRs should include tests — the unit suite runs `pytest tests/unit/` in under a second and uses mocked Slides API JSON fixtures (no network).
137
+
138
+ ## License
139
+
140
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1,50 @@
1
+ [project]
2
+ name = "slides-mcp"
3
+ version = "0.2.0"
4
+ description = "Google Slides MCP server with compact YAML DSL and brand-theme enforcement"
5
+ requires-python = ">=3.11"
6
+ dependencies = [
7
+ "mcp[cli]>=1.2.0",
8
+ "google-api-python-client>=2.140",
9
+ "google-auth>=2.34",
10
+ "google-auth-oauthlib>=1.2",
11
+ "google-auth-httplib2>=0.2",
12
+ "pyyaml>=6.0",
13
+ "pydantic>=2.8",
14
+ ]
15
+
16
+ [project.scripts]
17
+ slides-mcp = "slides_mcp.cli:main"
18
+ slides-mcp-auth = "slides_mcp.bootstrap:main"
19
+
20
+ [build-system]
21
+ requires = ["hatchling"]
22
+ build-backend = "hatchling.build"
23
+
24
+ [tool.hatch.build.targets.wheel]
25
+ packages = ["src/slides_mcp"]
26
+
27
+ [tool.hatch.build.targets.wheel.force-include]
28
+ "skills" = "slides_mcp/_skills_data"
29
+
30
+ [dependency-groups]
31
+ dev = [
32
+ "pytest>=8.0",
33
+ "pytest-asyncio>=0.24",
34
+ "ruff>=0.6",
35
+ "mypy>=1.11",
36
+ "python-pptx>=1.0",
37
+ ]
38
+
39
+ [tool.ruff]
40
+ line-length = 100
41
+ target-version = "py311"
42
+
43
+ [tool.ruff.lint]
44
+ select = ["E", "F", "I", "W", "UP", "B"]
45
+ ignore = ["E501"]
46
+
47
+ [tool.pytest.ini_options]
48
+ pythonpath = ["src"]
49
+ testpaths = ["tests"]
50
+ asyncio_mode = "auto"
@@ -0,0 +1,64 @@
1
+ # v0.2.0 — Skill package: `uvx slides-mcp install`
2
+
3
+ This release adds a **Claude Code skill** describing how to use slides-mcp's 17 tools effectively, and the **`uvx slides-mcp install`** command to drop it into any project (or your user-level skills directory).
4
+
5
+ ## The story
6
+
7
+ slides-mcp v0.1.1 shipped 17 tools across a 4-layer architecture (auth → DSL → theme → MCP surface). What it didn't ship: *how to USE those tools effectively.* Tool descriptions alone don't cover:
8
+
9
+ - Which tool to reach for first (bespoke beats escape-hatch; `patch_slide` beats hand-rolled `batchUpdate`)
10
+ - When to flip the opt-in `include_elements` geometry channel
11
+ - How to navigate clean / faithful / `_object_ids` read modes
12
+ - The audit → promote-OR-fix theme-hygiene loop
13
+ - `exec_batch_update` dry-run discipline
14
+
15
+ v0.2.0 packages all of that as a Claude Code skill: one `SKILL.md` + six `rules/*.md` files, installed via:
16
+
17
+ ```bash
18
+ uvx slides-mcp install # $CWD/.claude/skills/slides-mcp/
19
+ uvx slides-mcp install --global # ~/.claude/skills/slides-mcp/
20
+ uvx slides-mcp install --path /custom/dir
21
+ ```
22
+
23
+ ## New CLI surface
24
+
25
+ `slides-mcp` is now a subcommand dispatcher:
26
+
27
+ | Invocation | Effect |
28
+ |------------|--------|
29
+ | `slides-mcp` (no args) | Start the MCP stdio server (unchanged behavior for MCP clients) |
30
+ | `slides-mcp install` | Install the skill docs (NEW) |
31
+ | `slides-mcp auth ...` | Run OAuth consent (moved under the dispatcher; back-compat `slides-mcp-auth` entry preserved) |
32
+
33
+ The previous `slides-mcp = "slides_mcp.server:main"` script is now `slides-mcp = "slides_mcp.cli:main"`. MCP clients that invoke `slides-mcp` with no args get the identical stdio server as before.
34
+
35
+ ## What's in the skill
36
+
37
+ `skills/slides-mcp/SKILL.md` — tool priority table ("bespoke first, escape hatch last"), tools overview (17 entries, 1-liner each), and links out to:
38
+
39
+ - `rules/workflow.md` — decision tree: "I want to X, use Y"
40
+ - `rules/read-deck.md` — outline, slide, search, faithful vs clean, `include_elements` gating
41
+ - `rules/write-deck.md` — `patch_slide` semantics, `_object_ids`, translation writes, `clone_deck` replacements
42
+ - `rules/theme-hygiene.md` — audit → decide per-drift → promote OR fix
43
+ - `rules/bidi-edit.md` — the see-and-move loop; why RELATIVE transform matters; EMU math
44
+ - `rules/escape-hatch.md` — `exec_batch_update` safety; destructive denylist; audit log
45
+
46
+ ## Packaging notes
47
+
48
+ The `skills/` directory lives at the repo root (peer of `src/`). Hatchling `force-include` mounts it at `slides_mcp/_skills_data/` inside the built wheel so the install command can find it whether you ran `uv sync` (editable) or `uv pip install slides-mcp` (wheel).
49
+
50
+ Source-of-truth lives at `skills/slides-mcp/` in the repo. `.claude/skills/` is the install *target*, not stored in this repo.
51
+
52
+ ## What's NOT in this release (deliberate)
53
+
54
+ - **No PyPI publish wiring.** The `publish:` job in `.github/workflows/release.yml` remains commented out pending OIDC Trusted Publisher setup on pypi.org. Wheel + sdist are still attached to the GitHub Release; `uv tool install` from the GitHub artifact URL works today.
55
+ - **No MCP server auto-registration.** `slides-mcp install` copies skill docs only; it never touches your MCP client config. If you want the slides-mcp server wired into Claude Code, run `claude mcp add slides-mcp uvx slides-mcp` yourself.
56
+ - **No re-install / uninstall subcommands.** Install is idempotent (overwrites). For uninstall: `rm -rf .claude/skills/slides-mcp/`.
57
+
58
+ ## Design notes
59
+
60
+ The shape follows `looker-mcp-shim` (Node, the sibling project): one namespace, `SKILL.md` + `rules/*.md` split, three target modes (`--global` / `--path` / default-cwd), hardcoded `.claude/skills/` destination. The install layer is Claude-Code-specific today because only Claude Code auto-loads skills from a well-known directory; the *content* of the skill is framework-agnostic (no Claude-specific jargon) so it can be broadened to other MCP clients later without rewriting the rules.
61
+
62
+ ## Full changelog
63
+
64
+ See auto-generated notes (from conventional commit prefixes) below the narrative.
@@ -0,0 +1,65 @@
1
+ ---
2
+ name: slides-mcp
3
+ description: REQUIRED before using any slides-mcp tools. Covers reading decks as compact YAML, text edits, translation writes, theme hygiene, thumbnail rendering, and the exec_batch_update escape hatch. Read rules/*.md for the workflow slices.
4
+ metadata:
5
+ tags: slides, google-slides, mcp, presentation, deck, theme, bidi-edit
6
+ ---
7
+
8
+ ## When to use
9
+
10
+ ALWAYS read this skill BEFORE calling any slides-mcp tool. Use when:
11
+
12
+ - Reading a deck outline or individual slides as compact YAML DSL
13
+ - Editing slide text, moving shapes, or cloning decks for new clients
14
+ - Auditing color / font drift against a brand theme and promoting drift to the theme
15
+ - Rendering slide thumbnails for visual verification (bidi loop)
16
+ - Running arbitrary Slides API requests via the `exec_batch_update` escape hatch
17
+
18
+ **CRITICAL:** The DSL is ~100–150 tok/slide in `clean` mode. Use `faithful` mode only when you need raw geometry. Set `include_elements=True` on `get_slide` only when you intend to MOVE shapes next — the geometry channel costs ~50 extra tokens per slide.
19
+
20
+ ## Tool Priority — Bespoke First, Escape Hatch Last
21
+
22
+ The bespoke tools (`patch_slide`, `create_shape`, `duplicate_slot`, `clone_deck`) are always preferred over `exec_batch_update`. They are type-safe, theme-aware, and emit minimal requests. `exec_batch_update` is the escape hatch for rare operations outside the happy path — NOT the default.
23
+
24
+ | Need | Use | NOT (escape hatch) | Why bespoke wins |
25
+ |------|-----|-------------------|------------------|
26
+ | Text edit a slot | `patch_slide` with DSL change | hand-rolled `replaceAllText` | Uses `_object_ids` to avoid duplicate-hit |
27
+ | Move an existing shape | `patch_slide` with `elements[].at` diff | `updatePageElementTransform` | Emits RELATIVE mode — preserves scale/rotation |
28
+ | Add a new shape | `create_shape` | `createShape + updateShapeProperties + insertText` | One tool, theme-aware fills |
29
+ | Duplicate an existing shape | `duplicate_slot` | `duplicateObject + updatePageElementTransform` | Handles objectIds map + optional translation |
30
+ | Clone a deck (template) | `clone_deck` (with optional `replacements` map) | Drive copy + manual replaceAllText per pair | Batched cmd+F replacement in one call |
31
+ | Notes edit | `patch_slide` (notes field in DSL) | `deleteText + insertText` on notesPage | Threaded through `notes_object_id` |
32
+ | Any other Slides API Request | `exec_batch_update` (with `dry_run=True` first) | N/A | No bespoke wrapper — dry-run before firing |
33
+
34
+ **Rule:** If a bespoke tool exists for your task, use it. Only reach for `exec_batch_update` when none of the bespoke tools cover your operation (e.g. `updateTextStyle`, `insertTableRows`, `updateTableCellProperties`).
35
+
36
+ ## Tools Overview
37
+
38
+ | Tool | Purpose |
39
+ |------|---------|
40
+ | `auth_status` | Diagnostic: report token.json state without exposing secrets |
41
+ | `list_themes` / `list_archetypes` | Discover bundled + user themes / archetype YAMLs |
42
+ | `list_deck_layouts` / `list_slides_by` | Archetype inventory + structural grep over a deck |
43
+ | `get_deck_outline` | Whole-deck index, ~20 tok/slide |
44
+ | `get_slide` | One slide as compact YAML; `mode=clean/faithful`, opt-in `include_elements` |
45
+ | `search_deck` | Text substring search across slides |
46
+ | `patch_slide` | Apply DSL patch — text edits + translation writes in one call |
47
+ | `create_shape` | Insert a new shape at `[l,t,w,h]` with optional text / fill |
48
+ | `duplicate_slot` | Duplicate an existing pageElement, optionally translate by delta |
49
+ | `clone_deck` | Drive copy a deck, optionally with cmd+F-style text replacements |
50
+ | `audit_deck_colors` | Walk the whole deck, report colors / fonts not in the active theme |
51
+ | `promote_to_theme` | Add a drift value to the theme as a named role (writes to user config) |
52
+ | `render_thumbnail` | Render a slide as PNG and return as native MCP `ImageContent` |
53
+ | `render_thumbnail_url` | Return the short-lived contentUrl only (for non-agent callers) |
54
+ | `exec_batch_update` | Raw batchUpdate passthrough — ALWAYS `dry_run=True` on first call |
55
+
56
+ ## Workflow Guides
57
+
58
+ Read these before starting. Each rule doc is scoped to one axis of the workflow:
59
+
60
+ - [rules/workflow.md](rules/workflow.md) — **Start here.** Decision tree: what to use when
61
+ - [rules/read-deck.md](rules/read-deck.md) — Outline, slide, search, list_slides_by; clean vs faithful; `include_elements` gating
62
+ - [rules/write-deck.md](rules/write-deck.md) — `patch_slide` semantics; text edits, translation, `_object_ids`, `clone_deck` replacements
63
+ - [rules/theme-hygiene.md](rules/theme-hygiene.md) — `audit_deck_colors` + `promote_to_theme`; the living-theme workflow
64
+ - [rules/bidi-edit.md](rules/bidi-edit.md) — The see-and-move loop: `render_thumbnail` + `include_elements` + RELATIVE transforms
65
+ - [rules/escape-hatch.md](rules/escape-hatch.md) — `exec_batch_update` safely: `dry_run`, destructive denylist, audit log
@@ -0,0 +1,75 @@
1
+ # Bidi editing — see and move
2
+
3
+ The killer feature: you can SEE the rendered slide natively AND MOVE shapes by editing coordinates, in the same MCP session.
4
+
5
+ ## The loop
6
+
7
+ ```
8
+ 1. get_slide(deck_url, slide_id, include_elements=True)
9
+ → DSL with elements: [{id, at:[x,y,w,h]}]
10
+ → also auto-includes _object_ids for text slots
11
+
12
+ 2. render_thumbnail(deck_url, slide_id, size="MEDIUM")
13
+ → MCP ImageContent (PNG bytes) — agent sees it natively
14
+
15
+ 3. Decide: "move the JOON block 0.5in up and left"
16
+
17
+ 4. Modify elements[N].at in the DSL
18
+
19
+ 5. patch_slide(deck_url, slide_id, new_dsl_yaml, verify="auto")
20
+ → emits updatePageElementTransform with translateX/Y in EMU, applyMode: RELATIVE
21
+ → auto-renders a fresh thumbnail (verify=auto fires on any geometry diff)
22
+
23
+ 6. Consume the new thumbnail — was the move correct?
24
+
25
+ 7. If no: goto 4. If yes: done.
26
+ ```
27
+
28
+ ## Why RELATIVE mode matters
29
+
30
+ The Slides API `updatePageElementTransform` has two modes:
31
+
32
+ - **ABSOLUTE** — replaces the entire transform. You MUST specify scale / rotation or they reset to 1 / 0.
33
+ - **RELATIVE** — composed with the existing transform. If the shape was scaled 0.72× and rotated 15°, those stick; only the translation adds.
34
+
35
+ `patch_slide` always emits RELATIVE with `scaleX: 1, scaleY: 1, translateX: Δ, translateY: Δ`. This is correct for translation-only writes. If you need to resize or rotate, you have to use `exec_batch_update` with an ABSOLUTE transform (and handle scale / rotation yourself) — resize/rotate writes are deferred to Phase 2.
36
+
37
+ ## EMU units
38
+
39
+ The Slides API uses EMU (English Metric Units). `patch_slide` handles the conversion for you:
40
+
41
+ - 914,400 EMU = 1 inch
42
+ - 12,700 EMU = 1 pt
43
+
44
+ You edit `at` in inches in the DSL; the writer converts to EMU in the request.
45
+
46
+ ## What's NOT a bidi edit (yet)
47
+
48
+ - Resize (width / height change in `elements[].at`) → warning, no write
49
+ - Rotate / shear → no DSL channel; use `exec_batch_update` with full transform
50
+ - Add a new shape → `create_shape`, not DSL add-element
51
+ - Remove a shape → `exec_batch_update` with `deleteObject` + `confirm_destructive=True`
52
+
53
+ ## Render tiers
54
+
55
+ ```
56
+ render_thumbnail(deck_url, slide_id, size="SMALL") # fastest, low-res
57
+ render_thumbnail(deck_url, slide_id, size="MEDIUM") # default, balanced
58
+ render_thumbnail(deck_url, slide_id, size="LARGE") # high-res, slowest
59
+ ```
60
+
61
+ MEDIUM is fine for most visual verification. Go LARGE only when you need to read small body text in the image.
62
+
63
+ ## URL-only sibling
64
+
65
+ ```
66
+ render_thumbnail_url(deck_url, slide_id, size="MEDIUM")
67
+ ```
68
+
69
+ Returns just the short-lived contentUrl (no bytes). Use this for dashboard embeds or pipelines where you don't need the image in-context. Agents should use `render_thumbnail` (native bytes), not this URL form.
70
+
71
+ ## Token budget note
72
+
73
+ Thumbnails are expensive — ~640 tok on SMALL up to ~2,765 tok on LARGE. The auto-rendering gate in `patch_slide` only fires on geometry changes for exactly this reason. For text-only edits, no thumbnail is rendered by default.
74
+
75
+ If you're in a many-turn session and the token budget is tight, pass `verify="never"` to suppress the auto-thumbnail, then render on-demand later with `render_thumbnail` when you actually need to see the result.