From 6c22e698057c279aadcc9ae0ba64ec98782fd6a7 Mon Sep 17 00:00:00 2001 From: hugo Date: Sat, 28 Feb 2026 12:36:13 +0100 Subject: [PATCH] feat: more work on mcp server integration --- .github/copilot-instructions.md => AGENTS.md | 17 +- CLAUDE.md | 150 +------- GEMINI.md | 149 -------- package.json | 4 + .../engine/GenerationRouteRendererFactory.ts | 2 +- src/main/engine/MCPAgentConfigEngine.ts | 156 ++++++++ src/main/engine/MediaEngine.ts | 8 +- src/main/engine/OpenCodeManager.ts | 24 +- src/main/engine/PostEngine.ts | 6 +- src/main/engine/SharedRouteRenderer.ts | 4 +- src/main/engine/SharedSnapshotService.ts | 2 +- src/main/engine/mcp-views.ts | 299 ++++----------- .../engine/mcp-views/review-metadata.html | 132 +++++++ src/main/engine/mcp-views/review-post.html | 124 +++++++ src/main/engine/mcp-views/review-script.html | 116 ++++++ .../engine/mcp-views/review-template.html | 116 ++++++ src/main/ipc/handlers.ts | 51 +++ src/main/preload.ts | 7 + src/main/shared/electronApi.ts | 6 + src/renderer/components/Editor/Editor.tsx | 2 +- .../components/SettingsView/SettingsView.tsx | 100 +++++- src/renderer/components/Sidebar/Sidebar.tsx | 13 +- src/renderer/i18n/locales/de.json | 18 +- src/renderer/i18n/locales/en.json | 18 +- src/renderer/i18n/locales/es.json | 18 +- src/renderer/i18n/locales/fr.json | 18 +- src/renderer/i18n/locales/it.json | 18 +- .../GenerationRouteRendererFactory.test.ts | 2 +- tests/engine/MCPConfigEngine.test.ts | 339 ++++++++++++++++++ tests/engine/MCPServer.integration.test.ts | 2 + tests/engine/MediaEngine.test.ts | 10 +- tests/engine/PostEngine.test.ts | 10 +- tests/engine/PreviewServer.test.ts | 2 +- tests/engine/SharedSnapshotService.test.ts | 6 +- tests/engine/mainStartup.test.ts | 30 +- tests/engine/mcp-views.test.ts | 76 ++-- 36 files changed, 1420 insertions(+), 635 deletions(-) rename .github/copilot-instructions.md => AGENTS.md (93%) delete mode 100644 GEMINI.md create mode 100644 src/main/engine/MCPAgentConfigEngine.ts create mode 100644 src/main/engine/mcp-views/review-metadata.html create mode 100644 src/main/engine/mcp-views/review-post.html create mode 100644 src/main/engine/mcp-views/review-script.html create mode 100644 src/main/engine/mcp-views/review-template.html create mode 100644 tests/engine/MCPConfigEngine.test.ts diff --git a/.github/copilot-instructions.md b/AGENTS.md similarity index 93% rename from .github/copilot-instructions.md rename to AGENTS.md index 916b356..a2c9b63 100644 --- a/.github/copilot-instructions.md +++ b/AGENTS.md @@ -1,16 +1,11 @@ -# GitHub Copilot Instructions for Blogging Desktop Server (bDS) +# Agents Instructions for Blogging Desktop Server (bDS) This document provides context and best practices for GitHub Copilot when working on this Electron + TypeScript + SQLite blogging application. -## Project Overview +## Plan Mode -**Blogging Desktop Server (bDS)** is a desktop blogging application built with: -- **Electron** v28+ for cross-platform desktop -- **TypeScript** for all code (strict mode) -- **React** for the renderer UI -- **Drizzle ORM** for type-safe database access -- **@libsql/client** for SQLite (local database) -- **Zustand** for React state management +- Make the plan extremely concise. Sacrifice grammar for the sake of concision. +- At the end of each plan, give me a list of unresolved questions to answer, if any. --- @@ -143,7 +138,3 @@ This document provides context and best practices for GitHub Copilot when workin - Store Dropbox auth tokens in secure storage, not in code - Sanitize user input before rendering (XSS prevention) -## Plan Mode - -- Make the plan extremely concise. Sacrifice grammar for the sake of concision. -- At the end of each plan, give me a list of unresolved questions to answer, if any. diff --git a/CLAUDE.md b/CLAUDE.md index 916b356..43c994c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,149 +1 @@ -# GitHub Copilot Instructions for Blogging Desktop Server (bDS) - -This document provides context and best practices for GitHub Copilot when working on this Electron + TypeScript + SQLite blogging application. - -## Project Overview - -**Blogging Desktop Server (bDS)** is a desktop blogging application built with: -- **Electron** v28+ for cross-platform desktop -- **TypeScript** for all code (strict mode) -- **React** for the renderer UI -- **Drizzle ORM** for type-safe database access -- **@libsql/client** for SQLite (local database) -- **Zustand** for React state management - ---- - -## ⚠️ MANDATORY: Test-First Development - -**STOP!** Before writing ANY implementation code, you MUST: - -1. **Write a failing test first** that describes the expected behavior -2. **Run the test** to confirm it fails (Red) -3. **Write minimal code** to make the test pass (Green) -4. **Refactor** while keeping tests green - -> **No code without tests. No exceptions.** -> -> Tests must import and exercise the REAL implementation classes, not inline helper functions. -> Mock only external dependencies (database, filesystem), never the class under test. - ---- - -## ⚠️ MANDATORY: Fix All Test Failures - -**You MUST investigate and fix ALL test failures before completing any task.** - -- Never leave tests failing, even if they appear unrelated to your changes -- If a test failure is pre-existing, fix it as part of your current work -- Run the full test suite (`npm test`) before considering any task complete -- If you cannot fix a test, explain why and propose a solution - -> **Zero failing tests. No exceptions.** - ---- - -## ⚠️ MANDATORY: Remove Unused Code - -**Never keep unused code around. Always delete it completely.** - -- When a feature is removed, delete ALL related code (implementation, tests, types, configs) -- Do NOT comment out code "for later" - use version control history -- Do NOT skip tests for removed functionality - delete them -- Do NOT leave dead code paths, unused imports, or orphaned functions -- When refactoring, actively look for and remove any code that becomes unused - -> **Delete unused code immediately. No exceptions.** - ---- - -## ⚠️ MANDATORY: Build Verification After Code Changes - -**You MUST run the full build after making code changes.** - -- Run `npm run build` after any code modifications -- Fix ALL build errors before considering the task complete -- Build errors indicate issues that may not be caught by `tsc --noEmit` alone (e.g., event forwarding, renderer build) -- The build must complete successfully before the task is complete - -> **Successful build required. No exceptions.** - ---- - -## ⚠️ MANDATORY: No External JS/CSS in Preview or Generated HTML - -**Do not reference external JavaScript or CSS libraries (CDNs/remote URLs) from the preview server output or generated HTML.** - -- Preview HTML must reference only local/package-bundled assets -- Generated HTML must not include CDN-hosted JS/CSS libraries -- If a library is needed (e.g., Pico CSS, Lightbox), include it as a local dependency and serve/reference it locally -- Avoid introducing any new ` - -`; +export function reviewPostHtml( + options?: Parameters[0], +): string { + return loadViewHtml('review-post.html', options); } -export function reviewScriptHtml(): string { - return ` - -Review Script - - - -
-

