Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f53c6d6cb9 |
@@ -16,6 +16,7 @@
|
|||||||
"@boocode/server": "workspace:*",
|
"@boocode/server": "workspace:*",
|
||||||
"@fastify/static": "^7.0.4",
|
"@fastify/static": "^7.0.4",
|
||||||
"@fastify/websocket": "^10.0.1",
|
"@fastify/websocket": "^10.0.1",
|
||||||
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
||||||
"fastify": "^4.28.1",
|
"fastify": "^4.28.1",
|
||||||
"postgres": "^3.4.4",
|
"postgres": "^3.4.4",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
import { loadConfig } from './config.js';
|
import { loadConfig } from './config.js';
|
||||||
import { getSql, applySchema, pingDb, closeDb } from './db.js';
|
import { getSql, applySchema, pingDb, closeDb } from './db.js';
|
||||||
|
import { startMcpServer } from './services/mcp-server.js';
|
||||||
// v2.0.0 Phase 2B: workspace dependency on @boocode/server — reuse the
|
// v2.0.0 Phase 2B: workspace dependency on @boocode/server — reuse the
|
||||||
// inference loop, broker, and tool registry without duplication.
|
// inference loop, broker, and tool registry without duplication.
|
||||||
import { createInferenceRunner } from '@boocode/server/inference';
|
import { createInferenceRunner } from '@boocode/server/inference';
|
||||||
@@ -30,6 +31,15 @@ import { createDispatcher } from './services/dispatcher.js';
|
|||||||
import { probeAgents } from './services/agent-probe.js';
|
import { probeAgents } from './services/agent-probe.js';
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
|
// MCP mode: stdio transport, no HTTP server
|
||||||
|
if (process.argv.includes('--mcp')) {
|
||||||
|
const config = loadConfig();
|
||||||
|
const sql = getSql(config);
|
||||||
|
await applySchema(sql);
|
||||||
|
await startMcpServer(sql);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
const app = Fastify({
|
const app = Fastify({
|
||||||
|
|||||||
201
apps/coder/src/services/mcp-server.ts
Normal file
201
apps/coder/src/services/mcp-server.ts
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
/**
|
||||||
|
* BooCoder MCP Server — exposes task primitives as MCP tools.
|
||||||
|
*
|
||||||
|
* Started when `--mcp` flag is passed to the entry point. Runs stdio transport
|
||||||
|
* so external tools (opencode in Termius) can drive the task queue.
|
||||||
|
*/
|
||||||
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
||||||
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import type { Sql } from '../db.js';
|
||||||
|
import { applyOne, rejectOne } from './pending_changes.js';
|
||||||
|
|
||||||
|
// --- Tool handlers -----------------------------------------------------------
|
||||||
|
|
||||||
|
interface TaskRow {
|
||||||
|
id: string;
|
||||||
|
state: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PendingRow {
|
||||||
|
id: string;
|
||||||
|
file_path: string;
|
||||||
|
operation: string;
|
||||||
|
diff: string;
|
||||||
|
session_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface WorktreeRow {
|
||||||
|
id: string;
|
||||||
|
worktree_path: string;
|
||||||
|
agent: string;
|
||||||
|
started_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ProjectPathRow {
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
function textResult(data: unknown) {
|
||||||
|
return { content: [{ type: 'text' as const, text: JSON.stringify(data, null, 2) }] };
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Public entry ------------------------------------------------------------
|
||||||
|
|
||||||
|
export async function startMcpServer(sql: Sql): Promise<void> {
|
||||||
|
const server = new McpServer(
|
||||||
|
{ name: 'boocoder', version: '2.0.2' },
|
||||||
|
{ capabilities: { tools: {} } },
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1. boocoder.create_task
|
||||||
|
server.tool(
|
||||||
|
'boocoder.create_task',
|
||||||
|
'Create a new task in the BooCoder task queue',
|
||||||
|
{
|
||||||
|
project_id: z.string().describe('Project UUID'),
|
||||||
|
input: z.string().describe('Task description / prompt for the agent'),
|
||||||
|
agent: z.string().optional().describe('Agent name (optional — uses default if omitted)'),
|
||||||
|
model: z.string().optional().describe('Model override (optional)'),
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
const [row] = await sql<TaskRow[]>`
|
||||||
|
INSERT INTO tasks (project_id, input, agent, model, state)
|
||||||
|
VALUES (${args.project_id}, ${args.input}, ${args.agent ?? null}, ${args.model ?? null}, 'pending')
|
||||||
|
RETURNING id, state
|
||||||
|
`;
|
||||||
|
return textResult({ task_id: row!.id, state: row!.state });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 2. boocoder.list_pending_changes
|
||||||
|
server.tool(
|
||||||
|
'boocoder.list_pending_changes',
|
||||||
|
'List pending changes awaiting review',
|
||||||
|
{
|
||||||
|
session_id: z.string().optional().describe('Optional session filter'),
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
let rows: PendingRow[];
|
||||||
|
if (args.session_id) {
|
||||||
|
rows = await sql<PendingRow[]>`
|
||||||
|
SELECT id, file_path, operation, diff, session_id
|
||||||
|
FROM pending_changes
|
||||||
|
WHERE status = 'pending' AND session_id = ${args.session_id}
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
rows = await sql<PendingRow[]>`
|
||||||
|
SELECT id, file_path, operation, diff, session_id
|
||||||
|
FROM pending_changes
|
||||||
|
WHERE status = 'pending'
|
||||||
|
ORDER BY created_at ASC
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
const items = rows.map((r) => ({
|
||||||
|
id: r.id,
|
||||||
|
file_path: r.file_path,
|
||||||
|
operation: r.operation,
|
||||||
|
diff_preview: r.diff.slice(0, 200),
|
||||||
|
}));
|
||||||
|
return textResult(items);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 3. boocoder.apply
|
||||||
|
server.tool(
|
||||||
|
'boocoder.apply',
|
||||||
|
'Apply a pending change (write to disk)',
|
||||||
|
{
|
||||||
|
change_id: z.string().describe('Pending change UUID'),
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
// Resolve projectRoot from the change's session → project path
|
||||||
|
const [proj] = await sql<ProjectPathRow[]>`
|
||||||
|
SELECT p.path FROM pending_changes pc
|
||||||
|
JOIN sessions s ON pc.session_id = s.id
|
||||||
|
JOIN projects p ON s.project_id = p.id
|
||||||
|
WHERE pc.id = ${args.change_id}
|
||||||
|
`;
|
||||||
|
if (!proj) {
|
||||||
|
return textResult({ success: false, file_path: '', error: 'change not found or project path unresolved' });
|
||||||
|
}
|
||||||
|
const result = await applyOne(sql, args.change_id, proj.path);
|
||||||
|
return textResult({ success: result.success, file_path: result.file_path, error: result.error });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. boocoder.reject
|
||||||
|
server.tool(
|
||||||
|
'boocoder.reject',
|
||||||
|
'Reject a pending change (mark as rejected, no disk write)',
|
||||||
|
{
|
||||||
|
change_id: z.string().describe('Pending change UUID'),
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
await rejectOne(sql, args.change_id);
|
||||||
|
return textResult({ success: true });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 5. boocoder.dispatch_external_agent
|
||||||
|
server.tool(
|
||||||
|
'boocoder.dispatch_external_agent',
|
||||||
|
'Create a task targeting a specific external agent (ACP or PTY dispatch)',
|
||||||
|
{
|
||||||
|
project_id: z.string().describe('Project UUID'),
|
||||||
|
input: z.string().describe('Task prompt'),
|
||||||
|
agent: z.string().describe('Agent name (must match available_agents registry)'),
|
||||||
|
model: z.string().optional().describe('Model override (optional)'),
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
const [row] = await sql<TaskRow[]>`
|
||||||
|
INSERT INTO tasks (project_id, input, agent, model, state)
|
||||||
|
VALUES (${args.project_id}, ${args.input}, ${args.agent}, ${args.model ?? null}, 'pending')
|
||||||
|
RETURNING id, state
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Determine execution path from available_agents
|
||||||
|
const [agentRow] = await sql<{ supports_acp: boolean }[]>`
|
||||||
|
SELECT supports_acp FROM available_agents WHERE name = ${args.agent}
|
||||||
|
`;
|
||||||
|
const executionPath = agentRow?.supports_acp ? 'acp' : 'pty';
|
||||||
|
|
||||||
|
return textResult({ task_id: row!.id, state: row!.state, execution_path: executionPath });
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// 6. boocoder.list_worktrees
|
||||||
|
server.tool(
|
||||||
|
'boocoder.list_worktrees',
|
||||||
|
'List active worktrees from running tasks',
|
||||||
|
{},
|
||||||
|
async () => {
|
||||||
|
const rows = await sql<WorktreeRow[]>`
|
||||||
|
SELECT id, worktree_path, agent, started_at
|
||||||
|
FROM tasks
|
||||||
|
WHERE worktree_path IS NOT NULL AND state = 'running'
|
||||||
|
ORDER BY started_at DESC
|
||||||
|
`;
|
||||||
|
const items = rows.map((r) => ({
|
||||||
|
task_id: r.id,
|
||||||
|
worktree_path: r.worktree_path,
|
||||||
|
agent: r.agent,
|
||||||
|
started_at: r.started_at,
|
||||||
|
}));
|
||||||
|
return textResult(items);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
// Connect via stdio
|
||||||
|
const transport = new StdioServerTransport();
|
||||||
|
await server.connect(transport);
|
||||||
|
|
||||||
|
// Block until stdin closes (transport handles lifecycle)
|
||||||
|
await new Promise<void>((resolve) => {
|
||||||
|
process.stdin.on('end', resolve);
|
||||||
|
process.stdin.on('close', resolve);
|
||||||
|
});
|
||||||
|
|
||||||
|
await sql.end({ timeout: 5 });
|
||||||
|
}
|
||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -60,6 +60,9 @@ importers:
|
|||||||
'@fastify/websocket':
|
'@fastify/websocket':
|
||||||
specifier: ^10.0.1
|
specifier: ^10.0.1
|
||||||
version: 10.0.1
|
version: 10.0.1
|
||||||
|
'@modelcontextprotocol/sdk':
|
||||||
|
specifier: ^1.29.0
|
||||||
|
version: 1.29.0(zod@3.25.76)
|
||||||
fastify:
|
fastify:
|
||||||
specifier: ^4.28.1
|
specifier: ^4.28.1
|
||||||
version: 4.29.1
|
version: 4.29.1
|
||||||
|
|||||||
Reference in New Issue
Block a user