chore: cleaning up todo

This commit is contained in:
2026-03-01 21:53:15 +01:00
parent 02c92e865a
commit 3addd92728

703
TODO.md
View File

@@ -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