Waiting for script data...

-
- - - -`; +export function reviewScriptHtml( + options?: Parameters[0], +): string { + return loadViewHtml('review-script.html', options); } -export function reviewTemplateHtml(): string { - return ` - -Review Template - - - -
-

Waiting for template data...

-
- - - -`; +export function reviewTemplateHtml( + options?: Parameters[0], +): string { + return loadViewHtml('review-template.html', options); } -export function reviewMetadataHtml(): string { - return ` - -Review Metadata Changes - - - -
-

Waiting for metadata data...

-
- - - -`; +export function reviewMetadataHtml( + options?: Parameters[0], +): string { + return loadViewHtml('review-metadata.html', options); } diff --git a/src/main/engine/mcp-views/review-metadata.html b/src/main/engine/mcp-views/review-metadata.html new file mode 100644 index 0000000..701bd4d --- /dev/null +++ b/src/main/engine/mcp-views/review-metadata.html @@ -0,0 +1,132 @@ + + +Review Metadata + + + +
+

Waiting for metadata...

+
+ + + + diff --git a/src/main/engine/mcp-views/review-post.html b/src/main/engine/mcp-views/review-post.html new file mode 100644 index 0000000..d661b87 --- /dev/null +++ b/src/main/engine/mcp-views/review-post.html @@ -0,0 +1,124 @@ + + +Review Post + + + +
+

Waiting for post data...

+
+ + + + diff --git a/src/main/engine/mcp-views/review-script.html b/src/main/engine/mcp-views/review-script.html new file mode 100644 index 0000000..f352bc5 --- /dev/null +++ b/src/main/engine/mcp-views/review-script.html @@ -0,0 +1,116 @@ + + +Review Script + + + +
+

Waiting for script data...

+
+ + + + diff --git a/src/main/engine/mcp-views/review-template.html b/src/main/engine/mcp-views/review-template.html new file mode 100644 index 0000000..7aa7a0d --- /dev/null +++ b/src/main/engine/mcp-views/review-template.html @@ -0,0 +1,116 @@ + + +Review Template + + + +
+

Waiting for template data...

+
+ + + + diff --git a/src/main/ipc/handlers.ts b/src/main/ipc/handlers.ts index 055a6b1..d703616 100644 --- a/src/main/ipc/handlers.ts +++ b/src/main/ipc/handlers.ts @@ -125,6 +125,16 @@ function runWebContentsMenuAction(sender: any, action: AppMenuAction): boolean { } } +function buildMcpUrl(): string { + try { + const { getMCPServer } = require('../engine/MCPServer'); + const port = getMCPServer().getPort() ?? 4124; + return `http://127.0.0.1:${port}/mcp`; + } catch { + return 'http://127.0.0.1:4124/mcp'; + } +} + export function registerIpcHandlers(): void { // ============ Git Handlers ============ @@ -1562,6 +1572,47 @@ export function registerIpcHandlers(): void { registerBlogHandlers(safeHandle); registerPublishHandlers(safeHandle); + // ============ MCP Config Handlers ============ + + safeHandle('mcp:getAgents', async () => { + const { MCPAgentConfigEngine } = await import('../engine/MCPAgentConfigEngine'); + const engine = new MCPAgentConfigEngine({ + homeDir: require('os').homedir(), + platform: process.platform, + mcpUrl: buildMcpUrl(), + }); + return engine.getAgents(); + }); + + safeHandle('mcp:addToAgentConfig', async (_event: unknown, agentId: string) => { + const { MCPAgentConfigEngine } = await import('../engine/MCPAgentConfigEngine'); + const engine = new MCPAgentConfigEngine({ + homeDir: require('os').homedir(), + platform: process.platform, + mcpUrl: buildMcpUrl(), + }); + return engine.addToConfig(agentId as import('../engine/MCPAgentConfigEngine').MCPAgentId); + }); + + safeHandle('mcp:isConfigured', async (_event: unknown, agentId: string) => { + const { MCPAgentConfigEngine } = await import('../engine/MCPAgentConfigEngine'); + const engine = new MCPAgentConfigEngine({ + homeDir: require('os').homedir(), + platform: process.platform, + mcpUrl: buildMcpUrl(), + }); + return engine.isConfigured(agentId as import('../engine/MCPAgentConfigEngine').MCPAgentId); + }); + + safeHandle('mcp:getPort', async () => { + try { + const { getMCPServer } = await import('../engine/MCPServer'); + return getMCPServer().getPort(); + } catch { + return null; + } + }); + // ============ Event Forwarding ============ // Forward engine events to renderer diff --git a/src/main/preload.ts b/src/main/preload.ts index dcad04f..fae8e23 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -382,6 +382,13 @@ export const electronAPI: ElectronAPI = { once: (channel: string, callback: (...args: unknown[]) => void) => { ipcRenderer.once(channel, (_event, ...args) => callback(...args)); }, + + mcp: { + getAgents: () => ipcRenderer.invoke('mcp:getAgents'), + addToAgentConfig: (agentId: string) => ipcRenderer.invoke('mcp:addToAgentConfig', agentId), + isConfigured: (agentId: string) => ipcRenderer.invoke('mcp:isConfigured', agentId), + getPort: () => ipcRenderer.invoke('mcp:getPort'), + }, }; contextBridge.exposeInMainWorld('electronAPI', electronAPI); diff --git a/src/main/shared/electronApi.ts b/src/main/shared/electronApi.ts index 0ae58b6..36e5af0 100644 --- a/src/main/shared/electronApi.ts +++ b/src/main/shared/electronApi.ts @@ -836,5 +836,11 @@ export interface ElectronAPI { }; on: (channel: string, callback: (...args: unknown[]) => void) => () => void; once: (channel: string, callback: (...args: unknown[]) => void) => void; + mcp: { + getAgents: () => Promise>; + addToAgentConfig: (agentId: string) => Promise<{ success: boolean; configPath: string; error?: string }>; + isConfigured: (agentId: string) => Promise; + getPort: () => Promise; + }; } diff --git a/src/renderer/components/Editor/Editor.tsx b/src/renderer/components/Editor/Editor.tsx index ff9abae..54630bd 100644 --- a/src/renderer/components/Editor/Editor.tsx +++ b/src/renderer/components/Editor/Editor.tsx @@ -1674,7 +1674,7 @@ const Dashboard: React.FC = () => { {entry.count}
- {monthFormatter.format(new Date(entry.year, entry.month, 1))} + {monthFormatter.format(new Date(entry.year, entry.month - 1, 1))} {entry.year}
diff --git a/src/renderer/components/SettingsView/SettingsView.tsx b/src/renderer/components/SettingsView/SettingsView.tsx index 2e32cc0..a5a0183 100644 --- a/src/renderer/components/SettingsView/SettingsView.tsx +++ b/src/renderer/components/SettingsView/SettingsView.tsx @@ -10,7 +10,7 @@ import { import './SettingsView.css'; // Export category IDs for sidebar navigation -export type SettingsCategory = 'project' | 'editor' | 'content' | 'ai' | 'technology' | 'publishing' | 'data'; +export type SettingsCategory = 'project' | 'editor' | 'content' | 'ai' | 'technology' | 'publishing' | 'data' | 'mcp'; // Scroll to a settings section by category ID export const scrollToSettingsSection = (category: SettingsCategory) => { @@ -122,6 +122,62 @@ const SettingSection: React.FC<{ ); }; +/** Small component that shows the MCP server port or "Not running". */ +const MCPStatusBadge: React.FC = () => { + const { t } = useI18n(); + const [port, setPort] = React.useState(null); + + React.useEffect(() => { + window.electronAPI?.mcp?.getPort().then(setPort).catch(() => setPort(null)); + }, []); + + return ( + + {port ? t('settings.mcp.portRunning', { port: String(port) }) : t('settings.mcp.portStopped')} + + ); +}; + +/** Button to add bDS MCP server to an agent's config. Shows "Configured" if already present. */ +const MCPAgentButton: React.FC<{ agentId: string; agentLabel: string }> = ({ agentId, agentLabel }) => { + const { t } = useI18n(); + const [configured, setConfigured] = React.useState(false); + const [loading, setLoading] = React.useState(false); + + React.useEffect(() => { + window.electronAPI?.mcp?.isConfigured(agentId).then(setConfigured).catch(() => setConfigured(false)); + }, [agentId]); + + if (configured) { + return {t('settings.mcp.alreadyConfigured')}; + } + + return ( + + ); +}; + export const SettingsView: React.FC = () => { const { t } = useI18n(); const { @@ -417,6 +473,7 @@ export const SettingsView: React.FC = () => { const technologyKeywords = ['technology', 'python', 'runtime', 'worker', 'webworker', 'main thread', 'execution']; const publishingKeywords = ['publishing', 'ssh', 'deploy', 'server', 'host', 'upload', 'scp', 'rsync']; const dataKeywords = ['data', 'database', 'rebuild', 'maintenance', 'posts', 'media', 'scripts', 'links', 'folder', 'filesystem']; + const mcpKeywords = ['mcp', 'server', 'agent', 'claude', 'copilot', 'gemini', 'opencode', 'model context protocol', 'coding', 'configuration']; const renderProjectSettings = () => ( { ); + const renderMCPSettings = () => { + const agents = [ + { id: 'claude-code', label: 'Claude Code' }, + { id: 'github-copilot', label: 'GitHub Copilot' }, + { id: 'gemini-cli', label: 'Gemini CLI' }, + { id: 'opencode', label: 'OpenCode' }, + ]; + + return ( + + ); + }; + const renderDataSettings = () => ( <> { sectionHasMatches(aiKeywords) || sectionHasMatches(technologyKeywords) || sectionHasMatches(publishingKeywords) || - sectionHasMatches(dataKeywords); + sectionHasMatches(dataKeywords) || + sectionHasMatches(mcpKeywords); return (
@@ -1429,6 +1524,7 @@ export const SettingsView: React.FC = () => { {renderTechnologySettings()} {renderPublishingSettings()} {renderDataSettings()} + {renderMCPSettings()} ) : (
diff --git a/src/renderer/components/Sidebar/Sidebar.tsx b/src/renderer/components/Sidebar/Sidebar.tsx index 8b3ed7f..cfab478 100644 --- a/src/renderer/components/Sidebar/Sidebar.tsx +++ b/src/renderer/components/Sidebar/Sidebar.tsx @@ -167,7 +167,7 @@ const CalendarView: React.FC = ({ onDateSelect, selectedYear, onDateSelect(year, month); }} > - {MONTH_NAMES[month]} + {MONTH_NAMES[month - 1]} {count}
))} @@ -374,7 +374,7 @@ const MediaCalendarView: React.FC = ({ onDateSelect, sel onDateSelect(year, month); }} > - {MONTH_NAMES[month]} + {MONTH_NAMES[month - 1]} {count}
))} @@ -1261,7 +1261,7 @@ const SettingsNav: React.FC = () => { const { tabs, activeTabId, openTab } = useAppStore(); const [activeSection, setActiveSection] = useState(() => { const persisted = getPersistedSidebarSection('settings'); - if (persisted === 'project' || persisted === 'editor' || persisted === 'content' || persisted === 'ai' || persisted === 'technology' || persisted === 'publishing' || persisted === 'data') { + if (persisted === 'project' || persisted === 'editor' || persisted === 'content' || persisted === 'ai' || persisted === 'technology' || persisted === 'publishing' || persisted === 'data' || persisted === 'mcp') { return persisted; } return null; @@ -1343,6 +1343,13 @@ const SettingsNav: React.FC = () => { 🗄️ {t('sidebar.nav.data')} +