Files
bDS/MCP_PLAN.md

8.5 KiB

Chapter 3: MCP Server — Agent-Assisted Content Creation

Summary

Run a standalone MCP server on its own port (default 4124). External AI agents connect via Streamable HTTP at /mcp. Read access is open; writes go through user-reviewable proposals with MCP App review UIs powered by @modelcontextprotocol/ext-apps.

Dependencies

  • @modelcontextprotocol/sdk (v1.27+) — McpServer, StreamableHTTPServerTransport, createMcpExpressApp
  • @modelcontextprotocol/ext-apps (v1.1+) — registerAppTool, registerAppResource, RESOURCE_MIME_TYPE for interactive review UIs rendered inline in compliant hosts (Claude, ChatGPT, VS Code, etc.)
  • express / cors — pulled in transitively by the SDK; used by createMcpExpressApp

zod v4 already in project satisfies peer dep requirements.

Architecture

PreviewServer (HTTP on 127.0.0.1:4123)      ← unchanged
  └── /* → Blog preview routes

MCPServer (HTTP on 127.0.0.1:4124)           ← NEW, standalone
  └── /mcp → Streamable HTTP (POST + GET/DELETE)
       ├── tools, resources, prompts
       └── ui:// resources → MCP App review Views (via ext-apps)

The MCP SDK provides createMcpExpressApp which sets up Express with the correct Streamable HTTP handling. We use stateless mode (new McpServer per request) to avoid session management complexity.

MCPServer is a new engine class that owns the McpServer factory, tool/resource/prompt registration, and the ProposalStore. It runs independently of PreviewServer.

Implementation Steps

Step 1: Install deps

npm install @modelcontextprotocol/sdk @modelcontextprotocol/ext-apps

Step 2: ProposalStore (src/main/engine/ProposalStore.ts)

  • Map<string, Proposal> with TTL (30 min default)
  • create(type, data) → id, get(id), remove(id), cleanup()
  • Draft posts tracked by mapping proposalId → 'draftPost' (actual data in DB)
  • Periodic cleanup via setInterval

Step 3: MCPServer engine (src/main/engine/MCPServer.ts)

  • Constructor: inject engine getters (PostEngine, MediaEngine, ScriptEngine, TemplateEngine, MetaEngine, PostMediaEngine, TagEngine)
  • createServer() → factory that instantiates a fresh McpServer and registers all tools/resources/prompts (stateless mode — one per request)
  • start(port) → uses createMcpExpressApp + StreamableHTTPServerTransport in stateless mode, listens on 127.0.0.1:port
  • stop() → close HTTP server
  • cleanup() → discard proposals, stop intervals, stop server
  • Singleton pattern with getMCPServer() getter

Step 4: Resources (read-only data)

Register MCP resources mapping to existing engine methods:

Resource URI Engine Method
bds://posts PostEngine.getAllPosts()
bds://posts/{id} PostEngine.getPost(id)
bds://media MediaEngine.getAllMedia()
bds://media/{id} MediaEngine.getMedia(id)
bds://tags PostEngine.getTagsWithCounts()
bds://categories PostEngine.getCategoriesWithCounts()
bds://stats PostEngine.getBlogStats() + MediaEngine
bds://posts/{id}/backlinks PostEngine.getLinkedBy(id)
bds://posts/{id}/outlinks PostEngine.getLinksTo(id)
bds://posts/{id}/media PostMediaEngine.getLinkedMediaDataForPost(id)
bds://media/{id}/posts PostMediaEngine.getLinkedPostsForMedia(id)

Step 5: Read tools

  • search_posts — annotations: { readOnlyHint: true, openWorldHint: false }
    • Wraps PostEngine.searchPosts() with filter args (query, category, tags, year/month, offset, limit)

Step 6: Proposal tools (with MCP App Views)

Each proposal tool uses registerAppTool from @modelcontextprotocol/ext-apps/server to link the tool to a ui:// resource. The host renders the review View inline.

Each review View is a bundled HTML page (built with Vite vite-plugin-singlefile into a single self-contained HTML) that uses the App class from @modelcontextprotocol/ext-apps for bidirectional communication with the host.

Tool Action ui:// Resource Accept Discard
draft_post PostEngine.createPost(draft) ui://bds/review-post publishPost() deletePost()
propose_script Store in ProposalStore ui://bds/review-script ScriptEngine.createScript() remove from store
propose_template Store in ProposalStore ui://bds/review-template TemplateEngine.createTemplate() remove from store
propose_media_metadata Store diff in ProposalStore ui://bds/review-metadata MediaEngine.updateMedia() remove from store
propose_post_metadata Store diff in ProposalStore ui://bds/review-metadata PostEngine.updatePost() remove from store

Resource registration uses registerAppResource from @modelcontextprotocol/ext-apps/server. Each resource returns a bundled HTML string with mimeType: RESOURCE_MIME_TYPE.

Step 7: Accept/discard tools

  • accept_proposal({ proposalId }) — dispatch by type, commit change
  • discard_proposal({ proposalId }) — dispatch by type, clean up
  • Annotations: idempotentHint: true

Step 8: MCP Prompts

  • draft-blog-post(topic?, category?) — structured prompt guiding agent to read context and draft
  • improve-media-metadata(scope) — guide agent to review media and propose alt/caption
  • content-audit(category?) — guide agent to review posts for quality

Step 9: MCP App Review Views (src/main/mcp-apps/)

Four View HTML pages, each using vanilla JS + App class from @modelcontextprotocol/ext-apps:

  • review-post.html — title, metadata, rendered markdown, word count; accept/discard buttons call app.callServerTool()accept_proposal/discard_proposal
  • review-script.html — title, syntax-highlighted Python, validation status
  • review-template.html — title, kind badge, syntax-highlighted Liquid, validation
  • review-metadata.html — side-by-side diff (current vs proposed)

Each View:

  1. Receives tool result data from host via app.ontoolresult
  2. Renders focused review interface
  3. On accept/discard: calls app.callServerTool({ name: 'accept_proposal' | 'discard_proposal', arguments: { proposalId } })
  4. Updates UI to show outcome

Build step: vite build with vite-plugin-singlefile bundles each into a self-contained HTML string. These are read at runtime by registerAppResource handlers.

Step 10: Lifecycle in main.ts

  • MCPServer starts independently alongside PreviewServer during app init
  • On before-quit: call MCPServer.cleanup() (stops server + discards proposals)

Step 11: Update TODO.md

  • Remove stdio references per user request
  • Mark standalone server approach in the doc

Step 12: Tests (TDD per CLAUDE.md)

Each step above is preceded by failing tests:

  • ProposalStore: create/get/remove, TTL expiry, cleanup
  • MCPServer tools: tool definitions (names, schemas, annotations)
  • MCPServer resources: URI resolution, data returned
  • MCPServer prompts: message structure, arguments
  • Accept/discard: draft_post→accept publishes, propose_script→discard removes
  • Integration: HTTP POST to http://127.0.0.1:{port}/mcp, tool call, response format
  • Review Views: registerAppResource returns valid HTML with RESOURCE_MIME_TYPE

Key Files

File Purpose
src/main/engine/ProposalStore.ts NEW — in-memory proposal storage
src/main/engine/MCPServer.ts NEW — standalone MCP server engine
src/main/mcp-apps/review-post.html NEW — post review View
src/main/mcp-apps/review-script.html NEW — script review View
src/main/mcp-apps/review-template.html NEW — template review View
src/main/mcp-apps/review-metadata.html NEW — metadata diff View
src/main/mcp-apps/src/*.ts NEW — View scripts using App class
src/main/mcp-apps/vite.config.ts NEW — builds Views into single HTML files
src/main/main.ts MODIFY — start/cleanup MCPServer
tests/engine/ProposalStore.test.ts NEW
tests/engine/MCPServer.test.ts NEW
tests/engine/MCPServer.integration.test.ts NEW
TODO.md MODIFY — remove stdio, update transport section

Verification

  1. npm test — all tests pass (new + existing)
  2. npm run build — clean build
  3. Manual: start app, connect Claude Code or curl to http://127.0.0.1:4124/mcp with MCP protocol, verify tool listing and resource reading
  4. Manual: call draft_post tool, verify draft created, review View renders inline in host, accept publishes post

Unresolved Questions

None — scope is clear: standalone HTTP server on port 4124, MCP App Views via @modelcontextprotocol/ext-apps, no settings UI.