MCP Plan
MCP server that provides planning tooling.
Installation
mcp-plan is packaged as a Nix flake. Run it directly without installing:
nix run github:haras-unicorn/mcp-plan -- run
or build the mcp-plan binary with:
nix build github:haras-unicorn/mcp-plan
Releases
Prebuilt binaries for x86_64-linux and aarch64-linux, for each supported
database backend (sqlite, postgres, mysql or all), are attached to each
GitHub release as tarballs containing the mcp-plan binary.
To run with a specific backend (in this example sqlite) from releases:
curl -L -o mcp-plan.tar.gz \
https://github.com/haras-unicorn/mcp-plan/releases/latest/download/mcp-plan-x86_64-linux-sqlite.tar.gz
tar -xzf mcp-plan.tar.gz
./mcp-plan-x86_64-linux-sqlite run
To run with a binary supporting all backends from releases:
curl -L -o mcp-plan.tar.gz \
https://github.com/haras-unicorn/mcp-plan/releases/latest/download/mcp-plan-x86_64-linux.tar.gz
tar -xzf mcp-plan.tar.gz
./mcp-plan-x86_64-linux run
Pick the archive matching your backend:
mcp-plan-x86_64-linux.tar.gz(all backends)mcp-plan-x86_64-linux-sqlite.tar.gzmcp-plan-x86_64-linux-postgres.tar.gzmcp-plan-x86_64-linux-mysql.tar.gzmcp-plan-aarch64-linux.tar.gz(all backends)mcp-plan-aarch64-linux-{sqlite,postgres,mysql}.tar.gz
NixOS and home-manager
Add the flake as an input and apply its overlay so that mcp-plan is available
in your system configuration:
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
mcp-plan.url = "github:haras-unicorn/mcp-plan";
};
outputs =
{ nixpkgs, mcp-plan, ... }:
{
nixosConfigurations.my-machine = nixpkgs.lib.nixosSystem {
modules = [
{ nixpkgs.overlays = [ mcp-plan.overlays.default ]; }
];
};
};
}
Then add pkgs.mcp-plan to your packages, either in NixOS:
{ pkgs, ... }:
{
environment.systemPackages = [ pkgs.mcp-plan ];
}
or with home-manager:
{ pkgs, ... }:
{
home.packages = [ pkgs.mcp-plan ];
}
Binary cache
Builds are cached on the haras cachix cache. When the flake is used directly
(for example with nix run github:haras-unicorn/mcp-plan), the cache is
configured automatically through the flake’s nixConfig. To use it when the
package comes from an overlay, add the following to your nix configuration:
{
nix.settings = {
substituters = [ "https://haras.cachix.org" ];
trusted-public-keys = [
"haras.cachix.org-1:/HIo1JYqOIH1Nwk1EGXhuPPvDW0WekxIbY5CiXUZbYw="
];
};
}
Usage
mcp-plan is an MCP server that speaks the Model Context Protocol over stdio.
Add it as a stdio MCP server to any MCP client, for example:
{
"mcpServers": {
"mcp-plan": {
"command": "nix",
"args": ["run", "github:haras-unicorn/mcp-plan", "--", "run"]
}
}
}
If mcp-plan is already on your PATH, point the client at the binary directly
instead:
{
"mcpServers": {
"mcp-plan": {
"command": "mcp-plan",
"args": ["run"]
}
}
}
Configuration
mcp-plan reads its configuration from config.toml in the working directory,
overlaid with MCP_PLAN_* environment variables. Every setting is optional. The
full schema and a worked example live in the References section.
Command line
The binary accepts a subcommand:
mcp-plan run— start the MCP server over stdio (default).mcp-plan migrate— open the database and apply pending migrations, then exit.mcp-plan schema— write the configuration JSON schema to--output.
--config <path> is a global flag selecting a different configuration file
(defaults to config.toml), for example:
mcp-plan --config ./prod.toml migrate run
Configuration file
[database]
url = "sqlite://data/mcp-plan.db"
The file is split into three sections:
database— the databaseurl. See below for the supported schemes.runtime—tps_in,tps_out,max_task_duration_secs,queue_limit,max_retries.sources— a list of statically configured sources.
See the JSON schema and the example for the exact keys and defaults (References below).
Environment
Environment variables override file values. Use the MCP_PLAN prefix with __
as the section separator:
MCP_PLAN__RUNTIME__TPS_IN=1000 mcp-plan run
Logging
Logs are emitted as newline-delimited JSON on stderr, keeping stdout
exclusively for the MCP JSON-RPC protocol. Each line carries a level,
timestamp, target, a human-readable message, and structured fields (e.g.
task_id, duration_ms) that are safe to query with jq:
RUST_LOG=debug mcp-plan run 2> >(jq -r '"\(.level): \(.message)"')
Log verbosity is controlled via RUST_LOG (default info). Any standard
tracing filter is accepted (error, warn, info, debug, trace,
per-target filters, etc.).
Database
database.url selects the backend by scheme:
sqlite://data/mcp-plan.db— a SQLite file (relative to the working directory). The database file and its parent directory are created on first start. Use an absolute path (e.g.sqlite:///var/lib/mcp-plan.db) orsqlite://:memory:for an in-memory database.postgres://user:password@host:port/database— PostgreSQL.mysql://user:password@host:port/database— MySQL.
Each binary is built against a single backend (sqlite by default, or the
postgres/mysql build variants from the releases).
Both run and migrate connect to the database and apply pending migrations at
startup; migrate exits immediately afterwards so migrations can be run as a
separate init step (e.g. in multi-tenant deployments). SQLite runs in WAL mode
with foreign keys enabled.
Integration
mcp-plan is a regular MCP server, so any MCP client — including a custom agent
runtime — can drive it. Register it as a stdio server (see the Usage
example) and expose the plan__* tools to your agent.
To keep an autonomous agent working over time, add a scheduler of your choice (a
cron entry, a CI scheduled job, a loop inside an existing daemon or an OpenClaw
heartbeat/agent cron job) that periodically connects to mcp-plan and instructs
the agent to run a planning/delegation pass. An example heartbeat prompt is
provided in References section—you can point a cron job at it
verbatim or use it as a template:
# example: run an agent-driven planning pass on an interval
*/30 * * * * mcp-plan-with-agent --instruct assets/heartbeat.md
Exact wiring depends on your runtime; the example shows the shape. The tasks and sources live in the database, so each pass should continue where the previous one stopped.
References
Configuration schema
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "Config",
"description": "Runtime configuration for a single `mcp-plan` server.",
"type": "object",
"properties": {
"database": {
"$ref": "#/$defs/DatabaseConfig"
},
"runtime": {
"$ref": "#/$defs/RuntimeConfig"
},
"sources": {
"type": "array",
"items": {
"$ref": "#/$defs/SourceConfig"
}
}
},
"$defs": {
"DatabaseConfig": {
"description": "Database configuration.",
"type": "object",
"properties": {
"url": {
"description": "Database URL. Accepts `sqlite://data/mcp-plan.db`, `sqlite://:memory:`,\n`postgres://user:password@host:port/database` or\n`mysql://user:password@host:port/database`.",
"type": "string",
"default": "sqlite://data/mcp-plan.db"
}
}
},
"RuntimeConfig": {
"description": "Estimated throughput and queueing knobs used by the MCP tools.",
"type": "object",
"properties": {
"max_retries": {
"description": "A task is escalated once its retry count reaches this value.",
"type": "integer",
"format": "uint32",
"default": 3,
"minimum": 0
},
"max_task_duration_secs": {
"description": "Estimated task duration above which a task is flagged for planning.",
"type": "integer",
"format": "uint64",
"default": 600,
"minimum": 0
},
"queue_limit": {
"description": "Upper bound for the list returned by `queue()`.",
"type": "integer",
"format": "uint",
"default": 20,
"minimum": 0
},
"tps_in": {
"description": "Estimated input throughput of the model, in tokens per second.",
"type": "integer",
"format": "uint64",
"default": 800,
"minimum": 0
},
"tps_out": {
"description": "Estimated output throughput of the model, in tokens per second.",
"type": "integer",
"format": "uint64",
"default": 28,
"minimum": 0
}
}
},
"SourceConfig": {
"description": "A statically configured source, synced into the `sources` table.",
"type": "object",
"properties": {
"description": {
"type": "string",
"default": ""
},
"id": {
"type": "string"
},
"title": {
"type": "string"
},
"type": {
"$ref": "#/$defs/SourceType"
}
},
"required": ["id", "title"]
},
"SourceType": {
"description": "How a source is fed into the task graph.",
"type": "string",
"enum": ["manual", "poll"]
}
}
}
Configuration example
# mcp-plan configuration.
#
# Copy this file to `config.toml` (the default location) and adjust as needed.
# Every key is optional; the values below show the defaults.
#
# Environment overrides use the `MCP_PLAN` prefix with `__` as the section
# separator, e.g. `MCP_PLAN__RUNTIME__TPS_IN=1000`.
[database]
# Database URL. Any of the supported backends:
# sqlite://data/mcp-plan.db (relative file)
# sqlite:///var/lib/mcp-plan/mcp-plan.db (absolute file)
# sqlite://:memory:
# postgres://user:password@host:5432/database
# mysql://user:password@host:3306/database
url = "sqlite://data/mcp-plan.db"
[runtime]
# Estimated model throughput, tokens per second.
tps_in = 800
tps_out = 28
# If the estimated duration of a task exceeds this it is flagged for planning.
max_task_duration_secs = 600
# Upper bound for the list returned by `queue()`.
queue_limit = 20
# A task is escalated once its retry count reaches this value.
max_retries = 3
# Sources are configured statically and synced into the `sources` table.
# `type` is either "manual" or "poll".
[[sources]]
id = "gh-issues"
title = "GitHub issues"
description = "Fetch open issues and insert them as tasks."
type = "poll"
Heartbeat
# HEARTBEAT.md
This is a description of your heartbeat. Your heartbeat is a persistent cron job
that fires off every once in a while on a user-defined interval. You will use
the `plan__*` tools to create task trees for yourself and then execute those
tasks in a structured manner to avoid recreating a workflow for yourself on your
own on every heartbeat. The `plan__*` tools expose a way for you to manage task
trees in a structured manner. The task trees are stored as a self-referential
table in the database that the planning MCP server manages.
## Instructions
1. Call `plan__sources` to receive a list of task sources and instructions on
how to create new tasks from those sources. After fetching sources you should
create a list of tasks that could be added to your task tree as prose.
2. After creating tasks as prose you should use the `plan__task`,
`plan__children` and `plan__insert` tools to find fitting locations to put
new tasks in and put them there. It is always important to check if a
particular task already exists to not duplicate it before insertion. When a
source describes a task as coming from a particular external item (an issue,
a PR, a discussion, etc.) you should pass the canonical URL or reference for
that item as `object.link` when inserting the task. `link` is unique when
set, so you can use it to deduplicate: before inserting a task, call
`plan__task` with only `link` to see whether a task for that item already
exists, and skip the insert if it does. If an insert is rejected because
another task already holds that `link`, treat it as a duplicate and ignore it
rather than retrying. Keep in mind that inserting is a sort of tree traversal
to insert new tasks into the already present or new task trees.
3. After reading task sources and carefully inserting new tasks into task trees
you need to call `plan__queue` which creates a list of tasks which are most
important from all the task trees and hands them off to you for execution or
planning. The following points explain how to handle both kinds of tasks.
- Execution: You should delegate execution tasks to the appropriate `writer`,
`junior_dev` or `senior_dev` agents. If they fail and the configured
maximum amount of retries is reached (currently 3) then you have to
escalate the task to the user and mark the task as escalated via
`plan__escalate`. Otherwise, if they fail you have to mark the task as
failed with the `plan__fail` tool. If they succeed you have to mark the
task with the `plan__complete` tool. For each executed task no matter the
end result of the execution you have to call exactly one of the three
`plan__escalate`, `plan__fail`, and `plan__complete` tools. Sometimes
execution tasks can actually be research or prototyping tasks in which case
the result of the task execution, if successful, should result in you
either escalating to the user or traversing the plan trees via `plan__task`
or `plan__children` and calling `plan__insert` to create new tasks after
the research or prototyping has been done.
- Planning: You should break down these tasks yourself into smaller chunks
via the `plan__insert` tool by creating one level deep children tasks. If
some or all of the children also need planning, they will be planned out in
a future heartbeat to avoid infinite recursion of task planning during a
single heartbeat. During planning, you may also use the `plan__task` and
`plan__children` tools to inspect how the task that is being currently
planned relates to other tasks. It is important to note that you may be
required sometimes to create research or prototyping tasks that will result
in those tasks getting executed to create new tasks. You should never do
actual research or prototyping or any sort of work when planning and
instead you should create tasks that you can execute to create new tasks
after the research or prototyping tasks have been executed. After planning
out each task you have to mark the task you just planned out as completed
via `plan__complete`.
4. After all the tasks that were queued were handled as instructed, you have to
write a report on what happened during the heartbeat. When creating the
report you should never include full tasks in the report and always summarize
tasks and processes. The report should contain the following:
- A list of newly added tasks during the task sourcing stage (1. and 2.).
- A list of tasks that were queued (in 3.), their original state and their
new state along with methods used and potentially new tasks that were
created as a result of tasks that were more about research or prototyping.
## Notes
- Task descriptions should be written in markdown and contain the following:
- A task header with the title of the task and a short description of the task
in one paragraph.
- A "what" subheader that goes over exactly what is expected to be done.
- A "why" subheader that goes over why the task was created in the first place
that should also have some info on the entire chain of tasks that go from
the task tree root to the task at hand. Just remember to not leave
"references" to other tasks and rather mention them in prose because the
delegates don't have access to tasks like you do.
- A "acceptance criteria" subheader that goes over specifically what needs to
be checked and how it needs to be checked in order to verify that the task
is successfully completed. These instructions will be executed by the
delegate and you should instruct the delegate to give you a report on
exactly what criteria succeeded or failed in prose.
- An optional "how" subheader if there is a specific requirement for the task
on how it should be achieved. Write this only when the user asks for it or
when you have already done research or prototyping on what strategy a
delegate should take in order to complete the task.
- A task that is dedicated to research or prototyping may also have sub tasks
because sometimes research and prototyping can take more time than expected.
- Your delegate agents are not allowed to do planning for you. You are the sole
planner of everything and you should always use the delegate agent reports to
plan.
- After a task has been escalated the user will step in and either modify and
ready the task with the `plan__ready` tool with the chat agent or they will
execute the task themselves and mark the task as complete with
`plan__complete` via the chat agent.
- `plan__insert` always creates a task with the `ready` status and it is
expected that it will be queued on future heartbeats. Planning and execution
are task kinds and you have no control over those.
## Rules
- You are only ever allowed to estimate tokens and never to estimate what task
is for planning and what task is for execution. This decision is left to the
MCP server based on the configured max amount of time of an execution task and
the token speeds of the configured task execution models.
- You are never allowed to go more than one-level deep when planning out tasks.
- You are not allowed to insert tasks in different subtrees other than the
subtree of the task you are currently planning. You may, after executing
research or prototyping tasks, insert tasks in different subtrees other than
the subtree of the task that was currently researched or prototyped.
- Always plan and execute tasks in the order the `plan__queue` tool gave you the
tasks.
- If nothing needs work you should escalate to the user, make a one-line report
and exit cleanly.