MCP Tools

This document covers the MissionCache MCP server: the 44 tools that expose MissionCache's task database, MissionCache files, time tracking, and planning surfaces to Claude Code over the Model Context Protocol. It is the layer that makes /missioncache:new, /missioncache:load, and the rest of the slash commands work - the command files are thin wrappers that tell Claude which MCP tools to call in what order, and this doc is the reference for everything those tools do.

It assumes you have read architecture.md for the shared vocabulary (tasks.db, ~/.missioncache/active/<project>/, full_path, heartbeats and sessions, the repo model). If a term in this doc is not defined here, it is defined there.

If you are just trying to use MissionCache from a command or a script, the short version is: every tool lives under the mcp__plugin_missioncache_pm__ prefix in Claude Code (mcp__plugin_missioncache_pm__list_active_tasks, mcp__plugin_missioncache_pm__get_task, etc.), every tool returns a JSON dictionary, and errors come back as {"error": true, "code": "...", "message": "..."} instead of raising. The rest of this doc is for when you want to understand exactly what a tool does, what to pass it, and what to expect back.

What the MCP server is#

MissionCache ships a single MCP server, registered in plugin.json as:

"mcpServers": {
  "pm": {
    "type": "stdio",
    "command": "uvx",
    "args": [
      "--from", "${CLAUDE_PLUGIN_ROOT}/mcp-server",
      "--with", "${CLAUDE_PLUGIN_ROOT}/missioncache-db",
      "mcp-missioncache"
    ]
  }
}

The server name is pm (for "project management"), and combined with the plugin name (missioncache), Claude Code exposes every tool under the prefix mcp__plugin_missioncache_pm__<tool_name>. So the Python function list_active_tasks in mcp-server/src/mcp_missioncache/tools_tasks.py is callable from Claude as mcp__plugin_missioncache_pm__list_active_tasks.

The server itself is a FastMCP application. The entry point (server.py) is 31 lines long and does nothing but import the tool modules to trigger their @mcp.tool() decorators:

from .app import mcp  # noqa: F401

from . import tools_tasks     # noqa: F401
from . import tools_docs      # noqa: F401
from . import tools_tracking  # noqa: F401
from . import tools_iteration # noqa: F401
from . import tools_planning  # noqa: F401
from . import tools_active    # noqa: F401

def main():
    mcp.run(transport="stdio")

All the logic lives in those six tools_* modules. Every tool function is an async def decorated with @mcp.tool() that takes Annotated[...] parameters with Field(description=...) for MCP schema generation and returns a dict. That return shape is important - tools never raise across the MCP boundary; they catch MissionCacheError and return e.to_dict(), and they catch everything else and return {"error": True, "message": str(e)}. Claude always sees a dict, never an exception.

Why async, why dicts#

The async def signature is required by FastMCP even though MissionCache's tools are pure sync code internally - none of them await anything. The handlers would work identically if they were sync, and the MCP SDK handles them either way; async def is the current FastMCP convention and it costs nothing to follow.

The dict return is also a FastMCP quirk. Tools could return Pydantic models directly, and they do internally (ListTasksResult, TaskDetail, etc. in models.py), but then call .model_dump() before returning. This is the cheapest way to get a stable JSON-serializable shape without depending on FastMCP's schema inference. The models are still useful as internal contracts - you get type checking, field validation, and one place to update when the shape changes.

The seven modules#

Module Tools Purpose
tools_tasks.py 11 Task lifecycle: list, get, create, complete, reopen, rename, update fields and notes
tools_docs.py 7 MissionCache files: create, get, update context, get digest, update tasks, get progress, move between projects
tools_tracking.py 7 Time tracking and repository management
tools_iteration.py 3 Iteration log integration (used by missioncache-auto and the iteration loop)
tools_planning.py 7 Parallel agent execution plans
tools_active.py 2 Active-task pointer for the statusline: set/clear in-progress checklist tasks
tools_pm.py 7 Project management: action items, stakeholders, tickets, project due date, cross-project portfolio

Total: 44 tools. The rest of this doc walks through them module by module. The style is reference-oriented: each tool gets a brief "when to use this", its parameter list with types and defaults, and what comes back on success. Error behavior is uniform across tools and covered in the error handling section instead of being repeated per tool.

Task lifecycle tools (tools_tasks.py)#

These are the tools you reach for when you are working with tasks as first-class entities in tasks.db. They do not touch MissionCache files directly - that is what the tools_docs module is for.

list_active_tasks#

When to use: You want every active task in the DB, optionally filtered or grouped by repo. This is what /missioncache:load calls first to show the selection table.

Parameters:

Returns: A ListTasksResult with tasks (list of TaskSummary), total_count, filter_applied (human-readable filter description), and optional other_tasks (present only in prioritize-by-repo mode).

Each TaskSummary has: id, name, status, task_type, repo_name, repo_path, jira_key, category, tags, time_total_seconds, time_formatted, last_worked_on, last_worked_ago, has_missioncache_files. The time fields come from a single batch query (db.get_batch_task_times) rather than N individual lookups, which is the reason this tool is faster than calling get_task in a loop.

The last_worked_ago field is computed via db.get_effective_last_updated(task), which takes the max of the DB's updated_at and the mtime of the project's -tasks.md file, so editing the task file from outside MissionCache still advances the "last worked" timestamp.

list_completed_tasks#

