Skip to content

plans — full tool reference

Search, read, author and triage plans, ADRs and questions.

Endpointhttps://plans.mcp.devfellowship.com/mcp
Packagepackages/dfl-mcp-plans
Tools29
ToolDescription
search_plansHybrid semantic + keyword search over plans (and optionally ADRs) on plans.devfellowship.com. Blends pgvector cosine similarity with full-text tsvector ranking, so both exact keyword hits and conceptual paraphrases surface. Results are visibility-filtered to what YOU can read (your own personal plans + shared plans if you are member+). Use this to find prior art before drafting a plan, or to locate a plan by topic.
read_planRead a plan’s full markdown body plus metadata (status, version, ADR count, parent, tags, visibility/owner) by slug. Optionally pass a specific version; defaults to the latest. Visibility-enforced: if you cannot read the plan (e.g. it is someone else’s personal plan), this returns not-found.
list_plansList plans with optional filters (status, source, tag, has_children, has_pending_questions). Returns only plans YOU can read (your personal plans + shared plans if you are member+). Status is one of draft|fired|executing|done|archived. Use this to browse the inbox or filter by lifecycle state.
list_plan_tasksList the DevFellowship work tasks bound to a plan — the plan’s EXECUTION CHECKLIST, the read half of set_plan_tasks. Answers “what is still open on this plan”: by DEFAULT it returns only OPEN tasks (it hides done and no_longer_needed); pass include_finished:true for the whole bound set. The summary always counts ALL bound tasks, so you get “27 open of 33” plus a breakdown by status and by stage even when the rows are filtered. Each row carries identifier, name, live status, stage name, points, priority, owner, epic, acceptance criteria and updated_at, read from work.tasks with YOUR JWT (RLS applies). Rows are sorted by stage, then priority, then identifier. Visibility-enforced: a plan you cannot read returns not-found. Read-only — it never edits the plan or the tasks. Use it before set_plan_tasks (which REPLACES the whole list, so you need the current set first) and to report progress; use the work MCP’s update_task to advance a task.
list_plans_by_activityList the plans with ACTIVITY in a day window — the same rule as the calendar Day board (plans.devfellowship.com/calendar?view=day). Activity = the plan row (create, publish, status) plus every related entity: versions, comments, questions and answers, ADRs, child plans, bound tasks and bound content. The day is a calendar day in tz (default America/Sao_Paulo), not UTC. Window: date (one day, default today) OR from + to (inclusive, max 62 days). mode: “activity_on_day” (default — at least one event in the window; a plan touched yesterday and today is on both days) or “last_activity_on_day” (the LAST event of the plan is in the window: “plans updated for the last time yesterday”). owner: “me”, a user id, an e-mail, or a name/handle (“tainan” resolves to the profile and also matches the legacy literal owner); a name that matches two people is refused with the candidates. status: one value or a comma list. limit: 1-200, default 50. Each row: slug, title, status, owner, last_activity_at/kind, the events in the window (what happened), bound task counts by status (null = the task rail is unavailable, not zero), open and blocking question counts, and whether the latest body has a Verification section. Returns only plans you can read. Read-only. Use it to start a daily sweep, then list_plan_tasks and read_plan per plan.
create_planCreate a new plan (or upsert one by slug) on plans.devfellowship.com. The plan is owned by YOU (the calling user). visibility defaults to “shared” (member+ can see it); pass “personal” to keep it owner-only. Slug convention: YYYYMMDD-title-slug. Writing requires a member+ identity.
publish_planPublish (or re-publish) a plan body, creating a new version. Use this when you have edited a plan’s markdown and want to push the update. Upserts by slug: an existing plan keeps its owner and gets a new version; a new slug is created owned by you. Writing requires a member+ identity. ALWAYS pass base_body_sha256 (the body_sha256 read_plan returned for the version you edited) when updating an existing plan: it makes the publish a compare-and-swap that is REJECTED — with nothing written — if someone else published in the meantime, instead of silently erasing their version.
patch_statusTransition a plan’s lifecycle status: draft -> fired -> executing -> done (or archived). Transitioning to fired/executing is BLOCKED (409) when the plan still has unanswered blocking questions — answer them first. Use this when dispatching a plan (fired) or marking it complete (done).
set_linksReplace a plan’s external links list — the FIRST-CLASS sidebar links (Miro / Figma / GitHub / Epic / docs / any URL) rendered in the plans-app sidebar, SEPARATE from the URLs inside the markdown body. This REPLACES the entire list (not append) — pass the full desired set, or [] to clear all links. Owner-only (you must be the plan owner). kind is auto-detected server-side from the URL when omitted.
set_ownerReassign the owner column of plans to a canonical Supabase auth uid. Use this to (a) reconcile a legacy or non-uuid plans.owner value (an email, a handle, or a service name written by an older writer) to the auth uid that identifies the same person, or (b) fill in plans whose owner is NULL. plans.owner MUST hold the auth uid, because the canonical readability predicate plans.can_read_row matches owner = auth.uid()::text — a non-uuid owner therefore makes a personal plan unreadable by its own owner, which looks like the plan was deleted. Two modes, and you must pick one explicitly: pass slugs for TARGETED mode (reassigns exactly those plans, whatever their current owner — this is the mode that repairs a legacy owner), or pass only_null for FILTER mode (only_null: true touches only rows where owner IS NULL; only_null: false reassigns EVERY plan in the table and additionally requires confirm_reassign_all: true). If both slugs and only_null are given, slugs wins and the filter is not sent. Requires superadmin (IAM level >= 100) or a service viewer; the server answers 403 otherwise.
set_plan_tasksBind DevFellowship work tasks (work.tasks) to a plan — the plan’s EXECUTION CHECKLIST, rendered in the plans-app right sidebar with each task’s LIVE status and a done/total progress counter. The binding is stored in work.entity_connections (the first-class entity↔task join) and the panel reads each task’s name+status live from work.tasks. This REPLACES the plan’s task list (pass the full desired set, or [] to unbind all) but PRESERVES every other sidebar link (Miro/Figma/GitHub/…) — a task sync is never destructive to curated links. Allowed on a plan you own OR as an admin, since a checklist is execution state. The work MCP stays the source of truth for a task: create/advance tasks there (create_task / update_task) and the status flows in automatically. In the steady state you only need to pass each task_id; call this after every meaningful step so the plan reflects the current set of bound tasks.
list_adrsList architectural decision records (ADRs). Pass slug to list a single plan’s decisions (visibility-enforced — a plan you can’t read returns not-found), or omit it to list ADRs globally with optional filters (tag, decided_by). Use to review what was decided about a topic.
get_adrGet a single architectural decision record by plan slug + decision number (or id). Visibility-enforced via the parent plan. Use after list_adrs to read the full decision text.
create_plan_commentCreate a comment on a plan at plans.devfellowship.com — the same annotation the web UI produces with select-to-comment. Required: slug and body. Optional: selection_text (the highlighted text; defaults to “(plan)” for a plan-wide comment), selection_start, selection_end, version, and metadata (a JSON object for write attribution, e.g. the requesting human and channel for a bot). The author is taken from your authenticated session. Writing requires a member+ identity.
list_plan_commentsList the comments on a plan (the select-to-comment annotations from the web UI, plus any written by a bot). Returns id, version, selection_text, body, created_by, created_at and metadata. Optional version filter. Read-only; your normal plan visibility applies.
delete_plan_commentDelete one plan comment by id. The API allows the comment author or an admin; a comment you did not write is refused (403). Find the id with list_plan_comments. Returns the deleted row.
list_questionsList structured questions (blocking + non-blocking) with their options and current answers. TWO MODES: pass slug for ONE plan (grouped by round), or OMIT slug for the CROSS-PLAN query over every readable plan — that is how you answer “what is pending across all plans”. Filter with status (pending|answered|deferred|withdrawn|all; default pending), blocking, include_finished_plans, include_snoozed. Visibility-enforced: only questions on plans you can read. To fetch ONE question whose UUID you already have, use get_question — it needs no slug.
get_questionFetch ONE plan question by its UUID — no slug needed. Returns the question with its plan_slug, plan_title, options and answer history. Searches by identity, so it finds answered/deferred/withdrawn questions and questions on done or archived plans — all of which the pending inbox hides. Use this whenever you have a question id and do not know (or should not have to guess) which plan it belongs to. Visibility-enforced: a question on a plan you cannot read reports not-found rather than a permission error.
post_questionCreate a structured question on a plan (DTQ — drift/decision to question). Provide question_text and optional options [{letter,label,description}]. Set blocks_execution=true to make answering it a gate before the plan can be fired/executed. Writing requires a member+ identity.
update_questionEdit question_text and/or context on an existing question. Preserves its ID, status, options and answer history. Requires the plan editor identity. Other fields are rejected.
answer_questionRecord an answer to a plan question. THREE forms: (1) pick option letters — [“A”] single-select, [“A”,“B”] multi-select; (2) answer with FREE TEXT that rejects every offered option — pass [“OTHER”] and put the answer in freeform_notes, which records the question as answered just like a letter does; (3) pass [] with NO notes to clear an existing answer and put the question back to pending. Form 3 is a reset, not an answer — [] together with notes is rejected, because it would store the text and leave the question pending. The qid must be the UUID returned by list_questions.
withdraw_questionRetire an obsolete plan question by setting its status to “withdrawn”. Safe + idempotent: only questions still OPEN (pending or deferred) are withdrawn; answered or already-withdrawn questions are left intact. Use when a question no longer applies (e.g. the decision was resolved out of band). The question_id is the UUID returned by list_questions.
list_global_questionsList OPEN (pending, non-snoozed) questions across ALL readable plans — the cross-plan question inbox. Each item carries its plan_slug + plan_title so you can dedup before posting a new question. Visibility-enforced: only questions on plans you can read are returned. Same data as list_questions with no slug; widen beyond the open inbox with status (use “all”), include_finished_plans and include_snoozed. Use blocking=true to narrow to execution-gating questions only.
list_discord_channelsList the Discord channels the DevFellowship bot can see, grouped by category — the ONLY valid source of a channel_id for set_plan_discord_channel. A channel absent from this list cannot be bound (the server refuses ids that are not in it), so never paste, guess or infer a snowflake: call this first. Channels that look CLIENT-FACING (squad channels, client project categories) are tagged ⚠️ CLIENT-FACING — the DFL guild has client staff in those channels, so anything a plan posts there is seen by the client. Optionally filter with query (matches channel name AND category, accent-insensitive, e.g. “admin”, “squad”, “terravita”). Results are cached ~60s by the backend; pass refresh:true to force a live re-read.
set_plan_discord_channelBind a plan to ONE Discord channel so that EVERY COMMENT posted on that plan is announced in that channel (author + excerpt + link), or unbind it with channel_id: null. ⚠️ CONSEQUENCE, read before calling: this publishes plan activity to everyone in that Discord channel, and 15 of the DFL guild channels contain CLIENT staff — binding one of those means the client sees the plan’s comments. Rules: (1) channel_id MUST come from list_discord_channels — pasted, guessed or remembered snowflakes are refused server-side; (2) the PLAN OWNER or an admin (canonical IAM level >= 80) may bind or unbind — on SHARED plans; a personal plan is readable only by its owner, so it stays owner-only in effect; (3) binding a client-facing channel, or binding any channel on a personal plan, additionally requires confirm_client_exposure: true — this is the same blocking confirmation the web UI demands from a human, and you should only set it when the person you are acting for asked for THAT channel specifically. Unbinding is never blocked. Read the current binding with read_plan (it is in the plan metadata as discord_channel_id/discord_channel_name); this tool also reports the before → after transition.
search_entitiesFind a diagram, document, image or spec run across the DFL fleet and get its LOCATOR — the UUID (diagram / document / spec_run) or media id/URL (image) that attach_entity requires. This is the DISCOVERY step and the ONLY way to obtain that locator from inside the Plans MCP: diagrams and documents live in other apps, so you cannot attach an entity you have not looked up here. Never paste, guess or recall a UUID — a wrong-but-valid UUID silently points the plan at somebody else’s artifact. Omit query to list the most recently updated entities; omit type to search every searchable type at once (ux_path is not one of them — see type). Results are scoped to what YOU can see. Set include_latest_revision when you intend to PIN a diagram — it returns each diagram’s newest revision id, which is the rev attach_entity needs and which nothing else can give you.
list_plan_entitiesList the external entities (diagrams / documents / images) a plan references — the {{dfl-entity:…}} tokens embedded in its body, with each one’s type, locator, mode (live/pinned) and caption. Derived by parsing the plan body, so it is always in sync with what the plan actually says; it returns POINTERS, not the resolved diagram or document content. Read-only — it never edits the plan and never creates a version. Use it before attach_entity (to see what is already there — re-attaching is a no-op) and before detach_entity (to get the exact type + locator to remove).
attach_entityAttach an external entity (diagram / document / image / ux_path / spec_run) to a plan by inserting a {{dfl-entity:<type>:<locator>}} token into its body — without you having to republish the whole body. Get locator from search_entities first; do not guess a UUID. ATTACHING DOES CREATE A NEW PLAN VERSION, because adding a reference is an edit of the plan — that is expected and correct. What never versions the plan is the referenced entity’s own CONTENT changing: the body stores only the pointer, so the diagram or document stays live and the plan follows it with no new version. Idempotent — re-attaching the same type+locator changes nothing and creates no version. Requires edit rights on the plan (owner). To PIN a diagram to a fixed revision you must pass rev as well — mode: "pinned" on its own cannot be honoured and renders live content under a “pinning unavailable” badge.
detach_entityRemove an external entity reference from a plan — deletes the {{dfl-entity:<type>:<locator>}} token(s) from the body, leaving the rest of the plan untouched. This removes the POINTER only; the diagram / document / image / ux_path / spec_run itself is not deleted and stays in its own app — detaching a spec_run in particular does NOT discard the run, its items, its versions or its comments. Detaching DOES create a new plan version, because removing a reference is an edit of the plan. Idempotent — detaching something the plan does not reference changes nothing. Use list_plan_entities to get the exact type + locator. Requires edit rights on the plan (owner).

