MCP Tools
Deckyard ships a Model Context Protocol server, so an AI agent can list, read, create, edit, comment on and export presentations. It is a thin layer over the same storage and AI modules the editor uses: an agent’s write goes through the same slide-type validation and per-deck access checks as a person’s.
This page describes core Deckyard 1.34.0: 27 tools and 7 prompts. The source of truth is server/mcp/tools.js and server/mcp/prompts.js in the Deckyard repository.
Connecting
Section titled “Connecting”The server speaks two transports. Both register the same tools.
| Transport | How it runs | Authentication | Use it for |
|---|---|---|---|
| stdio | npm run mcp (node server/mcp/index.js), launched by the client | None: the process runs on your machine with direct database access | Claude Desktop, Claude Code, Cursor |
| Streamable HTTP | POST /mcp on a running Deckyard server, no separate process | An API key as Authorization: Bearer dk_live_... | Remote agents and hosted clients |
Add Deckyard to your client’s MCP configuration, pointing cwd at your Deckyard checkout:
{ "mcpServers": { "deckyard": { "command": "node", "args": ["server/mcp/index.js"], "cwd": "/path/to/deckyard", "env": { "DECKYARD_MCP_OWNER_EMAIL": "you@example.com" } } }}The server reads the .env in the Deckyard root, so it uses the same storage backend and LLM vendor as your instance.
DECKYARD_MCP_OWNER_EMAIL decides who the session acts as. It:
- filters
list_presentationsto decks that address owns, - becomes the owner of decks the session creates,
- turns on per-deck access checks: read for reading tools, collaborator-aware write access for mutating tools, and owner-only for
delete_presentation, - is the author of comments the session writes.
Without it, the stdio session is treated as a trusted local single-user process: no owner filter, no per-deck checks, and the comment write tools refuse to run because there is nobody to attribute a comment to. Decks created without an owner may be inaccessible in the web UI, so set it.
A stdio session has no organization of its own. It acts in the instance’s single organization and refuses to guess once there are several.
Streamable HTTP
Section titled “Streamable HTTP”The endpoint is /mcp on the Deckyard server itself; the MCP server initializes on the first request.
| Method | Path | Does |
|---|---|---|
POST | /mcp | Send a JSON-RPC request or a batch (array) of requests |
GET | /mcp | Open a server-sent event stream for an existing session |
DELETE | /mcp | Close a session |
OPTIONS | /mcp | CORS preflight |
Sending initialize opens a session and returns its id in the Mcp-Session-Id response header. Send that header on later requests to stay in the session. A POST without the header and without initialize is handled statelessly: it authenticates and answers on its own.
# Open a sessioncurl -i -X POST https://your-instance.com/mcp \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"curl","version":"1.0"}}}'
# Call a tool in that sessioncurl -X POST https://your-instance.com/mcp \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -H "Mcp-Session-Id: <id from the header above>" \ -d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"list_presentations","arguments":{}}}'| Property | Value |
|---|---|
Protocol version reported by initialize | 2024-11-05 |
| Session idle timeout | 30 minutes |
| Maximum concurrent sessions | 1000 (the least recently active one is evicted) |
| Request body limit | 1 MB |
| Stream keepalive | every 30 seconds |
| CORS | Access-Control-Allow-Origin: *; restrict it at your reverse proxy if needed |
The key is validated again on every request. A session is bound to the key owner that opened it: a request with a key from a different owner gets 403. An invalid or missing key gets 401, an expired session 404.
Supported JSON-RPC methods: initialize, tools/list, tools/call, prompts/list, prompts/get, ping.
Permissions and quota
Section titled “Permissions and quota”/mcp and /api/v1 accept the same API keys, and a key means the same thing on both. Each tool declares the one permission it needs, the same one its /api/v1 counterpart needs.
| Permission | Tools |
|---|---|
read | get_slide_types, list_presentations, get_presentation, get_presentation_url, list_themes, validate_presentation, preview_slide, preview_presentation |
write | create_presentation_from_slides, add_slide, update_slide, remove_slide, reorder_slides, duplicate_presentation, delete_presentation |
ai | create_presentation, append_slides, convert_slide, iterate_presentation, compress_presentation, analyze_presentation |
export | export_presentation |
comments:read | list_comments, list_recent_comments |
comments:write | add_comment, reply_to_comment, set_comment_status |
Over HTTP:
tools/listonly lists the tools the key has the permission for.- A call spends the key’s per-minute request bucket and daily counters, shared with
/api/v1: switching transport does not reset a limit. Tools needingaialso count against the daily AI limit,export_presentationagainst the daily export limit. The limits per tier are in Rate Limits. - The request bucket is spent before the permission is checked, so a refused call costs as much as an accepted one.
Over stdio there is no key, so there is no permission or quota gate.
Errors
Section titled “Errors”A refused or failed tool call is a normal JSON-RPC response whose result carries isError: true and a text item starting with Error:, for example Error: API key lacks required permission: write. It is not an HTTP error status. An unknown tool name or method is a JSON-RPC error with code -32601.
While the instance is in maintenance mode, every tool without the read-only flag is refused with a message saying when to retry. Tools marked read-only below keep working; tools/list marks them with the annotation readOnlyHint: true.
Conventions
Section titled “Conventions”- Presentation ids. Every tool that takes
presentationIdalso acceptsidas an alias. - Slide positions are 0-based indexes (
slideIndex,position,fromIndex,toIndex). - Return values are JSON objects serialized as text in the result’s
content, except the two preview tools, which return an HTML document as text. - URLs in results (
editUrl,presentUrl,downloadUrl) are only included when the instance hasAPP_URLorDOMAINconfigured. vendoron the AI tools overrides the configured LLM vendor for that call, e.g."openai"or"anthropic".langtakes a deck language code such asnl,en-GBorde.
The R/O column marks read-only tools, which stay available during maintenance.
Slide types and themes
Section titled “Slide types and themes”| Tool | Permission | R/O | Parameters | Returns |
|---|---|---|---|---|
get_slide_types | read | yes | category (structural, content, all; default all), lang (language of the example content) | types keyed by type, count, exampleLang, globalOptions |
list_themes | read | yes | none | themes: id, label, brandColors, hasBackgroundImages |
get_slide_types answers for the session’s organization: core types, file-based custom types, and the organization’s own published slide types (keyed custom-<slug>), minus the types the organization disabled. Types marked deprecated or opted out of AI use are left out. Each entry carries its canonical typeId, a schema derived from the type’s field definitions, and an example content object you can copy. documented: false means the type has no usage guidance yet. An optional usage string holds rules the organization set for filling that type; agents should follow it. globalOptions lists fields any slide may carry, such as a background image or logo.
Reading decks
Section titled “Reading decks”| Tool | Permission | R/O | Parameters | Returns |
|---|---|---|---|---|
list_presentations | read | yes | limit (default 50), ownership (owned, shared, all; default owned) | presentations (id, title, theme, createdAt, updatedAt, slideCount, permission on shared decks, editUrl), total, ownerFilter, ownership |
get_presentation | read | yes | id or presentationId | id, title, theme, lang, slides (index, id, type, content, notes), slideCount |
get_presentation_url | read | yes | id or presentationId | id, title, editUrl, presentUrl; a note instead of the URLs when no base URL is configured |
validate_presentation | read | yes | id or presentationId | slideCount, warningCount, warnings (per slide: slideIndex, type, title, warnings), isValid |
Shared decks (ownership: "shared" or "all") need the database storage backend.
Creating decks
Section titled “Creating decks”| Tool | Permission | R/O | Parameters | Returns |
|---|---|---|---|---|
create_presentation | ai | no | content (required: source text), title, theme (default default), lang (detected when omitted), speaker, ownerEmail, vendor | id, title, theme, slideCount, slides (index, type, title), editUrl, presentUrl |
create_presentation_from_slides | write | no | title (required), slides (required, 1-50 items of { type, content, notes? }), theme (default default), lang, ownerEmail, validation (strict or fix; default strict), auto_prepend_title (default false) | As create_presentation, plus lang, and appliedFixes when validation is fix |
duplicate_presentation | write | no | presentationId (required) | id, title, slideCount, editUrl of the copy |
create_presentation runs the AI generator over free text. create_presentation_from_slides writes the slides you pass without an AI pass: use it when the agent already knows the slide types and content.
Validation in create_presentation_from_slides checks exactly what each slide type declares:
strictstops at the first problem and writes nothing; the error message says which slide and field failed. A key the type does not declare is refused as an unknown field.fixapplies automatic fixes (truncating, padding, switching layout) and lists them inappliedFixes.
auto_prepend_title: true adds an empty title slide, using title and the theme’s default title slide type, when the first slide is not already one.
ownerEmail on both create tools overrides the session owner as the new deck’s owner.
Editing slides
Section titled “Editing slides”| Tool | Permission | R/O | Parameters | Returns |
|---|---|---|---|---|
add_slide | write | no | presentationId, type, content (all required), position (appends when omitted) | added, slideId, position, type, totalSlides |
update_slide | write | no | presentationId, slideIndex, content (all required), type | updated, slideIndex, type, content |
remove_slide | write | no | presentationId, slideIndex (both required) | removed, slideIndex, removedType, removedTitle, totalSlides |
reorder_slides | write | no | presentationId, fromIndex, toIndex (all required) | moved, slide (type, title), from, to |
convert_slide | ai | no | presentationId, slideIndex, targetType (all required), vendor | converted, slideIndex, fromType, toType, content |
update_slide merges content into the slide’s existing content and validates the result. Passing type converts the slide first, but only between the pairs the editor converts: content and image-text (both ways), image to image-text, list to content, and title and chapter title (both ways). Other pairs are refused; use convert_slide, which lets the AI restructure the content for any target type.
New slides from add_slide get the type’s defaults for fields you leave out.
Working on the whole deck with AI
Section titled “Working on the whole deck with AI”| Tool | Permission | R/O | Parameters | Returns |
|---|---|---|---|---|
append_slides | ai | no | presentationId, content (both required), vendor | appended, insertedAt, totalSlides, newSlides (type, title) |
iterate_presentation | ai | no | presentationId, command (both required), vendor | applied, targetSlideIndex, summary, modifications (slideIndex, action, reasoning), totalSlides |
compress_presentation | ai | no | presentationId (required), apply (default false), intensity (moderate or aggressive; default moderate), vendor | applied, merges, removals, recommendations, slidesAfter when applied |
analyze_presentation | ai | yes | presentationId (required), vendor | slideCount, suggestionCount, suggestions (slideIndex, category, body, proposedSlide) |
append_slidesgenerates content slides from new text and inserts them before any closing slides (payoff, end, follow-invite) at the end of the deck.iterate_presentationtakes an instruction in plain language, such as “make slide 3 punchier” or “split the KPI slide”, and saves the result.compress_presentationonly previews its merges and removals unlessapplyistrue; a preview needs read access, applying needs write access.analyze_presentationsuggests improvements and changes nothing.
Previewing and exporting
Section titled “Previewing and exporting”| Tool | Permission | R/O | Parameters | Returns |
|---|---|---|---|---|
preview_slide | read | yes | presentationId, slideIndex (both required) | A self-contained HTML document of one slide |
preview_presentation | read | yes | presentationId (required), slideRange (e.g. "0-4"; all slides when omitted) | A self-contained HTML document with the slides as a gallery |
export_presentation | export | yes | presentationId, format (both required), lang | id, title, format, downloadUrl, note |
The previews include their CSS and images, so a client can show them as an artifact. They need read, not export: they produce no file.
export_presentation does not render anything itself. It returns a URL on the instance that renders the file when opened in a browser signed in to Deckyard.
format | File |
|---|---|
pdf | Server-rendered PDF |
pptx | PowerPoint |
html | Self-contained HTML |
json | The deck source |
png-zip | One PNG per slide, zipped |
lang selects one language version of a multilingual deck.
Comments
Section titled “Comments”| Tool | Permission | R/O | Parameters | Returns |
|---|---|---|---|---|
list_comments | comments:read | yes | presentationId (required), status (open, resolved, dismissed, all; default all), slideId, since (ISO 8601), includeReplies (default false) | presentationId, presentationTitle, comments, total |
list_recent_comments | comments:read | yes | ownership (owned, shared, all; default all), authorEmail, status, since, limit (default 50, max 200) | comments, total, ownership, ownerFilter |
add_comment | comments:write | no | presentationId, body (both required; max 5000 characters), slideId | ok, comment |
reply_to_comment | comments:write | no | presentationId, commentId, body (all required) | ok, comment |
set_comment_status | comments:write | no | presentationId, commentId, status (resolved, open, dismissed; all required) | ok, comment |
- Comments are newest first. By default replies are nested under their comment;
includeReplies: truereturns them as separate rows. - Each comment carries the slide as it is now (index, type, title, or marked deleted), a snapshot of the slide from when the comment was written, and an
editUrlthat opens the editor on that slide. list_recent_commentscovers all decks the session can reach and, with the file storage backend, returns nothing.add_commentwith aslideIdanchors the comment to that slide and stores a snapshot of it.- Replying to a reply adds to the same thread: threads are one level deep.
set_comment_statusallows the same transitions as the app (open to resolved, open to dismissed, resolved to open) and only for the presentation’s owner or its original creator, who keeps that right after the deck changes owner.- The three write tools need the database storage backend and an acting user: an API key’s owner, or
DECKYARD_MCP_OWNER_EMAILover stdio. Comments written by an agent notify and update open editors like any other comment.
The matching REST endpoints are in Public API: Comments.
Deleting
Section titled “Deleting”| Tool | Permission | R/O | Parameters | Returns |
|---|---|---|---|---|
delete_presentation | write | no | presentationId, confirm (both required) | With confirm: true: deleted, id. Otherwise deleted: false with the deck’s title, slideCount and a message |
The deck is moved to trash. Only its owner can delete it; confirm: false is a dry run that tells the agent what it is about to delete.
Prompts
Section titled “Prompts”Prompts are parameterized workflows a client shows in its prompt menu (the / menu in Claude Desktop). The client collects the arguments, the server returns the instruction, and the model then drives the tools itself.
| Prompt | Does | Required arguments | Optional arguments |
|---|---|---|---|
create-presentation | Generate a deck from text, notes or a document | content | language, speaker |
create-from-structured-data | Build a deck from pre-structured slides, without AI rewriting | title, data | language |
improve-presentation | Analyze a deck and apply improvements | presentationId | focus |
refine-slide | Change one slide with a plain-language instruction | presentationId, instruction | |
compress-presentation | Make a deck shorter by merging or removing slides | presentationId | intensity |
add-content | Add slides to a deck from additional text | presentationId, content | |
deck-overview | Summarize a deck’s slides, theme and validation, or list decks | presentationId |
Adding tools in a fork
Section titled “Adding tools in a fork”A fork adds its own tools without editing core: a custom/mcp-tools.js that default-exports a registrar is loaded by both transports.
export default function registerCustomTools(server, ctx) { server.tool( 'my_tool', 'What the tool does, written for the agent', { type: 'object', properties: { presentationId: { type: 'string' } }, required: ['presentationId'], }, async ({ presentationId }, context) => { // ctx.storageScopeOf(context) is the scope every storage call takes return { url: ctx.presentationUrl(presentationId, 'present') }; }, { permission: 'read', readOnly: true }, );}ctx provides repoRoot, defaultOwnerEmail, getOwner(context), storageScopeOf(context), getAppBaseUrl() and presentationUrl(id, mode).
Two defaults fail closed:
- Declare
permission. A tool without one is hidden from, and refused to, every API key. Over stdio it still works, which makes the omission easy to miss. - Declare
readOnly: truefor a tool that changes nothing, or it is refused during maintenance.
Registering an existing name replaces that tool, which is how a fork wraps a core tool. The replacement keeps the original’s permission and readOnly unless it restates them, and removing a permission this way throws.
Related
Section titled “Related”- Public API - the REST interface with the same keys and permissions
- Quickstart: letting an AI agent do it