When to use: You want recently finished tasks, usually for a dashboard list, a monthly summary, or to find a project to reopen.

Parameters:

Returns: Same ListTasksResult shape as list_active_tasks. No other_tasks, no prioritization.

Time is not batch-fetched here - each TaskSummary does its own db.get_task_time() call. For small limits (default 20) this is fine; if you set limit very high, be aware you are doing N queries.

get_task#

When to use: You have a task ID or project name and you need everything - progress, time, subtasks, JIRA key, full path, file layout. This is the primary tool for /missioncache:load, and it is also what the dashboard hits when you open a task modal.

Parameters:

Provide exactly one of task_id or project_name. Providing both is not an error, but task_id wins.

Returns: A TaskDetail with every TaskSummary field plus full_path, parent_id, branch, pr_url, created_at, updated_at, completed_at, progress (a TaskProgress with completion_pct, total_items, completed_items, remaining_summary), prompt (optimized prompt config, usually null), subtasks (list of TaskSummary), and recent_updates.

The progress field is parsed live from <project>-tasks.md via _parse_task_progress(), so editing the checklist outside MissionCache shows up on the next get_task call. No caching.

find_task_for_directory#

When to use: Claude (or a hook) is running in a directory and needs to know "what MissionCache task, if any, owns this cwd". This is what the activity tracker and session_start hook use to decide whether to record heartbeats.

Parameters:

Returns: {"found": bool, "task": TaskDetail | None}. When found is False, task is None and there is nothing to do. When True, task is a full TaskDetail with subtasks disabled.

The resolution order is documented in find_task_for_cwd: per-session file (projects/<session-id>.json) first, then cwd directory match under ~/.missioncache/active/. (The legacy pending-project.json priority-1 branch and the now-removed pending-task.json artifact are documented in architecture.md; neither is in the live path.)

create_task#

When to use: You want a new task in the DB and, for coding tasks, a fresh directory under ~/.missioncache/active/<name>/. Slash commands generally call create_missioncache_files instead because it does more in one step, but create_task is the minimal primitive.

Parameters:

Returns: CreateTaskResult with task_id, task_name, task_type, category, and missioncache_path (the directory created on disk, or None for non-coding tasks).

Non-coding tasks do not get a directory - they exist only as DB rows and use add_task_update for progress notes. This is the split between "projects with files you edit" and "projects you log notes against" (meetings, reviews, investigations).

complete_task#

When to use: You are done with a task and want it out of the active list. For coding tasks, this also moves the MissionCache files from active/ to completed/.

Parameters:

Returns: CompleteTaskResult with task_id, task_name, previous_status, new_status="completed", completed_at, time_total_formatted. Plus active_children_count (the number of active forks that fork from this project) and, when that is non-zero, a warning string. Completing a parent that still has active forks is allowed - its context file stays readable and shared for them from completed/ - and the warning surfaces so it is not a surprise. The children lookup is advisory: it never fails the completion (active_children_count is None if the lookup itself errors after the completion committed).

If the task is already completed, it raises InvalidStateError with current_state="completed". No-op completions are surfaced rather than silently tolerated.

reopen_task#

When to use: A completed task was marked done prematurely and you need it back in the active list. For coding tasks, moves the MissionCache directory from completed/ back to active/.

Parameters: Same as complete_task - task_id or project_name, plus move_files: bool = True.

Returns: ReopenTaskResult with task_id, task_name, previous_status, new_status="active".

Raises InvalidStateError if the task is not currently completed. This is a strict check - you cannot "reopen" a task that was never completed in the first place.

add_task_update#

When to use: You want to append a timestamped note to a task's update log. Primarily used for non-coding tasks (meeting notes, 1:1 followups, investigation progress) where the file-based <project>-context.md is overkill.

Parameters:

Returns: {"update_id": int, "task_id": int, "task_name": str, "note": str}.

Updates are stored in the task_updates table (one row per call) and surfaced via get_task_updates and via get_task when include_updates=True on non-coding tasks.

get_task_updates#

When to use: You want the history of notes for a task, in reverse-chronological order.

Parameters:

Returns: {"task_id": int, "task_name": str, "updates": list[dict], "total_count": int}. Each update has id, note, created_at.

rename_task#

When to use: A project needs a new name. This is the safe path: it updates the DB row, moves the MissionCache directory, renames the files inside, and rewrites the template H1 titles - time tracking, heartbeats, sessions, and JIRA links survive because they key on the integer task ID, not the name.

Parameters:

Returns: RenameTaskResult with name (the canonical stored name - display this, not the typed input), old_name, normalized (whether trim/lowercase changed the input), full_path, files_renamed, h1_rewritten, sessions_updated, and warnings. On a real rename (changed: true) it also carries live_sessions under the NEW name - a rename moves the context file out from under any live peer session, which then needs to reload its paths.

Refuses with ALREADY_EXISTS when another project holds the target name (in the DB or on disk), with INVALID_STATE while a missioncache-auto run is in progress, and with VALIDATION_ERROR for legacy nested subtasks (rename the parent instead). Fork children are full projects and rename normally - only nested active/<parent>/<subtask>/ directories are refused.

update_task#

When to use: A task needs its JIRA key or category changed after creation - "link this project to PROJ-123", "this is actually an infra project". This is the conversational equivalent of the CLI's set-jira / set-category.

Parameters:

Returns: UpdateTaskResult with task_id, task_name, the stored jira_key and category after the update, and updated (which fields this call changed).

