Skip to content

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.
.mcp.json
{
"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 dataThe 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 Plans

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

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

get_plan

Read a plan's full markdown body plus metadata (status, version, ADR count, parent, tags, visibility/owner) by slug.

Read Plan

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

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

list_plans

List plans with optional filters (status, source, tag, has_children, has_pending_questions).

List Plans

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

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

list_plan_tasks

List the DevFellowship work tasks bound to a plan — the plan's EXECUTION CHECKLIST, the read half of set_plan_tasks.

List Plan Tasks

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

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

list_plans_by_activity

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 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.

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

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 Plan

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

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

publish_plan

Publish (or re-publish) a plan body, creating a new version.

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 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.

ParameterTypeRequiredDescription
slugstringyesThe plan slug to publish.
titlestringyesPlan title.
bodystringyesFull markdown body (a new version is stored).
sourcestringnoOrigin source. Default server-side.
statusenumnoOptionally set status while publishing. One of: draft, fired, executing, done, archived.
parent_slugstringnoParent plan slug (must exist).
tagsstring[]noTags array.
visibilityenumno"shared" or "personal". One of: shared, personal.
base_body_sha256stringnoCONDITIONAL WRITE (recommended): sha256 hex of the plan body you started from — the body_sha256 field 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_versionnumbernoCONDITIONAL 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 Plan Status

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

ParameterTypeRequiredDescription
slugstringyesThe plan slug to transition.
statusenumyesTarget status: draft|fired|executing|done|archived. One of: draft, fired, executing, done, archived.
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 Plan Links

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

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

set_owner

Reassign the owner column of plans to a canonical Supabase auth uid.

Set Plan Owner (Backfill / Reconcile)

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

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

set_plan_visibility

Set a single plan's visibility to personal or shared.

Set 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").

ParameterTypeRequiredDescription
slugstringyesThe plan slug to update (e.g. "20260616-plans-app-personal-shared-visibility").
visibilityenumyesTarget visibility: "shared" (visible to everyone) or "personal" (owner-only). One of: shared, personal.
ownerstringnoOptional 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 (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".

ParameterTypeRequiredDescription
slugsstring[]noExplicit list of plan slugs to update. Provide this OR filter (or both).
filterobjectnoFilter to select plans by attributes. Provide this OR slugs (or both).
visibilityenumyesTarget 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 Tasks

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

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

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 Comment

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

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

list_plan_comments

List the comments on a plan (the select-to-comment annotations from the web UI, plus any written by a bot).

List Plan Comments

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

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

delete_plan_comment

Delete one plan comment by id.

Delete Plan Comment

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

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

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 External Entities

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

ParameterTypeRequiredDescription
querystringnoFree-text match over the entity name/description (e.g. "arquitetura de dados", "onboarding"). Omit to get the most recently updated entities.
typeenumnoNarrow to one entity type. Omit to search all of them together. Entity type. diagram = a dfl-diagrams canvas, embedded live and pinnable to a revision. document = a dfl-documents document. image = a media asset rendered inline. ux_path = a flows spec served by dfl-ux-paths over its external-URL transport; ⚠️ its locator is a CAPABILITY URL when it points at DFL media — whoever holds it can read the spec, which is why it belongs in an auth-gated plan body and must never be mirrored into a repo or a config file. spec_run = one Spec Builder round (a versioned, priced set of candidate tasks); attach it ONCE per run, never one reference per task, and pin it with mode: "pinned" + a work.spec_run_versions.id as rev whenever a quote or a decision cites its point total. ⚠️ ux_path is deliberately NOT searchable and is absent from this list: a flows spec is a file behind an https URL, not a row in any table the plans-app can query. Attach one by passing its URL straight to attach_entity, which does accept the type. One of: diagram, document, image, spec_run.
limitnumbernoMax results to return (default 20, max 50). Default: 20.
include_latest_revisionbooleannoAlso resolve each DIAGRAM hit's newest saved revision (id, version number, date). Set this when you plan to pin: the id it returns is the rev that attach_entity({ 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 Entities

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

ParameterTypeRequiredDescription
slugstringyesThe plan slug whose entity references to list.

attach_entity

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 Entity to Plan

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

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

detach_entity

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 Entity from Plan

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

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

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 Channels

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

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

set_plan_discord_channel

Bind a plan to ONE Discord channel so that EVERY COMMENT posted on that plan is announced in that channel (author + excerpt + link), or unbind it with channel_id: null.

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 get_plan (it is in the plan metadata as discord_channel_id/discord_channel_name); this tool also reports the before → after transition.

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

search_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 ADRs (Decision Records)

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

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

get_adr

Get a single architectural decision record by plan slug + decision number (or id).

Get ADR (Decision Record)

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

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

search_decisions

Semantic search over architectural decision records (ADRs) using pgvector cosine similarity.

Search 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.

ParameterTypeRequiredDescription
querystringyesNatural language description of the decision context or question. Example: "how do we handle authentication for agents?" or "vector DB choice".
top_knumbernoMaximum number of results to return (default 5, max 20). Default: 5.
min_scorenumbernoMinimum cosine similarity threshold (0–1, default 0.7). Lower values return more results but with weaker relevance. Default: 0.7.

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 Questions (one plan, or across all plans)

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

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

get_question

Fetch ONE plan question by its UUID — no slug needed.

Get Question by UUID

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

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

post_question

Create a structured question on a plan (DTQ — drift/decision to 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.

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

update_question

Edit question_text and/or context on an existing 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.

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

answer_question

Record an answer to a plan 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.

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

withdraw_question

Retire an obsolete plan question by setting its status to "withdrawn".

Withdraw Plan Question

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

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

list_global_questions

List OPEN (pending, non-snoozed) questions across ALL readable plans — the cross-plan question inbox.

List Global Question Inbox

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

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

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 nameUse insteadRemoved after
read_planget_plan2026-12-04