chore: cleaning up todo
This commit is contained in:
703
TODO.md
703
TODO.md
@@ -6,138 +6,7 @@ independently.
|
||||
|
||||
---
|
||||
|
||||
## ~~1. Template Editor & Per-Entity Template Selection~~ ✅ Done
|
||||
|
||||
### Goal
|
||||
|
||||
Users can create, edit, and manage Liquid templates inside the application.
|
||||
Categories, tags, and individual posts can select which template to use for
|
||||
rendering. The bundled templates serve as defaults; user templates override them.
|
||||
|
||||
### Current State
|
||||
|
||||
- Liquid templates are bundled in `src/main/engine/templates/` (3 templates +
|
||||
partials + macros).
|
||||
- `PageRenderer` resolves templates from fixed directory roots.
|
||||
- No user-editable templates, no template CRUD, no per-entity template
|
||||
selection.
|
||||
- The `ScriptEngine` + `ScriptsView` combination already implements the exact
|
||||
pattern needed (file-based storage with YAML metadata, Monaco editor, CRUD,
|
||||
database index, file sync).
|
||||
|
||||
### Implementation Plan
|
||||
|
||||
#### 1.1 Database Schema
|
||||
|
||||
Add a `templates` table to `schema.ts`:
|
||||
|
||||
| Column | Type | Notes |
|
||||
|-------------|---------|----------------------------------------------|
|
||||
| id | text PK | UUID |
|
||||
| projectId | text FK | References projects |
|
||||
| slug | text | Unique per project |
|
||||
| title | text | Display name |
|
||||
| kind | text | `'post'`, `'list'`, `'not-found'`, `'partial'` |
|
||||
| filePath | text | Relative path within project `templates/` dir |
|
||||
| enabled | integer | 0/1 — disabled templates fall back to built-in |
|
||||
| version | integer | Incremented on each save |
|
||||
| createdAt | integer | Timestamp |
|
||||
| updatedAt | integer | Timestamp |
|
||||
|
||||
Add template selection fields:
|
||||
|
||||
- `CategoryMetadata`: add optional `postTemplateSlug` and `listTemplateSlug`
|
||||
fields (stored in `meta/project.json`).
|
||||
- Posts table: add optional `templateSlug` column for per-post overrides.
|
||||
- Tags table: add optional `postTemplateSlug` column for tag-level overrides.
|
||||
|
||||
#### 1.2 Engine Class — `TemplateEngine`
|
||||
|
||||
Follow the `ScriptEngine` pattern exactly:
|
||||
|
||||
- `createTemplate(input)` — write `.liquid` file with YAML frontmatter +
|
||||
database entry.
|
||||
- `updateTemplate(id, updates)` — update file + database, increment version.
|
||||
- `deleteTemplate(id)` — remove file + database entry.
|
||||
- `getTemplate(id)` / `getAllTemplates()` — read from database, load content
|
||||
from file.
|
||||
- `rebuildDatabaseFromFiles()` — scan `templates/` directory, rebuild database
|
||||
from file metadata.
|
||||
- `reconcileTemplatesFromGitChanges()` — sync database after git operations.
|
||||
- `validateTemplate(content)` — attempt Liquid parse, return errors.
|
||||
|
||||
Store templates as `.liquid` files in the project's `templates/` directory with
|
||||
YAML frontmatter:
|
||||
|
||||
```liquid
|
||||
---
|
||||
id: <uuid>
|
||||
projectId: <uuid>
|
||||
slug: custom-post
|
||||
title: Custom Post Layout
|
||||
kind: post
|
||||
enabled: true
|
||||
version: 3
|
||||
---
|
||||
<main>
|
||||
<article>{{ post.content | markdown }}</article>
|
||||
</main>
|
||||
```
|
||||
|
||||
#### 1.3 Template Resolution in PageRenderer
|
||||
|
||||
Modify `PageRenderer` to resolve templates with priority:
|
||||
|
||||
1. Post-specific template override (`posts.templateSlug`)
|
||||
2. Tag-level template override (first matching tag with a `postTemplateSlug`)
|
||||
3. Category-level template override (`CategoryMetadata.postTemplateSlug`)
|
||||
4. Built-in default template
|
||||
|
||||
Add the project's `templates/` directory to `resolvePageRendererTemplateRoots()`
|
||||
so Liquid's `{% render %}` can find user partials.
|
||||
|
||||
#### 1.4 IPC Handlers
|
||||
|
||||
Register in `handlers.ts`:
|
||||
|
||||
- `templates:create`, `templates:update`, `templates:delete`
|
||||
- `templates:get`, `templates:getAll`
|
||||
- `templates:validate`
|
||||
|
||||
Expose in `preload.ts` and update `electronApi.ts` types.
|
||||
|
||||
#### 1.5 UI — `TemplateEditorView`
|
||||
|
||||
Mirror `ScriptsView`:
|
||||
|
||||
- Sidebar activity: add "Templates" icon to ActivityBar.
|
||||
- Sidebar list: show all templates grouped by kind, with enabled/disabled
|
||||
state.
|
||||
- Tab content: Monaco editor with `liquid` or `html` language mode.
|
||||
- Metadata fields: title, slug, kind dropdown, enabled toggle.
|
||||
- Actions: save (Ctrl+S), validate syntax, delete.
|
||||
- Footer: created/updated timestamps.
|
||||
|
||||
#### 1.6 Template Assignment UI
|
||||
|
||||
In `SettingsView`, extend the category metadata section:
|
||||
|
||||
- Add "Post Template" and "List Template" dropdowns per category, populated
|
||||
from user templates of matching kind.
|
||||
|
||||
In the post editor metadata area:
|
||||
|
||||
- Add optional "Template Override" dropdown (only shows user templates of kind
|
||||
`post`).
|
||||
|
||||
#### 1.7 Starter Templates
|
||||
|
||||
On project creation, copy the bundled templates into the project's `templates/`
|
||||
directory so users have a working starting point they can modify.
|
||||
|
||||
---
|
||||
|
||||
## 2. Post Translation System
|
||||
## 1. Post Translation System
|
||||
|
||||
### Goal
|
||||
|
||||
@@ -159,7 +28,7 @@ publishing pipeline can generate multilingual output.
|
||||
|
||||
### Implementation Plan
|
||||
|
||||
#### 2.1 Database Schema
|
||||
#### 1.1 Database Schema
|
||||
|
||||
Extend the `posts` table:
|
||||
|
||||
@@ -172,7 +41,7 @@ No separate junction table needed. A translated post is simply a post with
|
||||
`translationOfId` pointing at its source. This keeps the model simple: each
|
||||
post belongs to exactly one language and optionally references one original.
|
||||
|
||||
#### 2.2 YAML Frontmatter
|
||||
#### 1.2 YAML Frontmatter
|
||||
|
||||
Extend `postFileUtils.ts` to read/write:
|
||||
|
||||
@@ -184,7 +53,7 @@ translationOf: <original-post-id>
|
||||
On `readPostFile()`, parse these fields. On `writePostFile()`, include them
|
||||
when present.
|
||||
|
||||
#### 2.3 PostEngine Extensions
|
||||
#### 1.3 PostEngine Extensions
|
||||
|
||||
Add methods:
|
||||
|
||||
@@ -196,7 +65,7 @@ Add methods:
|
||||
Modify `createPost()` and `updatePost()` to accept and persist the `language`
|
||||
and `translationOfId` fields.
|
||||
|
||||
#### 2.4 AI Translation Tools in OpenCodeManager
|
||||
#### 1.4 AI Translation Tools in OpenCodeManager
|
||||
|
||||
Add three new methods following the `analyzeMediaImage()` pattern:
|
||||
|
||||
@@ -221,7 +90,7 @@ Add three new methods following the `analyzeMediaImage()` pattern:
|
||||
Register these as IPC handlers: `chat:detectPostLanguage`,
|
||||
`chat:translatePost`, `chat:generatePostSummary`.
|
||||
|
||||
#### 2.5 Import Pipeline Integration
|
||||
#### 1.5 Import Pipeline Integration
|
||||
|
||||
In `ImportExecutionEngine`, after a post is imported and published:
|
||||
|
||||
@@ -234,7 +103,7 @@ In `ImportExecutionEngine`, after a post is imported and published:
|
||||
This is optional and should be configurable per import definition (a checkbox
|
||||
"Auto-detect language and translate" in `ImportAnalysisView`).
|
||||
|
||||
#### 2.6 UI — Translation Panel
|
||||
#### 1.6 UI — Translation Panel
|
||||
|
||||
In the post editor metadata area, add a "Translations" section:
|
||||
|
||||
@@ -246,7 +115,7 @@ In the post editor metadata area, add a "Translations" section:
|
||||
|
||||
In the sidebar post list, optionally show a language badge per post.
|
||||
|
||||
#### 2.7 Publishing Pipeline
|
||||
#### 1.7 Publishing Pipeline
|
||||
|
||||
In `PageRenderer` and `BlogGenerationEngine`:
|
||||
|
||||
@@ -256,533 +125,7 @@ In `PageRenderer` and `BlogGenerationEngine`:
|
||||
|
||||
---
|
||||
|
||||
## 3. MCP Server — Agent-Assisted Content Creation - done
|
||||
|
||||
### Goal
|
||||
|
||||
Host an MCP (Model Context Protocol) server inside the application so external
|
||||
AI agents (Claude Code, Cursor, etc.) can connect and work with bDS. The core
|
||||
principle: **read access is open, write access goes through user-reviewable
|
||||
drafts**. External agents never silently mutate data — they propose changes
|
||||
that the user accepts or discards inside the agent's UI.
|
||||
|
||||
This enables powerful workflows: an agent can read existing posts, draft a new
|
||||
one with pre-filled content, propose a Python script or Liquid template, or
|
||||
suggest image metadata improvements — and the user always has final say before
|
||||
anything enters the system.
|
||||
|
||||
### Current State
|
||||
|
||||
- `OpenCodeManager` already defines 16 data-access tools and 7 A2UI render
|
||||
tools with full implementations (`getToolDefinitions()`, `executeTool()`).
|
||||
- `PreviewServer` provides the architectural pattern for an in-process HTTP
|
||||
server with lifecycle management.
|
||||
- Posts already have a `draft` status (DB-only, no disk writes until publish)
|
||||
— the exact pattern needed for MCP draft posts.
|
||||
- Scripts and templates are file-first (immediately persisted) — MCP proposals
|
||||
need an in-memory staging layer before acceptance.
|
||||
- **`@modelcontextprotocol/sdk` v1.27.1 and `@modelcontextprotocol/ext-apps`
|
||||
v1.1.2 are installed.**
|
||||
- **`ProposalStore` engine is implemented and tested (18 tests).**
|
||||
- **`MCPServer` engine is implemented and tested (70 tests).** Standalone HTTP
|
||||
server on port 4124, stateless mode (new `McpServer` per request), registers
|
||||
5 static resources, 7 resource templates, 8 tools (6 model-facing +
|
||||
2 app-only), 3 prompts, and 4 `ui://` review-app resources.
|
||||
- **`mcp-views.ts` provides review HTML** for posts, scripts, templates, and
|
||||
metadata diffs via `@modelcontextprotocol/ext-apps` App class.
|
||||
- **Lifecycle integrated in `main.ts`** — MCP server starts on app ready and
|
||||
cleans up on before-quit.
|
||||
- **Origin validation** — rejects requests from non-localhost origins to
|
||||
prevent DNS rebinding attacks.
|
||||
- **`accept_proposal` / `discard_proposal` use app-only visibility** via
|
||||
`registerAppTool` with `visibility: ["app"]` — hidden from the agent LLM.
|
||||
|
||||
### Design Principles
|
||||
|
||||
1. **Use all three MCP primitives** — expose blog data via the appropriate
|
||||
MCP primitive for each use case:
|
||||
- **Resources** for passive, read-only data (post content, media metadata,
|
||||
category/tag lists, blog stats). These are application-controlled — the
|
||||
host decides when to fetch them.
|
||||
- **Tools** for parameterized actions (search, drafting, proposing changes).
|
||||
These are model-controlled — the LLM decides when to call them.
|
||||
- **Prompts** for user-triggered workflow templates (e.g., "draft a blog
|
||||
post", "audit content quality"). These surface as slash commands in the
|
||||
host UI.
|
||||
|
||||
2. **Tool annotations** — every tool declares MCP `annotations` to advertise
|
||||
its behavior to the host:
|
||||
- Read-only tools: `{ readOnlyHint: true, openWorldHint: false }`
|
||||
- Proposal tools: `{ readOnlyHint: false, destructiveHint: false }`
|
||||
- `accept_proposal`: `{ readOnlyHint: false, destructiveHint: false,
|
||||
idempotentHint: true }`
|
||||
- `discard_proposal`: `{ readOnlyHint: false, destructiveHint: true,
|
||||
idempotentHint: true }`
|
||||
|
||||
All annotations are hints — hosts must not make security decisions based
|
||||
on them alone, but they help hosts choose appropriate UI (e.g.,
|
||||
auto-approving reads, showing confirmation for destructive actions).
|
||||
|
||||
3. **Tool metadata** — every tool includes `title` (human-readable display
|
||||
name) and `description` (detailed explanation of what it does and when to
|
||||
use it). Descriptions are critical — they are what the LLM reads to decide
|
||||
whether and when to call a tool.
|
||||
|
||||
4. **Draft/Propose pattern for writes via MCP Apps** — every mutation goes
|
||||
through a user-gated flow using the MCP Apps extension:
|
||||
- Agent calls a `draft_*` or `propose_*` tool. These tools declare a
|
||||
`_meta.ui.resourceUri` pointing to a review UI (`ui://` resource).
|
||||
- The host (Claude Desktop, VS Code, etc.) renders the review app as a
|
||||
sandboxed iframe inline in the conversation.
|
||||
- The app shows the proposal (post preview, code block, metadata diff)
|
||||
with accept/discard buttons. The agent LLM is **not** in the loop.
|
||||
- User clicks accept → the app calls `accept_proposal` tool via the
|
||||
MCP App bridge (postMessage) → server commits the change.
|
||||
- User clicks discard → the app calls `discard_proposal` tool →
|
||||
server cleans up.
|
||||
- The tool result flows back to the agent so it can continue.
|
||||
|
||||
5. **Aligned with internal editors** — the MCP App review UIs mirror what
|
||||
the app's own editors show (post metadata + content, script content +
|
||||
validation, template content + validation, image metadata diff). This
|
||||
keeps the user experience consistent whether they work inside bDS or
|
||||
through an external agent.
|
||||
|
||||
6. **Capability negotiation** — during MCP initialization, the server
|
||||
declares its supported capabilities: `tools`, `resources`, `prompts`.
|
||||
For MCP Apps, the extension capability `io.modelcontextprotocol/ui` is
|
||||
negotiated via the `extensions` field. The server checks whether the
|
||||
client supports the Apps extension and adjusts proposal tool responses
|
||||
accordingly (structured preview data for Apps-capable hosts, formatted
|
||||
text for others).
|
||||
|
||||
7. **Input validation** — all tool inputs are validated at the MCP
|
||||
boundary (via Zod schemas in `inputSchema`) before forwarding to
|
||||
engine methods. Do not rely solely on downstream engine validation.
|
||||
|
||||
### Implementation Plan
|
||||
|
||||
#### 3.1 Dependencies
|
||||
|
||||
- `@modelcontextprotocol/sdk` — standard MCP server with transport handling.
|
||||
- `@modelcontextprotocol/ext-apps` — MCP Apps extension for serving
|
||||
interactive UI resources.
|
||||
|
||||
#### 3.2 Engine Class — `MCPServer`
|
||||
|
||||
Follow the `PreviewServer` pattern:
|
||||
|
||||
```
|
||||
src/main/engine/MCPServer.ts
|
||||
```
|
||||
|
||||
- Constructor accepts dependency injection (engines via getters).
|
||||
- `start(port)` — create standalone HTTP server on port 4124 using
|
||||
`StreamableHTTPServerTransport` in stateless mode.
|
||||
- `stop()` — clean shutdown.
|
||||
- Manages an in-memory `ProposalStore` for pending proposals (scripts,
|
||||
templates, metadata changes). Posts use the existing draft mechanism instead.
|
||||
- Serves `ui://` resources for the MCP App review UIs.
|
||||
- Exposes three MCP primitive types:
|
||||
- **Resources** — read-only blog data (posts, media, tags, categories,
|
||||
stats) accessible via URI-based `resources/read`.
|
||||
- **Tools** — parameterized actions (search, draft, propose, accept,
|
||||
discard). Agent-facing tools are visible to the LLM; app-internal
|
||||
tools (`accept_proposal`, `discard_proposal`) are called only by the
|
||||
MCP App via the App Bridge.
|
||||
- **Prompts** — user-triggered workflow templates surfaced as slash
|
||||
commands in the host.
|
||||
|
||||
#### 3.3 Resources (Read-Only Data)
|
||||
|
||||
Expose blog data as MCP Resources using URI templates. Resources are
|
||||
application-controlled — the host fetches them for context, they are not
|
||||
actions. Each resource is registered via `resources/list` and read via
|
||||
`resources/read`.
|
||||
|
||||
| Resource URI | Source | Description |
|
||||
|----------------------------------|----------------------------------|------------------------------------|
|
||||
| `bds://posts/{id}` | OpenCodeManager.read_post | Full post content + metadata |
|
||||
| `bds://posts` | OpenCodeManager.list_posts | Paginated post list |
|
||||
| `bds://media/{id}` | OpenCodeManager.get_media | Media item metadata |
|
||||
| `bds://media` | OpenCodeManager.list_media | Paginated media list |
|
||||
| `bds://tags` | OpenCodeManager.list_tags | All tags with counts |
|
||||
| `bds://categories` | OpenCodeManager.list_categories | All categories |
|
||||
| `bds://stats` | OpenCodeManager.get_blog_stats | Blog-wide statistics |
|
||||
| `bds://posts/{id}/backlinks` | OpenCodeManager.get_post_backlinks | Posts linking to this post |
|
||||
| `bds://posts/{id}/outlinks` | OpenCodeManager.get_post_outlinks | Posts this post links to |
|
||||
| `bds://posts/{id}/media` | OpenCodeManager.get_post_media | Media attached to a post |
|
||||
| `bds://media/{id}/posts` | OpenCodeManager.get_media_posts | Posts using a media item |
|
||||
| `bds://media/{id}/image` | OpenCodeManager.view_image | Image binary (for visual context) |
|
||||
|
||||
Use `bds://` as the custom URI scheme. Parameterized URIs use MCP resource
|
||||
templates (`resources/templates/list`).
|
||||
|
||||
Note: `notifications/resources/list_changed` is not emitted because the
|
||||
server runs in stateless mode (new `McpServer` per request, no persistent
|
||||
connection). If the server moves to session-based mode in the future,
|
||||
change notifications should be added.
|
||||
|
||||
List resources (`bds://posts`, `bds://media`) support cursor-based
|
||||
pagination following the MCP pagination spec. The initial response
|
||||
includes a `nextCursor` if more results exist; the host passes it back
|
||||
on the next `resources/read` call.
|
||||
|
||||
#### 3.4 Read Tools (Parameterized Queries)
|
||||
|
||||
Not all read operations fit the Resources model. Parameterized queries
|
||||
that accept complex search criteria are better modeled as tools with
|
||||
`readOnlyHint: true`.
|
||||
|
||||
| MCP Tool Name | Source | Annotations |
|
||||
|------------------------|----------------------------------|------------------------------------------|
|
||||
| search_posts | OpenCodeManager.search_posts | `readOnlyHint: true, openWorldHint: false` |
|
||||
|
||||
Each tool includes a `title`, a `description` explaining when it should
|
||||
be used, and a JSON Schema `inputSchema`. `search_posts` accepts query
|
||||
text, filters (status, category, tag, date range), and pagination
|
||||
parameters (cursor, limit).
|
||||
|
||||
Exclude `update_post_metadata`, `update_media_metadata` (writes go through
|
||||
proposals), and A2UI render tools (UI-specific, not useful for external
|
||||
agents).
|
||||
|
||||
#### 3.5 Proposal Tools (Draft-Based Writes)
|
||||
|
||||
These tools stage content for user review. Each declares a `_meta.ui` field
|
||||
pointing to a review UI resource, so the host renders an MCP App inline in the
|
||||
conversation. Proposals have a TTL (e.g., 30 min) and are auto-discarded if
|
||||
not accepted. All proposal tools use annotations
|
||||
`{ readOnlyHint: false, destructiveHint: false }`.
|
||||
|
||||
##### 3.5.1 `draft_post`
|
||||
|
||||
Creates a draft post using the existing PostEngine draft workflow.
|
||||
|
||||
**Input:** `{ title, content (markdown), excerpt?, tags?, categoryId? }`
|
||||
|
||||
**Tool definition `_meta`:**
|
||||
```json
|
||||
{ "ui": { "resourceUri": "ui://bds/review-post" } }
|
||||
```
|
||||
|
||||
**Action:**
|
||||
1. Call `PostEngine.createPost()` with status `draft`.
|
||||
2. Set tags and category if provided.
|
||||
3. Return `{ proposalId (= postId), type: 'draftPost', preview: { title,
|
||||
excerpt, tags, category, content, wordCount } }`.
|
||||
|
||||
**On user accept (via app):** `PostEngine.publishPost(postId)` — post
|
||||
transitions to published, markdown file is written to disk.
|
||||
|
||||
**On user discard (via app):** `PostEngine.deletePost(postId)` — draft is
|
||||
removed from the database.
|
||||
|
||||
##### 3.5.2 `propose_script`
|
||||
|
||||
Stages a Python script in memory for review.
|
||||
|
||||
**Input:** `{ title, content (python source), description? }`
|
||||
|
||||
**Tool definition `_meta`:**
|
||||
```json
|
||||
{ "ui": { "resourceUri": "ui://bds/review-script" } }
|
||||
```
|
||||
|
||||
**Action:**
|
||||
1. Validate Python syntax (basic parse check).
|
||||
2. Store in `ProposalStore` with a generated ID.
|
||||
3. Return `{ proposalId, type: 'script', preview: { title, slug (generated),
|
||||
description, content, syntaxValid, syntaxErrors? } }`.
|
||||
|
||||
**On user accept (via app):** `ScriptEngine.createScript()` — file + DB entry
|
||||
created.
|
||||
|
||||
**On user discard (via app):** Remove from `ProposalStore` — nothing was
|
||||
persisted.
|
||||
|
||||
##### 3.5.3 `propose_template`
|
||||
|
||||
Stages a Liquid template in memory for review.
|
||||
|
||||
**Input:** `{ title, content (liquid source), kind ('post'|'list'|'not-found'|
|
||||
'partial') }`
|
||||
|
||||
**Tool definition `_meta`:**
|
||||
```json
|
||||
{ "ui": { "resourceUri": "ui://bds/review-template" } }
|
||||
```
|
||||
|
||||
**Action:**
|
||||
1. Validate Liquid syntax via `TemplateEngine.validateTemplate()`.
|
||||
2. Store in `ProposalStore`.
|
||||
3. Return `{ proposalId, type: 'template', preview: { title, slug, kind,
|
||||
content, syntaxValid, syntaxErrors? } }`.
|
||||
|
||||
**On user accept (via app):** `TemplateEngine.createTemplate()` — file + DB
|
||||
entry created.
|
||||
|
||||
**On user discard (via app):** Remove from `ProposalStore`.
|
||||
|
||||
##### 3.5.4 `propose_media_metadata`
|
||||
|
||||
Stages metadata changes for an existing media item.
|
||||
|
||||
**Input:** `{ mediaId, title?, alt?, caption? }`
|
||||
|
||||
**Tool definition `_meta`:**
|
||||
```json
|
||||
{ "ui": { "resourceUri": "ui://bds/review-metadata" } }
|
||||
```
|
||||
|
||||
**Action:**
|
||||
1. Load current metadata via `MediaEngine`.
|
||||
2. Compute diff (current vs proposed for each changed field).
|
||||
3. Store in `ProposalStore`.
|
||||
4. Return `{ proposalId, type: 'mediaMetadata', preview: { filename,
|
||||
diff: { field, current, proposed }[] } }`.
|
||||
|
||||
**On user accept (via app):** Apply changes via `MediaEngine.updateMedia()`.
|
||||
|
||||
**On user discard (via app):** Remove from `ProposalStore`.
|
||||
|
||||
##### 3.5.5 `propose_post_metadata`
|
||||
|
||||
Stages metadata changes for an existing post (title, excerpt, slug, tags).
|
||||
|
||||
**Input:** `{ postId, title?, excerpt?, slug?, tags? }`
|
||||
|
||||
**Tool definition `_meta`:**
|
||||
```json
|
||||
{ "ui": { "resourceUri": "ui://bds/review-metadata" } }
|
||||
```
|
||||
|
||||
**Action:**
|
||||
1. Load current post via `PostEngine`.
|
||||
2. Compute diff.
|
||||
3. Store in `ProposalStore`.
|
||||
4. Return `{ proposalId, type: 'postMetadata', preview: { currentTitle,
|
||||
diff: { field, current, proposed }[] } }`.
|
||||
|
||||
**On user accept (via app):** Apply via `PostEngine.updatePost()`.
|
||||
|
||||
**On user discard (via app):** Remove from `ProposalStore`.
|
||||
|
||||
#### 3.6 App-Internal Tools (Accept / Discard)
|
||||
|
||||
These tools are called by the MCP App (via the App Bridge's `tools/call`
|
||||
mechanism), **not** by the agent LLM. They are registered with
|
||||
`registerAppTool` from `@modelcontextprotocol/ext-apps` using
|
||||
`visibility: ["app"]`, which signals to compliant hosts that these tools
|
||||
should not be shown to or invoked by the model.
|
||||
|
||||
##### `accept_proposal`
|
||||
|
||||
**Input:** `{ proposalId }`
|
||||
|
||||
**Annotations:** `{ readOnlyHint: false, destructiveHint: false,
|
||||
idempotentHint: true }`
|
||||
|
||||
Looks up the proposal type (draftPost, script, template, mediaMetadata,
|
||||
postMetadata) and executes the commit action:
|
||||
- draftPost → `PostEngine.publishPost()`
|
||||
- script → `ScriptEngine.createScript()`
|
||||
- template → `TemplateEngine.createTemplate()`
|
||||
- mediaMetadata → `MediaEngine.updateMedia()`
|
||||
- postMetadata → `PostEngine.updatePost()`
|
||||
|
||||
Returns `{ success, message }`.
|
||||
|
||||
##### `discard_proposal`
|
||||
|
||||
**Input:** `{ proposalId }`
|
||||
|
||||
**Annotations:** `{ readOnlyHint: false, destructiveHint: true,
|
||||
idempotentHint: true }`
|
||||
|
||||
Executes the cleanup action:
|
||||
- draftPost → `PostEngine.deletePost()`
|
||||
- All others → remove from `ProposalStore`
|
||||
|
||||
Returns `{ success, message }`.
|
||||
|
||||
#### 3.7 MCP App Review UIs
|
||||
|
||||
The review UIs are HTML pages served as `ui://` resources by the MCP server.
|
||||
They render inside the host's sandboxed iframe and use the
|
||||
`@modelcontextprotocol/ext-apps` App class for bidirectional communication.
|
||||
|
||||
Each review app:
|
||||
1. Receives the tool result data (proposal preview) from the host.
|
||||
2. Renders a focused review interface:
|
||||
- **Post review** (`ui://bds/review-post`): title, metadata fields,
|
||||
rendered markdown content preview, word count.
|
||||
- **Script review** (`ui://bds/review-script`): title, syntax-highlighted
|
||||
Python code, validation status/errors.
|
||||
- **Template review** (`ui://bds/review-template`): title, kind badge,
|
||||
syntax-highlighted Liquid code, validation status/errors.
|
||||
- **Metadata review** (`ui://bds/review-metadata`): side-by-side diff of
|
||||
current vs proposed values for each changed field.
|
||||
3. Shows accept and discard buttons.
|
||||
4. On user action, calls `accept_proposal` or `discard_proposal` via the
|
||||
App Bridge's `tools/call`.
|
||||
5. Updates its UI to show the outcome ("Published", "Created", "Discarded").
|
||||
|
||||
These apps are small, self-contained HTML pages — the "slim MCP apps" concept.
|
||||
They can share a common layout/style and differ only in the content they
|
||||
render. Since the host provides the sandboxing, the apps don't need their own
|
||||
authentication or security layer.
|
||||
|
||||
The review UIs should visually align with what the bDS internal editors show,
|
||||
so the user experience is consistent. Where practical, share CSS/component
|
||||
patterns between the bDS renderer UI and the MCP App review UIs.
|
||||
|
||||
#### 3.8 ProposalStore
|
||||
|
||||
In-memory store for pending proposals (not posts — those use the DB draft
|
||||
mechanism):
|
||||
|
||||
```typescript
|
||||
interface Proposal {
|
||||
id: string;
|
||||
type: 'script' | 'template' | 'mediaMetadata' | 'postMetadata';
|
||||
data: Record<string, unknown>; // type-specific payload
|
||||
createdAt: number;
|
||||
ttlMs: number; // default 30 minutes
|
||||
}
|
||||
```
|
||||
|
||||
- Simple `Map<string, Proposal>` with periodic cleanup of expired entries.
|
||||
- On app shutdown, all pending proposals are discarded (they were never
|
||||
committed).
|
||||
- No persistence needed — proposals are ephemeral by design.
|
||||
|
||||
For draft posts, the `proposalId` is the post ID itself. The `ProposalStore`
|
||||
tracks a mapping from `proposalId → 'draftPost'` so the accept/discard
|
||||
tools know to call PostEngine rather than look in the store.
|
||||
|
||||
#### 3.9 Transport
|
||||
|
||||
**Streamable HTTP** — standalone HTTP server on port 4124 using
|
||||
`StreamableHTTPServerTransport` in stateless mode (new `McpServer` per
|
||||
request). A single HTTP endpoint at `/mcp` accepts JSON-RPC POST
|
||||
requests and responds with `application/json` or `text/event-stream`
|
||||
(SSE).
|
||||
|
||||
**Security:**
|
||||
|
||||
- Bind to `127.0.0.1` (localhost only).
|
||||
- Validate the `Origin` header on all requests — reject non-localhost
|
||||
origins to prevent DNS rebinding attacks.
|
||||
- Requests without an `Origin` header are allowed (CLI tools like curl
|
||||
and local MCP clients typically do not send one).
|
||||
|
||||
#### 3.10 Lifecycle Integration
|
||||
|
||||
In `main.ts`:
|
||||
|
||||
- Initialize `MCPServer` in `initialize()`.
|
||||
- Start alongside `PreviewServer` in `app.whenReady()`.
|
||||
- Stop in `before-quit` handler (discard all pending proposals).
|
||||
- Respect active project context (tools operate on the active project).
|
||||
|
||||
#### 3.11 Configuration
|
||||
|
||||
In `SettingsView`, add an "MCP Server" section:
|
||||
|
||||
- Enable/disable toggle.
|
||||
- Port number (for HTTP transport).
|
||||
- Show connection instructions (stdio command or URL).
|
||||
- Proposal TTL setting (default 30 min).
|
||||
|
||||
#### 3.12 MCP Prompts (Workflow Templates)
|
||||
|
||||
Expose MCP Prompts for common agent workflows. Prompts are user-controlled —
|
||||
they surface as slash commands or command palette entries in the host and
|
||||
produce pre-structured messages that guide the LLM.
|
||||
|
||||
| Prompt Name | Arguments | Description |
|
||||
|------------------------|----------------------------|--------------------------------------|
|
||||
| `draft-blog-post` | `topic?`, `category?` | Guides agent through reading existing posts, understanding the blog's style, and drafting a new post on the given topic |
|
||||
| `improve-media-metadata` | `scope` (`all`\|`missing`) | Guides agent through reviewing media items and proposing alt text, captions, and titles |
|
||||
| `content-audit` | `category?` | Guides agent through reviewing posts for quality, broken links, missing metadata, and suggesting fixes |
|
||||
|
||||
Each prompt returns a structured message array (system + user messages) that
|
||||
sets up the LLM with context about the blog and clear instructions for the
|
||||
workflow. The host renders the prompt as a one-click action.
|
||||
|
||||
This is a lower-priority addition — prompts enhance discoverability but the
|
||||
core read/write flow works without them.
|
||||
|
||||
#### 3.13 Example Workflows
|
||||
|
||||
**Agent creates a blog post:**
|
||||
1. Agent LLM reads `bds://categories` and `bds://tags` resources to
|
||||
understand the blog.
|
||||
2. Agent LLM calls `draft_post({ title: "...", content: "...", tags: [...] })`.
|
||||
3. MCP server creates draft post (DB only), returns preview data.
|
||||
4. Host sees `_meta.ui.resourceUri`, fetches `ui://bds/review-post`, renders
|
||||
sandboxed iframe inline in conversation.
|
||||
5. Review app shows the full post preview with accept/discard buttons.
|
||||
6. User clicks accept → app calls `accept_proposal` via App Bridge →
|
||||
server publishes post → app shows "Published" → agent is informed.
|
||||
|
||||
**Agent writes a Python script:**
|
||||
1. Agent LLM reads `bds://posts` resource to understand the content model.
|
||||
2. Agent LLM calls `propose_script({ title: "Tag Cleanup", content: "..." })`.
|
||||
3. MCP server validates syntax, stages in ProposalStore, returns preview.
|
||||
4. Host renders `ui://bds/review-script` — shows syntax-highlighted code
|
||||
with validation results.
|
||||
5. User reviews code, clicks accept → app calls `accept_proposal` →
|
||||
server creates script via ScriptEngine.
|
||||
|
||||
**Agent suggests image metadata:**
|
||||
1. Agent LLM reads `bds://media/{id}/image` resource to see the image.
|
||||
2. Agent LLM calls `propose_media_metadata({ mediaId, alt: "...",
|
||||
caption: "..." })`.
|
||||
3. MCP server computes diff, stages in ProposalStore, returns diff preview.
|
||||
4. Host renders `ui://bds/review-metadata` — shows current vs proposed diff.
|
||||
5. User reviews diff, clicks accept → metadata is updated.
|
||||
|
||||
#### 3.14 MCP Apps Client Support
|
||||
|
||||
MCP Apps are currently supported by Claude (claude.ai), Claude Desktop,
|
||||
VS Code GitHub Copilot, Goose, Postman, and MCPJam. For hosts that don't
|
||||
support MCP Apps, the proposal tools still return structured text content
|
||||
that the agent can present as formatted text — the user would then need
|
||||
to instruct the agent to accept or discard via conversation, falling back
|
||||
to a simpler flow.
|
||||
|
||||
For hosts without Apps support, the MCP **Elicitation** primitive
|
||||
(`elicitation/request`) can serve as a lighter-weight confirmation
|
||||
mechanism — the server asks the user to confirm or reject a proposal via
|
||||
a simple dialog rather than a full review UI. This requires the host to
|
||||
support the `elicitation` capability. When neither Apps nor Elicitation is
|
||||
available, fall back to text-based accept/discard in the conversation.
|
||||
|
||||
#### 3.15 Testing
|
||||
|
||||
- Unit tests for tool definition mapping (Anthropic → MCP format).
|
||||
- Unit tests for resource URI resolution and resource template listing.
|
||||
- Unit tests for `ProposalStore` (create, accept, discard, TTL expiry).
|
||||
- Unit tests for accept/discard tool handlers.
|
||||
- Unit tests for tool annotations (verify correct hints on each tool).
|
||||
- Unit tests for prompt templates (verify message structure and arguments).
|
||||
- Integration tests: draft_post → app accept flow, propose_script → app
|
||||
discard flow.
|
||||
- Integration tests: start MCP server, send tool calls, verify responses.
|
||||
- Integration tests: capability negotiation (with/without Apps extension).
|
||||
- Integration tests: resource read, resource list with pagination, resource
|
||||
change notifications.
|
||||
- MCP App UI tests: render review apps, simulate user actions, verify
|
||||
tool calls through App Bridge.
|
||||
- Security tests: Origin header validation, session management, rate
|
||||
limiting (for Streamable HTTP transport).
|
||||
- Follow existing engine test patterns with mocked dependencies.
|
||||
|
||||
---
|
||||
|
||||
## 4. AI Post Summary, Title & Slug Suggestions
|
||||
## 2. AI Post Summary, Title & Slug Suggestions
|
||||
|
||||
### Goal
|
||||
|
||||
@@ -804,7 +147,7 @@ handle the metadata.
|
||||
|
||||
### Implementation Plan
|
||||
|
||||
#### 4.1 Backend — `analyzePost()` in OpenCodeManager
|
||||
#### 2.1 Backend — `analyzePost()` in OpenCodeManager
|
||||
|
||||
Add a new method following the `analyzeMediaImage()` pattern:
|
||||
|
||||
@@ -827,7 +170,7 @@ Add a new method following the `analyzeMediaImage()` pattern:
|
||||
|
||||
Register IPC handler: `chat:analyzePost`.
|
||||
|
||||
#### 4.2 Frontend — Post Editor AI Button
|
||||
#### 2.2 Frontend — Post Editor AI Button
|
||||
|
||||
In the post editor metadata area (`Editor.tsx`, around line 720):
|
||||
|
||||
@@ -837,7 +180,7 @@ In the post editor metadata area (`Editor.tsx`, around line 720):
|
||||
- On click: call `window.electronAPI.chat.analyzePost(postId, projectLanguage)`.
|
||||
- Show `AISuggestionsModal` with the results.
|
||||
|
||||
#### 4.3 Extend AISuggestionsModal
|
||||
#### 2.3 Extend AISuggestionsModal
|
||||
|
||||
The modal currently supports `title`, `alt`, `caption` fields. Adapt it to
|
||||
also support a post mode with `title`, `excerpt`, `slug` fields:
|
||||
@@ -858,7 +201,7 @@ interface SuggestionField {
|
||||
}
|
||||
```
|
||||
|
||||
#### 4.4 Applying Suggestions
|
||||
#### 2.4 Applying Suggestions
|
||||
|
||||
On "Apply Selected":
|
||||
|
||||
@@ -868,7 +211,7 @@ On "Apply Selected":
|
||||
- Slug: only apply if post has never been published. Show a warning and disable
|
||||
the checkbox if the post has `publishedAt` set.
|
||||
|
||||
#### 4.5 i18n
|
||||
#### 2.5 i18n
|
||||
|
||||
Add keys to all 5 locale files:
|
||||
|
||||
@@ -878,7 +221,7 @@ Add keys to all 5 locale files:
|
||||
- `aiSuggestions.slugLockedWarning`
|
||||
- `postEditor.quickActions`, `postEditor.analyzeWithAI`
|
||||
|
||||
#### 4.6 Excerpt Field in Editor
|
||||
#### 2.6 Excerpt Field in Editor
|
||||
|
||||
If the excerpt/summary is not currently editable in the post metadata area,
|
||||
add a multi-line text field for it between title and tags. This is needed both
|
||||
@@ -886,7 +229,7 @@ for manual editing and for applying AI suggestions.
|
||||
|
||||
---
|
||||
|
||||
## 5. Drag-and-Drop Image Insertion
|
||||
## 3. Drag-and-Drop Image Insertion
|
||||
|
||||
### Goal
|
||||
|
||||
@@ -907,7 +250,7 @@ as markdown images.
|
||||
|
||||
### Implementation Plan
|
||||
|
||||
#### 5.1 ProseMirror Drop Plugin
|
||||
#### 3.1 ProseMirror Drop Plugin
|
||||
|
||||
Create a new plugin in `src/renderer/plugins/dropImagePlugin.ts` following the
|
||||
`imageResolverPlugin` pattern:
|
||||
@@ -934,7 +277,7 @@ export const dropImagePlugin = $prose(() => {
|
||||
});
|
||||
```
|
||||
|
||||
#### 5.2 Drop Handler Flow
|
||||
#### 3.2 Drop Handler Flow
|
||||
|
||||
For each dropped file:
|
||||
|
||||
@@ -950,14 +293,14 @@ For each dropped file:
|
||||
5. **Resolve** — the existing `imageResolverPlugin` will automatically convert
|
||||
the relative path to a `bds-media://` URL for display.
|
||||
|
||||
#### 5.3 Visual Feedback
|
||||
#### 3.3 Visual Feedback
|
||||
|
||||
- On `dragover` with image files: add a CSS class to the editor container
|
||||
showing a drop zone indicator (border highlight or overlay).
|
||||
- On `dragleave` / `drop`: remove the indicator.
|
||||
- During import (for large files): show a small inline spinner or toast.
|
||||
|
||||
#### 5.4 Integration into MilkdownEditor
|
||||
#### 3.4 Integration into MilkdownEditor
|
||||
|
||||
In `MilkdownEditor.tsx`, register the new plugin alongside existing plugins:
|
||||
|
||||
@@ -971,7 +314,7 @@ import { dropImagePlugin } from '../../plugins/dropImagePlugin';
|
||||
Pass `postId` and the import callback to the plugin via the editor context or
|
||||
a shared ref.
|
||||
|
||||
#### 5.5 Paste Support (Optional Extension)
|
||||
#### 3.5 Paste Support (Optional Extension)
|
||||
|
||||
The same plugin can handle `paste` events with image files:
|
||||
|
||||
@@ -979,7 +322,7 @@ The same plugin can handle `paste` events with image files:
|
||||
- Same import → insert → link flow as drop.
|
||||
- This handles screenshots pasted from the clipboard.
|
||||
|
||||
#### 5.6 Error Handling
|
||||
#### 3.6 Error Handling
|
||||
|
||||
- Non-image files: ignore silently (don't prevent default, let editor handle
|
||||
text drops normally).
|
||||
@@ -987,7 +330,7 @@ The same plugin can handle `paste` events with image files:
|
||||
- Multiple files: process sequentially, insert at cursor position for first,
|
||||
then append after each previous insertion.
|
||||
|
||||
#### 5.7 Testing
|
||||
#### 3.7 Testing
|
||||
|
||||
- Unit test the plugin's file validation logic.
|
||||
- Integration test: mock `electronAPI.media.import`, verify correct calls and
|
||||
|
||||
Reference in New Issue
Block a user