Search Plans

Hybrid semantic + keyword search over plans (and optionally ADRs) on plans.devfellowship.com. Blends pgvector cosine similarity with full-text tsvector ranking, so both exact keyword hits and conceptual paraphrases surface. Results are visibility-filtered to what YOU can read (your own personal plans + shared plans if you are member+). Use this to find prior art before drafting a plan, or to locate a plan by topic.

ParameterTypeRequiredDescription
querystringyesSearch query — keywords or a natural-language concept. Example: “auth gating for the plans MCP”.
typeenumnoWhat to search: “plan”, “adr” (decision records), or “all” (default). One of: plan, adr, all. Default: "all".
limitnumbernoMax results to return (default 25, max 100). Default: 25.
active_onlybooleannoWhen true, exclude done/archived plans (only draft/fired/executing surface). Default: false.

Read Plan

Read a plan’s full markdown body plus metadata (status, version, ADR count, parent, tags, visibility/owner) by slug. Optionally pass a specific version; defaults to the latest. Visibility-enforced: if you cannot read the plan (e.g. it is someone else’s personal plan), this returns not-found.

ParameterTypeRequiredDescription
slugstringyesThe plan slug, e.g. “20260619-plans-mcp-per-domain”.
versionnumbernoSpecific version number to read. Omit for the latest version.
metadata_onlybooleannoWhen true, return only metadata (no body fetch). Default: false.

