AgentStack
MCP verified MIT Self-run

Pptlive

mcp-thomas-villani-pptlive · by thomas-villani

LLM-friendly API/CLI for driving powerpoint.

No reviews yet
0 installs
2 views
0.0% view→install

Install

$ agentstack add mcp-thomas-villani-pptlive

✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.

Security review

✓ Passed

No 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.

Are you the author of Pptlive? Claim this listing to set pricing, connect Stripe payouts, and keep 70% of every sale.
Sign up to claim

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.

Versions

  • v0.1.0 Imported from the upstream source.