At least one of jira_key / category is required. The category is validated against CATEGORIES before ANY write, so an invalid value never leaves a half-applied update (a valid jira_key in the same call is not written either).

MissionCache file tools (tools_docs.py)#

These tools operate on the -tasks.md, -context.md, -plan.md files under ~/.missioncache/active/<project>/. They are the bridge between the DB (which stores task metadata) and the filesystem (which stores human-readable project state).

create_missioncache_files#

When to use: You are starting a new project and you want the DB row, the directory, and the template files in one call. This is what /missioncache:new runs - it is the MissionCache equivalent of git init for a task.

Parameters:

Returns: {"success": True, "task_id": int, "task_name": str, "category": str | None, "files": MissionCacheFiles} where MissionCacheFiles has task_dir, plan_file, context_file, tasks_file, prompts_dir. When fork_of was passed, also fork_of, fork_linked (whether the DB link now points at the requested parent - False if the parent did not resolve unambiguously yet), and, when not linked, fork_warning.

Internally this is a four-step dance:

  1. Ensure the repo is registered (db.add_repo if new).
  2. Call project_files.create_missioncache_files() to generate the files from templates (injecting the **Fork of:** header when fork_of is set).
  3. Call db.scan_all_repos() to register the task row in the DB - its reconcile pass also links the **Fork of:** header into parent_id.
  4. Reconcile the task's repo_id via db.find_task_by_full_path / db.update_task_repo - this is the fix for the gotcha where scan_all_repos can assign the task to the wrong repo when multiple repos share a prefix.

The reconciliation step is the load-bearing part. Without it, the task would appear under the wrong repo in the dashboard's per-repo view.

get_missioncache_files#

When to use: You need the filesystem paths for a task's MissionCache files. Usually followed by a direct Read/Edit via Claude's standard file tools rather than more MissionCache tool calls.

Parameters:

Returns: {"task_id": int | None, "task_name": str, "files": MissionCacheFiles}.

If the task exists in the DB, uses task.full_path to resolve subtask directories correctly (nested under parent). If only project_name is given and the task is not in the DB, falls back to the default active/<name>/ layout.

update_context_file#

When to use: You want to append entries to <project>-context.md without loading the whole file, editing it by hand, and writing it back. This is the main tool for /missioncache:save and is much faster than multiple Read/Edit calls.

Parameters:

Returns: {"success": True, "file": str, "timestamp": str, "sections_updated": list[str], "waiting_on_unmatched": list[str], "journal_rolled_over": int, "sections_removed": list[str], "sections_unmatched": list[str], "bullets_removed": list[str], "bullets_unmatched": list[str]}, plus live_sessions: list[dict] when there are any.

All sections are optional. Passing None for a section means "don't touch it". Passing an empty list means "touch the section but add nothing" - surprisingly useful for forcing a timestamp update without changing content. The sections_updated field tells you which sections actually received non-empty input.

Removals run before additions within the same call, so one call can replace a section rather than needing two.

waiting_on_unmatched lists match values that resolved to no row - check it, a resolve is never silently dropped (same contract as update_tasks_file.unmatched). sections_unmatched and bullets_unmatched carry the same contract for the removal params. journal_rolled_over reports how many Recent Changes subsections the 12-entry cap moved into the per-project <name>-journal.md on this write (0 almost always; informational).

Free-form text is sanitized. Every string passed to recent_changes, key_decisions or gotchas goes through context_health.sanitize_bullet first: headings are demoted and indented two spaces, and a dangling code fence is closed. This is not cosmetic. Section boundaries are found by column-0 anchors (^## , ^### \d{4}), so an entry carrying a pasted block with its own ## heading used to end the section at that line and strand every entry below it - invisible to the cap, the digest and the dashboard. The text still renders as headings; it just cannot act as structure. See hooks.md for the PreCompact snapshot, which is where this actually bit.

The underlying writer (project_files.update_context_file) updates the "Last Updated" timestamp atomically on every call, regardless of which sections you touched. Cap overflow and the context rewrite happen under one sidecar lock, journal written first (a crash duplicates entries into the journal rather than losing them). imported_event runs inside that same lock, which is the whole reason it exists as a parameter: the "Cross-project events" convention used to require a hand-written ## section, and a direct Edit on a file another session may be writing is exactly what the parallel-session discipline forbids.

live_sessions is the notification hook, and it is not unique to this tool: every MCP tool that rewrites or moves a project's files carries it - update_context_file, update_tasks_file, move_to_project, the five PM mutators, rename_task, and complete_task / reopen_task (see each tool's section). Here it lists other live Claude Code sessions bound to the project that owns context_file - derived from the filename, so it covers writes into another project's context as well as your own - each as {session_id, title, last_active}. The key is omitted when the list is empty, when the context filename carries no project name (the subtask layout writes a bare context.md), and when this session's own id cannot be resolved (without it the caller cannot exclude itself, and since ListAgents never lists the calling session, its own row would always fail to match). Dashboard and CLI writes do not notify: the notification is acted on by Claude reading the tool response, and those writers have no Claude on the response side.

lead_session rides the same set of tools, alongside live_sessions and under the same conditions, whenever a session has been designated project manager with /missioncache:lead and it is not the caller. It is a single {session_id, title, since} object rather than a list, since there is one lead by construction, and its title is always the constant missioncache-lead. It is project-independent: the lead is bound to no project, so it appears on every write to every project, including ones with no live peers at all. The writing session tells it about the change the same way it tells project peers, per the "Telling the lead" procedure in rules/missioncache.md. The key is omitted when there is no lead, when the designated session is not running, and when the caller is the lead.