List Plans

List plans with optional filters (status, source, tag, has_children, has_pending_questions). Returns only plans YOU can read (your personal plans + shared plans if you are member+). Status is one of draft|fired|executing|done|archived. Use this to browse the inbox or filter by lifecycle state.

ParameterTypeRequiredDescription
statusstringnoFilter by status. Single value or comma-separated, e.g. “draft” or “draft,executing”.
sourcestringnoFilter by source. Single value or comma-separated, e.g. “claude-main” or “claude-main,telegram”.
tagstringnoFilter to plans carrying this tag.
has_childrenbooleannoWhen true, only plans that have child plans.
has_pending_questionsbooleannoWhen true, only plans with pending (unanswered) questions.

List Plan Tasks

List the DevFellowship work tasks bound to a plan — the plan’s EXECUTION CHECKLIST, the read half of set_plan_tasks. Answers “what is still open on this plan”: by DEFAULT it returns only OPEN tasks (it hides done and no_longer_needed); pass include_finished:true for the whole bound set. The summary always counts ALL bound tasks, so you get “27 open of 33” plus a breakdown by status and by stage even when the rows are filtered. Each row carries identifier, name, live status, stage name, points, priority, owner, epic, acceptance criteria and updated_at, read from work.tasks with YOUR JWT (RLS applies). Rows are sorted by stage, then priority, then identifier. Visibility-enforced: a plan you cannot read returns not-found. Read-only — it never edits the plan or the tasks. Use it before set_plan_tasks (which REPLACES the whole list, so you need the current set first) and to report progress; use the work MCP’s update_task to advance a task.

