Skip to main content
Glama

slidevim

ci

Vim for Google Slides — built so an agent can edit a deck instead of clicking one.

Computer-use agents are bad at Google Slides for a structural reason: a deck is a tree of objects, but the only interface is a canvas. So the model takes a screenshot, guesses at a pixel, clicks, screenshots again to see whether it worked, and repeats. Ten screenshots and a lot of tokens to delete three slides.

slidevim gives the deck a text interface and a command language instead:

$ slidevim exec $DECK ':g/TODO/d | :%s/2025/2026/g | :w'
deleted 1 slide
deleted 1 slide
7 substitutions on 4 slides
wrote 9 ops to google

No screenshots. No pixels. One round trip, and a diff you can inspect before it is sent.

Zero runtime dependencies. Node ≥ 22.6. MIT.


The idea

Vim's grammar is operator + motion, applied to a buffer of lines. slidevim keeps the grammar and rebinds the nouns:

vim

slidevim

buffer

the deck

line

a slide

word

an element on the slide (title, body, image, table)

dd

delete this slide

:%s/a/b/g

substitute across every slide

:g/pat/d

delete every slide matching a pattern

marks, registers, counts, u, .

all of them, unchanged

That mapping is the whole product. An agent that knows vim already knows this, which means the command language costs roughly zero tokens to teach.

What this takes from Vimium

Vimium is the direct ancestor, and its best idea is not the vim keybindings — it is f. Press f and every link on the page grows a short label; type the label and you have clicked it. You never aim at a pixel, never describe a target, never say "the third link in the nav". The thing you are looking at is labelled with exactly what you type to act on it.

That is the property slidevim is trying to give a deck, and it is worth considerably more to an agent than to a human, because an agent's alternative is a screenshot and a guess at a coordinate.

So slidevim has hints, and they work the way Vimium's do. Every slide and every element carries a code, in the buffer and in the preview:

  [l] @3 #s03 TITLE_AND_BODY
    [e] title  Revenue
    [w] tbl1   [table 3x3]
               Quarter | Revenue | Growth

Type the code where a label goes and it acts on that exact thing, with no motion and no cursor:

:text e Revenue and margin
:cell w 2,2 $9.9M
:drop w
:hint l | dd              # or put the cursor there first, for slide operators

Two differences from Vimium, both forced by the reader being a program:

  • Hints are always on. Vimium hides them behind f because they are visual clutter. For an agent they are four characters that remove a round trip, so they are cheaper shown than hidden.

  • Hints are assigned in document order, which makes them a pure function of the deck — the same deck yields the same codes in the next process, and every CLI and MCP call is a new process. They shift when slides are added or removed, exactly as Vimium's do when the page changes, so re-read the buffer after that.

For when you would rather name a thing than look one up, @3/tbl1 and #s03/tbl1 also work anywhere a label goes and also ignore the cursor. A bare tbl1 still means "in this command's range", as in vim.

What slidevim deliberately does not take from Vimium: modes and remappable keys. Insert mode exists to get out of the way of a human typing prose, and map exists because humans have muscle memory. Neither applies here.

Related MCP server: google-slides-mcp

Install

git clone https://github.com/jaisharmz/slidevim
cd slidevim && npm install && npm run build && npm link

Try it against the bundled fixture deck, no credentials needed:

slidevim demo                                   # print the buffer
slidevim demo ':g/TODO/d | :%s/2025/2026/g'     # run a program
slidevim demo ':g/TODO/d | :w' --dry-run        # the batchUpdate it would send
slidevim keys                                   # command reference

That third line is the quickest way to judge this project: it prints the real Google Slides API payload for a program, with no credential and no network.

Just tell Claude to make you a deck

The repo is set up as a Claude Code project. .mcp.json registers the MCP server; .claude/skills/presentation/ holds the workflow.

cd slidevim && npm install && npm run build
claude

make me a presentation about why residential streets should be 20mph

Claude creates a deck under decks/, opens a live preview, hands you the URL, and builds the deck while you watch. On the first run it asks once whether to trust the project's MCP server; say yes.

the preview, mid-build

Those yellow chips are the hints. They are the same codes the agent sees in its buffer, so what you are looking at and what it is typing are the same addresses.

Watching it work

A text interface is only convincing if you can see what it produced. slidevim serve opens a page that follows a local deck file, so you can put the preview on one side of the screen and let an agent edit on the other:

slidevim new deck.json "Slower Roads"
slidevim serve --file deck.json          # http://127.0.0.1:PORT
slidevim exec deck ':$new TITLE | :title Slower Roads | :w' --file deck.json

./examples/build-a-deck.sh runs the whole thing — five slides, a table, a web image, a substitution pass over the finished deck — with a pause between steps so you can watch each one land. No credential needed.

The preview is an approximation, not a renderer: it knows structure and content, because that is all the model carries. It has no idea what your theme looks like. For pixels, open the real deck — Slides updates live while the API writes to it, which is the same demo with better fonts.

Appending: :$new adds a slide at the end. A plain :new inserts after the cursor, and every new process starts with the cursor on slide 1, so a deck built with bare :new calls comes out backwards. This bit me the first time I built a deck with it.

The buffer

slidevim view $DECK renders the deck as text. Hints are in brackets, slides are numbered, elements are labelled, and > marks the cursor:

deck:1AbC… "Northwind Q3 Review" 6 slides [+]
  [ss] @1 #s01 TITLE
    [sa] title     Northwind Q3 Review
    [sd] subtitle  Prepared for the board, FY2025
> [sf] @2 #s02 TITLE_AND_BODY
    [sj] title  Agenda
    [sk] body   - Revenue
                - Pricing changes
                - Roadmap
         notes  Keep this to ninety seconds.
  [sl] @3 #s03 TITLE_AND_BODY
    [se] title  Revenue
    [sw] tbl1   [table 3x3]
                Quarter | Revenue | Growth
                Q1 2025 | $1.2M   | 8%

Every bracketed code is an address: :cell sw 2,2 $9.9M edits that table wherever the cursor is. So is @3/tbl1, for when naming beats looking up. A bare label means "in this command's range" — :text body … edits the body of the current slide, :%text body … edits every slide's. Notes have no hint because :notes addresses the slide, not an element on it.

The projection is one-way by design. Commands mutate the model and the view is re-rendered — nothing ever parses this format back into a deck, so there is no round trip to get subtly wrong.

Commands

