plans — full tool reference
Search, read, author and triage plans, ADRs and questions.
| Endpoint | https://plans.mcp.devfellowship.com/mcp |
| Package | packages/dfl-mcp-plans |
| Tools | 29 |
| Tool | Description |
|---|---|
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. |
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. |
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. |
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. |
list_plans_by_activity | 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. |
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. |
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. |
patch_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). |
set_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. |
set_owner | 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. |
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. |
list_adrs | 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. |
get_adr | 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. |
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. |
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. |
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. |
list_questions | 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. |
get_question | 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. |
post_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. |
update_question | 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. |
answer_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. |
withdraw_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. |
list_global_questions | 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. |
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. |
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. |
search_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. |
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). |
attach_entity | 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. |
detach_entity | 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). |
search_plans
Section titled “search_plans”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | yes | Search query — keywords or a natural-language concept. Example: “auth gating for the plans MCP”. |
type | enum | no | What to search: “plan”, “adr” (decision records), or “all” (default). One of: plan, adr, all. Default: "all". |
limit | number | no | Max results to return (default 25, max 100). Default: 25. |
active_only | boolean | no | When true, exclude done/archived plans (only draft/fired/executing surface). Default: false. |
read_plan
Section titled “read_plan”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug, e.g. “20260619-plans-mcp-per-domain”. |
version | number | no | Specific version number to read. Omit for the latest version. |
metadata_only | boolean | no | When true, return only metadata (no body fetch). Default: false. |
list_plans
Section titled “list_plans”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | no | Filter by status. Single value or comma-separated, e.g. “draft” or “draft,executing”. |
source | string | no | Filter by source. Single value or comma-separated, e.g. “claude-main” or “claude-main,telegram”. |
tag | string | no | Filter to plans carrying this tag. |
has_children | boolean | no | When true, only plans that have child plans. |
has_pending_questions | boolean | no | When true, only plans with pending (unanswered) questions. |
list_plan_tasks
Section titled “list_plan_tasks”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug whose bound tasks to list. |
include_finished | boolean | no | Include 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
Section titled “list_plans_by_activity”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
date | string | no | One calendar day, YYYY-MM-DD. Default: today in tz. Not with from/to. |
from | string | no | Range start, YYYY-MM-DD, inclusive. Needs to. |
to | string | no | Range end, YYYY-MM-DD, inclusive. Needs from. Max 62 days. |
tz | string | no | IANA timezone of the day boundary. Default America/Sao_Paulo. |
mode | enum | no | activity_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. |
owner | string | no | Owner filter: “me”, a user id, an e-mail, or a name/handle such as “tainan”. |
status | string | no | Plan status filter. One value or a comma list of draft|fired|executing|done|archived. |
limit | number | no | Max plans returned (1-200, default 50). |
create_plan
Section titled “create_plan”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | Unique plan slug, e.g. “20260619-my-new-plan”. |
title | string | yes | Human-readable plan title. |
body | string | yes | Full markdown body of the plan. |
source | string | no | Origin, e.g. “claude-main”, “telegram”. Default server-side. |
status | enum | no | Lifecycle status; defaults to draft when omitted. One of: draft, fired, executing, done, archived. |
parent_slug | string | no | Slug of a parent plan (must already exist). |
tags | string[] | no | Tags, e.g. [“infra”,“plans”]. |
visibility | enum | no | ”shared” (default) or “personal” (owner-only). One of: shared, personal. |
publish_plan
Section titled “publish_plan”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug to publish. |
title | string | yes | Plan title. |
body | string | yes | Full markdown body (a new version is stored). |
source | string | no | Origin source. Default server-side. |
status | enum | no | Optionally set status while publishing. One of: draft, fired, executing, done, archived. |
parent_slug | string | no | Parent plan slug (must exist). |
tags | string[] | no | Tags array. |
visibility | enum | no | ”shared” or “personal”. One of: shared, personal. |
base_body_sha256 | string | no | CONDITIONAL 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_version | number | no | CONDITIONAL WRITE (convenience): the latest_version you read. Weaker than base_body_sha256 — supply both when you have them; the hash wins. |
patch_status
Section titled “patch_status”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).
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug to transition. |
status | enum | yes | Target status: draft|fired|executing|done|archived. One of: draft, fired, executing, done, archived. |
set_links
Section titled “set_links”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug whose links to replace. |
links | object[] | yes | Replaces 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_owner
Section titled “set_owner”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
owner | string | yes | The 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. |
slugs | string[] | no | TARGETED mode: explicit plan slugs to reassign, regardless of their current owner. Max 200 per call (server-side limit). Provide this OR only_null. |
only_null | boolean | no | FILTER 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_all | boolean | no | Required 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
Section titled “set_plan_tasks”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug whose task list to replace. |
tasks | object[] | yes | Replaces the plan’s entire task list (max 40). Pass [] to unbind all tasks. Other sidebar links are left untouched. |
list_adrs
Section titled “list_adrs”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | no | Scope to a single plan’s ADRs. Omit for a global list. |
tag | string | no | Global mode: filter by tag. |
decided_by | string | no | Global mode: filter by who decided. |
get_adr
Section titled “get_adr”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug the ADR belongs to. |
number | number | no | The decision number within the plan (ADR-N). |
id | string | number | no | The decision record id (alternative to number). |
create_plan_comment
Section titled “create_plan_comment”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug to comment on. |
body | string | yes | The comment text. |
selection_text | string | no | The exact text the comment is anchored to. Omit for a plan-wide comment; the API stores “(plan)”. |
selection_start | number | no | Anchor start offset in the plan body. |
selection_end | number | no | Anchor end offset in the plan body. |
version | number | no | The plan version this comment is on. |
metadata | object | no | Optional 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
Section titled “list_plan_comments”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug. |
version | number | no | Only comments on this plan version. |
delete_plan_comment
Section titled “delete_plan_comment”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug that owns the comment. |
comment_id | number | yes | The comment id to delete. |
list_questions
Section titled “list_questions”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | no | Plan slug to scope to. OMIT for the cross-plan query over all readable plans. |
status | enum | no | Question 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. |
blocking | boolean | no | When true, return only questions that gate plan execution (blocks_execution). |
include_finished_plans | boolean | no | When true, do NOT exclude questions on done/archived plans. Default false (finished plans are noise for direction-setting, but they DO hide questions). |
include_snoozed | boolean | no | When true, include pending questions that are still snoozed (“ask me later”). |
get_question
Section titled “get_question”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
question_id | string | yes | The question UUID, in full (e.g. c541f864-88ea-4c57-af00-47f72f6033d3). |
post_question
Section titled “post_question”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug to attach the question to. |
question_text | string | yes | The question text. |
round | number | no | Question round (default 1). |
order_within_round | number | no | Ordering within the round (default 0). |
context | string | no | Background context shown with the question. |
author_reasoning | string | no | 1-2 sentences on WHY this question was created (stored, not shown in UI). |
multi_select | boolean | no | Allow selecting multiple options (default false). |
blocks_execution | boolean | no | When true, this question must be answered before the plan can transition draft->fired/executing. |
recommended_option_letter | string | no | Recommended option letter, e.g. “A”. |
status | enum | no | Initial status (default pending). One of: pending, answered, deferred, withdrawn. |
options | object[] | no | Answer options. |
update_question
Section titled “update_question”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug the question belongs to. |
question_id | string | yes | The existing question UUID. |
question_text | string | no | Replacement question text; must not be blank. |
context | string | no | Background context; an empty string clears it. |
answer_question
Section titled “answer_question”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug the question belongs to. |
question_id | string | yes | The question UUID (from list_questions). |
selected_option_letters | string[] | yes | Chosen 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_notes | string | no | The freeform text of the answer, or extra context alongside a letter. When this carries the actual decision, selected_option_letters must be [“OTHER”]. |
answer_text | string | no | Alias of freeform_notes. Accepted so the text is never silently dropped. |
withdraw_question
Section titled “withdraw_question”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug the question belongs to. |
question_id | string | yes | The question UUID (from list_questions). |
list_global_questions
Section titled “list_global_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.
| Parameter | Type | Required | Description |
|---|---|---|---|
status | enum | no | Question 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. |
blocking | boolean | no | When true, return only questions that gate plan execution (blocks_execution). |
include_finished_plans | boolean | no | When true, do NOT exclude questions on done/archived plans. Default false (finished plans are noise for direction-setting, but they DO hide questions). |
include_snoozed | boolean | no | When true, include pending questions that are still snoozed (“ask me later”). |
list_discord_channels
Section titled “list_discord_channels”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | no | Optional case- and accent-insensitive substring filter over channel name AND category. Omit to get the whole guild. |
refresh | boolean | no | Bypass 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
Section titled “set_plan_discord_channel”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug to bind (or unbind). |
channel_id | string | yes | The 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_exposure | boolean | no | Explicit 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_entities
Section titled “search_entities”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | no | Free-text match over the entity name/description (e.g. “arquitetura de dados”, “onboarding”). Omit to get the most recently updated entities. |
type | enum | no | Narrow 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. |
limit | number | no | Max results to return (default 20, max 50). Default: 20. |
include_latest_revision | boolean | no | Also 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({ mode: "pinned" }) 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
Section titled “list_plan_entities”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).
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug whose entity references to list. |
attach_entity
Section titled “attach_entity”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.
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug to attach the entity to. |
type | enum | yes | 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. 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. |
locator | string | yes | The 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. |
mode | enum | no | Resolution 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. |
rev | string | no | Revision 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. |
caption | string | no | Optional caption rendered with the embedded entity, e.g. “Arquitetura de dados”. |
anchor | string | no | Text 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
Section titled “detach_entity”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).
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug to remove the entity reference from. |
type | enum | yes | Entity 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. |
locator | string | yes | The 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. |