ParameterTypeRequiredDescription
slugstringyesThe plan slug whose bound tasks to list.
include_finishedbooleannoInclude finished tasks (done + no_longer_needed) in the returned rows. Default false — the common question is what is still open. The counts in the summary cover every bound task either way. Default: false.

List Plans by Activity Day

List the plans with ACTIVITY in a day window — the same rule as the calendar Day board (plans.devfellowship.com/calendar?view=day). Activity = the plan row (create, publish, status) plus every related entity: versions, comments, questions and answers, ADRs, child plans, bound tasks and bound content. The day is a calendar day in tz (default America/Sao_Paulo), not UTC. Window: date (one day, default today) OR from + to (inclusive, max 62 days). mode: “activity_on_day” (default — at least one event in the window; a plan touched yesterday and today is on both days) or “last_activity_on_day” (the LAST event of the plan is in the window: “plans updated for the last time yesterday”). owner: “me”, a user id, an e-mail, or a name/handle (“tainan” resolves to the profile and also matches the legacy literal owner); a name that matches two people is refused with the candidates. status: one value or a comma list. limit: 1-200, default 50. Each row: slug, title, status, owner, last_activity_at/kind, the events in the window (what happened), bound task counts by status (null = the task rail is unavailable, not zero), open and blocking question counts, and whether the latest body has a Verification section. Returns only plans you can read. Read-only. Use it to start a daily sweep, then list_plan_tasks and read_plan per plan.

ParameterTypeRequiredDescription
datestringnoOne calendar day, YYYY-MM-DD. Default: today in tz. Not with from/to.
fromstringnoRange start, YYYY-MM-DD, inclusive. Needs to.
tostringnoRange end, YYYY-MM-DD, inclusive. Needs from. Max 62 days.
tzstringnoIANA timezone of the day boundary. Default America/Sao_Paulo.
modeenumnoactivity_on_day (default): at least one event in the window. last_activity_on_day: the plan’s last event is in the window. One of: activity_on_day, last_activity_on_day.
ownerstringnoOwner filter: “me”, a user id, an e-mail, or a name/handle such as “tainan”.
statusstringnoPlan status filter. One value or a comma list of draft|fired|executing|done|archived.
limitnumbernoMax plans returned (1-200, default 50).

Create Plan

Create a new plan (or upsert one by slug) on plans.devfellowship.com. The plan is owned by YOU (the calling user). visibility defaults to “shared” (member+ can see it); pass “personal” to keep it owner-only. Slug convention: YYYYMMDD-title-slug. Writing requires a member+ identity.

ParameterTypeRequiredDescription
slugstringyesUnique plan slug, e.g. “20260619-my-new-plan”.
titlestringyesHuman-readable plan title.
bodystringyesFull markdown body of the plan.
sourcestringnoOrigin, e.g. “claude-main”, “telegram”. Default server-side.
statusenumnoLifecycle status; defaults to draft when omitted. One of: draft, fired, executing, done, archived.
parent_slugstringnoSlug of a parent plan (must already exist).
tagsstring[]noTags, e.g. [“infra”,“plans”].
visibilityenumno”shared” (default) or “personal” (owner-only). One of: shared, personal.

Publish Plan

Publish (or re-publish) a plan body, creating a new version. Use this when you have edited a plan’s markdown and want to push the update. Upserts by slug: an existing plan keeps its owner and gets a new version; a new slug is created owned by you. Writing requires a member+ identity. ALWAYS pass base_body_sha256 (the body_sha256 read_plan returned for the version you edited) when updating an existing plan: it makes the publish a compare-and-swap that is REJECTED — with nothing written — if someone else published in the meantime, instead of silently erasing their version.

ParameterTypeRequiredDescription
slugstringyesThe plan slug to publish.
titlestringyesPlan title.
bodystringyesFull markdown body (a new version is stored).
sourcestringnoOrigin source. Default server-side.
statusenumnoOptionally set status while publishing. One of: draft, fired, executing, done, archived.
parent_slugstringnoParent plan slug (must exist).
tagsstring[]noTags array.
visibilityenumno”shared” or “personal”. One of: shared, personal.
base_body_sha256stringnoCONDITIONAL WRITE (recommended): sha256 hex of the plan body you started from — the body_sha256 field read_plan returns. The publish is rejected, writing nothing, if the current body no longer hashes to this. This is the authoritative guard: the plans-app writes version rows best-effort, so the body can change without latest_version moving.
base_versionnumbernoCONDITIONAL WRITE (convenience): the latest_version you read. Weaker than base_body_sha256 — supply both when you have them; the hash wins.

Patch Plan Status

Transition a plan’s lifecycle status: draft -> fired -> executing -> done (or archived). Transitioning to fired/executing is BLOCKED (409) when the plan still has unanswered blocking questions — answer them first. Use this when dispatching a plan (fired) or marking it complete (done).

ParameterTypeRequiredDescription
slugstringyesThe plan slug to transition.
statusenumyesTarget status: draft|fired|executing|done|archived. One of: draft, fired, executing, done, archived.

Set Plan Links

Replace a plan’s external links list — the FIRST-CLASS sidebar links (Miro / Figma / GitHub / Epic / docs / any URL) rendered in the plans-app sidebar, SEPARATE from the URLs inside the markdown body. This REPLACES the entire list (not append) — pass the full desired set, or [] to clear all links. Owner-only (you must be the plan owner). kind is auto-detected server-side from the URL when omitted.