motions   j k gg G {n}G } { w b 0 $ /pat n N
edits     dd yy dj dG 3dd x p P cc u <C-r> . "ayy
marks     ma `a       ranges: % 1,5 . $ .+2 'a,'b /pat/

:new [layout]      insert a slide after the cursor (:new! before)
:title TEXT        set the title (also :sub, :body, :notes)
:text LABEL TEXT   set any element by label
:append LABEL TEXT append a paragraph
:image URL [alt]   add an image
:table RxC         add a table; :cell tbl1 2,3 TEXT
:drop LABEL        delete an element
:s/old/new/g       substitute (:%s for the whole deck)
:g/pat/CMD         run CMD on every matching slide (:v inverts)
:m ADDR  :t ADDR   move / copy slides
:sort              sort slides in range by title (:sort! reverses)
:skip :unskip      toggle "skip in presentation"
:hint CODE         put the cursor on what a hint code names (also :f)
:diff              show pending ops without writing
:w :e :q :wq       write, reload, quit

:new inserts after the cursor; :$new appends at the end. Use :$new when building a deck up from nothing.

Anywhere LABEL appears you may write a qualified address instead — @3/tbl1 by position, #s03/tbl1 by object id — which names its own slide and ignores the cursor and the range. :3cell tbl1 2,2 X is the ex-range spelling of the same thing.

Two deliberate deviations from vim:

  • Whitespace separates commands. gg 3dd :w is three commands. Agents write programs as JSON strings, and unambiguous separators are worth more than <Space> as a synonym for l.

  • /pattern and :ex run to a | or a newline, as in vim. A pattern may contain spaces, so terminate it before the next command: /q3 revenue | :s/2025/2026/g.

Nothing is written until :w. Until then :diff shows exactly what would be sent.

Use from an agent (MCP)

{
  "mcpServers": {
    "slidevim": { "command": "node", "args": ["/path/to/slidevim/dist/mcp.js"] }
  }
}

Eight tools: slides_new, slides_preview, slides_view, slides_exec, slides_diff, slides_write, slides_check_image, slides_keys. Only slides_write touches the deck, so an agent can plan and self-check for free.

A typical exchange:

slides_view  { deckId }                          -> the buffer
slides_exec  { deckId, program: ":g/TODO/d" }    -> the buffer, edited
slides_diff  { deckId }                          -> "2 pending ops: delete slide s02, …"
slides_write { deckId }                          -> "wrote 2 ops to google"

slidevim does not replace the agent's other tools

It is a text interface to one specific object, not a sandbox. The agent keeps its browser, its web search, its file system — and it should use them. Pictures are the obvious case: slidevim has no opinion about which image belongs on a slide, only about whether a given URL will work.

(the agent's own image search)  -> some candidate URLs
slides_check_image { url }      -> "ok image/jpeg 448 KB — Slides can fetch this"
slides_exec { program: ":image <url> a residential street with speed cushions" }

That middle step earns its place. createImage is fetched by Google's servers, not by yours, so a URL only works if it is publicly reachable, PNG/JPEG/GIF, and under 50 MB. Image search results routinely fail all three — they are pages about an image, or hotlink-protected, or behind a redirect that wants a cookie — and Slides reports every one of those as a bare 400. slides_check_image turns that into "content-type is text/html (this looks like a page about the image, not the image file)" at the moment of choosing, which is a thing an agent can actually act on. slidevim check-image <url> is the same check from the shell.

Credentials

slidevim never stores a credential. Point it at a token you already have:

export SLIDEVIM_ACCESS_TOKEN="$(gcloud auth print-access-token)"
# or, refreshed on demand:
export SLIDEVIM_TOKEN_CMD="gcloud auth print-access-token"
# or a standard OAuth client:
export SLIDEVIM_CLIENT_ID=… SLIDEVIM_CLIENT_SECRET=… SLIDEVIM_REFRESH_TOKEN=…

Scopes: presentations, drive.file.

Before pointing it at a deck you care about, look at the requests:

slidevim exec $DECK ':%s/2025/2026/g' --dry-run

--dry-run compiles to Google Slides batchUpdate requests and prints them, and it holds on every backend — the Google client sends nothing, the local --file deck is not rewritten, and the demo fixture is not touched.

How it works

command text ──parse──▶ AST ──execute──▶ deck model (in memory)
                                              │
                                    diff vs last-synced snapshot
                                              │
                                          DeckOp[]
                                         ╱        ╲
                              memory backend    Google backend
                                                 (batchUpdate)

Two decisions carry the design:

Undo is a snapshot; sync is a diff. Commands mutate the model freely and never journal an inverse. Undo restores a clone. :w diffs the working deck against the last synced snapshot and emits the minimal op list. Correctness of both falls out of the model rather than out of every command remembering to do the right thing — which is why :g/pat/normal 3dd syncs correctly despite nobody having thought about that combination.

The Google backend is a pure compiler plus a thin client. compileOps turns ops into API requests with no I/O, so the entire write path is unit-tested without a network or a credential.

Three places where the Slides API is sharper than it looks, and what this does about them:

  • createSlide.placeholderIdMappings lets the caller choose object ids, so model ids become the real Slides ids and stay stable across syncs.

  • updateSlidesPosition.insertionIndex is indexed against the arrangement before the move, so a slide moving later in the deck must aim one past its destination. Reordering is computed by simulation, not by comparing final indices — an op list of "final index" moves is wrong the moment there is more than one of them.

  • deleteText is a 400 on a shape with no text, so the compiler tracks which shapes and table cells actually hold text and only clears those.

Tests

npm test        # 126 tests, on Node 22 and Node 26
npm run typecheck

The suite's backbone is a round-trip property: for a corpus of programs, writing the deck and reading it back must reproduce the working deck exactly. On top of that, all 720 orderings of a six-slide deck are checked through diff-and-apply, which is what caught the multi-slide reordering bug that final-index moves produce.

The Google backend is covered by asserting the generated batchUpdate requests against the documented API shapes, and by mapping a recorded presentations.get payload into the model.

The write path against a live Google deck is the one thing the suite cannot reach: it needs an OAuth grant, which CI does not have. Everything up to the HTTP call is tested; the HTTP call itself is not.

Status

v0.1. Slide and element structure, text, tables, images, notes, ordering and skip state. Not yet: text styling (fonts, colours, sizes), speaker-note formatting, charts, and relayout of an existing slide — the Slides API has no request for that last one, so :layout only affects slides created in the same session, and says so.

License

MIT

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jaisharmz/slidevim'

If you have feedback or need assistance with the MCP directory API, please join our Discord server