Skip to content

REST + MCP API

Copy page

Base: http://127.0.0.1:7526/api/v1

Auth: Local loopback = owner (zero-auth). Hosted: better-auth session cookie (web GitHub sign-in) or Bearer better-auth API key (CLI owner key / project-scoped agent key from plandesk connect --to). Optional HTTP basic via PLANDESK_AUTH_PASSWORD.

MethodPathPurpose
GET/healthHealth check { ok: true }
GET/projectsList projects
POST/projectsCreate project { name, description? }
GET/projects/:idProject detail + summary counts
GET/projects/:id/canvas{ nodes, edges, layout }
PUT/projects/:id/canvasUpsert nodes, edges, layout
PATCH/projects/:idRename / update { name?, description? }
DELETE/projects/:idDelete project (cascades children)
GET/projects/:id/documentsDocument tree
POST/projects/:id/documentsCreate doc { title, body, link_to? }
GET/documents/:idDocument body, plus links and backlinks
PATCH/documents/:idUpdate title/body/status
DELETE/documents/:idDelete document
GET/projects/:id/tasksTask list (filter query params)
POST/projects/:id/tasksCreate task { label, status?, goal_id?, … }
PATCH/tasks/:idUpdate status, label, description, position
DELETE/tasks/:idDelete task (cascades its edges)
GET/tasks/:id/backlinksTasks and documents pointing at this task
GET/projects/:id/edgesList edges
POST/projects/:id/edgesCreate edge { from_type, from_id, to_type, to_id, label?, … }
DELETE/projects/:id/edges/:edgeIdDelete one edge
POST/projects/:id/goalsCreate goal { objective, verification_surface?, … }
GET/projects/:id/goalsList goals for a project
GET/goals/:idGoal detail incl. cycle_tasks
PATCH/goals/:idEdit goal contract fields
POST/goals/:id/pausePause an active goal
POST/goals/:id/resumeResume a paused goal
POST/goals/:id/completeComplete goal { evidence? }
POST/documents/:id/commentsAdd comment { body, passage? }
GET/documents/:id/commentsComments for a document
POST/tasks/:id/commentsAdd comment on a task
GET/tasks/:id/commentsComments for a task
POST/notes/:id/commentsAdd comment on a note
GET/notes/:id/commentsComments for a note
POST/submissions/:id/commentsAdd comment on a submission
GET/submissions/:id/commentsComments for a submission
GET/projects/:id/commentsComments across a project
POST/projects/:id/artifact-commentsAnnotate a file { artifact_id, body, passage?, anchor? }
GET/projects/:id/artifact-commentsAnnotations for a file ?artifact_id=…&include_resolved=
PATCH/comments/:idEdit / resolve { body?, resolved? }
DELETE/comments/:idDelete a comment (UI only)
POST/projects/:id/filesUpload a file { filename, mime, content_base64 } (≤10MB) → { id, url, … }
GET/files/:idFetch a file; image/* renders inline, everything else downloads
GET/projects/:id/artifactsList artifact summaries { id, title, kind, updated_at }
POST/projects/:id/artifactsCreate artifact { title, kind?, content? }
GET/artifacts/:idGet artifact incl. full content
PATCH/artifacts/:idUpdate { title?, kind?, content? }
GET/share/:token.mdAgent-ready Markdown for a shared task/document (404 unknown, 410 expired/revoked)
GET/projects/:id/agent-runsList agent runs (with nested progress events)
POST/agent-runs/:id/progressAppend a progress event to a run

A Goal is a durable, graph-native goal-altitude node. Every task belongs to a Goal (tasks.goal_id, NOT NULL). A project gets a default General goal; task-creation surfaces attach to it unless a goal_id is given. Goals decompose into cycle-sized tasks; get_next_task walks the active Goal’s frontier.

Goal fields: objective, verification_surface (JSON string — one of {"kind":"gate_command","command":"..."}, {"kind":"acceptance_checklist","items":[{"criterion":"..."}]}, {"kind":"human_sign_off"}), constraints, boundaries, iteration_policy, stop_condition, budget, status (active | paused | complete | blocked), last_verification ({ at, green, kind, detail? } or null).

Evidence-based completion: a Goal completes only when all its cycle-tasks are done and its verification_surface is observed green. The runner submits evidence to POST /goals/:id/complete; the API never runs the gate itself. Red evidence blocks the Goal and files a scope remediation task.

Comments are polymorphic: a single comments table keyed by target_type (document | task | note | submission | artifact) + target_id, plus project_id, passage, anchor, body, resolved. passage anchors a comment to a text selection; anchor holds a W3C Web Annotation selector (JSON) for artifact annotations that re-render. An artifact target is a file previewed via plandesk <file> — it is project-scoped (the file identity is the target_id), so its endpoints live under /projects/:id/artifact-comments. Comment bodies are HTML — the composer is a full editor (formatting, inline images, the same annotation overlay as the document editor).

POST /projects/:id/files uploads bytes (base64, ≤10MB) through a pluggable StorageAdapter — the default adapter stores content-addressed BLOBs in the workspace DB, so a self-hosted install needs no object storage. GET /files/:id serves the file back: image/* renders inline (Content-Type set, safe to embed), everything else forces a download (Content-Disposition: attachment) so an uploaded file can never execute as active content in the browser. Embed the returned url as ![alt](url) in a document, task, or comment body instead of inlining base64 — keeps bodies lean.

An artifact is a stored agent deliverable — a Markdown report, an RFC, an HTML diagram — distinct from the artifact-typed comment target above (which annotates any file the agent wrote, artifact or not). Artifacts are first-class rows (title, kind: markdown | html, content) so a human can annotate one with plandesk <file> and the agent can revise it in place with the same artifact_id: list_artifact_comments → address feedback → update_artifact.

GET /share/:token.md returns a single task or document as agent-ready Markdown — linked documents inlined, an instruction at the top to fetch every embedded image, relative URLs absolutized. The link is minted via the MCP create_share_link tool — or the REST routes POST /tasks/:id/share and POST /documents/:id/share — and reuses the same shares table and ClientView projection as the client-collaboration portal — scoped by policy to exactly one resource — and defaults to a 24h TTL (never disables expiry). Use it to give a delegated worker full context via a URL without granting it MCP access.

Endpoint: http://127.0.0.1:7526/mcp/ (Streamable HTTP transport)

Auth: same as the REST API above — local loopback needs no header at all; hosted needs Authorization: Bearer <better-auth API key> (the CLI owner key from plandesk login, or the project-scoped agent key plandesk connect --to writes to .plandesk/token).

ToolPurpose
list_projectsList accessible projects
get_projectTasks, docs summary, canvas snapshot
create_projectCreate a new project
scaffold_project_from_planCreate a project + tasks + edges + docs in one call
create_goalCreate a goal-altitude node with optional verification surface
get_goalGoal detail incl. cycle_tasks
list_goalsList goals for a project
pause_goalPause an active goal
resume_goalResume a paused goal
complete_goalComplete a goal with optional verification evidence
get_next_taskNext actionable todo on the active goal frontier (optional goal_id; reasons no_active_goal, multiple_active_goals)
claim_taskAtomically claim a task for an agent — guards against two agents taking the same one
create_taskAdd canvas node + task row (optional goal_id, tags)
update_taskStatus, label, description, position, tags (replaces set)
get_taskFetch a single task by id
list_tasksProject tasks, filterable by status and tags (OR)
create_documentMarkdown body; link_to takes one id or a list of task/document ids
update_documentPatch title/body/status line, folder, and link_to (task or document ids; unlink with delete_edge)
get_documentFetch a document, with links (outgoing) and backlinks (incoming); each entry carries an edge_id
list_documentsProject documents as a tree; filter by folder_id
create_folderCreate a document folder (optionally nested)
update_folderRename / re-parent a folder (cycles rejected)
create_noteCreate a free-form project note (Markdown body)
update_notePatch a note’s title or body
get_noteFetch a note by id
list_notesProject working notes
list_tagsProject tags (id, name, color)
create_edgeLabeled link between any two entities via from_type/from_id/to_type/to_id (task or document)
list_edgesEvery edge in a project, with typed endpoints
delete_edgeRemove one edge by edge_id (from a get_document links entry or list_edges); siblings are untouched
attach_fileUpload a file (image today), get back { file_id, url } to embed as ![alt](url)
create_artifactStore an agent deliverable (report, RFC, HTML diagram); returned artifact_id doubles as the comment target
get_artifactFetch a stored artifact by id, including full content
update_artifactRevise a stored artifact’s title, content, or kind
list_artifactsList artifact summaries for a project (id, title, kind, updated_at)
create_share_linkMint a public, expiring Markdown link for one task or document { url, markdown_url, expires_at }
list_commentsProject comments; filter by target_type, target_id, include_resolved
add_commentLeave a comment { target_type, target_id, body, passage? }
resolve_commentMark a comment resolved (no delete tool)
list_artifact_commentsAnnotations on a file artifact { project_id, artifact_id, include_resolved? }
add_artifact_commentAnnotate a file artifact { project_id, artifact_id, body, passage?, anchor? } (anchor = W3C selector JSON)
start_agent_runBegin external agent session
record_agent_progressAppend progress event
complete_agent_runClose run (completed or failed)
sync_pullFetch participant submissions into the triage inbox
list_submissionsList pulled submissions (triage inbox)
triage_submissionAccept a submission → real task (or reject)

45 tools in total. The last three are the collaboration tier — sharing a project with a client or team (create_share_link is a separate, lighter-weight primitive for handing one resource to a worker — see Share links above). At session start, list tools before calling them. Resolve the project from .plandesk/config.json when present — do not guess IDs. To stand up a whole plan at once use scaffold_project_from_plan; to execute it, loop get_next_taskupdate_task within a Goal. There is no delete tool by design — resolve comments rather than deleting them.

  • Unknown project → tool error not_found
  • Invalid status enum → invalid_argument
  • Token revoked → HTTP 401

Programmatic access without Claude/Codex: install @plandesk/mcp-client from npm (or use packages/plandesk-mcp-client from a cloned repo) with PLANDESK_URL and PLANDESK_MCP_TOKEN.