ParameterTypeRequiredDescription
slugstringyesThe plan slug whose links to replace.
linksobject[]yesReplaces the plan’s entire links list (Miro/Figma/GitHub/Epic/docs/… or any URL). kind auto-detected server-side if omitted. Pass [] to clear.

Set Plan Owner (Backfill / Reconcile)

Reassign the owner column of plans to a canonical Supabase auth uid. Use this to (a) reconcile a legacy or non-uuid plans.owner value (an email, a handle, or a service name written by an older writer) to the auth uid that identifies the same person, or (b) fill in plans whose owner is NULL. plans.owner MUST hold the auth uid, because the canonical readability predicate plans.can_read_row matches owner = auth.uid()::text — a non-uuid owner therefore makes a personal plan unreadable by its own owner, which looks like the plan was deleted. Two modes, and you must pick one explicitly: pass slugs for TARGETED mode (reassigns exactly those plans, whatever their current owner — this is the mode that repairs a legacy owner), or pass only_null for FILTER mode (only_null: true touches only rows where owner IS NULL; only_null: false reassigns EVERY plan in the table and additionally requires confirm_reassign_all: true). If both slugs and only_null are given, slugs wins and the filter is not sent. Requires superadmin (IAM level >= 100) or a service viewer; the server answers 403 otherwise.

ParameterTypeRequiredDescription
ownerstringyesThe Supabase auth uid (uuid) to assign as the new owner. This is what plans.owner stores and what the readability predicate matches against (owner = auth.uid()::text), so it must be the auth uid — not an email, a handle, or a display name.
slugsstring[]noTARGETED mode: explicit plan slugs to reassign, regardless of their current owner. Max 200 per call (server-side limit). Provide this OR only_null.
only_nullbooleannoFILTER mode: true reassigns only plans whose owner IS NULL; false reassigns EVERY plan in the table (and then confirm_reassign_all must be true). Provide this OR slugs.
confirm_reassign_allbooleannoRequired safety confirmation. Must be true for the whole-table combination (only_null: false with no slugs), which rewrites the owner of every plan. Ignored otherwise.

Set Plan Tasks

Bind DevFellowship work tasks (work.tasks) to a plan — the plan’s EXECUTION CHECKLIST, rendered in the plans-app right sidebar with each task’s LIVE status and a done/total progress counter. The binding is stored in work.entity_connections (the first-class entity↔task join) and the panel reads each task’s name+status live from work.tasks. This REPLACES the plan’s task list (pass the full desired set, or [] to unbind all) but PRESERVES every other sidebar link (Miro/Figma/GitHub/…) — a task sync is never destructive to curated links. Allowed on a plan you own OR as an admin, since a checklist is execution state. The work MCP stays the source of truth for a task: create/advance tasks there (create_task / update_task) and the status flows in automatically. In the steady state you only need to pass each task_id; call this after every meaningful step so the plan reflects the current set of bound tasks.

ParameterTypeRequiredDescription
slugstringyesThe plan slug whose task list to replace.
tasksobject[]yesReplaces the plan’s entire task list (max 40). Pass [] to unbind all tasks. Other sidebar links are left untouched.

List ADRs (Decision Records)

List architectural decision records (ADRs). Pass slug to list a single plan’s decisions (visibility-enforced — a plan you can’t read returns not-found), or omit it to list ADRs globally with optional filters (tag, decided_by). Use to review what was decided about a topic.

ParameterTypeRequiredDescription
slugstringnoScope to a single plan’s ADRs. Omit for a global list.
tagstringnoGlobal mode: filter by tag.
decided_bystringnoGlobal mode: filter by who decided.

Get ADR (Decision Record)

Get a single architectural decision record by plan slug + decision number (or id). Visibility-enforced via the parent plan. Use after list_adrs to read the full decision text.

ParameterTypeRequiredDescription
slugstringyesThe plan slug the ADR belongs to.
numbernumbernoThe decision number within the plan (ADR-N).
idstring | numbernoThe decision record id (alternative to number).

Create Plan Comment

Create a comment on a plan at plans.devfellowship.com — the same annotation the web UI produces with select-to-comment. Required: slug and body. Optional: selection_text (the highlighted text; defaults to “(plan)” for a plan-wide comment), selection_start, selection_end, version, and metadata (a JSON object for write attribution, e.g. the requesting human and channel for a bot). The author is taken from your authenticated session. Writing requires a member+ identity.

ParameterTypeRequiredDescription
slugstringyesThe plan slug to comment on.
bodystringyesThe comment text.
selection_textstringnoThe exact text the comment is anchored to. Omit for a plan-wide comment; the API stores “(plan)”.
selection_startnumbernoAnchor start offset in the plan body.
selection_endnumbernoAnchor end offset in the plan body.
versionnumbernoThe plan version this comment is on.
metadataobjectnoOptional JSON object with write attribution. Not identity: created_by comes from the session. Example for a bot: {“source”:“discord”,“actor_slug”:“discord-dfl-clawd”,“requester”:{“username”:“x”,“id”:“1”},“channel”:{“name”:“commands”,“id”:“2”}}.

List Plan Comments

List the comments on a plan (the select-to-comment annotations from the web UI, plus any written by a bot). Returns id, version, selection_text, body, created_by, created_at and metadata. Optional version filter. Read-only; your normal plan visibility applies.

ParameterTypeRequiredDescription
slugstringyesThe plan slug.
versionnumbernoOnly comments on this plan version.

Delete Plan Comment

Delete one plan comment by id. The API allows the comment author or an admin; a comment you did not write is refused (403). Find the id with list_plan_comments. Returns the deleted row.

ParameterTypeRequiredDescription
slugstringyesThe plan slug that owns the comment.
comment_idnumberyesThe comment id to delete.

List Questions (one plan, or across all plans)

