plan_book
Plan a phased book bible from identity through tone, then synchronize the completed plan to project configuration for consistent story development.
Instructions
Phased book bible planning (identity→…→tone); completes with sync to project config.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| phase | No | ||
| data | No | ||
| reset | No | ||
| status | No |
Output Schema
| Name | Required | Description | Default |
|---|---|---|---|
| result | Yes |
Implementation Reference
- src/storywright_mcp/planning.py:342-409 (handler)Core handler function `run_plan_book` that manages the planning state machine: loads project/bible, handles status/reset/phase progression, applies phase data via apply_phase_data, persists planning state, and calls finalize_bible on completion.
async def run_plan_book( phase: str | None = None, data: dict | None = None, reset: bool = False, status: bool = False, ) -> str: proj, cont = require_project() base = proj.base_path bible = BookBible.load(base) planning_state_path = base / ".planning_state.json" if status: if planning_state_path.exists(): state = json.loads(planning_state_path.read_text(encoding="utf-8")) current_phase = state.get("current_phase", "identity") completed = state.get("completed_phases", []) else: current_phase = "identity" completed = [] return ( f"# Planning status\n\nCurrent phase: **{current_phase}**\n" f"Completed: {', '.join(completed) or 'none'}\n\n" f"Phases: {', '.join(PLANNING_PHASES)}\n" ) if reset: if planning_state_path.exists(): planning_state_path.unlink() return "Planning reset. Call plan_book() with no arguments to begin at identity." if phase is None: if planning_state_path.exists(): state = json.loads(planning_state_path.read_text(encoding="utf-8")) phase = state.get("current_phase", "identity") else: phase = "identity" planning_state_path.write_text( json.dumps({"current_phase": phase, "completed_phases": []}), encoding="utf-8", ) if data is not None: bible = apply_phase_data(bible, phase, data, proj) bible.save(base) state = json.loads(planning_state_path.read_text(encoding="utf-8")) completed = state.get("completed_phases", []) if phase not in completed: completed.append(phase) state["completed_phases"] = completed next_phase = get_next_phase(phase) if next_phase == "finalize": finalize_bible(bible, proj, cont) if planning_state_path.exists(): planning_state_path.unlink() return ( f"# Planning complete\n\nBible saved; project synced.\n" f"- Title: {bible.title}\n- Characters: {len(bible.characters)}\n" f"- Chapters scaffolded: {bible.chapter_count}\n\n" f"Next: get_book_bible() or start_chapter(1)." ) state["current_phase"] = next_phase planning_state_path.write_text(json.dumps(state), encoding="utf-8") phase = next_phase return _phase_prompt_markdown(phase, bible) - src/storywright_mcp/planning.py:23-157 (handler)`apply_phase_data` — applies phase-specific JSON data to the BookBible model for each planning phase (identity, world, characters, structure, deaths, gags, tone).
def apply_phase_data(bible: BookBible, phase: str, data: dict, project) -> BookBible: if phase == "identity": if "title" in data: bible.title = data["title"] if "genre" in data: bible.genre = data["genre"] if "logline" in data: bible.logline = data["logline"] if "themes" in data: from .models.bible import Theme bible.themes = [ Theme(name=t["name"], description=t.get("description", "")) if isinstance(t, dict) else t for t in (data.get("themes") or []) ] if "comparable_books" in data: bible.comparable_books = data["comparable_books"] elif phase == "world": if "world_name" in data: bible.world_name = data["world_name"] if "world_description" in data: bible.world_description = data["world_description"] if "world_rules" in data: bible.world_rules = data["world_rules"] if "magic_systems" in data: from .models.bible import MagicSystem bible.magic_systems = [] for m in data.get("magic_systems") or []: if isinstance(m, dict): bible.magic_systems.append( MagicSystem( name=m.get("name", ""), description=m.get("description", ""), rules=m.get("rules", []), limitations=m.get("limitations", []), cost=m.get("cost", ""), ) ) if "locations" in data: from .models.bible import Location bible.locations = [] for loc in data.get("locations") or []: if isinstance(loc, dict): bible.locations.append( Location( name=loc.get("name", ""), description=loc.get("description", ""), significance=loc.get("significance", ""), ) ) elif phase == "characters": from .models.bible import CharacterProfile bible.characters = [] for c in data.get("characters") or []: if isinstance(c, dict): bible.characters.append( CharacterProfile( name=c.get("name", ""), role=c.get("role", "supporting"), backstory=c.get("backstory", ""), motivation=c.get("motivation", ""), flaw=c.get("flaw", ""), voice_notes=c.get("voice_notes", ""), comedy_hook=c.get("comedy_hook", ""), appearance=c.get("appearance", ""), ) ) elif phase == "structure": if "act_structure" in data: bible.act_structure = data["act_structure"] if "chapter_count" in data: bible.chapter_count = data["chapter_count"] if "plot_beats" in data: from .models.bible import PlotBeat bible.plot_beats = [] for b in data.get("plot_beats") or []: if isinstance(b, dict): bible.plot_beats.append( PlotBeat( chapter=b.get("chapter", 1), beat=b.get("beat", ""), description=b.get("description", ""), ) ) elif phase == "deaths": if "has_deaths" in data: project.has_deaths = data["has_deaths"] if "deaths" in data: from .models.bible import DeathPlan bible.deaths = [] for d in data.get("deaths") or []: if isinstance(d, dict): bible.deaths.append( DeathPlan( character=d.get("character", ""), chapter=d.get("chapter", 1), circumstances=d.get("circumstances", ""), death_style=d.get("death_style", ""), ) ) elif phase == "gags": from .models.bible import RunningGagPlan bible.running_gags = [] for g in data.get("running_gags") or []: if isinstance(g, dict): bible.running_gags.append( RunningGagPlan( name=g.get("name", ""), owner=g.get("owner", ""), setup_chapter=g.get("setup_chapter", 1), description=g.get("description", ""), escalation_pattern=g.get("escalation_pattern", ""), ) ) elif phase == "tone": if "tone_notes" in data: bible.tone_notes = data["tone_notes"] if "comparable_books" in data: bible.comparable_books = data["comparable_books"] return bible - src/storywright_mcp/planning.py:160-211 (handler)`finalize_bible` — syncs bible data into project/continuity objects: sets project name/genre, creates Character entries, death schedules, running gags, and scaffolds chapters.
def finalize_bible(bible: BookBible, project, continuity) -> None: if not project.name or project.name == "Untitled": project.name = bible.title or project.name if bible.genre and not project.genre: project.genre = bible.genre for char_profile in bible.characters: if not any(c.name == char_profile.name for c in project.characters): project.characters.append( Character( name=char_profile.name, role=char_profile.role, description=char_profile.backstory, voice_notes=char_profile.voice_notes, comedy_hook=char_profile.comedy_hook, ) ) continuity.living_characters.append( CharacterState(name=char_profile.name, status=CharacterStatus.ALIVE) ) for death_plan in bible.deaths: if not any(d.character == death_plan.character for d in project.death_schedule): project.death_schedule.append( DeathEntry( character=death_plan.character, chapter=death_plan.chapter, circumstances=death_plan.circumstances, death_style=death_plan.death_style, ) ) for gag_plan in bible.running_gags: if not any(g.name == gag_plan.name for g in project.running_gags): project.running_gags.append( RunningGag( name=gag_plan.name, owner=gag_plan.owner, setup_chapter=gag_plan.setup_chapter, description=gag_plan.description, escalation_pattern=gag_plan.escalation_pattern, ) ) if not project.chapters: for i in range(1, bible.chapter_count + 1): project.chapters.append( Chapter(num=i, title=f"Chapter {i}", target_words=5000, status=ChapterStatus.NOT_STARTED) ) project.save() continuity.save(project.base_path) - src/storywright_mcp/app.py:130-141 (registration)Tool registration via FastMCP @mcp.tool() decorator. The `plan_book` async function delegates to `run_plan_book` from planning module.
@mcp.tool() async def plan_book( phase: str | None = None, data: dict | None = None, reset: bool = False, status: bool = False, ) -> str: """Phased book bible planning (identity→…→tone); completes with sync to project config.""" try: return await run_plan_book(phase=phase, data=data, reset=reset, status=status) except ValueError as e: return str(e) - Defines PLANNING_PHASES array (identity→world→characters→structure→deaths→gags→tone→finalize) and the `get_next_phase` helper that drives phase progression.
# Planning phases for plan_book tool PLANNING_PHASES = [ "identity", # Title, genre, logline, themes "world", # World name, description, magic, locations "characters", # Character profiles "structure", # Act structure, plot beats "deaths", # Death schedule (if applicable) "gags", # Running gags "tone", # Tone notes, comparable books "finalize", # Review and save ]