diff --git a/src/api/routes/index.ts b/src/api/routes/index.ts index e7b497f..c9f5d39 100644 --- a/src/api/routes/index.ts +++ b/src/api/routes/index.ts @@ -7,3 +7,4 @@ export { handleAuthRoutes } from './auth' export { handleProjectRoutes } from './projects' export { handleTaskRoutes } from './tasks' export { handleAgentRoutes } from './agents' +export { handleMCPRoutes } from './mcp' diff --git a/src/api/routes/mcp.ts b/src/api/routes/mcp.ts new file mode 100644 index 0000000..a8fb48c --- /dev/null +++ b/src/api/routes/mcp.ts @@ -0,0 +1,362 @@ +/** + * MCP HTTP API for Agent Communication + * Provides MCP tools via HTTP POST endpoints + */ + +import { db } from '../../db/client' +import { tasks, agents, projects } from '../../db/schema' +import { eq, and, or } from 'drizzle-orm' + +/** + * Route handler for /api/mcp/* + */ +export async function handleMCPRoutes(req: Request, url: URL): Promise { + const path = url.pathname.replace('/api/mcp', '') + + // POST /api/mcp/get_next_task + if (path === '/get_next_task' && req.method === 'POST') { + return await handleGetNextTask(req) + } + + // POST /api/mcp/update_task_status + if (path === '/update_task_status' && req.method === 'POST') { + return await handleUpdateTaskStatus(req) + } + + // POST /api/mcp/create_branch + if (path === '/create_branch' && req.method === 'POST') { + return await handleCreateBranch(req) + } + + // POST /api/mcp/create_pull_request + if (path === '/create_pull_request' && req.method === 'POST') { + return await handleCreatePullRequest(req) + } + + // POST /api/mcp/ask_user_question + if (path === '/ask_user_question' && req.method === 'POST') { + return await handleAskUserQuestion(req) + } + + // GET /api/mcp/tools - List available tools + if (path === '/tools' && req.method === 'GET') { + return Response.json({ + success: true, + tools: [ + { + name: 'get_next_task', + description: 'Get the next available task from the backlog', + endpoint: '/api/mcp/get_next_task', + method: 'POST', + params: { + agentId: 'string (required)', + }, + }, + { + name: 'update_task_status', + description: 'Update the status of a task', + endpoint: '/api/mcp/update_task_status', + method: 'POST', + params: { + taskId: 'string (required)', + status: 'string (required)', + errorMessage: 'string (optional)', + }, + }, + { + name: 'create_branch', + description: 'Create a Git branch for a task', + endpoint: '/api/mcp/create_branch', + method: 'POST', + params: { + taskId: 'string (required)', + branchName: 'string (required)', + }, + }, + { + name: 'create_pull_request', + description: 'Create a pull request in Gitea', + endpoint: '/api/mcp/create_pull_request', + method: 'POST', + params: { + taskId: 'string (required)', + title: 'string (required)', + description: 'string (optional)', + branchName: 'string (required)', + }, + }, + { + name: 'ask_user_question', + description: 'Ask the user a question', + endpoint: '/api/mcp/ask_user_question', + method: 'POST', + params: { + taskId: 'string (required)', + question: 'string (required)', + }, + }, + ], + }) + } + + return Response.json({ message: 'Not found' }, { status: 404 }) +} + +/** + * Get next available task + */ +async function handleGetNextTask(req: Request): Promise { + try { + const body = await req.json() + const { agentId } = body + + if (!agentId) { + return Response.json( + { success: false, message: 'agentId is required' }, + { status: 400 } + ) + } + + // Get next task in backlog, ordered by priority + const [task] = await db + .select() + .from(tasks) + .where(eq(tasks.state, 'backlog')) + .orderBy(tasks.priority, tasks.createdAt) + .limit(1) + + if (!task) { + return Response.json({ + success: true, + message: 'No tasks available in backlog', + data: null, + }) + } + + // Get project info + const [project] = await db + .select() + .from(projects) + .where(eq(projects.id, task.projectId)) + .limit(1) + + // Assign task to agent + await db + .update(tasks) + .set({ + state: 'in_progress', + assignedAgentId: agentId, + }) + .where(eq(tasks.id, task.id)) + + // Update agent status + await db + .update(agents) + .set({ + status: 'busy', + currentTaskId: task.id, + }) + .where(eq(agents.id, agentId)) + + return Response.json({ + success: true, + data: { + task, + project, + }, + }) + } catch (error: any) { + console.error('get_next_task error:', error) + return Response.json( + { success: false, message: error.message }, + { status: 500 } + ) + } +} + +/** + * Update task status + */ +async function handleUpdateTaskStatus(req: Request): Promise { + try { + const body = await req.json() + const { taskId, status, errorMessage } = body + + if (!taskId || !status) { + return Response.json( + { success: false, message: 'taskId and status are required' }, + { status: 400 } + ) + } + + const validStatuses = [ + 'backlog', + 'in_progress', + 'needs_input', + 'ready_to_test', + 'approved', + 'staging', + 'production', + ] + + if (!validStatuses.includes(status)) { + return Response.json( + { success: false, message: 'Invalid status' }, + { status: 400 } + ) + } + + await db + .update(tasks) + .set({ + state: status, + ...(errorMessage && { errorMessage }), + }) + .where(eq(tasks.id, taskId)) + + // If task is completed, update agent + if (['ready_to_test', 'approved'].includes(status)) { + const [task] = await db + .select() + .from(tasks) + .where(eq(tasks.id, taskId)) + .limit(1) + + if (task?.assignedAgentId) { + await db + .update(agents) + .set({ + status: 'idle', + currentTaskId: null, + tasksCompleted: db.sql`${agents.tasksCompleted} + 1`, + }) + .where(eq(agents.id, task.assignedAgentId)) + } + } + + return Response.json({ + success: true, + message: `Task ${taskId} updated to ${status}`, + }) + } catch (error: any) { + console.error('update_task_status error:', error) + return Response.json( + { success: false, message: error.message }, + { status: 500 } + ) + } +} + +/** + * Create branch + */ +async function handleCreateBranch(req: Request): Promise { + try { + const body = await req.json() + const { taskId, branchName } = body + + if (!taskId || !branchName) { + return Response.json( + { success: false, message: 'taskId and branchName are required' }, + { status: 400 } + ) + } + + await db + .update(tasks) + .set({ + branchName, + }) + .where(eq(tasks.id, taskId)) + + return Response.json({ + success: true, + message: `Branch ${branchName} created for task ${taskId}`, + }) + } catch (error: any) { + console.error('create_branch error:', error) + return Response.json( + { success: false, message: error.message }, + { status: 500 } + ) + } +} + +/** + * Create pull request + */ +async function handleCreatePullRequest(req: Request): Promise { + try { + const body = await req.json() + const { taskId, title, description, branchName } = body + + if (!taskId || !title || !branchName) { + return Response.json( + { success: false, message: 'taskId, title, and branchName are required' }, + { status: 400 } + ) + } + + // TODO: Integrate with Gitea API to actually create PR + // For now, just update task with placeholder PR URL + const prUrl = `https://git.fuq.tv/pulls/${taskId}` + + await db + .update(tasks) + .set({ + prUrl, + state: 'ready_to_test', + }) + .where(eq(tasks.id, taskId)) + + return Response.json({ + success: true, + message: 'Pull request created', + data: { + prUrl, + }, + }) + } catch (error: any) { + console.error('create_pull_request error:', error) + return Response.json( + { success: false, message: error.message }, + { status: 500 } + ) + } +} + +/** + * Ask user question + */ +async function handleAskUserQuestion(req: Request): Promise { + try { + const body = await req.json() + const { taskId, question } = body + + if (!taskId || !question) { + return Response.json( + { success: false, message: 'taskId and question are required' }, + { status: 400 } + ) + } + + await db + .update(tasks) + .set({ + state: 'needs_input', + errorMessage: question, + }) + .where(eq(tasks.id, taskId)) + + return Response.json({ + success: true, + message: 'Question saved. Task marked as needs_input.', + }) + } catch (error: any) { + console.error('ask_user_question error:', error) + return Response.json( + { success: false, message: error.message }, + { status: 500 } + ) + } +} diff --git a/src/index.ts b/src/index.ts index 47351a6..c848222 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,7 @@ import { runMigrations } from './db/migrate' import { testConnection } from './db/client' -import { handleAuthRoutes, handleProjectRoutes, handleTaskRoutes, handleAgentRoutes } from './api/routes' +import { handleAuthRoutes, handleProjectRoutes, handleTaskRoutes, handleAgentRoutes, handleMCPRoutes } from './api/routes' console.log('🚀 Starting AiWorker Backend...') console.log(`Bun version: ${Bun.version}`) @@ -59,6 +59,11 @@ const server = Bun.serve({ return handleAgentRoutes(req, url) } + // MCP routes for agent communication + if (url.pathname.startsWith('/api/mcp')) { + return handleMCPRoutes(req, url) + } + // Generic API info if (url.pathname.startsWith('/api/')) { return Response.json({