List structured questions (blocking + non-blocking) with their options and current answers. TWO MODES: pass slug for ONE plan (grouped by round), or OMIT slug for the CROSS-PLAN query over every readable plan — that is how you answer “what is pending across all plans”. Filter with status (pending|answered|deferred|withdrawn|all; default pending), blocking, include_finished_plans, include_snoozed. Visibility-enforced: only questions on plans you can read. To fetch ONE question whose UUID you already have, use get_question — it needs no slug.

ParameterTypeRequiredDescription
slugstringnoPlan slug to scope to. OMIT for the cross-plan query over all readable plans.
statusenumnoQuestion status to match. Default “pending” (the open inbox). Use “all” to search every status — a question you cannot find is very often answered. One of: pending, answered, deferred, withdrawn, all.
blockingbooleannoWhen true, return only questions that gate plan execution (blocks_execution).
include_finished_plansbooleannoWhen true, do NOT exclude questions on done/archived plans. Default false (finished plans are noise for direction-setting, but they DO hide questions).
include_snoozedbooleannoWhen true, include pending questions that are still snoozed (“ask me later”).

Get Question by UUID

Fetch ONE plan question by its UUID — no slug needed. Returns the question with its plan_slug, plan_title, options and answer history. Searches by identity, so it finds answered/deferred/withdrawn questions and questions on done or archived plans — all of which the pending inbox hides. Use this whenever you have a question id and do not know (or should not have to guess) which plan it belongs to. Visibility-enforced: a question on a plan you cannot read reports not-found rather than a permission error.

ParameterTypeRequiredDescription
question_idstringyesThe question UUID, in full (e.g. c541f864-88ea-4c57-af00-47f72f6033d3).

Post Plan Question

Create a structured question on a plan (DTQ — drift/decision to question). Provide question_text and optional options [{letter,label,description}]. Set blocks_execution=true to make answering it a gate before the plan can be fired/executed. Writing requires a member+ identity.

ParameterTypeRequiredDescription
slugstringyesThe plan slug to attach the question to.
question_textstringyesThe question text.
roundnumbernoQuestion round (default 1).
order_within_roundnumbernoOrdering within the round (default 0).
contextstringnoBackground context shown with the question.
author_reasoningstringno1-2 sentences on WHY this question was created (stored, not shown in UI).
multi_selectbooleannoAllow selecting multiple options (default false).
blocks_executionbooleannoWhen true, this question must be answered before the plan can transition draft->fired/executing.
recommended_option_letterstringnoRecommended option letter, e.g. “A”.
statusenumnoInitial status (default pending). One of: pending, answered, deferred, withdrawn.
optionsobject[]noAnswer options.

Update Plan Question Text

Edit question_text and/or context on an existing question. Preserves its ID, status, options and answer history. Requires the plan editor identity. Other fields are rejected.

ParameterTypeRequiredDescription
slugstringyesThe plan slug the question belongs to.
question_idstringyesThe existing question UUID.
question_textstringnoReplacement question text; must not be blank.
contextstringnoBackground context; an empty string clears it.

Answer Plan Question

Record an answer to a plan question. THREE forms: (1) pick option letters — [“A”] single-select, [“A”,“B”] multi-select; (2) answer with FREE TEXT that rejects every offered option — pass [“OTHER”] and put the answer in freeform_notes, which records the question as answered just like a letter does; (3) pass [] with NO notes to clear an existing answer and put the question back to pending. Form 3 is a reset, not an answer — [] together with notes is rejected, because it would store the text and leave the question pending. The qid must be the UUID returned by list_questions.

ParameterTypeRequiredDescription
slugstringyesThe plan slug the question belongs to.
question_idstringyesThe question UUID (from list_questions).
selected_option_lettersstring[]yesChosen option letters, e.g. [“A”] (single) or [“A”,“B”] (multi). Use [“OTHER”] with freeform_notes when the real answer is none of the offered options — that still records the question as answered. Use [] with no notes ONLY to reset an answer back to pending.
freeform_notesstringnoThe freeform text of the answer, or extra context alongside a letter. When this carries the actual decision, selected_option_letters must be [“OTHER”].
answer_textstringnoAlias of freeform_notes. Accepted so the text is never silently dropped.

Withdraw Plan Question

Retire an obsolete plan question by setting its status to “withdrawn”. Safe + idempotent: only questions still OPEN (pending or deferred) are withdrawn; answered or already-withdrawn questions are left intact. Use when a question no longer applies (e.g. the decision was resolved out of band). The question_id is the UUID returned by list_questions.

ParameterTypeRequiredDescription
slugstringyesThe plan slug the question belongs to.
question_idstringyesThe question UUID (from list_questions).

List Global Question Inbox

List OPEN (pending, non-snoozed) questions across ALL readable plans — the cross-plan question inbox. Each item carries its plan_slug + plan_title so you can dedup before posting a new question. Visibility-enforced: only questions on plans you can read are returned. Same data as list_questions with no slug; widen beyond the open inbox with status (use “all”), include_finished_plans and include_snoozed. Use blocking=true to narrow to execution-gating questions only.

ParameterTypeRequiredDescription
statusenumnoQuestion status to match. Default “pending” (the open inbox). Use “all” to search every status — a question you cannot find is very often answered. One of: pending, answered, deferred, withdrawn, all.
blockingbooleannoWhen true, return only questions that gate plan execution (blocks_execution).
include_finished_plansbooleannoWhen true, do NOT exclude questions on done/archived plans. Default false (finished plans are noise for direction-setting, but they DO hide questions).
include_snoozedbooleannoWhen true, include pending questions that are still snoozed (“ask me later”).

List Discord Channels

