/** * MCP Server factory * * Creates and configures the 9plan MCP server with all tools and prompts. * Manages session state across tool calls. */ import { z } from "zod"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import type { SessionStore } from "./db/session-store.js"; import { createNewSession, resumeSession, getSessionPath, } from "./container.js"; import { logger } from "./logger.js"; import { NinePlanError, formatResponse } from "./types.js"; // Import all tools import { sessionCreateToolConfig, handleSessionResume, sessionResumeToolConfig, handleQueueAdd, queueAddToolConfig, handleQueuePull, queuePullToolConfig, handlePlanDefer, planDeferToolConfig, handlePlanComplete, planCompleteToolConfig, handlePlanDiscard, planDiscardToolConfig, handleHistorySearch, historySearchToolConfig, handleHistoryGet, historyGetToolConfig, } from "./tools/index.js"; // Import prompts import { BOOTSTRAP_PROMPT_CONTENT } from "./prompts/bootstrap.js"; /* eslint-disable @typescript-eslint/no-unnecessary-condition */ /** * Session state manager * Tracks the currently active session across tool calls */ class SessionManager { private currentStore: SessionStore | null = null; private currentSessionName: string | null = null; /** * Set the current session */ setSession(sessionName: string, store: SessionStore): void { // Close previous session if exists this.close(); this.currentSessionName = sessionName; this.currentStore = store; logger.info({ sessionName }, "Session activated"); } /** * Get the current session store, throwing if none active */ getStore(): SessionStore { if (!this.currentStore || !this.currentSessionName) { throw new NinePlanError( "SESSION_NOT_FOUND", "No session is currently active", "Use 9plan_session_create to create a new session, or 9plan_session_resume to load an existing one.", ); } return this.currentStore; } /** * Get the current session name */ getSessionName(): string { if (!this.currentSessionName) { throw new NinePlanError( "SESSION_NOT_FOUND", "No session is currently active", "Use 9plan_session_create to create a new session, or 9plan_session_resume to load an existing one.", ); } return this.currentSessionName; } /** * Check if a session is active */ hasSession(): boolean { return this.currentStore !== null && this.currentSessionName !== null; } /** * Close the current session */ close(): void { if (this.currentStore) { this.currentStore.close(); logger.info({ sessionName: this.currentSessionName }, "Session closed"); } this.currentStore = null; this.currentSessionName = null; } } /** * Create the MCP server with all tools registered */ export function createServer(): McpServer { const server = new McpServer({ name: "9plan", version: "1.0.0", }); const sessionManager = new SessionManager(); // Register session create tool - handles session creation internally server.registerTool( "9plan_session_create", sessionCreateToolConfig, (args) => { const taskDescription = args.task_description; const { sessionName, store } = createNewSession(taskDescription); const sessionPath = getSessionPath(sessionName); sessionManager.setSession(sessionName, store); const response = formatResponse( sessionName, `Session created: ${sessionName} Directory: ${sessionPath} The session is ready. Use 9plan_queue_add to add plans, then 9plan_queue_pull to begin work.`, ); return { content: [{ type: "text" as const, text: response }], }; }, ); // Register session resume tool server.registerTool( "9plan_session_resume", sessionResumeToolConfig, (args) => { const sessionName = args.session_name; const store = resumeSession(sessionName); sessionManager.setSession(sessionName, store); return handleSessionResume({ session_name: sessionName }); }, ); // Register queue tools server.registerTool("9plan_queue_add", queueAddToolConfig, (args) => { const store = sessionManager.getStore(); const sessionName = sessionManager.getSessionName(); const queueAddArgs = { context: args.context, goal: args.goal, approach: args.approach, success_criteria: args.success_criteria, testing: args.testing, ...(args.inputs !== undefined && { inputs: args.inputs }), ...(args.outputs !== undefined && { outputs: args.outputs }), ...(args.position !== undefined && { position: args.position, }), }; return handleQueueAdd(store, sessionName, queueAddArgs); }); server.registerTool( "9plan_queue_pull", queuePullToolConfig, (_args) => { const store = sessionManager.getStore(); const sessionName = sessionManager.getSessionName(); return handleQueuePull(store, sessionName, {}); }, ); // Register plan lifecycle tools server.registerTool("9plan_plan_defer", planDeferToolConfig, (args) => { const store = sessionManager.getStore(); const sessionName = sessionManager.getSessionName(); const deferArgs = { reason: args.reason, ...(args.position !== undefined && { position: args.position, }), }; return handlePlanDefer(store, sessionName, deferArgs); }); server.registerTool( "9plan_plan_complete", planCompleteToolConfig, (args) => { const store = sessionManager.getStore(); const sessionName = sessionManager.getSessionName(); return handlePlanComplete(store, sessionName, { outcome: args.outcome, }); }, ); server.registerTool( "9plan_plan_discard", planDiscardToolConfig, (args) => { const store = sessionManager.getStore(); const sessionName = sessionManager.getSessionName(); return handlePlanDiscard(store, sessionName, { reason: args.reason, }); }, ); // Register history tools server.registerTool( "9plan_history_search", historySearchToolConfig, (args) => { const store = sessionManager.getStore(); const sessionName = sessionManager.getSessionName(); const searchArgs = { query: args.query, ...(args.max_results !== undefined && { max_results: args.max_results, }), }; return handleHistorySearch(store, sessionName, searchArgs); }, ); server.registerTool( "9plan_history_get", historyGetToolConfig, (args) => { const store = sessionManager.getStore(); const sessionName = sessionManager.getSessionName(); return handleHistoryGet(store, sessionName, { plan_id: args.plan_id, }); }, ); // Register bootstrap prompt server.registerPrompt( "bootstrap", { description: "Set up a new 9plan session for a complex task. Guides you through clarifying requirements, decomposing into plans, and capturing dependencies.", argsSchema: { task: z .string() .optional() .describe("Optional initial task description to start with"), }, }, (args) => { const taskArg = args.task; const taskText = taskArg ? `\n\nThe user has provided an initial task: "${taskArg}"\n\nStart by clarifying the task and then proceed with the workflow.` : ""; return { messages: [ { role: "user" as const, content: { type: "text" as const, text: BOOTSTRAP_PROMPT_CONTENT + taskText, }, }, ], }; }, ); logger.info("9plan MCP server initialized with 9 tools and 1 prompt"); return server; }