feat: first round of mcp standalone server
This commit is contained in:
109
src/cli/bds-mcp.ts
Normal file
109
src/cli/bds-mcp.ts
Normal file
@@ -0,0 +1,109 @@
|
||||
/**
|
||||
* bds-mcp — standalone MCP server for Blogging Desktop Server.
|
||||
*
|
||||
* Launched by coding agents (Claude Desktop, etc.) via:
|
||||
* ELECTRON_RUN_AS_NODE=1 /path/to/app.app/Contents/MacOS/app bds-mcp.cjs
|
||||
*
|
||||
* No `electron` imports in this file. Engine modules may import from
|
||||
* `electron` — those imports are satisfied because we run as ELECTRON_RUN_AS_NODE.
|
||||
*/
|
||||
|
||||
import * as path from 'path';
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { platformConfigPath } from './platform';
|
||||
import { initDatabase } from '../main/database/connection';
|
||||
import { projects } from '../main/database/schema';
|
||||
import { DbNotifier } from '../main/engine/CliNotifier';
|
||||
import { PostEngine } from '../main/engine/PostEngine';
|
||||
import { MediaEngine } from '../main/engine/MediaEngine';
|
||||
import { PostMediaEngine } from '../main/engine/PostMediaEngine';
|
||||
import { TagEngine } from '../main/engine/TagEngine';
|
||||
import { ScriptEngine } from '../main/engine/ScriptEngine';
|
||||
import { TemplateEngine } from '../main/engine/TemplateEngine';
|
||||
import { MetaEngine } from '../main/engine/MetaEngine';
|
||||
import { MCPServer } from '../main/engine/MCPServer';
|
||||
|
||||
// ── Bootstrap ────────────────────────────────────────────────────────────────
|
||||
|
||||
const userData = platformConfigPath();
|
||||
const dbPath = path.join(userData, 'bds.db');
|
||||
|
||||
// __dirname points to Contents/Resources/ in the packaged app (bds-mcp.cjs
|
||||
// is placed there by extraResources). The drizzle/ migrations folder is also
|
||||
// shipped to Contents/Resources/drizzle/ via extraResources.
|
||||
const migrationsFolder = path.join(__dirname, 'drizzle');
|
||||
|
||||
const db = initDatabase({ dbPath, migrationsFolder });
|
||||
|
||||
async function main(): Promise<void> {
|
||||
// 1. Open + migrate the local database.
|
||||
await db.initializeLocal();
|
||||
|
||||
const localDb = db.getLocal();
|
||||
|
||||
// 2. Verify an active project exists.
|
||||
const activeProject = await localDb
|
||||
.select()
|
||||
.from(projects)
|
||||
.where(eq(projects.isActive, true))
|
||||
.get();
|
||||
|
||||
if (!activeProject) {
|
||||
process.stderr.write(
|
||||
'[bds-mcp] No active project found. Open the Blogging Desktop Server app ' +
|
||||
'and ensure at least one project is active.\n',
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 3. Construct engines with the DbNotifier so every mutation writes a
|
||||
// db_notifications row that the running Electron app can pick up.
|
||||
const notifier = new DbNotifier(localDb as any);
|
||||
|
||||
const mediaEngine = new MediaEngine(notifier);
|
||||
const postEngine = new PostEngine({ notifier, mediaEngine });
|
||||
const postMediaEngine = new PostMediaEngine(mediaEngine);
|
||||
const tagEngine = new TagEngine(postEngine);
|
||||
const scriptEngine = new ScriptEngine(notifier);
|
||||
const templateEngine = new TemplateEngine(notifier);
|
||||
const metaEngine = new MetaEngine();
|
||||
|
||||
// 4. Create the MCP server with an 8-hour proposal TTL (CLI sessions can
|
||||
// last overnight).
|
||||
const mcpServer = new MCPServer(
|
||||
{
|
||||
postEngine,
|
||||
mediaEngine,
|
||||
postMediaEngine,
|
||||
scriptEngine,
|
||||
templateEngine,
|
||||
metaEngine,
|
||||
tagEngine,
|
||||
},
|
||||
{ proposalTtlMs: 8 * 60 * 60 * 1000 },
|
||||
);
|
||||
|
||||
// 5. Graceful shutdown — two paths, no racing.
|
||||
// process.exit() makes this non-reentrant even without explicit guards.
|
||||
async function shutdown(): Promise<never> {
|
||||
await mcpServer.cleanup();
|
||||
await db.close();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
// Signal handlers own interruption; registered before startCli() so they
|
||||
// are active during the entire session.
|
||||
process.once('SIGTERM', shutdown);
|
||||
process.once('SIGINT', shutdown);
|
||||
|
||||
// 6. Start the MCP stdio server and block until stdin closes.
|
||||
await mcpServer.startCli();
|
||||
|
||||
// 7. Normal exit path: stdin closed → startCli() resolved.
|
||||
await shutdown();
|
||||
}
|
||||
|
||||
main().catch((err: unknown) => {
|
||||
process.stderr.write(`[bds-mcp] Fatal error: ${String(err)}\n`);
|
||||
process.exit(1);
|
||||
});
|
||||
31
src/cli/platform.ts
Normal file
31
src/cli/platform.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* Pure-Node helper to resolve the same path as Electron's
|
||||
* `app.getPath('userData')` without loading Electron.
|
||||
*
|
||||
* | Platform | Path |
|
||||
* |----------|------------------------------------------------|
|
||||
* | macOS | ~/Library/Application Support/<appName> |
|
||||
* | Windows | %APPDATA%\<appName> |
|
||||
* | Linux | ~/.config/<appName> |
|
||||
*/
|
||||
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
const APP_NAME = 'Blogging Desktop Server';
|
||||
|
||||
export function platformConfigPath(): string {
|
||||
const home = os.homedir();
|
||||
const platform = process.platform;
|
||||
|
||||
if (platform === 'darwin') {
|
||||
return path.join(home, 'Library', 'Application Support', APP_NAME);
|
||||
}
|
||||
if (platform === 'win32') {
|
||||
const appData = process.env['APPDATA'] ?? path.join(home, 'AppData', 'Roaming');
|
||||
return path.join(appData, APP_NAME);
|
||||
}
|
||||
// Linux and others
|
||||
const configDir = process.env['XDG_CONFIG_HOME'] ?? path.join(home, '.config');
|
||||
return path.join(configDir, APP_NAME);
|
||||
}
|
||||
Reference in New Issue
Block a user