Install
$ agentstack add mcp-thomas-villani-pptlive ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
pptlive
[](https://pypi.org/project/pptlive/) [](https://pypi.org/project/pptlive/) [](https://github.com/thomas-villani/pptlive/blob/main/LICENSE) [](https://thomas-villani.github.io/pptlive/)
Drive a running Microsoft PowerPoint instance from Python — xlwings, but for PowerPoint. Built for both human scripting and LLM agents. Windows-only.
📖 Documentation — full Python API, CLI, MCP server, and cookbook.
The live-app sibling of python-pptx (which works the .pptx on disk) and the PowerPoint counterpart of wordlive. Use it when the user already has the deck open and you want to edit it live — no close-the-file, let-the-agent-write, re-open dance.
| Library | Target | Mechanism | | ----------- | ------------------------- | ------------------------ | | python-pptx | a .pptx file on disk | OOXML I/O | | pptlive | a running POWERPNT.exe| COM automation (pywin32) |
Install
pip install pptlive
# with the MCP server for Claude Desktop & other MCP agents (see "MCP server")
pip install "pptlive[mcp]"
# add to a project
uv add pptlive
(Requires Python 3.11+ and pywin32 on Windows.)
Python
import pptlive as pl
with pl.attach() as ppt:
deck = ppt.presentations.active
# Reads (structured, side-effect-free)
slides = deck.slides.list() # [{index, id, layout, title, shape_count, has_notes}]
outline = deck.outline() # [{slide, title, bullets:[...]}]
grid = deck.slides[2].read() # every shape: anchor_id, name, id, type, geometry, text
title = deck.slides[2].placeholder("title").text
notes = deck.slides[1].notes.text
# Polite writes — preserve the user's viewed slide + Selection.
with deck.edit("Revise the agenda slide"):
deck.anchor_by_id("ph:2:title").set_text("Agenda")
deck.anchor_by_id("ph:2:body").set_text("Intro\nDemo\nQ&A")
# Slide lifecycle — also one Ctrl-Z per edit() block.
names = deck.layouts() # [{index, name}] — what set_layout/add accept
with deck.edit("Add a results slide"):
new = deck.slides.add(layout="two_content", index=4)
deck.slides[7].duplicate() # copy lands at slide 8
deck.slides[9].move_to(2)
deck.slides[4].set_layout("title_and_content")
# Shapes & geometry — points throughout (pl.units for inches/cm).
with deck.edit("Lay out the results slide"):
shapes = deck.slides[4].shapes
shapes.add_textbox("Revenue up 12%", left=pl.units.inches(1), top=72)
star = shapes.add_shape("star", left=400, top=120, width=120, height=120,
fill="#C00000", line="none") # fill / border on creation
logo = shapes.add_picture("logo.png", left=600, top=40, # embedded, never linked
alt_text="Acme logo") # a drift-proof re-id handle
deck.slides[4].shapes["Picture 3"].move(top=140) # absolute, points
star.set_fill(fill="#1F1F1F", line="#FFFFFF", line_width=2) # fill ≠ font color
star.reorder("front") # z-order: front/back/forward/backward
star.delete()
# Pictures — alt text doubles as a re-identification handle; export one shape for vision.
logo.set_alt_text("Acme logo (top-right)") # survives z-order drift
chart_png = deck.slides[4].shapes["Chart 2"].export_image() # just that shape, native size
# Charts — a chart is a shape; its data lives in an embedded Excel workbook.
with deck.edit("Add a revenue chart"):
chart = deck.slides[4].shapes.add_chart(
"column", ["Q1", "Q2", "Q3"], {"Revenue": [10, 20, 30], "Profit": [3, 6, 9]}
).chart
chart.set_type("line") # change the kind
chart.recolor_text("#FFFFFF") # all chart text white (dark-theme fix)
data = chart.read() # {chart_type, categories, series:[...]}
# SmartArt — a diagram is a shape too; its content is a node tree.
with deck.edit("Add a process diagram"):
sa = deck.slides[3].shapes.add_smartart(
"process", ["Discover", "Design", "Build", "Ship"] # flat list…
).smartart
sa.set_nodes([{"text": "CEO", "children": ["VP Eng", "VP Sales"]}]) # …or a tree
sa.recolor_text("#FFFFFF") # every node label white
tree = sa.read() # {layout, nodes:[{text, level, children}]}
# Text structure — paragraphs, formatting, bullets. (Per-anchor formatting;
# for deck-wide styling use deck.theme / deck.master below.)
with deck.edit("Polish the body copy"):
body = deck.anchor_by_id("ph:4:body")
body.set_text("Revenue up 12%\nChurn down 3%\nNPS +9")
body.apply_list("bulleted") # bullets on every paragraph
body.paragraph(2).format_paragraph(indent_level=2, alignment="left")
body.paragraph(1).format_text(bold=True, size=24, color="#2E74B5")
body.insert_paragraph_after("Cash runway: 30 months") # append a bullet
# Tables — a table is a shape (Shape.has_table); cells are cell:S:N:R:C anchors.
with deck.edit("Add a metrics table"):
table = deck.slides[4].shapes.add_table(rows=3, columns=2).table
table.cell(1, 1).set_text("Metric")
table.cell(1, 2).set_text("Q3")
table.add_row(["Revenue", "$4.2M"]) # appends + fills a row
deck.anchor_by_id("cell:4:5:1:1").format_text(bold=True) # a Cell is an anchor
grid = table.read() # {slide, shape, rows, columns, cells:[...]}
# Deck-wide styling — theme (palette + fonts) and master (text styles +
# background) restyle every inheriting slide at once. Global + anti-polite,
# but still one Ctrl-Z; your view doesn't move.
with deck.edit("Rebrand the deck"):
deck.theme.set_color("accent1", "#C00000") # recolor the whole deck
deck.theme.set_font("major", "Georgia") # major = headings, minor = body
deck.master.format_text_style("body", 1, font="Georgia", size=28)
deck.master.set_background("#1F1F1F") # solid fill
palette = deck.theme.read() # {colors:{slot:#RRGGBB}, fonts:{major, minor}}
# Find / replace — fuzzy traversal across shapes, table cells, and notes
# (no deck-wide character stream, so this is a walk, not a range).
hits = deck.find("Q3 Reuslts") # [{anchor_id, start, length, text, ...}]
with deck.edit("Fix the typo everywhere"):
deck.find_replace("Q3 Reuslts", "Q3 Results", all=True)
# Review comments — slide-anchored at an (x, y) point, and threaded.
review = deck.slides[2].comments # per-slide CommentCollection
with deck.edit("Leave review notes"):
c = review.add("Tighten this headline", left=100, top=80)
c.reply("Agreed — will do")
roll = deck.comments() # deck-wide roll-up {total, slides:[...]}
# Whole-deck snapshot — one low-res PNG per slide so a vision model can SEE
# the whole deck cheaply (max_dim caps each slide's long edge). A read — polite.
snaps = deck.snapshot(max_dim=1000) # [Snapshot(slide, image, path), ...]
# Output tier — explicit, never implicit (pptlive never auto-saves).
deck.save() # persist to the existing file
deck.save_as("v2.pptx", overwrite=True) # write + rebind the working file
deck.export_pdf("deck.pdf") # a read: no rebind, dirty flag kept
# Media + narrated-video export — build a deck, narrate it, export an MP4.
with deck.edit("Narrate the deck"):
deck.slides[1].add_audio("intro.mp3") # embed; autoplay + pace the slide
deck.slides[2].add_video("demo.mp4") # a video clip (stays visible)
result = deck.export_video("deck.mp4", resolution=1080) # async CreateVideo; blocks to done
assert result.ok and result.status == "done" # result.path is the MP4
# Render — let a vision model *see* the slide it just built (export → read → iterate).
png = deck.slides[4].export_image(width=1280) # temp PNG (or pass a path); polite
# ...hand `png` to your image tool, look, then revise.
# Read what the user is looking at, and (opt-in) target it with the here: anchor.
sel = deck.selection() # {type, slide, anchor_id, shapes, ...}
if sel.anchor_id:
with deck.edit("Bold the selected text"):
deck.anchor_by_id("here:").format_text(bold=True)
# Live slide show — drive the presentation like a clicker (deliberately moves the screen).
deck.show.start() # run from the top
deck.show.goto(5); deck.show.next(); deck.show.black() # jump, advance, blank
deck.show.state() # {running, state, current_slide, ...}
deck.show.end()
Anchors
Addressing is hierarchical (slide → shape → text), not a global character stream — there is no deck-wide range:. Anchor ids are colon-separated, slide-index first:
| anchor_id | resolves to | | -------------- | ----------- | | shape:S:N | Nth shape (1-based z-order) on slide S — the canonical handle | | shapeid:S:ID | shape with stable Shape.Id ID on slide S — the delete-proof handle (survives a delete/restack that shifts shape:S:N) | | ph:S:KIND | placeholder of semantic KIND (title/ctrtitle/subtitle/body/footer/date/slidenum) — the LLM-preferred form | | para:S:N:P | paragraph P (1-based) of shape N on slide S | | cell:S:N:R:C | cell (row R, col C) of the table in shape N on slide S — a Cell is an anchor, so it takes every text/format verb | | notes:S | speaker-notes body of slide S | | comments:S | the review comments on slide S — a read selector (a container, addressed for reply/delete by (slide, 1-based index)) | | here: | whatever the user has selected right now — the shape, or the paragraph holding the text caret (the opt-in way to act on the live selection) |
z-order drifts when shapes are added or removed (or restacked via Shape.reorder), so shape:S:N is resolved live and never cached; every shape listing also emits name (Shape.Name) and id (Shape.Id, stable across reorder). The drift-proof forms are ph:S:KIND, .Name, and shapeid:S:ID. para:S:N:P and cell:S:N:R:C also resolve live (the paragraph/row count shifts as text or rows are inserted/deleted).
CLI
JSON in, JSON out, deterministic exit codes — designed to drop straight into an LLM tool-use loop. Global flags (--json/--text, --doc NAME) go before the subcommand.
pptlive status # open decks, active one, slide in view
pptlive slides # [{index, id, layout, title, shape_count, has_notes}]
pptlive outline # title + body bullets per slide
pptlive slide read 2 # every shape on slide 2
pptlive shapes --slide 2 # shapes on slide 2 (anchor_id, name, id, type, geometry)
pptlive read anchor --anchor-id ph:2:title # read any text anchor (ph:/shape:/notes:)
pptlive read notes --slide 1 # sugar for --anchor-id notes:1
pptlive write --anchor-id ph:2:body --text "Intro\nDemo\nQ&A"
pptlive replace --anchor-id shape:3:1 --text "New text"
pptlive find --text "Q3 Reuslts" [--in slide:2] # fuzzy locate across shapes/cells/notes
pptlive replace --find "Q3 Reuslts" --text "Q3 Results" --all # fuzzy replace (or --occurrence N)
pptlive slide layouts # the layout names add/set-layout accept
pptlive slide add --layout two_content [--index 4]
pptlive slide duplicate --slide 7
pptlive slide move --slide 9 --to 2
pptlive slide set-layout --slide 4 --layout title_and_content
pptlive slide delete --slide 5
pptlive slide export --slide 2 --out slide2.png [--width 1280] [--format png] # render to image
pptlive slide geometry 2 # slide size + shape boxes + overlaps + off-slide (no render)
pptlive shape add --slide 4 --kind textbox --text "Revenue up 12%" --left 72 --top 72
pptlive shape add --slide 4 --kind shape --shape-type star --left 400 --top 120 --width 120 --height 120
pptlive shape add --slide 4 --kind picture --path logo.png --left 600 --top 40 --alt-text "Acme logo"
pptlive shape add --slide 4 --kind table --rows 3 --cols 2 --left 72 --top 120
pptlive shape move --anchor-id shape:4:3 --left 100 --top 140
pptlive shape resize --anchor-id shape:4:3 --width 300 --height 200
pptlive shape delete --anchor-id shape:4:3
pptlive shape set-alt --anchor-id shape:4:3 --alt-text "Acme logo" # drift-proof re-id handle
pptlive shape fill --anchor-id shape:4:3 --fill "#C00000" --line none --line-width 2 # fill/border
pptlive shape order --anchor-id shape:4:3 --to front # z-order: front/back/forward/backward
pptlive shape export --anchor-id shape:4:3 --out logo.png # render one shape (native size)
pptlive shape animate --anchor-id shape:4:3 --effect fly_in [--trigger after_previous] [--exit]
pptlive slide animations 4 # a slide's shape animations in play order
pptlive shape add --slide 4 --kind chart --chart-type column \
--categories "Q1,Q2,Q3" --series '{"Revenue":[10,20,30]}'
pptlive chart read --slide 4 --shape 5 # {chart_type, categories, series}
pptlive chart set-type --slide 4 --shape 5 --chart-type line
pptlive chart set-data --slide 4 --shape 5 --categories "A,B" --series '{"S":[1,2]}'
pptlive chart recolor-text --slide 4 --shape 5 --color "#FFFFFF" # all chart text (dark-theme fix)
pptlive shape add --slide 3 --kind smartart --smartart-kind process \
--nodes '["Discover","Design","Build","Ship"]' # flat list or {text,children} tree
pptlive smartart read --slide 3 --shape 2 # {layout, nodes:[{text, level, children}]}
pptlive smartart set-nodes --slide 3 --shape 2 --nodes '[{"text":"CEO","children":["Eng","Sales"]}]'
pptlive smartart recolor-text --slide 3 --shape 2 --color "#FFFFFF" # every node label
pptlive theme read # deck palette (12 slots) + heading/body fonts
pptlive theme set-color --slot accent1 --color "#C00000" # recolors the whole deck
pptlive theme set-font --which major --name "Georgia" # major = headings, minor = body
pptlive master read # master text styles (title/body/default) + background
pptlive master format-text-style --style body --level 1 --font "Georgia" --size 28
pptlive master set-background --color "#1F1F1F" # deck-wide; solid fill
pptlive paragraphs --anchor-id ph:4:body # [{anchor_id (para:S:N:P), text, indent_level, bullet}]
pptlive insert --anchor-id para:4:2:3 --text "New bullet" [--before|--after]
pptlive format-paragraph --anchor-id para:4:2:1 --alignment center --indent-level 2
pptlive format-text --anchor-id ph:4:title --bold --size 40 --color "#2E74B5"
pptlive list apply --anchor-id ph:4:body --type bulleted [--char "•"]
pptlive list remove --anchor-id ph:4:body
pptlive table read --slide 4 --shape 5 # grid of cells, each with its cell:S:N:R:C anchor
pptlive table add-row --slide 4 --shape 5 --values '["Revenue", "$4.2M"]'
pptlive table delete-row --slide 4 --shape 5 --row 2
pptlive write --anchor-id cell:4:5:1:1 --text "Metric" # a cell takes write/format-text/...
pptli
…
## Source & license
This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [thomas-villani](https://github.com/thomas-villani)
- **Source:** [thomas-villani/pptlive](https://github.com/thomas-villani/pptlive)
- **License:** MIT
- **Homepage:** https://thomas-villani.github.io/pptlive/
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.