Plans
Search, read, author, and triage plans on plans.devfellowship.com — the
DFL planning system (plans, ADRs/decision records, and the questions/DTQ
surface). This MCP wraps the same REST backend the web UI and the
read-plan/search-plans/publish-plan skills use, so there is one backend
and one visibility policy.
| Endpoint | https://plans.mcp.devfellowship.com/mcp |
|---|---|
| Tools | 32 in 7 groups |
| Package | packages/dfl-mcp-plans |
| Auth |
Your dfl-auth login token as Authorization: Bearer <token>. Every call runs as
you, under RLS. See Auth & security.
|
{ "mcpServers": { "dfl-plans": { "type": "http", "url": "https://plans.mcp.devfellowship.com/mcp", "headers": { "Authorization": "Bearer YOUR_ACCESS_TOKEN" } } }}Other clients (Cursor, VS Code, codex, the Anthropic SDK): see Getting started, step 3.
| Backing data | The plans REST API (plans.devfellowship.com/api) — plans, versions, ADRs, questions. |
Find and read plans, and see which tasks a plan carries and which plans moved on a given day.
search_plans
Hybrid semantic + keyword search over plans (and optionally ADRs) on plans.devfellowship.com.
search_plansSearch 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. |
get_plan
Read a plan's full markdown body plus metadata (status, version, ADR count, parent, tags, visibility/owner) by slug.
get_planRead 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
List plans with optional filters (status, source, tag, has_children, has_pending_questions).
list_plansList 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
List the DevFellowship work tasks bound to a plan — the plan's EXECUTION CHECKLIST, the read half of set_plan_tasks.
list_plan_tasksList 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
List the plans with ACTIVITY in a day window — the same rule as the calendar Day board (plans.devfellowship.com/calendar?view=day).
list_plans_by_activityList 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 get_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). |
set_plan_visibility and set_plan_visibility_batch moved here from
Platform (ops), where they were
plans_set_visibility and plans_set_visibility_batch (plan task T2.5). An
owner can change the visibility of an own plan; only a superadmin can reassign
owner. The plans-app decides, with your forwarded identity.
Create and publish a plan, change its status, owner, visibility and links, and bind its tasks.
create_plan
Create a new plan (or upsert one by slug) on plans.devfellowship.com.
create_planCreate 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
Publish (or re-publish) a plan body, creating a new version.
publish_planPublish 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 get_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 get_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
Transition a plan's lifecycle status: draft -> fired -> executing -> done (or archived).
patch_statusPatch 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
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.
set_linksSet 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
Reassign the owner column of plans to a canonical Supabase auth uid.
set_ownerSet 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_visibility
Set a single plan's visibility to personal or shared.
set_plan_visibilitySet Plan Visibility
Set a single plan's visibility to personal or shared. personal plans are only visible to their owner; shared plans are visible to everyone. Use this when a plan should be hidden from the shared inbox (mark it personal), or when a personal draft is ready to be shared with the team. Identify the plan by its slug (e.g. "20260616-plans-app-personal-shared-visibility").
| Parameter | Type | Required | Description |
|---|---|---|---|
slug | string | yes | The plan slug to update (e.g. "20260616-plans-app-personal-shared-visibility"). |
visibility | enum | yes | Target visibility: "shared" (visible to everyone) or "personal" (owner-only). One of: shared, personal. |
owner | string | no | Optional owner to assign. Only honored server-side for superadmin callers; normal callers cannot reassign ownership and this field is ignored for them. |
set_plan_visibility_batch
Set the visibility (personal or shared) of MANY plans in one call.
set_plan_visibility_batchSet Plan Visibility (Batch)
Set the visibility (personal or shared) of MANY plans in one call. Select the plans either by an explicit list of slugs, or by a filter (any combination of status, source, tag, owner) — at least one of slugs or filter is required. Returns a summary of how many plans were updated, how many were skipped (e.g. already at the target visibility or not permitted), and the list of skipped slugs. Use this for bulk re-classification, e.g. "mark all my draft plans personal" or "share every plan tagged release".
| Parameter | Type | Required | Description |
|---|---|---|---|
slugs | string[] | no | Explicit list of plan slugs to update. Provide this OR filter (or both). |
filter | object | no | Filter to select plans by attributes. Provide this OR slugs (or both). |
visibility | enum | yes | Target visibility to apply to every matched plan: "shared" or "personal". One of: shared, personal. |
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.
set_plan_tasksSet 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. |
Comments
Section titled “Comments”Read, add and delete comments on a plan.
create_plan_comment
Create a comment on a plan at plans.devfellowship.com — the same annotation the web UI produces with select-to-comment.
create_plan_commentCreate 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
List the comments on a plan (the select-to-comment annotations from the web UI, plus any written by a bot).
list_plan_commentsList 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
Delete one plan comment by id.
delete_plan_commentDelete 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. |
External entities
Section titled “External entities”A plan can embed an artifact that lives in another DFL app — a diagram from
diagrams.devfellowship.com, a document from documents.devfellowship.com, an
image from the media bucket — instead of merely linking it. The plan body holds
an External Entity Reference (EER): a token that points at the artifact and
nothing else.
{{dfl-entity:<type>:<locator>}}{{dfl-entity:<type>:<locator>|mode=live;caption=Arquitetura;rev=<uuid|iso8601>}}{{dfl-diagram:<uuid>}} ← legacy alias for {{dfl-entity:diagram:<uuid>}}type is diagram, document or image. The locator is a UUID for
diagram and document; for image it is a media id or an https:// URL.
Options after the | are k=v pairs separated by ; — mode (live, the
default, or pinned), caption, rev.
Find a diagram, document, image or spec run, and put it in a plan or take it out.
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.
search_entitiesSearch 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
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.
list_plan_entitiesList 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
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.
attach_entityAttach 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
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.
detach_entityDetach 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. |
Discord channel binding
Section titled “Discord channel binding”Announce the comments of a plan in one Discord channel.
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.
list_discord_channelsList 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
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.
set_plan_discord_channelSet 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 get_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. |
ADRs (Decision Records)
Section titled “ADRs (Decision Records)”search_decisions (semantic search over the ADRs) moved here from
Platform (ops), where it was decisions_search
(plan task T2.5).
Read and search the architecture decisions recorded in plans.
list_adrs
List architectural decision records (ADRs).
list_adrsList 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
Get a single architectural decision record by plan slug + decision number (or id).
get_adrGet 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). |
search_decisions
Semantic search over architectural decision records (ADRs) using pgvector cosine similarity.
search_decisionsSearch ADRs (Decision Records)
Semantic search over architectural decision records (ADRs) using pgvector cosine similarity. Embed a natural language query and retrieve the most relevant past decisions. Use this when you need to check what was decided about a topic before proposing a direction.
| Parameter | Type | Required | Description |
|---|---|---|---|
query | string | yes | Natural language description of the decision context or question. Example: "how do we handle authentication for agents?" or "vector DB choice". |
top_k | number | no | Maximum number of results to return (default 5, max 20). Default: 5. |
min_score | number | no | Minimum cosine similarity threshold (0–1, default 0.7). Lower values return more results but with weaker relevance. Default: 0.7. |
Questions (DTQ)
Section titled “Questions (DTQ)”Ask a person a decision question on a plan, answer it, and see every open question in one inbox.
list_questions
List structured questions (blocking + non-blocking) with their options and current answers.
list_questionsList 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
Fetch ONE plan question by its UUID — no slug needed.
get_questionGet 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
Create a structured question on a plan (DTQ — drift/decision to question).
post_questionPost 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
Edit question_text and/or context on an existing question.
update_questionUpdate 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
Record an answer to a plan question.
answer_questionAnswer 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
Retire an obsolete plan question by setting its status to "withdrawn".
withdraw_questionWithdraw 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
List OPEN (pending, non-snoozed) questions across ALL readable plans — the cross-plan question inbox.
list_global_questionsList 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"). |
Deprecated names
Section titled “Deprecated names”These old names still work until their removal date. Each one calls the same handler as its new name. Use the new name in new code.
These old names still answer until the date shown. Call the new name.
| Deprecated name | Use instead | Removed after |
|---|---|---|
read_plan | get_plan | 2026-12-04 |