The caller identifies itself through CLAUDE_CODE_SESSION_ID in the MCP subprocess's own environment. Do not try to derive this from the process tree: a single claude process hosts many sessions at once (measured: twelve sessions under one pid), so pid to session is one-to-many and cannot identify the caller. The env var is imperfect - during a resume a subprocess was once observed carrying an id the conversation did not use - but a wrong exclusion only costs one spurious ask-the-user, never a wrong write.

A session is listed only when its recorded pid proves it is still running. project_state is never cleaned on session exit, so it is a full history of everything that ever loaded the project - one real project here held 34 rows going back two months against zero live sessions. Those old rows carry no pid record and report unknown liveness, so unknown is treated as dead. (_detect_parallel_sessions in the SessionStart hook does the opposite and keeps unknowns, because it has already narrowed its candidates to transcripts touched in the last ten minutes. A lookup by project name has no such prefilter and cannot borrow that rule.)

title is the address: Claude Code's cross-session SendMessage reaches a peer by its session title, which the session_title hook keeps equal to the project name. A null title means that hook never ran for the session; the caller falls back to asking the user which session to address. See "Cross-session notifications" in rules/missioncache.md for the send and receive protocol, and hooks.md for how titles are assigned.

get_context_digest#

When to use: Resuming a project (/missioncache:load Step 2). This is the read path for context files - it replaces reading the whole file, and it is the ONLY way to read a context file past the 256KB Read-tool cap (parsing happens server-side via plain file I/O).

Parameters:

Returns: {"success": True, "file": str, "last_updated": str | None, "hub": str | None, "fork_of": str | None, "related_projects": str | None, "waiting_on": str | None, "next_steps": str | None, "recent_changes_last3": list[str], "section_index": list[{"name", "line"}], "file_size_bytes": int, "health_warnings": list[str], "is_fork": bool, "parent_digest": dict | None, "fork_parent_error": str | None}.

waiting_on and next_steps are the verbatim section bodies. section_index gives every ## heading with its 1-based line number, so a follow-up targeted Read (offset/limit) can fetch one specific section without loading the file. health_warnings carries the same per-project findings as missioncache-db health.

