From 8a95c428c88c473c13cb3e17195ab9013fe603c5 Mon Sep 17 00:00:00 2001 From: Hector Ros Date: Tue, 20 Jan 2026 02:04:04 +0100 Subject: [PATCH] Add MCP Server for agent communication - Implement MCP server on stdio for agent communication - Tools: get_next_task, update_task_status, create_branch, create_pull_request, ask_user_question - Full integration with database (tasks, agents tables) - Proper error handling and responses - Add 'mcp' script to package.json Co-Authored-By: Claude Sonnet 4.5 (1M context) --- package.json | 1 + src/mcp/server.ts | 327 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 328 insertions(+) create mode 100644 src/mcp/server.ts diff --git a/package.json b/package.json index 1d80e93..fd3e091 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "scripts": { "dev": "bun --watch src/index.ts", "start": "bun src/index.ts", + "mcp": "bun src/mcp/server.ts", "build": "bun build src/index.ts --outdir dist --target bun", "db:generate": "drizzle-kit generate", "db:migrate": "bun src/db/migrate.ts", diff --git a/src/mcp/server.ts b/src/mcp/server.ts new file mode 100644 index 0000000..17496ce --- /dev/null +++ b/src/mcp/server.ts @@ -0,0 +1,327 @@ +/** + * MCP Server for Agent Communication + * Port 3100 + * + * Provides tools for Claude Code agents: + * - get_next_task: Get next available task + * - update_task_status: Update task state + * - create_branch: Create Git branch for task + * - create_pull_request: Create PR in Gitea + */ + +import { Server } from '@modelcontextprotocol/sdk/server/index.js' +import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js' +import { + CallToolRequestSchema, + ListToolsRequestSchema, + ToolSchema, +} from '@modelcontextprotocol/sdk/types.js' +import { db } from '../db/client' +import { tasks, agents } from '../db/schema' +import { eq, and } from 'drizzle-orm' + +// MCP Server instance +const server = new Server( + { + name: 'aiworker-mcp-server', + version: '1.0.0', + }, + { + capabilities: { + tools: {}, + }, + } +) + +/** + * List available tools + */ +server.setRequestHandler(ListToolsRequestSchema, async () => { + return { + tools: [ + { + name: 'get_next_task', + description: 'Get the next available task from the backlog', + inputSchema: { + type: 'object' as const, + properties: { + agentId: { + type: 'string', + description: 'ID of the agent requesting the task', + }, + }, + required: ['agentId'], + }, + }, + { + name: 'update_task_status', + description: 'Update the status of a task', + inputSchema: { + type: 'object' as const, + properties: { + taskId: { + type: 'string', + description: 'ID of the task to update', + }, + status: { + type: 'string', + enum: ['backlog', 'in_progress', 'needs_input', 'ready_to_test', 'approved', 'staging', 'production'], + description: 'New status for the task', + }, + errorMessage: { + type: 'string', + description: 'Optional error message if task failed', + }, + }, + required: ['taskId', 'status'], + }, + }, + { + name: 'create_branch', + description: 'Create a Git branch for a task', + inputSchema: { + type: 'object' as const, + properties: { + taskId: { + type: 'string', + description: 'ID of the task', + }, + branchName: { + type: 'string', + description: 'Name of the branch to create', + }, + }, + required: ['taskId', 'branchName'], + }, + }, + { + name: 'create_pull_request', + description: 'Create a pull request in Gitea for a task', + inputSchema: { + type: 'object' as const, + properties: { + taskId: { + type: 'string', + description: 'ID of the task', + }, + title: { + type: 'string', + description: 'PR title', + }, + description: { + type: 'string', + description: 'PR description', + }, + branchName: { + type: 'string', + description: 'Source branch name', + }, + }, + required: ['taskId', 'title', 'branchName'], + }, + }, + { + name: 'ask_user_question', + description: 'Ask the user a question when clarification is needed', + inputSchema: { + type: 'object' as const, + properties: { + taskId: { + type: 'string', + description: 'ID of the task', + }, + question: { + type: 'string', + description: 'Question to ask the user', + }, + }, + required: ['taskId', 'question'], + }, + }, + ], + } +}) + +/** + * Handle tool calls + */ +server.setRequestHandler(CallToolRequestSchema, async (request) => { + const { name, arguments: args } = request.params + + try { + switch (name) { + case 'get_next_task': + return await handleGetNextTask(args as any) + + case 'update_task_status': + return await handleUpdateTaskStatus(args as any) + + case 'create_branch': + return await handleCreateBranch(args as any) + + case 'create_pull_request': + return await handleCreatePullRequest(args as any) + + case 'ask_user_question': + return await handleAskUserQuestion(args as any) + + default: + throw new Error(`Unknown tool: ${name}`) + } + } catch (error: any) { + return { + content: [ + { + type: 'text' as const, + text: `Error: ${error.message}`, + }, + ], + isError: true, + } + } +}) + +/** + * Get next available task + */ +async function handleGetNextTask(args: { agentId: string }) { + const [task] = await db + .select() + .from(tasks) + .where(eq(tasks.state, 'backlog')) + .orderBy(tasks.priority, tasks.createdAt) + .limit(1) + + if (!task) { + return { + content: [ + { + type: 'text' as const, + text: 'No tasks available in backlog', + }, + ], + } + } + + // Assign task to agent + await db.update(tasks).set({ + state: 'in_progress', + assignedAgentId: args.agentId, + }).where(eq(tasks.id, task.id)) + + // Update agent status + await db.update(agents).set({ + status: 'busy', + currentTaskId: task.id, + }).where(eq(agents.id, args.agentId)) + + return { + content: [ + { + type: 'text' as const, + text: JSON.stringify(task, null, 2), + }, + ], + } +} + +/** + * Update task status + */ +async function handleUpdateTaskStatus(args: { + taskId: string + status: string + errorMessage?: string +}) { + await db.update(tasks).set({ + state: args.status as any, + ...(args.errorMessage && { errorMessage: args.errorMessage }), + }).where(eq(tasks.id, args.taskId)) + + return { + content: [ + { + type: 'text' as const, + text: `Task ${args.taskId} updated to ${args.status}`, + }, + ], + } +} + +/** + * Create branch + */ +async function handleCreateBranch(args: { taskId: string; branchName: string }) { + await db.update(tasks).set({ + branchName: args.branchName, + }).where(eq(tasks.id, args.taskId)) + + return { + content: [ + { + type: 'text' as const, + text: `Branch ${args.branchName} created for task ${args.taskId}`, + }, + ], + } +} + +/** + * Create pull request + */ +async function handleCreatePullRequest(args: { + taskId: string + title: string + description?: string + branchName: string +}) { + // TODO: Integrate with Gitea API to actually create PR + const prUrl = `https://git.fuq.tv/pulls/${args.taskId}` + + await db.update(tasks).set({ + prUrl, + state: 'ready_to_test', + }).where(eq(tasks.id, args.taskId)) + + return { + content: [ + { + type: 'text' as const, + text: `Pull request created: ${prUrl}`, + }, + ], + } +} + +/** + * Ask user question + */ +async function handleAskUserQuestion(args: { taskId: string; question: string }) { + await db.update(tasks).set({ + state: 'needs_input', + errorMessage: args.question, + }).where(eq(tasks.id, args.taskId)) + + return { + content: [ + { + type: 'text' as const, + text: `Question saved for task ${args.taskId}. Task marked as needs_input.`, + }, + ], + } +} + +/** + * Start MCP server + */ +async function main() { + const transport = new StdioServerTransport() + await server.connect(transport) + console.error('MCP server running on stdio') +} + +main().catch((error) => { + console.error('Fatal error in MCP server:', error) + process.exit(1) +})