🐱 Medium-horizon agent planning MCP server
9plan docs implementation-plan.md
33 kB
Markdown
at main

9plan MCP Server: Implementation Design Document #

Version: 1.2.0
Date: January 2026
Status: Draft


Table of Contents #

  1. Executive Summary
  2. Architecture Overview
  3. Technology Stack
  4. Project Structure
  5. Data Model
  6. Tool Specifications
  7. Prompt Specifications
  8. File Formats
  9. Logging
  10. Error Handling
  11. Configuration
  12. Dependency Injection & Testability
  13. Implementation Phases
  14. Known Limitations

1. Executive Summary #

1.1 Purpose #

9plan is an MCP server that provides a session-scoped work queue for long time-horizon agent task sequencing. It enables AI agents to decompose complex tasks into discrete, self-contained plans that can be executed across conversation boundaries—even surviving context compaction.

1.2 Core Value Proposition #

Challenge 9plan Solution
Agents lose context during long tasks Persistent sessions with three-word identifiers for easy resumption
Complex tasks require decomposition Structured plans with explicit inputs/outputs for dependency tracking
Work ordering is ambiguous Single queue with front/back positioning semantics
Progress is lost on interruption Plan files serve as checkpoints; notes preserve state

1.3 Design Principles #

  1. Self-Contained Plans: Each plan contains enough context for an agent to execute without asking clarifying questions
  2. Semantic Dependencies: Cross-plan dependencies are resolved by searching history, not by ID references
  3. Single Queue Discipline: No priority tiers or backlogs—one queue that must eventually empty
  4. Hybrid Storage: SQLite for structural integrity, flat files for agent-accessible content
  5. Session Breadcrumbs: Tool responses include session names to aid context recovery

2. Architecture Overview #

2.1 High-Level Architecture #

┌─────────────────────────────────────────────────────────────────┐
│                        MCP Client                                │
│              (Claude, Cursor, VS Code, etc.)                     │
└─────────────────────────┬───────────────────────────────────────┘
                          │ stdio / HTTP
                          ▼
