MCP Tool Reference
The relay embeds a Model Context Protocol server so external AI agents (Claude Code/Desktop, Cursor, Zed, ChatGPT, Notion, and any other MCP-capable client) can create and manipulate DocuShark documents — not just edit existing ones. This is the "publish target" surface: an agent can author a real document, with prose and diagrams, and a human drops into the editor when they want to touch it visually.
This page is the authoritative reference for the MCP surface. The REST/WS wire spec lives at REST API Reference.
Endpoint & transport
- HTTP endpoint:
POST /mcp. JSON-RPC 2.0 over HTTP (streamable-HTTP MCP). - Send
Accept: application/json. If you sendAccept: text/event-stream, the server replies with SSE framing; plain JSON clients should not advertise the event-stream type.
Where it listens — [mcp].expose
expose | Where /mcp lives | Use |
|---|---|---|
local (default) | a loopback-only listener on [mcp].port (default 9877) | desktop / self-host on one machine |
public | folded onto the relay's main HTTP listener (the one already serving /ws + REST, [server].port) | a relay reachable over the network, so a remote MCP client can connect |
With expose = "public" there is no second port or TLS endpoint: /mcp and its discovery doc ride the relay's real origin. [mcp].port is ignored.
Authentication
Every request carries Authorization: Bearer <token>. Two credential types are accepted, tried in this order:
- Static MCP token — a per-host bearer (generated on first run, persisted to
mcp_tokenunder the data dir, logged at startup). Authenticates as the single-tenant (default) workspace. This is the desktop / local default. Refused whenexpose = "public"— it resolves to the catch-alldefaultworkspace, so a network-reachable (potentially multi-tenant) pod accepts only the JWT below. - Relay app token (JWT) — an RS256 token minted by the OIDC issuer; validated via JWKS. The workspace is taken from the token's
wspclaim (see Token Format). This is how a multi-workspace deployment scopes an agent to one workspace.
A missing or invalid credential returns 401 with no disambiguation.
OAuth discovery (self-serve auth)
So an MCP client can authenticate without a hand-pasted token, /mcp advertises its authorization server per the MCP auth profile (2025-06-18):
- An unauthenticated
/mcprequest returns401withWWW-Authenticate: Bearer resource_metadata="<origin>/.well-known/oauth-protected-resource". GET /.well-known/oauth-protected-resource(RFC 9728) returns the resource identifier (<origin>/mcp) and the configured authorization server ([auth].issuer). The client runs the auth-code + PKCE dance there and comes back with a JWT. The discovery URLs echo the origin the request arrived on (honoringX-Forwarded-Protobehind a TLS-terminating proxy), so they're correct whether reached on loopback or a public host.
Document model
A DocuShark document carries two surfaces in one object:
- Canvas pages (
pages) — the diagram: shapes and connectors. - Prose pages (
richTextPages) — the written body, stored as HTML.
A document may also carry a reference library (references) — citations as CSL-JSON keyed by id, with an explicit display order — manipulated by the citation tools below.
The MCP tools operate on both. Prose is authored in Markdown by default (agents produce it reliably) and rendered to the HTML the editor persists; pass format: "html" to supply HTML directly.
Tools
All tools are namespaced docushark_*.
Read
| Tool | Purpose |
|---|---|
list_documents | List documents in the workspace (id, name, modifiedAt, source, and page counts). pageCount is the canvas + prose total; canvasPageCount and prosePageCount give the breakdown (a document has a diagram canvas and a separate prose body). |
get_document | Document metadata + canvas pages summary + prosePages summary + fields (the document's values). The map of what exists. |
get_page | The shapes on one canvas page, as DSL objects. |
get_shape | One shape on a page, by id, as a DSL object (the read-one companion to get_page). |
get_prose | All prose pages (or one, with pageId): id, name, order, HTML content. |
get_outline | A prose page's heading outline: ordered { index, level, title, id } (id is the heading's durable block id). index is used by the structural tools. |
list_references | The document's reference library as CSL-JSON in display order, plus the active style. |
resolve_doi | Resolve a doi to a CSL-JSON reference via doi.org content negotiation, without writing — preview before add_reference. |
list_fields | The document's fields (reusable values) in display order, each { name, value }. |
get_skills | Agent onboarding: with no args, the content contract (rules for valid prose + shapes) plus a recipe catalogue; with { skill }, that recipe's full steps. Call it first if unsure how a tool expects its input — authoring valid content avoids the relay having to heal it. |
list_icons | Discover icon IDs to put on shapes: { id, name, category } entries plus the match total and available categories. Filter with query (substring over id + name) and/or category; cap with limit (default 50, max 200). Apply an id via add_shape/update_shape (iconId). |
list_files | The document's embedded files/blobs: { id, name, mimeType, size }. Companion to the blob://<hash> image refs get_prose returns. |
get_file | One embedded file by id/hash — its metadata (and content where applicable). |
get_table | A prose table's structure — rows, cells, and any colspan/rowspan/colwidth — for span-aware edits via edit_table. |
get_storage | The workspace's storage usage: blob + document bytes against the quota. |
Author
| Tool | Purpose |
|---|---|
create_document | Create a new document (one blank canvas page + one blank prose page). Returns { id, name }. The starting point for authoring from scratch. |
rename_document | Rename a document (set its title). Updates the title live for anyone who has the document open. Returns { id, name }. |
Prose (write)
| Tool | Purpose |
|---|---|
add_prose_page | Append a prose page. Markdown by default. |
set_prose | Write a prose page. Replaces the whole body by default; pass anchor (the current text of a block) or id (a durable blk-… block id) to replace only that block — a targeted edit. Markdown by default; format: "html" supplies styled HTML directly (marks, cell spans, task lists round-trip). |
insert_block | Insert a prose block at a position relative to a durable block id. |
delete_block | Delete a prose block by its durable id. |
move_block | Move a prose block to a new position by id. |
rename_prose_page | Rename a prose page. |
Structure (write)
| Tool | Purpose |
|---|---|
insert_section | Insert a heading (+ optional body) at start/end or after a heading index. |
restructure_outline | promote/demote a heading's level, or move a section to a new index. |
edit_table | Structural prose-table edits — add/remove rows & columns, merge/split cells, set headers or column widths (span-aware). Pair with get_table. |
Diagram (write)
| Tool | Purpose |
|---|---|
add_canvas_page | Append a blank canvas page; returns { id }. Target it with the returned id in add_shape(s)/generate_diagram to build a second diagram in the same document. |
rename_canvas_page | Rename a canvas page. |
add_shape | Add one shape (rectangle, ellipse, text, connector). Rectangles/ellipses may carry an icon: iconId (from list_icons), iconDisplayMode (inside/badge/icon-only), iconSize. |
add_shapes | Add many shapes in one all-or-nothing call. |
connect | Connect two existing shapes with a connector. |
update_shape | Patch an existing shape (x, y, w, h, text, style, and icon fields iconId/iconDisplayMode/iconSize — an empty iconId clears it). |
generate_diagram | Build a whole diagram from a nodes + edges graph; the relay auto-positions (layered with crossing minimization, or grid) and wires connectors to typed anchors with orthogonal obstacle-avoiding routing (routing: "straight" opts out). Writes the live Y.Doc on a resident doc (a connected editor sees the shapes immediately) — like the other shape tools. |
add_file | Embed a file (blob) in the document — a PDF, image, spreadsheet, or dataset — and sync the document's blob references. Companion to list_files/get_file. |
Icons on shapes. Call list_icons (filter by query/category) to find an id — e.g. cloud-aws/cloud-azure/cloud-gcp service icons for architecture diagrams, or devops/databases/languages/frameworks — then set iconId on a rectangle/ellipse via add_shape/update_shape. iconDisplayMode: "icon-only" makes a pure icon node (no fill/stroke, icon fills the bounds); inside (default) or badge keep the box. generate_diagram nodes don't take icons directly — build those with add_shape(s).
Manage (write)
| Tool | Purpose |
|---|---|
delete_shape | Delete a shape by id. Cascade-removes any connectors attached to it (start or end), so no dangling connectors are left; returns the ids actually deleted. |
delete_prose_page | Delete a prose page by id. Refuses to delete the last remaining prose page. In a connected editor the page's tab may persist until reload (the prose page list isn't yet live-synced); its content is cleared immediately. |
delete_canvas_page | Delete a canvas page by id. Refuses to delete the last remaining canvas page. Repoints the active page if the deleted one was active; a connected app reloads. |
delete_document | Permanently delete a whole document and all its pages, prose, and blobs — cannot be undone on the server. Anyone currently viewing it has it moved to their local Trash. Returns { deleted: id }. |
reorder_shapes | Set a page's z-order. order must be a permutation of the page's current shape ids (later ids render on top). |
reorder_prose_pages | Set the order of a document's prose pages. order must be a permutation of the current prose page ids. Tab order may update on reload (page list not yet live-synced). |
reorder_canvas_page | Set the order of a document's canvas pages. order must be a permutation of the current canvas page ids. Tab order updates on reload (page list not live-synced). |
Citations (write)
| Tool | Purpose |
|---|---|
add_reference | Add reference(s) to the document's library. Supply either doi (resolved via doi.org to CSL-JSON) or items (raw CSL-JSON object(s)). Dedups by DOI then id; returns the ids added + how many were skipped. |
This populates the library only — it doesn't yet insert an inline citation or bibliography into the prose (do that in the editor). The library is stored as the document's top-level references field and round-trips durably; a connected editor sees new references on reload, as references aren't live-synced yet. Formatting (CSL → APA/MLA/Chicago/Vancouver) is done in the editor, not the relay — the MCP surface only reads and writes CSL-JSON.
Fields (write)
| Tool | Purpose |
|---|---|
set_fields | Set/update document fields — reusable named values (e.g. Company → Acme Inc., Version → 2.0). Upsert by name: a new name is created, an existing name's value is replaced. Returns the names written + which were newly added. |
A field is a name → value pair that propagates everywhere it's referenced. To reference a field in prose, write in Markdown via set_prose/add_prose_page — it becomes a live field placeholder (<span data-field data-name="name">) that renders the current value and updates when the value changes. ( inside inline code or a code block stays literal.)
Fields are stored as the document's top-level fields object — { fields: { "<name>": { name, value } }, order: [...] } — and round-trip durably. On a connected (resident) document, set_fields writes the live CRDT library and broadcasts, so collaborators see new values immediately, per item (a concurrent editor's field isn't clobbered).
(Renames: rename_document, rename_prose_page, rename_canvas_page.)
Write feedback — the fixes array
Write tools succeed even when they have to adjust your input, and they say so rather than failing silently. When an adjustment happened, the result carries a fixes array; a clean write omits it entirely. Each entry has an action and a human reason:
dropped_unknown— an unrecognized key (e.g.fillColorforstyle.fill) was dropped. Thefieldnames it.dropped_invalid— a recognized field carried an unaccepted value (e.g.routingMode:"zigzag"), dropped to the field default.clamped— a value was coerced into range (e.g. headinglevel9 → 6,labelPosition1.5 → 1.0).- prose heals (
unwrapped/rebuilt_table/stripped_children) from the prose validator (set_prose / add_prose_page).
add_shapes tags each fix with the offending shape index. Read the array, correct the source, and re-send — nothing is silently lost. (Prefer authoring valid input up front; see get_skills.)
Concurrency
Live docs (a client is connected/editing). When a document is resident on the relay, writes apply to the authoritative Y.Doc and broadcast a CRDT delta, so connected editors see the change immediately (they merge it — no reload):
- Shape tools (
add_shape/add_shapes/connect/update_shape/delete_shape/reorder_shapes/generate_diagram) write the live shape map when the doc is resident and the target page is the active page. (Canvas page-list tools —add_canvas_page/rename_canvas_page— are JSON-only, like the prose page list; a new page's tab may lag until reload.) - Prose tools (
set_prose/add_prose_page/insert_section/restructure_outline/insert_block/delete_block/move_block) apply to the page's liveprose:<pageId>fragment when the doc is resident — so an agent's prose appears in a connected editor live, and an MCP read reflects an editor's un-snapshotted prose. (A newadd_prose_pagepage's tab may lag until the prose page list syncs; its content lands immediately.) - Whole-page prose writes are surgical (JP-441). Even a full
set_prosebody replace diffs against the live fragment and rewrites only the block window that actually changed — so an unchanged block's identity, a collaborator's caret and scroll position, and concurrent edits outside the changed window all survive. It is not a blunt whole-fragment swap. - Anchored / block-targeted edits (
set_prosewithanchororid, plusinsert_block/delete_block/move_block) touch only the addressed block.anchordoubles as a block-level compare-and-swap — it must match exactly one block, so a stale anchor is refused (ERR_ANCHOR_*) rather than clobbering drifted content. Durable block ids (blk-…, surfaced byget_outline/get_proseand auto-filled on write) are the drift-proof alternative; a stale id fails hard (ERR_ID_NOT_FOUND/ERR_ID_AMBIGUOUS/ERR_ID_ANCHOR_MISMATCH).
Cold docs (no client connected). Writes persist through an optimistic-concurrency check on the document's serverVersion: read the current version, apply, save only if it still matches, retrying on conflict — so a concurrent editor's change is never silently clobbered. The relay's snapshot flatten later projects live edits back into this JSON, so cold reads stay current.
Limits & current constraints
- Local documents are read-only via MCP. Renderer-owned (local) documents are mirrored read-only; writes target relay documents only, and a write to a local id is refused with a clear message.
- Prose is HTML. Markdown in is rendered (GFM tables / strikethrough / task-lists on); HTML pass-through is re-parsed by the editor against its schema on load, which drops anything unmodelled.
- Prose write limits. A single prose write's content is capped at ~1 MiB (
set_prose/add_prose_page/insert_section; over it →ERR_PROSE_TOO_LARGE, advertised asmaxLength). Nesting deeper than 64 levels is truncated (real prose nests <~10) — a safety bound so pathological input can't exhaust the server. - Storage quota (shared meter). Document writes count against the workspace storage meter (blob bytes + document JSON bytes). A growing write that would exceed the workspace
quota_bytesis refused withERR_STORAGE_QUOTA; a single document over the per-document ceiling (max_doc_bytes) is refused withERR_DOC_TOO_LARGE. Shrinking/equal writes and deletes always land. See Token Format. - Outlines are flat. A section is a heading plus the content up to the next heading; nesting is conveyed by
level, not containment.movemoves a single section, not its descendants. generate_diagramlayout is relay-side and self-contained. Layered mode runs a full Sugiyama pipeline (cycle handling, crossing minimization, deterministic coordinates); connectors attach to typed anchors and are routed orthogonally around intervening shapes with explicitwaypoints(routing: "straight"opts out), so the document looks right before any editor opens it. The same input always reproduces the same geometry. The editor re-routes connectors with the same algorithm when shapes move. Connectors route around nodes, not around each other — heavily parallel edges can still overlap. Node caps: 500 nodes / 1000 edges per call.- Rate limits (per workspace). Writes draw from the shared write bucket (
[tenancy.limits] writes_per_sec/writes_burst, shared with WS sync); reads draw from a separate bucket (reads_per_sec/reads_burst,0= unlimited). Over the bucket → HTTP429withERR_RATE_LIMIT. Tunable viaRELAY_*env on Cloud pods. - No open-in-editor link yet.
create_documentreturns the documentid; a deep link back into the editor is not yet wired.
Example: author a document from scratch
// 1. create
{"method":"tools/call","params":{"name":"docushark_create_document",
"arguments":{"name":"Architecture RFC"}}}
// → { "id": "doc-…" }
// 2. discover the page ids
{"method":"tools/call","params":{"name":"docushark_get_document",
"arguments":{"docId":"doc-…"}}}
// → pages:[{id:"page-…"}], prosePages:[{id:"page-…"}]
// 3. write prose (Markdown)
{"method":"tools/call","params":{"name":"docushark_set_prose",
"arguments":{"docId":"doc-…","pageId":"<prose page>",
"content":"# Overview\n\n## Goals\n\n## Approach"}}}
// 4. draw a diagram
{"method":"tools/call","params":{"name":"docushark_generate_diagram",
"arguments":{"docId":"doc-…","pageId":"<canvas page>",
"nodes":[{"id":"client","label":"Client"},{"id":"relay","label":"Relay"}],
"edges":[{"from":"client","to":"relay","label":"WebSocket"}]}}}A runnable end-to-end smoke test of every tool lives in scripts/mcp-smoke.sh.
Per-provider verification
The bar for this surface is "smooth on every MCP provider," not "works on one." Each provider should be able to (a) create a document, (b) generate a structured diagram, and (c) restructure an outline. Status:
| Provider | Status |
|---|---|
| Claude Code | ✅ verified (create → prose → outline → diagram round-trip) |
| Claude Desktop | ☐ to verify |
| Cursor | ☐ to verify |
| Zed | ☐ to verify |
| ChatGPT (custom GPT) | ☐ to verify |
| Notion custom agents | ☐ to verify |
| Atlassian Rovo | ☐ to verify |
To add a provider: point it at /mcp on the relay (loopback http://127.0.0.1:9877/mcp with expose = "local", or the relay's origin https://<host>/mcp with expose = "public") with the bearer token, run the three acceptance steps, and tick the box.
