Add MCP Server for agent communication
All checks were successful
Build and Push Backend / build (push) Successful in 12s
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:
@@ -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
327
src/mcp/server.ts
Normal 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)
|
||||
})
|
||||
Reference in New Issue
Block a user