List the Discord channels the DevFellowship bot can see, grouped by category — the ONLY valid source of a channel_id for set_plan_discord_channel. A channel absent from this list cannot be bound (the server refuses ids that are not in it), so never paste, guess or infer a snowflake: call this first. Channels that look CLIENT-FACING (squad channels, client project categories) are tagged ⚠️ CLIENT-FACING — the DFL guild has client staff in those channels, so anything a plan posts there is seen by the client. Optionally filter with query (matches channel name AND category, accent-insensitive, e.g. “admin”, “squad”, “terravita”). Results are cached ~60s by the backend; pass refresh:true to force a live re-read.

ParameterTypeRequiredDescription
querystringnoOptional case- and accent-insensitive substring filter over channel name AND category. Omit to get the whole guild.
refreshbooleannoBypass the backend 60s cache and re-read the guild live. Use when a channel was just created or renamed; otherwise leave off.

Set Plan Discord Channel

Bind a plan to ONE Discord channel so that EVERY COMMENT posted on that plan is announced in that channel (author + excerpt + link), or unbind it with channel_id: null. ⚠️ CONSEQUENCE, read before calling: this publishes plan activity to everyone in that Discord channel, and 15 of the DFL guild channels contain CLIENT staff — binding one of those means the client sees the plan’s comments. Rules: (1) channel_id MUST come from list_discord_channels — pasted, guessed or remembered snowflakes are refused server-side; (2) the PLAN OWNER or an admin (canonical IAM level >= 80) may bind or unbind — on SHARED plans; a personal plan is readable only by its owner, so it stays owner-only in effect; (3) binding a client-facing channel, or binding any channel on a personal plan, additionally requires confirm_client_exposure: true — this is the same blocking confirmation the web UI demands from a human, and you should only set it when the person you are acting for asked for THAT channel specifically. Unbinding is never blocked. Read the current binding with read_plan (it is in the plan metadata as discord_channel_id/discord_channel_name); this tool also reports the before → after transition.

ParameterTypeRequiredDescription
slugstringyesThe plan slug to bind (or unbind).
channel_idstringyesThe Discord channel snowflake, taken VERBATIM from list_discord_channels — or null to UNBIND (stop all Discord notifications for this plan). This field is REQUIRED even when unbinding: omitting it is an error, never a silent no-op, so a forgotten argument can never quietly change a client channel’s notifications.
confirm_client_exposurebooleannoExplicit acknowledgement that plan comments will become visible to everyone in the target channel. REQUIRED (true) when the channel is client-facing or the plan is personal; the bind is refused without it and nothing is written. Do not set it pre-emptively “just in case” — it is the record that a human chose this channel. Ignored when unbinding.

Search External Entities

Find a diagram, document, image or spec run across the DFL fleet and get its LOCATOR — the UUID (diagram / document / spec_run) or media id/URL (image) that attach_entity requires. This is the DISCOVERY step and the ONLY way to obtain that locator from inside the Plans MCP: diagrams and documents live in other apps, so you cannot attach an entity you have not looked up here. Never paste, guess or recall a UUID — a wrong-but-valid UUID silently points the plan at somebody else’s artifact. Omit query to list the most recently updated entities; omit type to search every searchable type at once (ux_path is not one of them — see type). Results are scoped to what YOU can see. Set include_latest_revision when you intend to PIN a diagram — it returns each diagram’s newest revision id, which is the rev attach_entity needs and which nothing else can give you.