The digest also carries the PM layer (DB-side, best-effort): due_date (the project's target date or None) and action_items_open (open action items as {"id", "label", "what", "requested_by", "assignee", "due_date", "overdue", "source"}), with the PM health warnings merged into health_warnings. When the DB is unavailable these degrade to None/[] - a resume never fails on them.

For a fork (its context header carries **Fork of:** <parent>), is_fork is True and parent_digest describes the parent's shared context: {"name", "context_file", "last_updated", "context_mtime", "changed_since_seen"}. The parent is resolved from active/ then completed/, so a completed parent's shared layer stays reachable. If the parent cannot be read, parent_digest is None and fork_parent_error explains why - is_fork stays True so the caller can still show the fork and prompt a manual re-read rather than silently downgrading to a plain resume. changed_since_seen is True/False when seen_mtime was passed, else None.

update_tasks_file#

When to use: You want to mark tasks as completed, add new tasks, or update the "Remaining" summary line in <project>-tasks.md without editing the file directly.

Parameters:

Returns: {"success": True, ...} plus the progress result from project_files.update_tasks_file (completion percentage, counts, completed_numbers, unmatched, removed_numbers, remove_unmatched), and live_sessions when other live sessions are bound to the project (same contract as update_context_file; the tasks file is project state a live peer works from). Legacy unprefixed tasks.md files carry no project name and skip the lookup.

Active-task pointers are swept for both completed and removed numbers, so the statusline never keeps a Task: line pointing at an item the file no longer has.

move_to_project#

When to use: Splitting a project, or moving a topic that belongs somewhere else. Use it instead of a sections_remove call plus an imported_event call: a split done as two calls can half-fail and leave the content in both files or in neither.

Parameters:

Returns: {"success": True, "source_project", "target_project", "sections_moved", "bullets_moved", "waiting_on_moved", "tasks_moved", "unmatched", "summary"}, plus live_sessions.

Both sides also get a **Related projects:** header line via upsert_related_projects - "received part of this project" on the source, "moved content here" on the target - so the link is discoverable from either file. imported_event is not the only writer of that line.

A section name that appears more than once in the target file makes every later write to it fail with an INVALID_STATE error naming missioncache-db repair; the same guard applies to update_context_file's next_steps, key_decisions, gotchas and key_files writes.

unmatched carries anything that resolved to nothing, under the same never-silently-drop contract as waiting_on_unmatched - a move must never claim to have carried something it did not find.

live_sessions here is merged and deduped across both projects. Both files changed, and a session bound to either one is working from content that just moved under it.

Locking. This is the only operation in the codebase that holds more than one project's lock. It takes all four project files' sidecar locks in sorted path order - two sessions moving in opposite directions between the same pair would otherwise take the same two locks in opposite orders and deadlock. It also calls the unlocked write cores rather than _atomic_update_text / _atomic_update_context_with_journal: POSIX flock blocks a second acquisition of the same lockfile from the same process, so a lock-taking wrapper called under the outer locks would hang forever with no error and no timeout.

Crash window. Up to six writes happen (two contexts, two tasks files, and a journal on either side if a Recent Changes entry trips the cap). The target side is written fully first and the source side last, so a crash between them leaves the content in both files rather than in neither - the same duplicate-rather-than-lose reasoning the journal write uses. To recover, check the target, and if the content arrived, drop the leftovers from the source with sections_remove / bullets_remove / tasks_remove.

get_missioncache_progress#

When to use: You want the completion percentage and counts for a task without the full TaskDetail payload. Cheaper than get_task if you only care about progress.

Parameters:

Returns: {"task_id": int | None, "file": str, "progress": TaskProgress}.

You can pass either - if both are provided, tasks_file wins. If neither is provided, returns a VALIDATION_ERROR.

Time tracking and repo tools (tools_tracking.py)#

These tools drive the MissionCache heartbeat system and the repository registry. Most of them are called by hooks, not by Claude directly, but they are exposed as MCP tools so commands and scripts can use them too.

record_heartbeat#

When to use: You want to ping the DB to say "I am working on this task now". The MissionCache activity tracker hook calls this on every UserPromptSubmit, but you can also call it manually from a slash command (as /missioncache:load does after loading a project).

Parameters:

Provide exactly one of task_id or directory.

Returns: HeartbeatResult with heartbeat_id, task_id, task_name. In auto-detect mode when no task is found, returns {"recorded": False, "message": "..."} instead.

Heartbeats are raw timestamps - they don't become time on a task until process_heartbeats aggregates them into sessions.

process_heartbeats#

When to use: You want to flush accumulated heartbeats into sessions rows, which is what the dashboard actually reads for time totals. The MissionCache PreCompact and Stop hooks call this automatically, but you can call it on demand after a batch of work.

Parameters: None.

Returns: ProcessHeartbeatsResult with processed_count (how many heartbeats were aggregated in this call).

The aggregation is idempotent - processed heartbeats are marked and never re-aggregated. See architecture.md's invariants section for the detailed guarantee.

get_task_time#

When to use: You want the total time spent on a task over a period.

Parameters:

Returns: {"task_id": int, "task_name": str, "period": str, "total_seconds": int, "formatted": str, "session_count": int}.

The formatted string is "1h 23m"-style output from db.format_duration. The session_count is per the full period, not per the current day.

list_repos#

When to use: You want to see every tracked repository. Useful for debugging "why is this task not showing up under the right repo".

Parameters:

Returns: {"repos": list[dict], "total_count": int}. Each repo dict has id, path, short_name, is_active.

add_repo#

When to use: You want to register a repository explicitly. create_task and create_missioncache_files both auto-register on demand, so this tool is mainly for one-off setup.

Parameters:

Returns: {"repo_id": int, "path": str, "short_name": str}.

set_task_repo#

When to use: A task got created against the wrong repository - typically /missioncache:new capturing the wrong working directory - or the project's source of truth moved. /missioncache:load offers this on a repo mismatch.

Parameters:

Returns: {"task_id", "task_name", "repo_id", "repo_short_name", "repo_path", "changed"} - plus previous_repo_id when a rebind actually happened. Rebinding to the repo the task already has returns changed: False with a message instead of erroring. A target repo that is not registered returns REPO_NOT_FOUND.

scan_repos#

When to use: You manually created MissionCache files outside of MissionCache tools (or moved them around) and you need the DB to pick them up.

Parameters:

Returns: {"scanned_count": int, "tasks": list[{"id", "name", "repo_id"}]}.

Scanning walks ~/.missioncache/active/ and ~/.missioncache/completed/ under each repo's MissionCache paths, creates missing DB rows, and updates full_path for existing ones. It does not delete DB rows for tasks whose files are gone - that cleanup is manual.

Iteration log tools (tools_iteration.py)#

These tools are a thin wrapper around iteration_log.py, which writes to <project>-auto-log.md. They exist so MissionCache Auto and the sequential iteration loop can log their progress without having to duplicate file-writing logic. You will rarely call them manually.

log_iteration#

When to use: You completed one iteration of a long-running task and want to record what happened to the auto log.

Parameters:

Returns: {"success": True, "task_name": str, "iteration": int, "status": str, "log_file": str}.

log_iteration_completion#

When to use: The iteration loop finished (either successfully or by timing out) and you want to write the final summary entry.

Parameters:

Returns: {"success": True, "task_name": str, "completed": bool, "timed_out": bool, "total_iterations": int, "duration_seconds": int}.

get_iteration_status#

When to use: You want to know where an iteration loop left off without reading the log file by hand.

Parameters:

Returns: {"task_name": str, "iteration_log": {...}, "prompts": {...}}. The iteration_log dict has the parsed state from the auto-log file (iteration count, last status, completed/timed_out flags). The prompts dict has status info from prompts/ if it exists.

Planning tools (tools_planning.py)#

These are the newest set of tools and they serve a different use case than everything above: instead of tracking a single project's progress, they coordinate parallel subagent execution via the Claude Task tool. The workflow is (1) create a plan, (2) register each agent with its dependencies, (3) ask the orchestrator for ready agents, (4) spawn them in parallel, (5) each subagent reports back via update_agent_status, (6) mark the plan complete when done.

This is orthogonal to MissionCache Auto's parallel mode - MissionCache Auto uses multiprocessing and spawns its own Claude CLI subprocesses, whereas these tools let an orchestrating Claude drive a parallel agent swarm via the existing Task tool in a single conversation. They write to the DB (the plans, agent_executions, agent_dependencies tables) and the dashboard can surface them, but the plan tables are considered scoped follow-up territory and some of the DB-layer code for them is still sitting dormant in analytics_db.py (see the missioncache-release context file for the cleanup plan).

If you are not building a parallel-agent orchestrator, you do not need any of these. If you are, they give you the primitives.

create_plan#

Parameters:

Returns: {"plan_id": int, "name": str, "task_id": int | None, "status": "draft"}.

Plans start in draft state and transition to running as soon as any agent is running.

register_agent_execution#

Parameters:

Returns: {"execution_id": int, "plan_id": int, "agent_id": str, "agent_name": str, "dependencies": list, "dependency_records": list[int], "max_attempts": int}.

update_agent_status#

When to use: A subagent finished (or failed) and is reporting back. This is the callback the subagent includes in its own prompt instructions (via spawn_parallel_agents).

Parameters:

Returns: {"updated": True, "plan_id": int, "agent_id": str, "status": str}.

After the update, the tool calls _update_plan_counters() to recompute the plan's status based on all its agents. A plan is completed when every agent is in a terminal state (completed, failed, or skipped) with no failures; failed if any agent failed; running if any agent is running.

get_plan_status#

Parameters:

Returns: {"plan": dict, "agents": list[dict], "summary": {"total", "pending", "blocked", "running", "completed", "failed"}}.

This is the status snapshot for a plan - the plan metadata, every agent's current state, and a count breakdown by status.

get_ready_agents#

When to use: You are the orchestrator and you want the list of agents that can run next.

Parameters:

Returns: {"ready_agents": list[dict], "count": int}. Each ready agent has agent_id, agent_name, prompt, execution_id.

An agent is ready if its status is pending and all its dependencies are completed. Note: failed dependencies do not block. If agent 02 depends on 01 and 01 failed, 02 is still ready to run. This is intentional - the plan can complete partially and you have to deal with the failures yourself if you care about strict cascade blocking.

spawn_parallel_agents#

When to use: You want the actual Task tool invocation arguments for every ready agent in a plan. This is the tool the orchestrator calls when it is about to invoke Task for each agent.

Parameters:

Returns: {"ready_count": int, "task_calls": list[dict], "instructions": str, "plan_id": int, "plan_name": str}.

Each entry in task_calls has subagent_type, description, prompt, run_in_background: True. The prompt for each agent is wrapped with explicit update_agent_status call instructions so the subagent knows how to report completion. The instructions field is a human-readable reminder that you should dispatch every Task call in a single message to get true parallelism (multiple tool calls in one message run concurrently; multiple messages serialize).

complete_plan#

When to use: Every agent has finished and you want to finalize the plan's completed_at timestamp.

Parameters:

Returns: {"plan_id": int, "status": str, "completed": True}.

Most plans will auto-transition via _update_plan_counters, but this tool is the explicit way to finalize a plan regardless of its automatic state.

Active-task pointer tools (tools_active.py)#

These two tools drive the statusline's Task: field: a per-session pointer file recording which checklist items from a project's -tasks.md are currently in progress. The pointer is scoped by session ID so concurrent Claude sessions don't clobber each other's display.

set_active_missioncache_tasks#

When to use: The user signals which checklist task they are working on ("let's do 54a") - /missioncache:load Step 5 calls this so the statusline reflects the focus.

Parameters:

Returns: {"success": True, "project_name", "task_numbers", "pointer_path"} with the validated set. Replaces any prior pointer for the session (idempotent).

The pointer auto-clears when update_tasks_file marks the pointed-at items [x].

clear_active_missioncache_tasks#

When to use: Focus shifts off-task without completing anything - the statusline field should hide rather than show stale focus.

Parameters:

Returns: {"success": True, "session_id", "cleared": bool} (cleared is False when there was no pointer to remove).

Project management tools (tools_pm.py)#

Thin wrappers over missioncache_db.pm_items - the single write path shared with the dashboard's REST endpoints and the missioncache-db CLI. Every mutation writes SQLite (the source of truth) and re-renders the read-only mirror sections in the project's context file under the sidecar lock, so a change made here is visible in the dashboard immediately and on the next /missioncache:load.

Because every mutation rewrites the context file, each mutating tool here returns live_sessions when other live sessions are bound to the project (same contract as update_context_file, key present only when non-empty). update_action_item resolves the project from the item row, since it is addressed by item_id.

add_action_item#

When to use: A meeting transcript or conversation produced a commitment - the user's or a colleague's. Blocking dependencies belong in Waiting on instead.

Parameters: project_name: str, what: str, requested_by: str | None = None, assignee: str = "me", due_date: str | None = None (YYYY-MM-DD), source: str | None = None (meeting + date, transcript path), notes: str | None = None.

Returns: {"success": True, "item": {...}} - the item carries its stable id (rendered as AI-<id>).

update_action_item#

When to use: Complete (status="done", record the outcome in notes), reopen, drop, reassign, or re-date an item. Only passed fields change; due_date="none" clears the date. completed_at is managed automatically on status transitions.

Parameters: item_id: int, then optional status, what, requested_by, assignee, due_date, notes.

list_action_items#

When to use: Project-scoped with project_name, or omit it for the cross-project scope ("what's due this week, anywhere") - each item then carries its project_name. Filters: status, assignee (case-insensitive; me = the user's own commitments), overdue_only, due_within_days.

Returns: {"success": True, "count": int, "items": [...]} with an overdue flag per item.

set_stakeholder#

When to use: Record who matters to this project and in what role. Upserts on (project, name); remove=True deletes. Renders into ## Stakeholders.

Parameters: project_name: str, name: str, role: str | None = None, notes: str | None = None, remove: bool = False.

set_ticket#

When to use: Link an external ticket. System-agnostic: label + url is the whole contract; system ("jira", "monday", ...) is a display hint never branched on; status is a free-text cache MissionCache never fetches. Upserts on (project, label); remove=True unlinks. When url is omitted, a JIRA-style label gets its URL from the user's prefix map if one matches.

Parameters: project_name: str, label: str, url: str | None = None, system: str | None = None, status: str | None = None, notes: str | None = None, remove: bool = False.

set_project_due_date#

When to use: Committed work got a target date (never fabricate one onto uncommitted backlog - the estimation discipline applies). Renders a **Due:** header line; health flags it within 7 days of the date. Pass "none" (or omit) to clear.

Parameters: project_name: str, due_date: str | None = None (YYYY-MM-DD).

get_portfolio#

The cross-project read: what is urgent, what is waiting on you, what is waiting on others, which projects have a live session, and which session is the designated lead. It is the data source for /missioncache:brief and for the lead session's delta loop, and it produces the SAME ranking and counts as the dashboard's Attention view because both call missioncache_db.portfolio.build_portfolio.

Parameters: scope ("focus" default - projects with a live session or worked in the last recent_days; "all" - every project with something outstanding), recent_days (7), max_projects (12), max_rows_per_project (3).

Returns counts (never clipped), on_me (three buckets, capped per bucket), on_others (grouped by project, rows capped), projects (sorted by urgency, display strings shortened), live_sessions (pid-alive sessions bound to a project, by title; the chat side cross-checks against ListAgents), lead_session ({title, since} or null), scope, and watermark (a change token; equal watermarks mean nothing worth recomputing changed).

Read-only and session-neutral: unlike get_task, it never binds the calling session to a project. A project-manager session is not a project.

Error handling#

Every tool in the server follows the same error-handling pattern, which is worth memorizing because it is the contract for the entire MCP surface.

Success shape#

On success, a tool returns a JSON dict. Keys vary by tool, but there is no top-level "success": true marker on every response - some tools include it for convenience (create_missioncache_files, update_context_file, log_iteration), others don't (list_active_tasks, get_task). The presence of an "error" key is the reliable way to tell success from failure.

Error shape#

On error, a tool returns:

{
  "error": true,
  "code": "TASK_NOT_FOUND",
  "message": "Task not found: my-project",
  "details": {"task_id": "my-project"}
}

The code is one of the values defined in errors.py:ErrorCode:

Code Meaning
TASK_NOT_FOUND Task ID or name does not resolve
REPO_NOT_FOUND Repository path is not registered
FILE_NOT_FOUND MissionCache file (or other file) does not exist
VALIDATION_ERROR Input parameter failed validation
DB_ERROR SQLite operation failed
PERMISSION_ERROR Filesystem permission denied
INVALID_STATE Operation not allowed in current state (e.g., reopening a task that isn't completed)
OPERATION_FAILED Catch-all for operation-specific failures

The details field is tool-specific and may contain things like task_id, current_state, expected_state, field (for validation errors), or path (for file errors).

Unexpected errors#

If a tool hits an exception that is not an MissionCacheError subclass, it logs via logger.exception(...) and returns:

{
  "error": true,
  "message": "<exception str>"
}

No code field, no details. This is the "something unexpected broke" path and should show up in the MCP server's stderr log (uvx pipes logs through stdio; check wherever your Claude Code logs MCP server stderr). If you see an error response without a code, treat it as a bug and go read the server logs.

Why no raises#

Tools do not raise across the MCP boundary because FastMCP would serialize the exception as a protocol error, and Claude would see a generic MCP failure instead of the structured MissionCache error. Returning a dict lets Claude reason about what went wrong (task not found vs validation error vs DB crash) and react appropriately from a slash command.

The tradeoff is that tool callers must check "error" in result after every call. A helper wrapper could raise on error dicts, but none exists today - the MCP contract is "dicts in, dicts out" and every call site handles the check inline.

Shared patterns#

These are patterns that show up in multiple tools. If you are writing a new one, following them keeps the surface consistent.

task_id vs project_name#

Many tools accept either a task_id: int or a project_name: str. The convention is:

Tools that only accept task_id (add_task_update, get_task_time, get_task_updates) are the exception - they exist because the parameter is almost always known at call time.

Path validation#

Any tool that takes a filesystem path runs it through helpers._validate_path(path, field_name, must_be_under=...). This:

  1. Rejects empty strings.
  2. Rejects null bytes (common injection vector).
  3. Resolves the path (Path(path).resolve()).
  4. If must_be_under is set, verifies the resolved path is inside that directory.

The must_be_under check is what prevents update_context_file from writing to arbitrary files - it is always called with must_be_under=settings.root, so a path like /tmp/evil.md or ~/.missioncache/../evil.md gets rejected.

Tools that should not restrict paths (e.g., record_heartbeat with a directory arg pointing at a repo root) omit must_be_under and just get null-byte and empty-string checks.

Time filters#

Time-related tools that accept a period parameter (get_task_time, some batch helpers) all use the same vocabulary:

Pass anything else and the underlying DB method either returns "all" implicitly or raises, depending on which method you hit. Stick to the three documented values.

Single-shot reads vs batch reads#

Two patterns show up when fetching time:

list_active_tasks uses the batch form when include_time=True to avoid the N+1 problem. list_completed_tasks does not, because its limit is small by default. If you are writing a new tool that returns a list with time info, prefer the batch form.

Adding a new tool#

If you have a new operation that belongs in the MCP server, the pattern is:

  1. Pick the right module. Task lifecycle goes in tools_tasks.py, file ops in tools_docs.py, time/repo in tools_tracking.py, iteration log in tools_iteration.py, planning in tools_planning.py, active-task pointer in tools_active.py. If it does not fit any of these, consider whether it belongs in the server at all before creating a new module.
  2. Write the signature.
    @mcp.tool()
    async def my_tool(
        param1: Annotated[str, Field(description="Clear description")],
        param2: Annotated[int | None, Field(description="Optional param")] = None,
    ) -> dict:
        """Short description shown in MCP help."""
        db = get_db()
        try:
            # ... do work ...
            return {"success": True, ...}
        except MissionCacheError as e:
            return e.to_dict()
        except Exception as e:
            logger.exception("Error in my_tool")
            return {"error": True, "message": str(e)}
    
  3. Use typed returns where it helps. If your tool returns a fixed shape, add a Pydantic model in models.py and .model_dump() it at the return site. If the shape is more ad-hoc, return a plain dict.
  4. Validate inputs early. Call _validate_path for paths, return VALIDATION_ERROR dicts for bad enum values, raise MissionCacheError subclasses for expected failures.
  5. No new imports in server.py. The tool is registered automatically when its module is imported - server.py imports tools_<module> once, and that triggers every @mcp.tool() in the file. Do not reach into server.py.
  6. Reload the plugin to pick it up. claude plugins install missioncache@local and restart Claude Code. The MCP server restart happens on the next Claude session.
  7. Add a test. mcp-server/tests/ has fixtures for DB setup and patterns for calling tools directly (bypassing MCP transport). Follow them.

The thing that makes this server pleasant to extend is that tools are flat, independent functions with no shared state beyond the DB instance. There is no plugin registry, no base class, no middleware. Adding a tool is additive - you cannot accidentally break an existing one.

Troubleshooting#

"The tool is missing from Claude's tool list"#

Cause: The plugin cache is stale. MCP tools are discovered at plugin load, and a new tool added after you installed the plugin won't show up until you reinstall.

Fix: claude plugins install missioncache@local (or missioncache@missioncache if you are on the marketplace path) and restart your Claude Code session. /reload-plugins does not cover MCP servers.

"I'm calling the tool but getting error: true, message: <python exception> with no code"#

Cause: An unhandled exception hit the except Exception fallback. This is not an MissionCacheError - it is something unexpected.

Fix: Check the MCP server stderr. uvx pipes stderr through stdio to Claude Code, which logs it to its own files. The server uses logger.exception which includes a full traceback. Once you have the traceback, treat it as a normal Python bug: find the line, fix the root cause.

"Tool returns FILE_NOT_FOUND but the file exists"#

Cause: Path validation is resolving to a different place than you expected. _validate_path calls Path.resolve(), which follows symlinks and normalizes ... If the resolved path falls outside MISSIONCACHE_ROOT, you get VALIDATION_ERROR - if it resolves inside but the file is not there, you get FILE_NOT_FOUND.

Fix: Print the path you are passing and run it through Path(path).resolve() manually in a Python REPL. Compare against the expected ~/.missioncache/active/<name>/... shape.

"Task shows up under the wrong repo after create_missioncache_files"#

Cause: This is the bug that motivated the find_task_by_full_path / update_task_repo reconciliation in create_missioncache_files. scan_all_repos() matches tasks to repos by prefix, and when two repos share a prefix, it can pick the wrong one.

Fix: Already fixed in current code. If you see it anyway, make sure you are on a recent MissionCache version and that create_missioncache_files is the tool you are calling (not a manual create_task + scan_all_repos sequence, which does not include the reconciliation step).

"list_active_tasks is slow for big project counts"#

Cause: include_time=True is the default, and even with batch lookups, 100+ tasks means 100+ rows joined across heartbeats, sessions, and tasks.

Fix: Pass include_time=False if you are not displaying time. The tool returns in under a millisecond without time, versus tens of milliseconds with time. For the dashboard, which always wants time, the batch query is fine; for scripts that just want the task list, skip it.

"Heartbeats aren't showing up as time"#

Cause: record_heartbeat writes to heartbeats (unprocessed) but get_task_time reads from sessions (processed). The aggregation step in between only happens when process_heartbeats runs.

Fix: Call process_heartbeats manually, or wait for the next PreCompact / Stop hook. See architecture.md's invariants for the full guarantee.

"Plan agents report status but plan status doesn't update"#

Cause: _update_plan_counters only runs inside update_agent_status. If you are mutating the DB directly (e.g., via sqlite3 or through missioncache_db internals), the plan counter sync is skipped.

Fix: Always go through update_agent_status for status updates. If you need to bulk-update a plan, consider making a new MCP tool that takes a list of (agent_id, status) pairs and loops through update_agent_status - don't bypass it.

Where to go from here#