Skip to main content
Glama
kekegg

Lingsu Slides MCP Server

by kekegg
README.md
<!--
  ============================================================================
   Lingsu Slides — README
  ----------------------------------------------------------------------------
   This is the GitHub homepage README.  It is intentionally heavier on
   screenshots, badges, and a TL;DR than the rest of the repo so that
   a visitor landing on the page sees the value proposition in <30 s.
  ============================================================================
-->

<div align="center">

# Lingsu Slides

**A PowerPoint editor with an embedded AI-agent MCP server — so a human and a model can edit the same deck in real time, with one undo history.**

[![Python](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org)
[![PySide6](https://img.shields.io/badge/PySide6-6.6%2B-44cc11.svg)](https://wiki.qt.io/Qt_for_Python)
[![MCP](https://img.shields.io/badge/MCP-1.0%20%7C%202.x-orange.svg)](https://modelcontextprotocol.io)
[![License](https://img.shields.io/badge/license-academic%20research%20use%20only-blue.svg)](LICENSE)
[![Tests](https://img.shields.io/badge/tests-769%20passing-brightgreen.svg)](#tests)
[![Headless](https://img.shields.io/badge/headless--mode-supported-9cf)](https://github.com/kekegg/lingsu-slides)

![hero shot — gradients, shadows, lists, and z-order](docs/hero_v3.png)

</div>

> **TL;DR.** Open a `.pptx` in the GUI; an embedded MCP server on
> `http://127.0.0.1:8765/mcp` lets any Model-Context-Protocol client
> (Claude Desktop, Cursor, a custom agent) edit the same Document
> through 46 typed tools. The MCP and the GUI share a single
> `UndoStack`, so a human dragging a shape and an agent adding a slide
> are interleaved cleanly. Want zero GUI? `python -m slideslingsu
> --headless` runs the same MCP server over stdio for batch use.

---

## Table of contents

- [Highlights](#highlights)
- [Why this exists](#why-this-exists)
- [Install](#install)
- [Run](#run)
- [AI agent integration (MCP)](#ai-agent-integration-mcp)
- [What you can build](#what-you-can-build)
- [Keyboard shortcuts](#keyboard-shortcuts)
- [Architecture](#architecture)
- [Project layout](#project-layout)
- [Roadmap](#roadmap)
- [Tests](#tests)
- [Third-party components](#third-party-components)
- [Citation](#citation)
- [License](#license)
- [Disclaimer](#disclaimer)

---

## Highlights

- **Three input channels, one Document.**
  GUI for humans. In-app Python (`F5`) for power users. MCP server
  for AI agents — all writing through the same `Command + UndoStack`
  so undo is shared, not per-channel.
- **46 MCP tools.** `document.*` (34), `app.*` (8), `script.*` (2),
  `clipboard.*` (2), plus one streaming `/mcp` HTTP endpoint exposed
  by the GUI on `127.0.0.1:8765`. Connect any MCP-compatible agent in
  minutes.
- **pptx round-trip with extensions.** Reads and writes the OOXML
  core (text, auto-shapes, images, tables, slide backgrounds) *and*
  the decorative bits PowerPoint editors we care about — `gradFill`,
  `blipFill` picture fills, `effectLst` shadows, bullet/numbered
  lists, custom fonts — through direct OOXML injection (no vendor
  lock-in).
- **PowerPoint-faithful editing.** Double-click a table cell and the
  table becomes an editable grid: `Tab` walks the cells, the arrow
  keys hop to the neighbour when the caret sits on a cell edge, `Tab`
  in the last cell adds a row — and every edited cell is its own
  undo step. `Insert > Icons` gives you a searchable, categorised
  icon panel whose thumbnails are drawn by the very same geometry
  engine the canvas paints.
- **Real OOXML preset geometry.** 145 auto-shape presets, plus a
  formula engine that evaluates the canonical DrawingML definitions
  (`avLst` / `gdLst` / `pathLst`) — so the curved and circular arrows
  look like PowerPoint's and expose **draggable adjustment control
  points** that write back to `<a:avLst>`.
- **Headless mode.** Run `python -m slideslingsu --headless` for a
  pure MCP stdio server with no Qt. CI, batch generation, scripting
  pipelines all use this.
- **Inline editing that respects runs, lists, fonts.** Double-click
  a text shape → in-place editor with **per-run font / size /
  color / bold / italic / underline**, **bullet/numbered list
  markers that round-trip verbatim**, and continuous **undo**
  across every keystroke.
- **Sandboxed Python runtime.** `F5` opens a script panel that
  exposes only `Document`, `Slide`, `Shape` APIs (no `os`,
  no `subprocess`, no `eval`) — so a user (or an agent via MCP)
  can drive the deck programmatically without leaving the tool.

---

## Why this exists

Most PowerPoint editors either have a GUI but no agent hook, or
have an agent hook (python-pptx scripts) but no live visual canvas.
Lingsu Slides is the **both** lane:

- You edit visually, an AI edits via MCP — both see the same
  `Document`, both share the same undo, both round-trip through
  `python-pptx` for save/load.
- Conversely, run `python -m slideslingsu --headless` and the
  project is **just an MCP server + Document library** — perfect for
  a server-side "generate 50 slides from this JSON" pipeline
  where you don't want a Qt loop allocated.

In short: the same code powers both the GUI and the bot.

---

## Install

```bash
git clone https://github.com/kekegg/lingsu-slides.git
cd lingsu-slides
pip install -r requirements.txt
```

> **Tested on:** macOS / Windows / Linux, Python 3.10+, PySide6 6.6+,
> python-pptx ≥ 1.0, MCP ≥ 1.0 (both the 1.x and the 2.x SDK).

## Run

```bash
# GUI with embedded MCP server (recommended)
python -m slideslingsu

# Open a specific file
python -m slideslingsu path/to/deck.pptx

# GUI only (no MCP thread)
python -m slideslingsu --no-mcp

# Headless MCP server over stdio (no GUI; perfect for CI / agents)
python -m slideslingsu --headless
```

When the GUI is up you should see this on stderr:

```
mcp server ready on http://127.0.0.1:8765/mcp
```

…which the rest of your pipeline can connect to.

---

## AI agent integration (MCP)

Any MCP-compatible client (Claude Desktop, Cline, Cursor, or your
own agent) speaks to the deck via the GUI's embedded server.

### Claude Desktop — stdio bridge

stdio-only clients (the default `command + args` config style) can't
talk to the GUI's HTTP endpoint directly. Run the GUI with
`--stdio-bridge` to auto-spawn a tiny bridge subprocess that proxies
stdio → in-process HTTP:

```bash
python -m slideslingsu --stdio-bridge
```

Then in `~/Library/Application Support/Claude/claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "lingsu-slides": {
      "command": "python",
      "args": ["-m", "slideslingsu.mcp.bridge"],
      "env": { "SLIDESLINGSU_MCP_URL": "http://127.0.0.1:8765/mcp" }
    }
  }
}
```

Claude can now issue `tool_use: document.add_slide`, `shape.set_property`,
`script.run` against your live deck.

### Tool namespaces

| namespace | count | what it does |
| --- | --- | --- |
| `document.*` | 34 | shape CRUD, slides, formatting, z-order, alignment, list management |
| `app.*`     | 8  | save/load, export (png / json), env queries |
| `script.*`  | 2  | run / sample sandboxed Python scripts |
| `clipboard.*`| 2  | inspect / clear the in-process clipboard |

The full tool surface is implemented in
[`slideslingsu/mcp/server.py`](slideslingsu/mcp/server.py) and pinned by
[`tests/test_mcp_server.py`](tests/test_mcp_server.py);
[`scripts/mcp_client_demo.py`](scripts/mcp_client_demo.py) is a
runnable end-to-end client.

---

## What you can build

### Example: chat-driven deck

In your MCP client:

```
"add 3 slides for a Q4 board update; first slide: 'Q4 Results'
with our top-line revenue chart, second: a 5-bullet summary of
regional performance, third: a 4-quadrant SWOT"
```

…then watch the agent call `document.add_slide`, `document.add_shape`,
`document.set_property('shadow', True)`, etc.

### Example: pure-script generation (no GUI)

```python
from slideslingsu.core.commands import UndoStack
from slideslingsu.core.document import Document
from slideslingsu.core.pptx_io import save_pptx
from slideslingsu.script.runtime import ScriptRuntime

doc = Document("deck")
rt = ScriptRuntime(doc, UndoStack(doc))

# 5 color tiles with shadow + gradient fill
for i, color in enumerate(["#4F8EF7", "#26C485", "#F7B84F", "#F76E4F", "#9B59B6"]):
    rect = rt.add_rect(60 + i * 180, 200, 160, 240, fill=color)
    rt.set_property(rect.id, "fill_type", "gradient")
    rt.set_property(rect.id, "fill2", "#FFFFFF")
    rt.set_property(rect.id, "shadow", True)

# bullet list
t = rt.add_text(60, 220, 420, 200, "Apple\nBanana\nCherry")
rt.set_property(t.id, "list_type", "bullet")

save_pptx(doc, "output.pptx")
```

Same `Document` model the GUI uses; nothing in the script path knows
about Qt.

### Example: 8-line deck

```python
from slideslingsu.core.commands import UndoStack
from slideslingsu.core.document import Document
from slideslingsu.script.runtime import ScriptRuntime

doc = Document("deck")
rt = ScriptRuntime(doc, UndoStack(doc))

rt.add_text(40, 40, 880, 90, "Q4 Review", font_size=44, bold=True)
ids = []
for i, c in enumerate(["#4F8EF7", "#26C485", "#F7B84F", "#F76E4F", "#9B59B6"]):
    rect = rt.add_rect(60 + i * 180, 200, 160, 240, fill=c)
    rt.set_property(rect.id, "fill_type", "gradient")
    rt.set_property(rect.id, "fill_angle", 90)
    ids.append(rect.id)
rt.align_shapes(ids, "bottom")
```

See [`examples/`](examples/) for more.

---

## Keyboard shortcuts

| key | action |
| --- | --- |
| `Cmd/Ctrl-N`       | New deck |
| `Cmd/Ctrl-O`       | Open… |
| `Cmd/Ctrl-S`       | Save / Save As… |
| `Cmd/Ctrl-Z`       | Undo (inside a text box: undo the last characters typed) |
| `Cmd/Ctrl-Shift-Z` | Redo |
| `Cmd/Ctrl-X`       | Cut shapes |
| `Cmd/Ctrl-C`       | Copy shapes / slides (panel focus) |
| `Cmd/Ctrl-V`       | Paste |
| `Cmd/Ctrl-D`       | Duplicate |
| `Cmd/Ctrl-A`       | Select All |
| `Esc`              | Deselect / cancel the current edit |
| `Delete`           | Delete selection |
| `F2`               | Enter (or commit) editing on a selected text shape |
| `F5`               | Open script-editor dock |
| `Tab`              | In a text box: insert a tab. In a table: next cell |
| `Shift+Tab`        | In a table: previous cell |
| `Shift-Click`      | Multi-select |
| `Triple-Click`     | Select current line in editor |
| `Quad-Click`       | Select all in editor |

---

## Architecture

The `Document` model (`slideslingsu/core/document.py`) is **pure Python** —
no Qt imports. All three input channels (GUI, in-app script editor,
MCP server) mutate the `Document` through `Command` objects on a
single `UndoStack`:

```
                ┌──────────────┐
                │  Document    │  ← one shared model, pure Python
                │  + UndoStack │
                └──┬───────┬───┘
                   │       │
    ┌──────────────┤       ├───────────────┐
    │              │       │               │
┌───▼────┐    ┌────▼──┐    ┌────▼───┐    ┌────▼────────┐
│ PySide6│    │ in-app│    │  MCP   │    │  MCP         │
│  GUI   │    │Python │    │ stdio  │    │  HTTP server │
│(human) │    │ (F5)  │    │        │    │  (GUI-embedded)│
└────────┘    └───────┘    └────────┘    └──────────────┘
```

This means:

- The script editor and the MCP server don't need Qt to run — they're
  fully testable headless.
- An agent connected over MCP can undo/redo alongside a human user;
  both share the same history.
- A crash in the GUI doesn't lose data — the `Document` is plain
  Python objects that round-trip through `python-pptx`.

---

## Project layout

```
slideslingsu/
├── run.py                       # convenience launcher
├── scripts/
│   ├── headless_demo.py         # pure-Python usage, no Qt
│   ├── mcp_client_demo.py       # end-to-end stdio MCP test
│   ├── gui_smoke_test.py        # headless GUI smoke test
│   └── user_stress_test.py      # soak test for the GUI
├── slideslingsu/
│   ├── __main__.py              # `python -m slideslingsu`
│   ├── app.py                   # QApplication + embedded MCP QThread
│   ├── core/
│   │   ├── document.py          # Document / Slide / Shape (Qt-free)
│   │   ├── commands.py          # Command pattern + UndoStack
│   │   ├── clipboard.py         # in-process shape clipboard
│   │   ├── slide_clipboard.py   # in-process slide clipboard
│   │   ├── paste.py             # paste / duplicate helpers
│   │   └── pptx_io.py           # python-pptx + gradFill / blipFill / effectLst
│   ├── gui/
│   │   ├── main_window.py       # QMainWindow, menus, toolbar
│   │   ├── canvas.py            # QGraphicsScene + View, in-place editor
│   │   ├── shape_items.py       # QGraphicsObject per Shape kind
│   │   ├── presets.py           # 145 OOXML preset geometries (deckcraft port)
│   │   ├── preset_defs.py       # verbatim DrawingML geometry addenda
│   │   ├── preset_xml.py        # DrawingML formula engine + path interpreter
│   │   ├── table_editor.py      # in-canvas table grid editor
│   │   ├── icon_panel.py        # Insert > Icons panel
│   │   ├── slide_panel.py       # thumbnail list (left) + slide cmds
│   │   ├── property_panel.py    # inspector (right)
│   │   ├── outline_editor.py    # text-only outline dock
│   │   └── script_editor.py     # Python editor + output
│   ├── script/
│   │   └── runtime.py           # sandboxed AST-checked Python exec()
│   └── mcp/
│       ├── server.py            # 46 MCP tools over stdio / HTTP
│       └── bridge.py            # stdio → HTTP proxy for Claude Desktop
└── tests/                       # 769 passing
```

---

## Roadmap

- [x] Core editor with UndoStack
- [x] pptx round-trip + gradFill / blipFill / effectLst / list
- [x] Embedded MCP server (stdio + HTTP)
- [x] Stdio bridge for Claude Desktop
- [x] Sandboxed in-app Python script editor
- [x] In-place text editor with per-run formatting
- [x] 145 OOXML preset geometries + canonical formula engine
- [x] Draggable adjustment control points (`<a:avLst>` round-trip)
- [x] In-canvas table grid editor (Tab / arrow-key navigation)
- [x] `Insert > Icons` panel with categories, search and recents
- [x] Bullet / numbered list editing with smart Enter
- [x] F2 + click-outside commit for fast iteration
- [ ] PyInstaller `.app` / `.exe` packaging (Phase A)
- [ ] Streamable-HTTP MCP transport (Phase B; currently HTTP+stdio bridge)
- [ ] Native speaker notes / handout view
- [ ] Multi-document tabs

---

## Tests

```bash
QT_QPA_PLATFORM=offscreen pytest tests/    # 769 passing, ~25 s
```

Coverage includes:

- `test_pptx_load_rich.py` — round-trip preservation of every
  PPTX feature
- `test_mcp_server.py` — every documented MCP tool, plus cross-channel
  undo interleaving
- `test_text_edit_v3.py` — F2 / click-outside / click-count +
  bullet Enter / cursor positioning
- `test_text_edit_parity.py` — Tab, type-to-replace, Esc, editor-owned undo
- `test_table_inline_editor.py` — the in-canvas grid editor and its
  per-cell undo granularity
- `test_icon_panel.py` — icon catalog, search, recents, pptx round-trip
- `test_preset_arc_geometry.py` — the DrawingML formula engine and the
  curved / circular arrow outlines
- `test_gradient_and_picture_fill.py` — gradient angles and picture fills
- `test_shape_items.py` — gradient / shadow / list rendering parity

The GUI smoke test drives the real window offscreen:

```bash
QT_QPA_PLATFORM=offscreen python scripts/gui_smoke_test.py   # 22 cases
```

---

## Third-party components

The Software depends on, or derives from, the following. Each stays
under its own license; see [`LICENSE`](LICENSE) Section 6.

| component | license |
| --- | --- |
| PySide6 / Qt for Python | LGPL-3.0 (or the Qt Commercial License) |
| python-pptx | MIT |
| lxml | BSD-3-Clause |
| Pillow | MIT-CMU (HPND-style) |
| mcp (Model Context Protocol SDK) | MIT |
| deckcraft (preset-geometry reference) | MIT OR Apache-2.0 |
| LibreOffice (`presetShapeDefinitions.xml`) | MPL-2.0 |
| ONLYOFFICE (geometry cross-reference) | AGPL-3.0 |

The preset-geometry engine carries the upstream attribution inline
in `slideslingsu/gui/preset_defs.py`, `slideslingsu/gui/preset_xml.py`
and `slideslingsu/gui/presets.py`. Do not remove it.

---

## Citation

If you use Lingsu Slides in academic work, please cite it:

```
Lingsu Slides: A PowerPoint editor with an embedded AI-agent MCP server
and an in-app Python scripting console. Zhengwei Xie.
https://github.com/kekegg/lingsu-slides
```

---

## License

**Academic research use only.** Lingsu Slides is released under a
custom, proprietary academic-use license — *not* an OSI-approved
open-source license, and deliberately more restrictive than MIT, BSD,
or Apache.

You may use, study, modify and run it for academic research,
education and other non-commercial scholarly work, and publish the
slides you produce with it. You may **not** use it for any commercial
purpose, sell or sublicense it, or use the Licensor's name to imply
endorsement, without prior written permission.

- [`LICENSE`](LICENSE) — the authoritative English text
- [`LICENSE.zh-CN.md`](LICENSE.zh-CN.md) — 中文对照译本(仅供参考,以英文版为准)

---

## Disclaimer

The Software is provided "AS IS", without warranty of any kind. It is
a research and authoring tool: it is not a certified authoring
product, it does not reproduce `.pptx` files byte-for-byte, and its
rendering is not guaranteed to be pixel-identical to Microsoft
PowerPoint. Review the decks you produce before presenting or
publishing them, and make sure you hold the rights to every piece of
content, font and image you put into them.

<sub align="center">

Inspired by [deckcraft](https://github.com/storytold/deckcraft)'s
preset-geometry reference and powered by [PySide6](https://wiki.qt.io/Qt_for_Python)
+ [python-pptx](https://github.com/scanny/python-pptx).

</sub>

TDQS

B3/5.0

Scored across 46 tools

Disambiguation3/5

Most tools target distinct slide, shape, document, or app operations, and descriptions clarify slide-level versus shape-level actions. However, there are several overlapping pairs: remove_slide versus delete_slides, copy_slide/paste_slide versus copy/paste for shapes, and duplicate_slide versus duplicate for shapes. These boundaries are inferable but create real misselection risk.

Naming Consistency4/5

Names consistently use a namespace.action snake_case convention such as document.get_slide, app.save_document_as, and clipboard.peek. Minor inconsistencies appear with remove_slide/delete_slides and shape clipboard tools named copy/cut/paste rather than copy_shapes/cut_shapes/paste_shapes, but the overall pattern is predictable.

Tool Count2/5

With 46 tools, the surface is very large for a slide-editing server and includes many granular operations that could be consolidated or handled through properties/scripting. While the breadth supports a full editor, the count is heavy enough to burden tool selection and maintenance.

Completeness4/5

The server covers document lifecycle, slide CRUD/duplication/reordering, shape CRUD/grouping/alignment/z-order, text and property editing, clipboard operations, export, and undo/redo. Minor gaps remain, such as inserting a slide at an arbitrary index, richer table/cell editing, and slide layout/master operations, but core workflows are complete.

Maintenance

ActivityMaintained
ResponsivenessNo issues