┌─────────────────────────────────────────────────────────────────┐
│                     9plan MCP Server                             │
│  ┌─────────────┐  ┌─────────────┐  ┌─────────────────────────┐  │
│  │   Tools     │  │  Prompts    │  │   Server Instructions   │  │
│  │ (12 total)  │  │ (bootstrap) │  │   (plan format spec)    │  │
│  └──────┬──────┘  └─────────────┘  └─────────────────────────┘  │
│         │                                                        │
│  ┌──────▼──────────────────────────────────────────────────┐    │
│  │                   Core Services                          │    │
│  │  ┌────────────┐  ┌────────────┐  ┌────────────────────┐ │    │
│  │  │  Session   │  │   Queue    │  │      History       │ │    │
│  │  │  Manager   │  │  Manager   │  │  Search (FTS5)     │ │    │
│  │  └────────────┘  └────────────┘  └────────────────────┘ │    │
│  └──────────────────────────────────────────────────────────┘    │
│                              │                                   │
│  ┌───────────────────────────▼──────────────────────────────┐   │
│  │                    Storage Layer                          │   │
│  │     SQLite (session.db)    +    Plan Files (plans/*.txt)  │   │
│  └───────────────────────────────────────────────────────────┘   │
└─────────────────────────────────────────────────────────────────┘

2.2 Plan Lifecycle State Machine #

                         ┌──────────────────┐
                         │     Created      │
                         │    (queued)      │
                         └────────┬─────────┘
                                  │ 9plan_queue_pull
                                  ▼
                         ┌──────────────────┐
              ┌──────────│     Active       │──────────┐
              │          └──────────────────┘          │
              │                   │                    │
 9plan_plan_defer          9plan_plan_complete    9plan_plan_discard
              │                   │                    │
              ▼                   ▼                    ▼
     ┌──────────────┐    ┌──────────────────┐   ┌──────────────┐
     │   Queued     │    │    Completed     │   │   Discarded  │
     │  (deferred)  │    │  (in history)    │   │  (deleted)   │
     └──────────────┘    └──────────────────┘   └──────────────┘

2.3 Key Architectural Decisions #

Decision Rationale
SQLite + flat files SQLite protects queue integrity; flat files allow agent filesystem access
stdio transport Most MCP clients spawn servers via stdio; HTTP can be added later
Single queue (no priorities) Prevents agents from abandoning low-priority work
Three-word session names Human-memorable, easy to communicate
Alphanumeric plan IDs Short (5 chars), collision-resistant
FTS5 for history search Built into SQLite, semantic search over completed plans

3. Technology Stack #

3.1 Core Dependencies #

Component Technology Version Rationale
Runtime Node.js ≥22.5.0 Required for built-in node:sqlite
MCP Framework @modelcontextprotocol/sdk ^1.22.0 Official SDK
MCP Transport @karashiiro/mcp ^1.0.0 Handles stdio + HTTP with sessions
Schema Validation Zod ^3.25.0 Runtime validation, env validation
Database node:sqlite built-in Zero deps, FTS5 support
Session Names unique-names-generator ^4.7.1 Pre-built dictionaries
Plan IDs nanoid ^5.1.6 Cryptographically secure, custom alphabets
App Directories env-paths ^3.0.0 Cross-platform standard directories
Logging pino ^9.0.0 Ultra-fast structured JSON logging

3.2 Optional Dependencies (HTTP Transport) #

Component Technology Version
Web Framework hono ^4.0.0
Node Adapter @hono/node-server ^1.0.0

3.3 Development Dependencies #

Component Technology
Build TypeScript ^5.8.0
Dev Runner tsx
Testing Vitest
Log Formatting pino-pretty

3.4 package.json #

{
  "name": "9plan-mcp-server",
  "version": "1.0.0",
  "type": "module",
  "main": "dist/index.js",
  "scripts": {
    "build": "tsc",
    "dev": "tsx watch src/index.ts",
    "start": "node dist/index.js",
    "test": "vitest"
  },
  "dependencies": {
    "@karashiiro/mcp": "^1.0.0",
    "@modelcontextprotocol/sdk": "^1.22.0",
    "env-paths": "^3.0.0",
    "nanoid": "^5.1.6",
    "pino": "^9.0.0",
    "unique-names-generator": "^4.7.1",
    "zod": "^3.25.0"
  },
  "optionalDependencies": {
    "hono": "^4.0.0",
    "@hono/node-server": "^1.0.0"
  },
  "devDependencies": {
    "@types/node": "^22.0.0",
    "pino-pretty": "^11.0.0",
    "tsx": "^4.0.0",
    "typescript": "^5.8.0",
    "vitest": "^3.0.0"
  },
  "engines": {
    "node": ">=22.5.0"
  }
}

4. Project Structure #

9plan-mcp-server/
├── src/
│   ├── index.ts                    # Entry point, transport setup
│   ├── server.ts                   # McpServer factory, tool/prompt registration
│   ├── types.ts                    # Core TypeScript interfaces
│   ├── env.ts                      # Environment config with Zod
│   ├── logger.ts                   # Pino logger configuration
│   │
│   ├── tools/
│   │   ├── index.ts                # Tool registration aggregator
│   │   ├── session-create.ts
│   │   ├── session-resume.ts
│   │   ├── queue-add.ts
│   │   ├── queue-pull.ts
│   │   ├── plan-defer.ts
│   │   ├── plan-complete.ts
│   │   ├── plan-discard.ts
│   │   ├── history-search.ts
│   │   ├── history-get.ts
│   │   ├── admin-validate.ts       # Invariant checking
│   │   ├── admin-sessions.ts       # List all sessions
│   │   └── admin-state.ts          # Detailed state dump
│   │
│   ├── prompts/
│   │   └── bootstrap.ts
│   │
│   ├── db/
│   │   ├── schema.sql
│   │   └── session-store.ts
│   │
│   ├── generators/
│   │   ├── session-name.ts         # Uses unique-names-generator
│   │   └── plan-id.ts              # Uses nanoid
│   │
│   └── files/
│       └── plan-files.ts
│
├── tests/
│   ├── unit/
│   │   ├── session-store.test.ts
│   │   ├── plan-files.test.ts
│   │   └── generators.test.ts
│   └── integration/
│       └── workflow.test.ts
│
├── package.json
├── tsconfig.json
├── vitest.config.ts
└── README.md

5. Data Model #

5.1 SQLite Schema #

PRAGMA foreign_keys = ON;

CREATE TABLE IF NOT EXISTS sessions (
    name TEXT PRIMARY KEY,
    task_description TEXT,
    created_at TEXT DEFAULT (datetime('now'))
) STRICT;

CREATE TABLE IF NOT EXISTS plans (
    id TEXT PRIMARY KEY,
    session_name TEXT NOT NULL REFERENCES sessions(name) ON DELETE CASCADE,
    status TEXT NOT NULL CHECK (status IN ('queued', 'active', 'completed', 'discarded')),
    queue_position INTEGER,
    
    goal TEXT NOT NULL,
    context TEXT,
    inputs TEXT,
    outputs TEXT,
    approach TEXT,
    success_criteria TEXT,
    notes TEXT,
    outcome TEXT,
    
    created_at TEXT DEFAULT (datetime('now')),
    completed_at TEXT
) STRICT;

CREATE INDEX IF NOT EXISTS idx_plans_queue 
    ON plans(session_name, status, queue_position) 
    WHERE status = 'queued';

CREATE INDEX IF NOT EXISTS idx_plans_active
    ON plans(session_name, status)
    WHERE status = 'active';

-- FTS5 for history search
CREATE VIRTUAL TABLE IF NOT EXISTS plans_fts USING fts5(
    id, goal, context, inputs, outputs, outcome,
    content='plans', content_rowid='rowid'
);

-- Triggers to keep FTS in sync
CREATE TRIGGER IF NOT EXISTS plans_ai AFTER INSERT ON plans BEGIN
    INSERT INTO plans_fts(rowid, id, goal, context, inputs, outputs, outcome)
    VALUES (new.rowid, new.id, new.goal, new.context, new.inputs, new.outputs, new.outcome);
END;

CREATE TRIGGER IF NOT EXISTS plans_ad AFTER DELETE ON plans BEGIN
    INSERT INTO plans_fts(plans_fts, rowid, id, goal, context, inputs, outputs, outcome)
    VALUES ('delete', old.rowid, old.id, old.goal, old.context, old.inputs, old.outputs, old.outcome);
END;

CREATE TRIGGER IF NOT EXISTS plans_au AFTER UPDATE ON plans BEGIN
    INSERT INTO plans_fts(plans_fts, rowid, id, goal, context, inputs, outputs, outcome)
    VALUES ('delete', old.rowid, old.id, old.goal, old.context, old.inputs, old.outputs, old.outcome);
    INSERT INTO plans_fts(rowid, id, goal, context, inputs, outputs, outcome)
    VALUES (new.rowid, new.id, new.goal, new.context, new.inputs, new.outputs, new.outcome);
END;

5.2 TypeScript Interfaces #

export type PlanStatus = 'queued' | 'active' | 'completed' | 'discarded';

export interface Session {
    name: string;
    taskDescription: string | null;
    createdAt: string;
}

export interface Plan {
    id: string;
    sessionName: string;
    status: PlanStatus;
    queuePosition: number | null;
    goal: string;
    context: string | null;
    inputs: string | null;
    outputs: string | null;
    approach: string | null;
    successCriteria: string | null;
    notes: string | null;
    outcome: string | null;
    createdAt: string;
    completedAt: string | null;
}

export interface QueuedPlan {
    id: string;
    goal: string;
    queuePosition: number;
}

export interface SessionState {
    sessionName: string;
    sessionPath: string;
    taskDescription: string | null;
    queue: QueuedPlan[];
    activePlan: { id: string; filePath: string } | null;
    completedCount: number;
}

export interface HistoryMatch {
    id: string;
    goal: string;
    outcome: string | null;
    relevanceScore: number;
}

6. Tool Specifications #

All tools use the prefix 9plan_ with snake_case.

6.1 Session Tools #

9plan_session_create #

Creates a new session with a randomly-generated three-word identifier.

Input: { task_description?: string }

Output: { session_name: string, session_path: string }

Behavior:

  1. Generate unique three-word name
  2. Create session directory with session.db and plans/
  3. Initialize SQLite with schema
  4. Return session name and path

9plan_session_resume #

Loads an existing session by name.

Input: { session_name: string } (format: word-word-word)

Output: Full session state including queue, active plan, and completed count.


6.2 Queue Tools #

9plan_queue_add #

Adds a new plan to the queue.

Input:

{
    context: string,      // Where this plan fits in overall task
    goal: string,         // What this plan accomplishes
    approach: string,     // How to accomplish the goal
    success_criteria: string,
    inputs?: string,      // Dependencies from other plans
    outputs?: string,     // What this produces
    position?: 'front' | 'back'  // Default: 'back'
}

Output: { plan_id: string, plan_path: string, queue_position: number }

Position Semantics:

  • front: Blocking work that must complete first
  • back: Work that should happen eventually

9plan_queue_pull #

Removes the front plan from queue and marks it active.

Input: None

Output: { plan_id: string, plan_path: string, goal: string }

Errors:

  • PLAN_ALREADY_ACTIVE: Must complete/defer/discard current plan first
  • QUEUE_EMPTY: Task complete! 🎉

6.3 Plan Lifecycle Tools #

9plan_plan_defer #

Returns active plan to queue without completing it.

Input: { reason: string, position?: 'front' | 'back' }

Behavior: Appends reason to Notes section with timestamp, returns to queue.

Use Cases:

  • Blocked: Can't proceed → position: 'front'
  • Decomposition: Breaking into subplans → position: 'back'

9plan_plan_complete #

Marks active plan as done.

Input: { outcome: string } — Summary including outputs produced

Behavior:

  1. Store outcome in database
  2. Index into FTS5 for history search
  3. Delete plan file
  4. Return queue status

9plan_plan_discard #

Abandons active plan without completing.

Input: { reason: string }

Behavior: Marks as discarded (NOT in searchable history), deletes file.

Use When: Plan is obsolete, unnecessary, or misconceived.


6.4 History Tools #

Searches completed plans for outputs matching a query.

Input: { query: string, max_results?: number }

Output: Array of matches with { id, goal, outcome, relevance_rank }

Uses FTS5 MATCH with BM25 ranking across goal, context, inputs, outputs, outcome.


9plan_history_get #

Retrieves a specific completed plan by ID.

Input: { plan_id: string } (5 alphanumeric chars)

Output: Full plan content including outcome.


6.5 Admin Tools #

Admin tools support validation and debugging. Prefixed with admin_ to distinguish from core workflow tools.

9plan_admin_validate #

Checks session state against all invariants.

Input: None (uses current session)

Output:

{
    valid: boolean,
    invariants: {
        single_active_plan: boolean,
        queue_order_preserved: boolean,
        files_match_database: boolean,
        no_orphaned_files: boolean
    },
    issues: string[]
}

Use Case: Verify session consistency during debugging or validation testing.


9plan_admin_sessions #

Lists all sessions on the system.

Input: None

Output:

{
    sessions: Array<{
        name: string,
        created: string,
        task_description: string | null,
        queue_length: number,
        completed_count: number,
        has_active_plan: boolean
    }>
}

Use Case: Find existing sessions, clean up old ones, verify session creation.


9plan_admin_state #

Returns detailed state dump for current session.

Input: None (uses current session)

Output:

{
    session_name: string,
    task_description: string | null,
    queue: Array<{ id: string, goal: string, position: number }>,
    active_plan: { id: string, goal: string, file_path: string } | null,
    completed_count: number,
    plan_files: string[],
    file_db_match: boolean
}

Use Case: Detailed inspection during validation or debugging.


7. Prompt Specifications #

bootstrap #

Guides initial session setup and planning decomposition.

Arguments: { task?: string }

Content:

You are helping the user set up a new 9plan session for a complex task.

WORKFLOW:
1. CLARIFY: Ask questions to understand scope, components, dependencies
2. CREATE SESSION: Use 9plan_session_create with task description
3. DECOMPOSE: Break into discrete, self-contained plans
4. CAPTURE DEPENDENCIES: Specify inputs/outputs for each plan
5. ENQUEUE: Add plans with 9plan_queue_add (respect dependency order)
6. START WORK: Pull first plan with 9plan_queue_pull

PLAN QUALITY CHECKLIST:
□ Goal is specific and measurable
□ Approach has concrete steps
□ Success criteria are observable
□ Inputs reference other plans by description
□ Outputs are specific enough to search for

NOTE: Adding A, B, C at 'front' gives [C, B, A] — add in reverse order if needed.

8. File Formats #

8.1 Plan File Format #

Location: ~/.9plan/sessions/{session-name}/plans/{plan-id}.txt

# Context
{Where this plan fits in the overall task}

# Goal
{What this plan accomplishes}

# Inputs
{Dependencies - format: "- description: source"}

# Outputs
{What this produces - format: "- description: details"}

# Approach
{Detailed, actionable steps}

# Success Criteria
{Observable completion conditions}

# Notes
{Working scratchpad - updated during execution}

9. Logging #

9.1 Logger Setup #

// src/logger.ts
import pino from 'pino';
import { env } from './env.js';

export const logger = process.env.NODE_ENV === 'development'
    ? pino({
          level: env.NINEPLAN_LOG_LEVEL,
          transport: {
              target: 'pino-pretty',
              options: { colorize: true, translateTime: 'SYS:standard' },
          },
      })
    : pino({ level: env.NINEPLAN_LOG_LEVEL });

export type Logger = pino.Logger;

9.2 Session-Scoped Child Loggers #

export class SessionStore {
    private log: Logger;
    
    constructor(sessionName: string) {
        this.log = logger.child({ session: sessionName });
    }
    
    addPlan(plan: Plan): void {
        this.log.info({ planId: plan.id, goal: plan.goal }, 'Plan added');
    }
}

9.3 Log Levels #

Level Usage
debug SQL queries, queue state
info Session created, plan pulled/completed
warn Collision retry, missing optional field
error DB error, file write failure

10. Error Handling #

10.1 Error Categories #

Category Description
SESSION_NOT_FOUND Session does not exist
PLAN_NOT_FOUND Plan ID does not exist
NO_ACTIVE_PLAN Operation requires active plan
PLAN_ALREADY_ACTIVE Cannot pull; another plan is active
QUEUE_EMPTY Queue is empty (task complete)
VALIDATION_ERROR Input validation failed
FILE_SYSTEM_ERROR Filesystem operation failed
DATABASE_ERROR SQLite operation failed

10.2 Error Response Format #

[Session: {name}] Error: {category}

{message}

Suggested action: {suggestion}

11. Configuration #

11.1 Environment Variables #

// src/env.ts
import { z } from 'zod';
import envPaths from 'env-paths';
import { join } from 'node:path';

const paths = envPaths('9plan', { suffix: '' });

const envSchema = z.object({
    NINEPLAN_SESSIONS_PATH: z.string().default(join(paths.data, 'sessions')),
    NINEPLAN_LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
    NINEPLAN_TRANSPORT: z.enum(['stdio', 'http']).default('stdio'),
    NINEPLAN_HTTP_PORT: z.coerce.number().default(8080),
    NINEPLAN_HTTP_HOST: z.string().default('127.0.0.1'),
});

export const env = envSchema.parse(process.env);

11.2 Entry Point #

// src/index.ts
import { serveStdio } from "@karashiiro/mcp/stdio";
import { serveHttp } from "@karashiiro/mcp/http";
import { createServer } from "./server.js";
import { logger } from "./logger.js";
import { env } from "./env.js";

if (env.NINEPLAN_TRANSPORT === "http") {
    const handle = await serveHttp(createServer, {
        port: env.NINEPLAN_HTTP_PORT,
        host: env.NINEPLAN_HTTP_HOST,
        endpoint: "/mcp",
    });
    
    logger.info({ port: env.NINEPLAN_HTTP_PORT }, '9plan running (HTTP)');
    
    process.on("SIGINT", async () => {
        await handle.close();
        process.exit(0);
    });
} else {
    logger.info('9plan running (stdio)');
    await serveStdio(createServer);
}

11.3 MCP Client Configuration #

Claude Desktop (~/Library/Application Support/Claude/claude_desktop_config.json):

{
    "mcpServers": {
        "9plan": {
            "command": "node",
            "args": ["/path/to/9plan-mcp-server/dist/index.js"]
        }
    }
}

12. Dependency Injection & Testability #

We use constructor injection for testability without a DI framework. This keeps the codebase simple while allowing full mockability in tests.

12.1 The Pattern #

All classes accept their dependencies via an options object in the constructor:

// src/types.ts - Dependency interfaces
export interface SessionStoreDeps {
    logger: Logger;
    generatePlanId: () => string;
    planFiles: PlanFileHandler;
}

export interface PlanFileHandlerDeps {
    logger: Logger;
}

12.2 Implementation Example #

// src/db/session-store.ts
import type { Logger } from '../logger.js';
import type { Plan, SessionStoreDeps } from '../types.js';

export class SessionStore {
    private log: Logger;
    private generatePlanId: () => string;
    private planFiles: PlanFileHandler;

    constructor(
        private sessionName: string,
        private sessionPath: string,
        deps: SessionStoreDeps,
    ) {
        this.log = deps.logger.child({ session: sessionName });
        this.generatePlanId = deps.generatePlanId;
        this.planFiles = deps.planFiles;
    }

    addPlan(input: PlanInput): Plan {
        const id = this.generatePlanId();
        this.log.info({ planId: id, goal: input.goal }, 'Adding plan');
        // ... implementation
    }
}

12.3 Production Wiring #

// src/container.ts - Simple factory functions, no framework needed
import { logger } from './logger.js';
import { SessionStore } from './db/session-store.js';
import { PlanFileHandler } from './files/plan-files.js';
import { generatePlanId } from './generators/plan-id.js';

export function createPlanFileHandler(sessionPath: string): PlanFileHandler {
    return new PlanFileHandler(sessionPath, { logger });
}

export function createSessionStore(
    sessionName: string,
    sessionPath: string,
): SessionStore {
    return new SessionStore(sessionName, sessionPath, {
        logger,
        generatePlanId,
        planFiles: createPlanFileHandler(sessionPath),
    });
}

12.4 Testing with Mocks #

// tests/unit/session-store.test.ts
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { SessionStore } from '../../src/db/session-store.js';

describe('SessionStore', () => {
    // Mock factory - creates fresh mocks for each test
    const createMocks = () => ({
        logger: {
            info: vi.fn(),
            error: vi.fn(),
            warn: vi.fn(),
            debug: vi.fn(),
            child: vi.fn(() => ({
                info: vi.fn(),
                error: vi.fn(),
                warn: vi.fn(),
                debug: vi.fn(),
            })),
        },
        generatePlanId: vi.fn(() => 'test1'),
        planFiles: {
            write: vi.fn(),
            read: vi.fn(),
            delete: vi.fn(),
            exists: vi.fn(() => false),
        },
    });

    it('should generate ID and log when adding plan', () => {
        const mocks = createMocks();
        const store = new SessionStore('test-session', '/tmp/test', mocks);

        store.addPlan({
            goal: 'Test the thing',
            context: 'Testing context',
            approach: 'Test approach',
            successCriteria: 'Tests pass',
        });

        expect(mocks.generatePlanId).toHaveBeenCalled();
        expect(mocks.logger.child).toHaveBeenCalledWith({ session: 'test-session' });
    });

    it('should write plan file when adding plan', () => {
        const mocks = createMocks();
        const store = new SessionStore('test-session', '/tmp/test', mocks);

        store.addPlan({
            goal: 'Test the thing',
            context: 'Testing context',
            approach: 'Test approach',
            successCriteria: 'Tests pass',
        });

        expect(mocks.planFiles.write).toHaveBeenCalled();
    });

    it('should throw when plan file write fails', () => {
        const mocks = createMocks();
        mocks.planFiles.write.mockImplementation(() => {
            throw new Error('Disk full');
        });

        const store = new SessionStore('test-session', '/tmp/test', mocks);

        expect(() => store.addPlan({
            goal: 'Test',
            context: 'Context',
            approach: 'Approach',
            successCriteria: 'Criteria',
        })).toThrow('Disk full');
    });
});

12.5 Testing Tools #

Tools receive their dependencies through the session store:

// tests/unit/tools/queue-add.test.ts
import { describe, it, expect, vi } from 'vitest';
import { handleQueueAdd } from '../../../src/tools/queue-add.js';

describe('9plan_queue_add', () => {
    it('should return plan ID and path on success', async () => {
        const mockStore = {
            addPlan: vi.fn(() => ({
                id: 'abc12',
                queuePosition: 1,
            })),
            getSessionPath: vi.fn(() => '/tmp/sessions/test-session'),
        };

        const result = await handleQueueAdd(mockStore, {
            goal: 'Do something',
            context: 'Context here',
            approach: 'Step by step',
            success_criteria: 'It works',
        });

        expect(result.plan_id).toBe('abc12');
        expect(result.plan_path).toContain('abc12.txt');
    });
});

12.6 Why Not a DI Container? #

Aspect Manual Injection DI Container (inversify/tsyringe)
Testability ✅ Full ✅ Full
Lines of setup ~20 ~80+
Extra dependencies None reflect-metadata, decorators
Decorator noise None @injectable() on every class
Type safety ✅ Native TS Requires Symbol tokens
Learning curve None Moderate

For a project with <10 injectable services, manual wiring is simpler and equally testable.


13. Implementation Phases #

Phase 1: Foundation #

Phase 2: Session Tools #

Phase 3: Queue Tools #

Phase 4: Plan Lifecycle Tools #

Phase 5: History Tools #

Phase 6: Admin Tools #

Phase 7: Polish #

Phase 8: Agent Validation Infrastructure #


14. Known Limitations #

Front-Insertion Order Reversal #

Adding A, B, C at front gives [C, B, A, ...]. Add subplans in reverse order to achieve correct sequence.

Context Compaction Mid-Plan #

If compaction occurs while executing a plan, the agent loses session awareness. Session name is included in all tool responses to aid recovery.

Single Agent Per Session #

No concurrent access support—designed for single-agent workflows.


Appendix: Server Instructions #

The MCP server's instructions field:

9plan manages session-scoped work queues for agent task sequencing.

QUICK START:
1. Create session: 9plan_session_create
2. Add plans: 9plan_queue_add ('front' for blocking, 'back' for eventual)
3. Get work: 9plan_queue_pull
4. Execute the plan (read file for full context)
5. Complete: 9plan_plan_complete (or defer/discard)
6. Repeat until queue is empty

PLAN FILE FORMAT:
# Context, # Goal, # Inputs, # Outputs, # Approach, # Success Criteria, # Notes

DEPENDENCY RESOLUTION:
- Use 9plan_history_search to find outputs from completed plans
- Inputs reference plans by description, not ID

SESSION RECOVERY:
- All responses include [Session: name] prefix
- Resume with: 9plan_session_resume

End of Implementation Design Document