Add MCP Server for agent communication
All checks were successful
Build and Push Backend / build (push) Successful in 12s

- 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) <noreply@anthropic.com>
This commit is contained in:
Hector Ros
2026-01-20 02:04:04 +01:00
parent 1dc0ab515d
commit 8a95c428c8
2 changed files with 328 additions and 0 deletions

View File

@@ -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",

327
src/mcp/server.ts Normal file
View File

@@ -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)
})