OMA protocol reference · MCP server 0.2.0
Everything the agent needs to work here.
Everything an authorized model needs to read and write ohmyartifact artifacts: the markdown formats, the overlay JSON schema, and each MCP tool with its parameters, results, and errors. Style rules are in the hosted connection’s authoring guide, oma://guide.
endpoint https://ohmyartifact.com/mcptransport Streamable HTTP · JSON responses · statelessaccess Authorization: Bearer <agent key>
Overview
ohmyartifact is a hosted workspace for slide decks and documents. Every artifact is stored in the account’s database-backed workspace as one markdown string. A person edits it on the canvas; an authorized model edits it over MCP. Both write the same artifact, so edits from either side stay in sync.
- Two kinds of artifact:
document (one GFM markdown file) and slideshow (Marp-style: slides separated by ---, each with an optional ```overlays JSON block).
- OMA hosts the MCP service. Each connection is authenticated to a workspace and receives the authoring guide as its instructions on connect.
- Every write is optimistically concurrent: it carries the
version you last read and is rejected as stale version if anyone saved in between. Every read and write returns the current version.
- Every write is validated as a whole: an invalid overlay or an unsupported mermaid line rejects the entire write with one message per problem, and nothing is saved.
Connect
Sign in to your OMA workspace and choose Connect an agent. Pick the agent, and OMA issues that agent its own revocable key inside a ready-to-paste setup. Agentic CLIs (Claude Code, Codex, Gemini CLI) install the server themselves from the pasted text; Cursor and custom clients get the config entry. Every agent works from the same endpoint and the same bearer key shape, so nothing below is client-specific.
# Claude Code — the setup prompt runs this for you
claude mcp add --scope user --transport http ohmyartifact https://ohmyartifact.com/mcp \
--header "Authorization: Bearer oma_mcp_…"
# Codex — ~/.codex/config.toml
[mcp_servers.ohmyartifact]
url = "https://ohmyartifact.com/mcp"
http_headers = { Authorization = "Bearer oma_mcp_…" }
# Gemini CLI
gemini mcp add --transport http ohmyartifact https://ohmyartifact.com/mcp \
--header "Authorization: Bearer oma_mcp_…"
# Cursor (~/.cursor/mcp.json) and most other MCP hosts
{ "mcpServers": { "ohmyartifact": {
"url": "https://ohmyartifact.com/mcp",
"headers": { "Authorization": "Bearer oma_mcp_…" } } } }
# Your own agent, no SDK: one POST per JSON-RPC message
curl -sS https://ohmyartifact.com/mcp \
-H "Authorization: Bearer oma_mcp_…" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'
- The server is stateless: no
Mcp-Session-Id is required, and a restart never strands a client. Send initialize once per process to receive the authoring guide as instructions.
- POST responses are application/json, never an SSE stream, so a minimal client can parse them directly. An
Accept header of application/json alone is fine.
- A missing or revoked key answers
401. Keys never expire on their own; rotate or revoke them from Connect an agent, per agent.
The loop
list_artifacts (optionally with a query) to find an id, then read_artifact. Keep the version; for a deck, the outline gives you slide numbers.
- Pick the narrowest write:
edit_artifact for a sentence, replace_slide for one slide, insert_slide for a new one, update_artifact only to rewrite most of it.
- Send the version you read. On
stale version, read again, reapply your change to the fresh text, and write once more. Do not loop.
- On
invalid content, fix the fields named in the message and retry. Never drop an element to make an error go away.
Slide decks
A deck is one markdown string. Slides are separated by a line containing only ---. Each slide is markdown, optionally followed by exactly one fenced ```overlays block holding a JSON array of positioned elements on a 1280×720 canvas.
### Summary
# Three numbers that matter
Availability beat the SLO for the fourth straight quarter while request volume grew 2.4×.
```overlays
[
{"type":"stat","slot":"bottom","value":"99.97%","caption":"Availability · SLO 99.9%","color":"terracotta"},
{"type":"stat","slot":"bottom","value":"184 ms","caption":"p95 latency · −31% QoQ"},
{"type":"stat","slot":"bottom","value":"0","caption":"Sev-1 incidents · 2 last quarter"}
]
```
---
### Incidents
# One root cause behind both Sev-2s
…
Parsing rules
- A leading YAML frontmatter block (
--- … --- at the very top) is stripped and ignored.
- A
--- line inside any fenced code block is content, not a separator. Empty chunks between separators are dropped. A deck always has at least one slide.
- The overlays block is
```overlays on its own line, the JSON array, then ```. Only one per slide is parsed; whatever precedes and follows it is the slide body.
- A slide's body starts with an optional
<!-- background: URL --> line for a full-bleed background image (URL or data: URL). Removing the line removes the image.
Stored form
Every write re-serializes the deck: each slide is its trimmed body, then (if present) a blank line and the overlays fence with every element given an id; slides are joined with \n\n---\n\n and the deck ends with a newline. read_artifact returns this form, so old_text for edit_artifact must be copied from it.
How slide markdown renders
| Markdown | Renders as |
### Label directly above # Title | Small uppercase accent eyebrow. |
# Title | Serif headline; one per slide. |
| paragraph, bullets | Body copy in the left column at 22px. The body narrows to the left column when anything sits in a right-side slot. |
> quote | Accent callout. |
| pipe table | Card table. |
```mermaid | A diagram, converted to ```drawing on save. |
<!-- background: URL --> | Full-bleed background image behind the slide. |
Slide tools (replace_slide, insert_slide) take the markdown of one slide, without --- separators, and validate it the same way.
Overlays schema
The ```overlays block is a JSON array. Each element is one of five types, selected by type. The full JSON Schema (draft 2020-12) is served as the resource oma://schema/overlays; the server validates every write against it, choosing the variant by type so errors name the field that is wrong. Elements without an id get one on save. Keep the id of any element you were given so the app can tell an edit from a replacement.
Placement (all types)
| Field | Type | Description |
| id | string | Stable id; generated when omitted. Preserve it on edits. |
| slot | enum | Where to place the element. Use this instead of coordinates. Several elements in bottom sit side by side; several in right stack. |
| x | number 0–1280 | Left edge in px on the 1280×720 canvas. Only for exact placement; an element with x/y instead of slot was pinned by a person — leave it alone. |
| y | number 0–720 | Top edge in px. |
| width | number > 0 | Optional explicit width. |
| height | number > 0 | Optional explicit height. |
| rotation | number = 0 | Degrees clockwise. |
Slots
righttop-rightbottombottom-leftbottom-right
text
| Field | Type | Description |
| typerequired | "text" | |
| textrequired | string, non-empty | The text. |
| fontSize | number 12–120 = 32 | A label such as INTERNAL or DRAFT is a text in top-right at 16. |
| color | tone = none | none is plain text; terracotta an accent callout (at most one per slide); ivory/sand a neutral card; ink a dark card; blue/green/amber/red a tag or status. |
{"type":"text","slot":"top-right","text":"INTERNAL","fontSize":16,"color":"blue"}
stat
| Field | Type | Description |
| typerequired | "stat" | |
| valuerequired | string, non-empty | The big number, e.g. "99.97%". |
| caption | string | Small label under the value; carries the unit and the delta. |
| color | tone = ivory | Card tone. |
{"type":"stat","slot":"bottom","value":"184 ms","caption":"p95 latency · −31% QoQ"}
panel
| Field | Type | Description |
| typerequired | "panel" | |
| bodyrequired | string, non-empty | Markdown: paragraphs, **bold**, *italic*, `code`, ~~struck~~, - bullets, 1. steps. An indented line under a list item is that item's caption. |
| eyebrow | string | Small uppercase label above the title, e.g. COST OF CHANGE. |
| meta | string | Small mono text right-aligned on the eyebrow row, e.g. 3 files. |
| title | string | Headline; one line, states the point. |
| footer | string | Mono line under a divider at the bottom, e.g. events › actions › providers. |
| color | tone = terracotta | Accent of the edge, eyebrow and code. ink makes the whole panel dark. |
| edge | "top" | "left" = top | Which side carries the accent line. |
{"type":"panel","slot":"right","eyebrow":"OPTION B","meta":"3 files","title":"Read-through cache",
"body":"- Adds one hop on a miss\n ~4 ms at p95\n- Invalidation stays in the writer",
"footer":"edge › cache › api","color":"blue","edge":"left"}
image
| Field | Type | Description |
| typerequired | "image" | |
| srcrequired | string (uri) | Image URL or data: URL. |
| alt | string | Alternative text. |
{"type":"image","slot":"right","src":"https://example.com/architecture.png","alt":"Request path after the cache"}
card
| Field | Type | Description |
| typerequired | "card" | |
| itemsrequired | array of text | stat | panel | Independent items laid out side by side. Items carry their own type-specific fields (and an optional id) but no placement: no slot, x, y, width, height or rotation. |
| tone | tone = none | Background box around all items; none means no box, the items float together. |
{"type":"card","slot":"bottom","tone":"sand","items":[
{"type":"stat","value":"2.4×","caption":"Request volume YoY"},
{"type":"stat","value":"+0.8%","caption":"Infra spend QoQ","color":"green"},
{"type":"text","text":"Reserved capacity absorbed the growth.","fontSize":20}
]}
Tones
The one enum behind color on text/stat/panel and tone on card:
noneplain, no box
ivoryneutral card
sandneutral card, warmer
inkdark card, once per deck
terracottathe accent, once per slide
bluetag / status
greentag / status
ambertag / status
redtag / status
Validation
- The block must be a JSON array of objects. Each object's
type selects its schema; an unknown type is rejected.
- Required fields, enums and numeric ranges are enforced exactly as listed above. Fields outside the schema are not validated.
- Failures are collected per element and reported together as
invalid content, prefixed with slide N: in deck-wide writes. Nothing is saved.