ParameterTypeRequiredDescription
querystringnoFree-text match over the entity name/description (e.g. “arquitetura de dados”, “onboarding”). Omit to get the most recently updated entities.
typeenumnoNarrow to one entity type. Omit to search all of them together. Entity type. diagram = a dfl-diagrams canvas, embedded live and pinnable to a revision. document = a dfl-documents document. image = a media asset rendered inline. ux_path = a flows spec served by dfl-ux-paths over its external-URL transport; ⚠️ its locator is a CAPABILITY URL when it points at DFL media — whoever holds it can read the spec, which is why it belongs in an auth-gated plan body and must never be mirrored into a repo or a config file. spec_run = one Spec Builder round (a versioned, priced set of candidate tasks); attach it ONCE per run, never one reference per task, and pin it with mode: "pinned" + a work.spec_run_versions.id as rev whenever a quote or a decision cites its point total. ⚠️ ux_path is deliberately NOT searchable and is absent from this list: a flows spec is a file behind an https URL, not a row in any table the plans-app can query. Attach one by passing its URL straight to attach_entity, which does accept the type. One of: diagram, document, image, spec_run.
limitnumbernoMax results to return (default 20, max 50). Default: 20.
include_latest_revisionbooleannoAlso resolve each DIAGRAM hit’s newest saved revision (id, version number, date). Set this when you plan to pin: the id it returns is the rev that attach_entity(&#123; mode: "pinned" &#125;) requires, and a pinned attach without one renders live content under a “pinning unavailable” badge. Costs one extra request per diagram hit, so at most 10 are resolved — narrow the query if you need more. Ignored for documents and images (they have no revision history). A diagram can still come back with no revision — see the note printed under that hit for the actual reason, which since 2026-08-04 is almost never “you are not the author”.

List Plan Entities

List the external entities (diagrams / documents / images) a plan references — the {{dfl-entity:…}} tokens embedded in its body, with each one’s type, locator, mode (live/pinned) and caption. Derived by parsing the plan body, so it is always in sync with what the plan actually says; it returns POINTERS, not the resolved diagram or document content. Read-only — it never edits the plan and never creates a version. Use it before attach_entity (to see what is already there — re-attaching is a no-op) and before detach_entity (to get the exact type + locator to remove).

ParameterTypeRequiredDescription
slugstringyesThe plan slug whose entity references to list.

Attach Entity to Plan

Attach an external entity (diagram / document / image / ux_path / spec_run) to a plan by inserting a {{dfl-entity:<type>:<locator>}} token into its body — without you having to republish the whole body. Get locator from search_entities first; do not guess a UUID. ATTACHING DOES CREATE A NEW PLAN VERSION, because adding a reference is an edit of the plan — that is expected and correct. What never versions the plan is the referenced entity’s own CONTENT changing: the body stores only the pointer, so the diagram or document stays live and the plan follows it with no new version. Idempotent — re-attaching the same type+locator changes nothing and creates no version. Requires edit rights on the plan (owner). To PIN a diagram to a fixed revision you must pass rev as well — mode: "pinned" on its own cannot be honoured and renders live content under a “pinning unavailable” badge.

ParameterTypeRequiredDescription
slugstringyesThe plan slug to attach the entity to.
typeenumyesEntity type. diagram = a dfl-diagrams canvas, embedded live and pinnable to a revision. document = a dfl-documents document. image = a media asset rendered inline. ux_path = a flows spec served by dfl-ux-paths over its external-URL transport; ⚠️ its locator is a CAPABILITY URL when it points at DFL media — whoever holds it can read the spec, which is why it belongs in an auth-gated plan body and must never be mirrored into a repo or a config file. spec_run = one Spec Builder round (a versioned, priced set of candidate tasks); attach it ONCE per run, never one reference per task, and pin it with mode: "pinned" + a work.spec_run_versions.id as rev whenever a quote or a decision cites its point total. diagram, document and spec_run take a UUID (respectively public.diagrams.id, documents.document.id, work.ai_spec_inputs.id), lowercased by the server so one entity cannot fork into two registry rows. image takes a media id or an https:// URL. ux_path takes an https:// URL ONLY — a bare id is rejected, and so are the git-shaped shorthands repo@ref:flow and repo:path@ref, which look like bare ids and are made to fail loudly rather than half-resolve. One of: diagram, document, image, ux_path, spec_run.
locatorstringyesThe entity locator, copied verbatim from search_entities (or, for a ux_path, the spec URL — that type is not searchable). diagram, document and spec_run take a UUID (respectively public.diagrams.id, documents.document.id, work.ai_spec_inputs.id), lowercased by the server so one entity cannot fork into two registry rows. image takes a media id or an https:// URL. ux_path takes an https:// URL ONLY — a bare id is rejected, and so are the git-shaped shorthands repo@ref:flow and repo:path@ref, which look like bare ids and are made to fail loudly rather than half-resolve.
modeenumnoResolution mode. live (default) always renders the current state of the entity — this is the point of the feature. pinned freezes it to the state a decision was taken against (use inside ADR/decision blocks), and REQUIRES rev to actually take effect: pinned without a rev renders live content under a badge saying the pin could not be applied. One of: live, pinned.
revstringnoRevision to pin to. ONLY meaningful together with mode: "pinned", and only on the two VERSIONED types — diagram (a public.diagram_versions id) and spec_run (a work.spec_run_versions id). It is ignored (and reported as pin state “unsupported”) on a document, an image or a ux_path, which have no revision history to pin to. Preferred form: the EXACT version id (a UUID), which pins to precisely that checkpoint. An ISO-8601 timestamp is also accepted, but resolves APPROXIMATELY — the newest checkpoint at or before that instant, so how close it lands depends on how often a checkpoint happened to be cut. dfl-diagrams’ own guidance to consumers creating a new pin is to store the exact version id, so prefer it. ⚠️ PIN A spec_run WHENEVER THE PLAN CITES ITS POINTS. A quote is derived from points_total, so an unpinned reference means the number moves the moment anyone edits an item — the same failure as an unpinned diagram cited inside an ADR. Get the id from get_spec_run on the engineering MCP. search_entities({ type: “diagram”, include_latest_revision: true }) returns each diagram’s newest revision id; pass it as rev.
captionstringnoOptional caption rendered with the embedded entity, e.g. “Arquitetura de dados”.
anchorstringnoText of an existing ## Heading in the body to append the token under. Omit to append under an ## Entidades section at the end of the plan (created if absent).

Detach Entity from Plan

Remove an external entity reference from a plan — deletes the {{dfl-entity:<type>:<locator>}} token(s) from the body, leaving the rest of the plan untouched. This removes the POINTER only; the diagram / document / image / ux_path / spec_run itself is not deleted and stays in its own app — detaching a spec_run in particular does NOT discard the run, its items, its versions or its comments. Detaching DOES create a new plan version, because removing a reference is an edit of the plan. Idempotent — detaching something the plan does not reference changes nothing. Use list_plan_entities to get the exact type + locator. Requires edit rights on the plan (owner).

ParameterTypeRequiredDescription
slugstringyesThe plan slug to remove the entity reference from.
typeenumyesEntity type of the reference to remove. diagram, document and spec_run take a UUID (respectively public.diagrams.id, documents.document.id, work.ai_spec_inputs.id), lowercased by the server so one entity cannot fork into two registry rows. image takes a media id or an https:// URL. ux_path takes an https:// URL ONLY — a bare id is rejected, and so are the git-shaped shorthands repo@ref:flow and repo:path@ref, which look like bare ids and are made to fail loudly rather than half-resolve. One of: diagram, document, image, ux_path, spec_run.
locatorstringyesThe entity locator to remove, exactly as returned by list_plan_entities. diagram, document and spec_run take a UUID (respectively public.diagrams.id, documents.document.id, work.ai_spec_inputs.id), lowercased by the server so one entity cannot fork into two registry rows. image takes a media id or an https:// URL. ux_path takes an https:// URL ONLY — a bare id is rejected, and so are the git-shaped shorthands repo@ref:flow and repo:path@ref, which look like bare ids and are made to fail loudly rather than half-resolve.