# MCP Standalone CLI Plan ## Goal Ship a bundled CLI tool (`bds-mcp`) alongside the Electron app that: - Runs as a self-contained MCP server over **stdio** (for Claude Desktop, VS Code, etc.) - Spins up **HTTP :4125** alongside stdio for App View callbacks (same shared state) - Works entirely against the **SQLite database** — no Electron process required - Signals the running app of changes via a **`db_notifications`** table - Is installed into agent configs by the app's existing agent-config UI (with add **and remove**) --- ## Architecture ``` ┌─────────────────────────────────┐ ┌────────────────────────────────────┐ │ bds-mcp (bundled CLI binary) │ │ bDS Electron app │ │ │ │ │ │ StdioServerTransport ──────┐ │ │ HTTP MCPServer :4124 │ │ StreamableHTTP :4125 ──────┤ │ │ ├── same engines │ │ │ │ │ └── NotificationWatcher │ │ PostEngine ├───┼─────┼──► SQLite (WAL mode, shared) │ │ MediaEngine │ │ │ posts / media / scripts … │ │ ScriptEngine │ │ │ db_notifications ← new │ │ TemplateEngine │ │ └────────────────────────────────────┘ │ MCPServer (tools/res) │ │ ▲ │ ProposalStore (in-memory) │ │ setInterval polls └─────────────────────────────┘ │ refresh caches + IPC to renderer │ agent (Claude Desktop, VS Code…) ─┘ (stdio) ``` ### Port mapping | Context | Port | Transport | |---|---|---| | bDS app running | :4124 | HTTP (existing, unchanged) | | CLI stdio mode — agent | — | stdio | | CLI stdio mode — App View callbacks | :4125 | HTTP | --- ## Required Changes ### A — `db_notifications` table (new Drizzle migration) ```sql CREATE TABLE db_notifications ( id INTEGER PRIMARY KEY AUTOINCREMENT, entity TEXT NOT NULL, -- 'post' | 'media' | 'script' | 'template' entityId TEXT NOT NULL, action TEXT NOT NULL, -- 'created' | 'updated' | 'deleted' origin TEXT NOT NULL, -- 'cli' | 'app' seenAt INTEGER, -- NULL = unprocessed by app createdAt INTEGER NOT NULL ); ``` The CLI writes a row after every mutation. The app's `NotificationWatcher` polls for `seenAt IS NULL AND origin = 'cli'`, refreshes the relevant engine cache, emits IPC events to the renderer, then stamps `seenAt`. --- ### B — Draft mode for scripts and templates (new Drizzle migration) Scripts and templates currently write the file immediately on create. To allow CLI-created proposals to be durable (survive CLI restarts) without polluting the filesystem until accepted, add the same draft pattern that posts already use. ```sql -- scripts ALTER TABLE scripts ADD COLUMN status TEXT NOT NULL DEFAULT 'published' CHECK (status IN ('draft', 'published')); ALTER TABLE scripts ADD COLUMN content TEXT; -- draft body; NULL when on-disk -- templates ALTER TABLE templates ADD COLUMN status TEXT NOT NULL DEFAULT 'published' CHECK (status IN ('draft', 'published')); ALTER TABLE templates ADD COLUMN content TEXT; -- draft body; NULL when on-disk ``` Behaviour: - **`propose_script` / `propose_template`** → `INSERT` with `status: 'draft'`, body in `content`, `filePath: ''` — no file written yet; `ProposalStore` maps `proposalId → rowId` - **`accept_proposal`** → write file to disk, set `status: 'published'`, clear `content` - **`discard_proposal`** → `DELETE` the row (never touched the filesystem) --- ### C — `platformConfigPath()` — userData without Electron (`src/cli/platform.ts`) Pure-Node helper to resolve the same path as Electron's `app.getPath('userData')`: | Platform | Path | |---|---| | macOS | `~/Library/Application Support/Blogging Desktop Server` | | Windows | `%APPDATA%\Blogging Desktop Server` | | Linux | `~/.config/Blogging Desktop Server` | Used by the CLI entrypoint to find the SQLite database without loading Electron. --- ### D — `CliNotifier` interface + injected DB writer ```typescript interface CliNotifier { notify(entity: string, id: string, action: 'created' | 'updated' | 'deleted'): Promise; } ``` - Electron app passes a no-op implementation (keeps engines clean, no code change needed in the app path) - CLI passes a DB writer implementation that inserts into `db_notifications` Injected as an optional constructor argument on `PostEngine`, `ScriptEngine`, `TemplateEngine`, `MediaEngine`. --- ### E — `NotificationWatcher` (new engine, app-side only) `src/main/engine/NotificationWatcher.ts` - `setInterval` polling every 2 s - Reads `db_notifications WHERE seenAt IS NULL AND origin = 'cli'` - For each row: invalidates the relevant engine's cache, calls `mainWindow.webContents.send('entity:changed', { entity, entityId, action })` - Stamps `seenAt = Date.now()` on processed rows - Started in `main.ts` alongside `PreviewServer` and `MCPServer` --- ### F — `MCPServer.startCli()` (extend existing `MCPServer.ts`) ```typescript async startCli(appViewsPort = 4125): Promise { // 1. Stdio transport — primary agent connection const stdioTransport = new StdioServerTransport(); const stdioMcp = this.createServer({ mode: 'cli', appViewsPort }); await stdioMcp.connect(stdioTransport); // 2. HTTP :4125 — App View callbacks, same ProposalStore await this.startHttp(appViewsPort, { mode: 'cli' }); } ``` Both `McpServer` instances are created via the same `createServer()` factory sharing the same `ProposalStore` instance. App View HTML receives the `:4125` base URL injected as a `` tag at render time so `callServerTool()` points to the right endpoint regardless of mode. --- ### G — `src/cli/bds-mcp.ts` — CLI entrypoint (no Electron imports) ``` 1. platformConfigPath() → userData dir 2. getDatabase(path.join(userData, 'bds.db')) 3. SELECT active project (isActive = 1); exit with message if none 4. instantiate engines with CliNotifier DB writer 5. new MCPServer({ postEngine, mediaEngine, … }) 6. mcpServer.startCli() ``` No `electron` imports. Fully standalone Node process. --- ### H — esbuild/Vite build target for CLI bundle New build target in `vite.config.ts` (or `package.json` scripts): ``` src/cli/bds-mcp.ts → dist/cli/bds-mcp.cjs target: node, format: cjs, bundle: true externals: native modules only (better-sqlite3) ``` Runs as part of `npm run build` before the Electron build. --- ### I — electron-builder `extraResources` ```json "build": { "extraResources": [ { "from": "dist/cli/bds-mcp.cjs", "to": "bds-mcp.cjs" } ] } ``` Placed at `Contents/Resources/bds-mcp.cjs` in the macOS app bundle. --- ### J — `MCPAgentConfigEngine` — add Claude Desktop + stdio support + remove #### New agent type ```typescript export type MCPAgentId = | 'claude-code' | 'claude-desktop' // ← NEW (stdio) | 'github-copilot' | 'gemini-cli' | 'opencode'; ``` #### Extended options ```typescript export interface MCPAgentConfigOptions { homeDir: string; platform: NodeJS.Platform; mcpUrl: string; // stdio agents (set from app at runtime) execPath: string; // process.execPath (packaged) or 'node' (dev) scriptPath: string; // path to bds-mcp.cjs } ``` #### Claude Desktop entry ```typescript case 'claude-desktop': return { command: this.execPath, args: [this.scriptPath], env: { ELECTRON_RUN_AS_NODE: '1' }, }; ``` Config file path: - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json` - Windows: `%APPDATA%\Claude\claude_desktop_config.json` - Linux: `~/.config/Claude/claude_desktop_config.json` #### Remove from config (new method) ```typescript removeFromConfig(agentId: MCPAgentId): AgentConfigResult ``` - Reads the config file - Deletes `config[serversKey][SERVER_NAME]` - If `serversKey` object is now empty, removes the key entirely - Writes back; returns `{ success, configPath }` - No-ops gracefully if file or entry does not exist This powers **Remove** buttons in the agent-config UI alongside the existing **Add** buttons, so users never have to hunt down config files manually. --- ### K — `ScriptEngine` / `TemplateEngine` — draft lifecycle methods New methods on each engine: ```typescript // ScriptEngine createDraftScript(data: CreateScriptInput): Promise // status: 'draft', no file publishScript(id: string): Promise // write file, status: 'published' deleteDraftScript(id: string): Promise // only if status: 'draft' // TemplateEngine createDraftTemplate(data: CreateTemplateInput): Promise publishTemplate(id: string): Promise deleteDraftTemplate(id: string): Promise ``` --- ### L — `ProposalStore` — map proposals to DB row IDs For `draftPost` proposals the store already maps `proposalId → postId` (the DB row). Extend this consistently: - `proposeScript` → maps `proposalId → scriptId` (DB row, `status: 'draft'`) - `proposeTemplate` → maps `proposalId → templateId` (DB row, `status: 'draft'`) - `proposeMediaMetadata` → stays in-memory (no draft DB row needed, metadata diff only) - `proposePostMetadata` → stays in-memory (same reason) `accept_proposal` dispatches to `publishScript` / `publishTemplate` / `publishPost` etc. `discard_proposal` dispatches to `deleteDraftScript` / `deleteDraftTemplate` / `deletePost`. --- ## Unresolved Questions None — all design decisions resolved: | Question | Decision | |---|---| | Active project in CLI | Use `isActive = 1` from DB; fail fast if none | | Proposal durability | Scripts/templates use DB draft rows; metadata proposals stay in-memory | | App View URL in CLI mode | Injected as `` at render time | | Claude Desktop support | stdio via bundled `bds-mcp.cjs` + `ELECTRON_RUN_AS_NODE=1` | | Remove from config | New `removeFromConfig()` method + Remove buttons in UI | --- ## Implementation Order (TDD per AGENTS.md) 1. **Migrations** (A + B) — schema changes first, all other work builds on them 2. **`platformConfigPath()`** (C) — pure function, easy to test 3. **`CliNotifier` + DB writer** (D) — unit tests with mock DB 4. **`ScriptEngine`/`TemplateEngine` draft lifecycle** (K) — tests first 5. **`ProposalStore` DB mapping** (L) — extend existing tests 6. **`NotificationWatcher`** (E) — test with mock DB rows 7. **`MCPAgentConfigEngine` — `claude-desktop` + `removeFromConfig`** (J) — extend existing tests 8. **`MCPServer.startCli()`** (F) — test with in-process transports 9. **`src/cli/bds-mcp.ts`** (G) — integration test: spawn, send `initialize`, assert response 10. **Build target + `extraResources`** (H + I) — verify `npm run build` produces bundle 11. **UI: Remove buttons** — renderer component changes, follows J 12. **`NotificationWatcher` wired in `main.ts`** (E, app side) Each step: write failing test → implement → green → next.