Complete documentation for future sessions

- CLAUDE.md for AI agents to understand the codebase
- GITEA-GUIDE.md centralizes all Gitea operations (API, Registry, Auth)
- DEVELOPMENT-WORKFLOW.md explains complete dev process
- ROADMAP.md, NEXT-SESSION.md for planning
- QUICK-REFERENCE.md, TROUBLESHOOTING.md for daily use
- 40+ detailed docs in /docs folder
- Backend as submodule from Gitea

Everything documented for autonomous operation.

Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hector Ros
2026-01-20 00:36:53 +01:00
commit db71705842
49 changed files with 19162 additions and 0 deletions

View File

@@ -0,0 +1,452 @@
# Ciclo de Vida de los Agentes
## Estados del Agente
```
┌──────────────┐
│ Initializing │
└──────┬───────┘
┌──────┐ ┌──────┐
│ Idle │◄───►│ Busy │
└───┬──┘ └──┬───┘
│ │
│ │
▼ ▼
┌───────┐ ┌───────┐
│ Error │ │Offline│
└───────┘ └───────┘
```
## Inicialización
### 1. Creación del Pod
```typescript
// Backend crea el pod
const agentManager = new AgentManager()
const agent = await agentManager.createAgent(['javascript', 'react'])
// Resultado
{
id: 'agent-abc123',
podName: 'claude-agent-abc123',
namespace: 'agents',
status: 'initializing'
}
```
### 2. Arranque del Contenedor
```bash
# En el pod (entrypoint.sh)
echo "🤖 Starting agent: $AGENT_ID"
# 1. Setup SSH
echo "$GIT_SSH_KEY" > /root/.ssh/id_ed25519
chmod 600 /root/.ssh/id_ed25519
# 2. Configure Claude Code MCP
cat > /root/.claude-code/config.json <<EOF
{
"mcpServers": {
"aiworker": {
"url": "$MCP_SERVER_URL"
}
}
}
EOF
# 3. Send initial heartbeat
curl -X POST "$MCP_SERVER_URL/heartbeat" \
-H "Content-Type: application/json" \
-H "X-Agent-ID: $AGENT_ID" \
-d '{"status":"idle"}'
# 4. Start work loop
exec /usr/local/bin/agent-loop.sh
```
### 3. Registro en el Sistema
```typescript
// Backend detecta el heartbeat y actualiza
await db.update(agents)
.set({
status: 'idle',
lastHeartbeat: new Date(),
})
.where(eq(agents.id, agentId))
logger.info(`Agent ${agentId} is now active`)
```
## Asignación de Tarea
### 1. Agent Polling
```bash
# agent-loop.sh
while true; do
echo "📋 Checking for tasks..."
TASK=$(curl -s -X POST "$MCP_SERVER_URL/tools/call" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"get_next_task\",
\"arguments\": {\"agentId\": \"$AGENT_ID\"}
}")
TASK_ID=$(echo "$TASK" | jq -r '.content[0].text | fromjson | .task.id // empty')
if [ -n "$TASK_ID" ]; then
echo "🎯 Got task: $TASK_ID"
process_task "$TASK_ID"
else
sleep 10
fi
done
```
### 2. Backend Asigna Tarea
```typescript
// services/mcp/handlers.ts - getNextTask()
async function getNextTask(args: { agentId: string }) {
// 1. Buscar siguiente tarea en backlog
const task = await db.query.tasks.findFirst({
where: eq(tasks.state, 'backlog'),
orderBy: [desc(tasks.priority), asc(tasks.createdAt)],
})
if (!task) {
return { content: [{ type: 'text', text: JSON.stringify({ message: 'No tasks' }) }] }
}
// 2. Asignar al agente
await db.update(tasks)
.set({
state: 'in_progress',
assignedAgentId: args.agentId,
assignedAt: new Date(),
startedAt: new Date(),
})
.where(eq(tasks.id, task.id))
// 3. Actualizar agente
await db.update(agents)
.set({
status: 'busy',
currentTaskId: task.id,
})
.where(eq(agents.id, args.agentId))
// 4. Retornar tarea
return {
content: [{
type: 'text',
text: JSON.stringify({ task }),
}],
}
}
```
## Trabajo en Tarea
### Fase 1: Setup
```bash
# Clone repo
git clone "$PROJECT_REPO" "/workspace/task-$TASK_ID"
cd "/workspace/task-$TASK_ID"
# Create branch (via MCP)
curl -X POST "$MCP_SERVER_URL/tools/call" \
-d "{\"name\": \"create_branch\", \"arguments\": {\"taskId\": \"$TASK_ID\"}}"
# Checkout branch
git fetch origin
git checkout "$BRANCH_NAME"
```
### Fase 2: Implementación
```bash
# Start Claude Code session
claude-code chat --message "
I need you to work on this task:
Title: $TASK_TITLE
Description: $TASK_DESC
Instructions:
1. Analyze the codebase
2. Implement the changes
3. Write tests
4. Commit with clear messages
5. Use MCP tools when done
Start working now.
"
```
### Fase 3: Preguntas (opcional)
```typescript
// Si el agente necesita info
await mcp.callTool('ask_user_question', {
taskId,
question: 'Should I add TypeScript types?',
context: 'The codebase is in JavaScript...',
})
// Cambiar estado a needs_input
await mcp.callTool('update_task_status', {
taskId,
status: 'needs_input',
})
// Hacer polling cada 5s hasta respuesta
let response
while (!response) {
await sleep(5000)
const check = await mcp.callTool('check_question_response', { taskId })
if (check.hasResponse) {
response = check.response
}
}
// Continuar con la respuesta
await mcp.callTool('update_task_status', {
taskId,
status: 'in_progress',
})
```
### Fase 4: Finalización
```bash
# Create PR
curl -X POST "$MCP_SERVER_URL/tools/call" \
-d "{
\"name\": \"create_pull_request\",
\"arguments\": {
\"taskId\": \"$TASK_ID\",
\"title\": \"$TASK_TITLE\",
\"description\": \"Implemented feature X...\"
}
}"
# Deploy preview
curl -X POST "$MCP_SERVER_URL/tools/call" \
-d "{
\"name\": \"trigger_preview_deploy\",
\"arguments\": {\"taskId\": \"$TASK_ID\"}
}"
# Update status
curl -X POST "$MCP_SERVER_URL/tools/call" \
-d "{
\"name\": \"update_task_status\",
\"arguments\": {
\"taskId\": \"$TASK_ID\",
\"status\": \"ready_to_test\"
}
}"
```
## Liberación del Agente
```typescript
// Cuando tarea completa (ready_to_test o completed)
await db.update(agents)
.set({
status: 'idle',
currentTaskId: null,
tasksCompleted: sql`tasks_completed + 1`,
})
.where(eq(agents.id, agentId))
logger.info(`Agent ${agentId} completed task ${taskId}, now idle`)
```
## Manejo de Errores
### Timeout de Tarea
```bash
# agent-loop.sh con timeout
timeout 7200 claude-code chat --message "$TASK_PROMPT" || {
STATUS=$?
if [ $STATUS -eq 124 ]; then
echo "⏰ Task timeout after 2 hours"
# Notify backend
curl -X POST "$MCP_SERVER_URL/tools/call" \
-d "{
\"name\": \"update_task_status\",
\"arguments\": {
\"taskId\": \"$TASK_ID\",
\"status\": \"needs_input\",
\"metadata\": {\"reason\": \"timeout\"}
}
}"
# Log error
curl -X POST "$MCP_SERVER_URL/tools/call" \
-d "{
\"name\": \"log_activity\",
\"arguments\": {
\"agentId\": \"$AGENT_ID\",
\"level\": \"error\",
\"message\": \"Task timeout: $TASK_ID\"
}
}"
fi
}
```
### Crash del Agente
```typescript
// Backend detecta agente sin heartbeat
async function checkStaleAgents() {
const staleThreshold = new Date(Date.now() - 5 * 60 * 1000) // 5 min
const staleAgents = await db.query.agents.findMany({
where: lt(agents.lastHeartbeat, staleThreshold),
})
for (const agent of staleAgents) {
logger.warn(`Agent ${agent.id} is stale`)
// Mark current task as needs attention
if (agent.currentTaskId) {
await db.update(tasks)
.set({
state: 'backlog',
assignedAgentId: null,
})
.where(eq(tasks.id, agent.currentTaskId))
}
// Delete agent pod
await k8sClient.deletePod(agent.k8sNamespace, agent.podName)
// Remove from DB
await db.delete(agents).where(eq(agents.id, agent.id))
// Create replacement
await agentManager.createAgent()
}
}
// Run every minute
setInterval(checkStaleAgents, 60000)
```
## Terminación Graciosa
```bash
# agent-entrypoint.sh
cleanup() {
echo "🛑 Shutting down agent..."
# Send offline status
curl -X POST "$MCP_SERVER_URL/heartbeat" \
-d '{"status":"offline"}' 2>/dev/null || true
# Kill background jobs
kill $HEARTBEAT_PID 2>/dev/null || true
echo "👋 Goodbye"
exit 0
}
trap cleanup SIGTERM SIGINT
# Wait for signals
wait
```
## Auto-Scaling
```typescript
// Auto-scaler que corre cada 30s
async function autoScale() {
// Get metrics
const pendingTasks = await db.query.tasks.findMany({
where: eq(tasks.state, 'backlog'),
})
const idleAgents = await db.query.agents.findMany({
where: eq(agents.status, 'idle'),
})
const busyAgents = await db.query.agents.findMany({
where: eq(agents.status, 'busy'),
})
const totalAgents = idleAgents.length + busyAgents.length
// Decision logic
let targetAgents = totalAgents
// Scale up if:
// - More than 3 pending tasks
// - No idle agents
if (pendingTasks.length > 3 && idleAgents.length === 0) {
targetAgents = Math.min(totalAgents + 2, 10) // Max 10
}
// Scale down if:
// - No pending tasks
// - More than 2 idle agents
if (pendingTasks.length === 0 && idleAgents.length > 2) {
targetAgents = Math.max(totalAgents - 1, 2) // Min 2
}
if (targetAgents !== totalAgents) {
logger.info(`Auto-scaling: ${totalAgents}${targetAgents}`)
await agentManager.scaleAgents(targetAgents)
}
}
setInterval(autoScale, 30000)
```
## Métricas del Ciclo de Vida
```typescript
// Endpoint para métricas de agentes
router.get('/agents/metrics', async (req, res) => {
const agents = await db.query.agents.findMany()
const metrics = {
total: agents.length,
byStatus: {
idle: agents.filter((a) => a.status === 'idle').length,
busy: agents.filter((a) => a.status === 'busy').length,
error: agents.filter((a) => a.status === 'error').length,
offline: agents.filter((a) => a.status === 'offline').length,
},
totalTasksCompleted: agents.reduce((sum, a) => sum + a.tasksCompleted, 0),
avgTasksPerAgent:
agents.reduce((sum, a) => sum + a.tasksCompleted, 0) / agents.length || 0,
totalRuntime: agents.reduce((sum, a) => sum + a.totalRuntimeMinutes, 0),
}
res.json(metrics)
})
```
## Dashboard Visualization
En el frontend, mostrar:
- **Estado actual** de cada agente (idle/busy/error)
- **Tarea actual** si está busy
- **Historial** de tareas completadas
- **Métricas** (tareas/hora, uptime, etc.)
- **Botones** para restart/delete agente
- **Logs en tiempo real** de cada agente

View File

@@ -0,0 +1,499 @@
# Claude Code Agents - Pods en Kubernetes
## Dockerfile del Agente
```dockerfile
# Dockerfile
FROM node:20-alpine
# Install dependencies
RUN apk add --no-cache \
git \
openssh-client \
curl \
bash \
vim
# Install Bun
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:$PATH"
# Install Claude Code CLI
RUN npm install -g @anthropic-ai/claude-code
# Create workspace
WORKDIR /workspace
# Copy agent scripts
COPY scripts/agent-entrypoint.sh /usr/local/bin/
COPY scripts/agent-loop.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/agent-*.sh
# Git config
RUN git config --global user.name "AiWorker Agent" && \
git config --global user.email "agent@aiworker.dev" && \
git config --global init.defaultBranch main
# Setup SSH
RUN mkdir -p /root/.ssh && \
ssh-keyscan -H git.aiworker.dev >> /root/.ssh/known_hosts
ENTRYPOINT ["/usr/local/bin/agent-entrypoint.sh"]
```
## Agent Entrypoint Script
```bash
#!/bin/bash
# scripts/agent-entrypoint.sh
set -e
echo "🤖 Starting AiWorker Agent..."
echo "Agent ID: $AGENT_ID"
# Setup SSH key
if [ -n "$GIT_SSH_KEY" ]; then
echo "$GIT_SSH_KEY" > /root/.ssh/id_ed25519
chmod 600 /root/.ssh/id_ed25519
fi
# Configure Claude Code with MCP Server
cat > /root/.claude-code/config.json <<EOF
{
"mcpServers": {
"aiworker": {
"command": "curl",
"args": [
"-X", "POST",
"-H", "Content-Type: application/json",
"-H", "X-Agent-ID: $AGENT_ID",
"$MCP_SERVER_URL/rpc"
]
}
}
}
EOF
# Send heartbeat
send_heartbeat() {
curl -s -X POST "$MCP_SERVER_URL/heartbeat" \
-H "Content-Type: application/json" \
-d "{\"agentId\":\"$AGENT_ID\",\"status\":\"$1\"}" > /dev/null 2>&1 || true
}
# Start heartbeat loop in background
while true; do
send_heartbeat "idle"
sleep 30
done &
HEARTBEAT_PID=$!
# Trap signals for graceful shutdown
trap "kill $HEARTBEAT_PID; send_heartbeat 'offline'; exit 0" SIGTERM SIGINT
# Start agent work loop
exec /usr/local/bin/agent-loop.sh
```
## Agent Work Loop
```bash
#!/bin/bash
# scripts/agent-loop.sh
set -e
echo "🔄 Starting agent work loop..."
while true; do
echo "📋 Checking for tasks..."
# Get next task via MCP
TASK=$(curl -s -X POST "$MCP_SERVER_URL/tools/call" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"get_next_task\",
\"arguments\": {
\"agentId\": \"$AGENT_ID\"
}
}")
TASK_ID=$(echo "$TASK" | jq -r '.content[0].text | fromjson | .task.id // empty')
if [ -z "$TASK_ID" ] || [ "$TASK_ID" = "null" ]; then
echo "💤 No tasks available, waiting..."
sleep 10
continue
fi
echo "🎯 Got task: $TASK_ID"
# Extract task details
TASK_TITLE=$(echo "$TASK" | jq -r '.content[0].text | fromjson | .task.title')
TASK_DESC=$(echo "$TASK" | jq -r '.content[0].text | fromjson | .task.description')
PROJECT_REPO=$(echo "$TASK" | jq -r '.content[0].text | fromjson | .task.project.giteaRepoUrl')
echo "📝 Task: $TASK_TITLE"
echo "📦 Repo: $PROJECT_REPO"
# Log activity
curl -s -X POST "$MCP_SERVER_URL/tools/call" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"log_activity\",
\"arguments\": {
\"agentId\": \"$AGENT_ID\",
\"level\": \"info\",
\"message\": \"Starting task: $TASK_TITLE\"
}
}" > /dev/null
# Clone repository
REPO_DIR="/workspace/task-$TASK_ID"
if [ ! -d "$REPO_DIR" ]; then
echo "📥 Cloning repository..."
git clone "$PROJECT_REPO" "$REPO_DIR"
fi
cd "$REPO_DIR"
# Create branch via MCP
echo "🌿 Creating branch..."
BRANCH_RESULT=$(curl -s -X POST "$MCP_SERVER_URL/tools/call" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"create_branch\",
\"arguments\": {
\"taskId\": \"$TASK_ID\"
}
}")
BRANCH_NAME=$(echo "$BRANCH_RESULT" | jq -r '.content[0].text | fromjson | .branchName')
echo "🌿 Branch: $BRANCH_NAME"
# Fetch and checkout
git fetch origin
git checkout "$BRANCH_NAME" 2>/dev/null || git checkout -b "$BRANCH_NAME"
# Start Claude Code session
echo "🧠 Starting Claude Code session..."
# Create task prompt
TASK_PROMPT="I need you to work on the following task:
Title: $TASK_TITLE
Description:
$TASK_DESC
Instructions:
1. Analyze the codebase
2. Implement the required changes
3. Write tests if needed
4. Commit your changes with clear messages
5. When done, use the MCP tools to:
- create_pull_request with a summary
- trigger_preview_deploy
- update_task_status to 'ready_to_test'
If you need clarification, use ask_user_question.
Start working on this task now."
# Run Claude Code (with timeout of 2 hours)
timeout 7200 claude-code chat --message "$TASK_PROMPT" || {
STATUS=$?
if [ $STATUS -eq 124 ]; then
echo "⏰ Task timeout"
curl -s -X POST "$MCP_SERVER_URL/tools/call" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"update_task_status\",
\"arguments\": {
\"taskId\": \"$TASK_ID\",
\"status\": \"needs_input\",
\"metadata\": {\"reason\": \"timeout\"}
}
}" > /dev/null
else
echo "❌ Claude Code exited with status $STATUS"
fi
}
echo "✅ Task completed: $TASK_ID"
# Cleanup
cd /workspace
rm -rf "$REPO_DIR"
# Brief pause before next task
sleep 5
done
```
## Pod Specification
```yaml
# k8s/agents/claude-agent-pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: claude-agent-{{ AGENT_ID }}
namespace: agents
labels:
app: claude-agent
agent-id: "{{ AGENT_ID }}"
managed-by: aiworker
spec:
restartPolicy: Never
serviceAccountName: claude-agent
containers:
- name: agent
image: aiworker/claude-agent:latest
imagePullPolicy: Always
env:
- name: AGENT_ID
value: "{{ AGENT_ID }}"
- name: MCP_SERVER_URL
value: "http://aiworker-backend.control-plane.svc.cluster.local:3100"
- name: ANTHROPIC_API_KEY
valueFrom:
secretKeyRef:
name: aiworker-secrets
key: anthropic-api-key
- name: GITEA_URL
value: "http://gitea.gitea.svc.cluster.local:3000"
- name: GIT_SSH_KEY
valueFrom:
secretKeyRef:
name: git-ssh-keys
key: private-key
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
volumeMounts:
- name: workspace
mountPath: /workspace
volumes:
- name: workspace
emptyDir:
sizeLimit: 10Gi
```
## Agent Manager (Backend)
```typescript
// services/kubernetes/agent-manager.ts
import { K8sClient } from './client'
import { db } from '../../db/client'
import { agents } from '../../db/schema'
import { eq } from 'drizzle-orm'
import crypto from 'crypto'
import { logger } from '../../utils/logger'
export class AgentManager {
private k8sClient: K8sClient
constructor() {
this.k8sClient = new K8sClient()
}
async createAgent(capabilities: string[] = []) {
const agentId = crypto.randomUUID()
// Create agent pod in K8s
const { podName, namespace } = await this.k8sClient.createAgentPod(agentId)
// Insert in database
await db.insert(agents).values({
id: agentId,
podName,
k8sNamespace: namespace,
status: 'initializing',
capabilities,
lastHeartbeat: new Date(),
})
logger.info(`Created agent: ${agentId}`)
return {
id: agentId,
podName,
namespace,
}
}
async deleteAgent(agentId: string) {
const agent = await db.query.agents.findFirst({
where: eq(agents.id, agentId),
})
if (!agent) {
throw new Error('Agent not found')
}
// Delete pod
await this.k8sClient.deletePod(agent.k8sNamespace, agent.podName)
// Delete from database
await db.delete(agents).where(eq(agents.id, agentId))
logger.info(`Deleted agent: ${agentId}`)
}
async scaleAgents(targetCount: number) {
const currentAgents = await db.query.agents.findMany()
if (currentAgents.length < targetCount) {
// Scale up
const toCreate = targetCount - currentAgents.length
logger.info(`Scaling up: creating ${toCreate} agents`)
for (let i = 0; i < toCreate; i++) {
await this.createAgent()
await new Promise(resolve => setTimeout(resolve, 1000)) // Stagger creation
}
} else if (currentAgents.length > targetCount) {
// Scale down
const toDelete = currentAgents.length - targetCount
logger.info(`Scaling down: deleting ${toDelete} agents`)
// Delete idle agents first
const idleAgents = currentAgents.filter(a => a.status === 'idle').slice(0, toDelete)
for (const agent of idleAgents) {
await this.deleteAgent(agent.id)
}
}
}
async autoScale() {
// Get pending tasks
const pendingTasks = await db.query.tasks.findMany({
where: eq(tasks.state, 'backlog'),
})
// Get available agents
const availableAgents = await db.query.agents.findMany({
where: eq(agents.status, 'idle'),
})
const busyAgents = await db.query.agents.findMany({
where: eq(agents.status, 'busy'),
})
const totalAgents = availableAgents.length + busyAgents.length
// Simple scaling logic
const targetAgents = Math.min(
Math.max(2, pendingTasks.length, busyAgents.length + 1), // At least 2, max 1 per pending task
10 // Max 10 agents
)
if (targetAgents !== totalAgents) {
logger.info(`Auto-scaling agents: ${totalAgents}${targetAgents}`)
await this.scaleAgents(targetAgents)
}
}
async cleanupStaleAgents() {
const staleThreshold = new Date(Date.now() - 5 * 60 * 1000) // 5 minutes
const staleAgents = await db.query.agents.findMany({
where: (agents, { lt }) => lt(agents.lastHeartbeat, staleThreshold),
})
for (const agent of staleAgents) {
logger.warn(`Cleaning up stale agent: ${agent.id}`)
await this.deleteAgent(agent.id)
}
}
}
// Start autoscaler
setInterval(async () => {
const manager = new AgentManager()
await manager.autoScale()
await manager.cleanupStaleAgents()
}, 30000) // Every 30 seconds
```
## Agent Logs Streaming
```typescript
// api/routes/agents.ts
import { Router } from 'express'
import { K8sClient } from '../../services/kubernetes/client'
import { db } from '../../db/client'
import { agents } from '../../db/schema'
import { eq } from 'drizzle-orm'
const router = Router()
const k8sClient = new K8sClient()
router.get('/:agentId/logs/stream', async (req, res) => {
const { agentId } = req.params
const agent = await db.query.agents.findFirst({
where: eq(agents.id, agentId),
})
if (!agent) {
return res.status(404).json({ error: 'Agent not found' })
}
res.setHeader('Content-Type', 'text/event-stream')
res.setHeader('Cache-Control', 'no-cache')
res.setHeader('Connection', 'keep-alive')
try {
const logStream = await k8sClient.streamPodLogs(agent.k8sNamespace, agent.podName)
logStream.on('data', (chunk) => {
res.write(`data: ${chunk.toString()}\n\n`)
})
logStream.on('end', () => {
res.end()
})
req.on('close', () => {
logStream.destroy()
})
} catch (error) {
res.status(500).json({ error: 'Failed to stream logs' })
}
})
export default router
```
## Monitoring Agents
```bash
# Ver todos los agentes
kubectl get pods -n agents -l app=claude-agent
# Ver logs de un agente
kubectl logs -n agents claude-agent-abc123 -f
# Entrar a un agente
kubectl exec -it -n agents claude-agent-abc123 -- /bin/bash
# Ver recursos consumidos
kubectl top pods -n agents
```

View File

@@ -0,0 +1,567 @@
# Comunicación Agentes-Backend
## Arquitectura de Comunicación
```
┌─────────────────────┐
│ Claude Code Agent │
│ (Pod en K8s) │
└──────────┬──────────┘
│ MCP Protocol
│ (HTTP/JSON-RPC)
┌─────────────────────┐
│ MCP Server │
│ (Backend Service) │
└──────────┬──────────┘
┌──────┴──────┐
│ │
▼ ▼
┌────────┐ ┌────────┐
│ MySQL │ │ Gitea │
└────────┘ └────────┘
```
## MCP Protocol Implementation
### Request Format
```json
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "get_next_task",
"arguments": {
"agentId": "agent-uuid"
}
}
}
```
### Response Format
```json
{
"jsonrpc": "2.0",
"id": 1,
"result": {
"content": [
{
"type": "text",
"text": "{\"task\": {...}}"
}
]
}
}
```
## HTTP Client en Agente
```typescript
// agent/mcp-client.ts
class MCPClient {
private baseUrl: string
private agentId: string
constructor(baseUrl: string, agentId: string) {
this.baseUrl = baseUrl
this.agentId = agentId
}
async callTool(toolName: string, args: any) {
const response = await fetch(`${this.baseUrl}/rpc`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Agent-ID': this.agentId,
},
body: JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
method: 'tools/call',
params: {
name: toolName,
arguments: args,
},
}),
})
if (!response.ok) {
throw new Error(`MCP call failed: ${response.statusText}`)
}
const data = await response.json()
if (data.error) {
throw new Error(data.error.message)
}
return data.result
}
async listTools() {
const response = await fetch(`${this.baseUrl}/rpc`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Agent-ID': this.agentId,
},
body: JSON.stringify({
jsonrpc: '2.0',
id: Date.now(),
method: 'tools/list',
params: {},
}),
})
const data = await response.json()
return data.result.tools
}
}
// Usage
const mcp = new MCPClient(
process.env.MCP_SERVER_URL,
process.env.AGENT_ID
)
const task = await mcp.callTool('get_next_task', {
agentId: process.env.AGENT_ID,
})
```
## Server-Side Handler
```typescript
// backend: api/routes/mcp.ts
import { Router, Request, Response } from 'express'
import { handleToolCall } from '../../services/mcp/handlers'
import { tools } from '../../services/mcp/tools'
import { logger } from '../../utils/logger'
const router = Router()
// JSON-RPC endpoint
router.post('/rpc', async (req: Request, res: Response) => {
const { jsonrpc, id, method, params } = req.body
if (jsonrpc !== '2.0') {
return res.status(400).json({
jsonrpc: '2.0',
id,
error: {
code: -32600,
message: 'Invalid Request',
},
})
}
const agentId = req.headers['x-agent-id'] as string
if (!agentId) {
return res.status(401).json({
jsonrpc: '2.0',
id,
error: {
code: -32001,
message: 'Missing agent ID',
},
})
}
try {
switch (method) {
case 'tools/list':
return res.json({
jsonrpc: '2.0',
id,
result: {
tools: tools.map((t) => ({
name: t.name,
description: t.description,
inputSchema: t.inputSchema,
})),
},
})
case 'tools/call':
const { name, arguments: args } = params
logger.info(`MCP call from ${agentId}: ${name}`)
const result = await handleToolCall(name, {
...args,
agentId,
})
return res.json({
jsonrpc: '2.0',
id,
result,
})
default:
return res.status(404).json({
jsonrpc: '2.0',
id,
error: {
code: -32601,
message: 'Method not found',
},
})
}
} catch (error: any) {
logger.error('MCP error:', error)
return res.status(500).json({
jsonrpc: '2.0',
id,
error: {
code: -32603,
message: 'Internal error',
data: error.message,
},
})
}
})
export default router
```
## Heartbeat System
### Agent-Side Heartbeat
```bash
# In agent pod
while true; do
curl -s -X POST "$MCP_SERVER_URL/heartbeat" \
-H "Content-Type: application/json" \
-H "X-Agent-ID: $AGENT_ID" \
-d "{\"status\":\"idle\"}"
sleep 30
done &
```
### Server-Side Heartbeat Handler
```typescript
// api/routes/mcp.ts
router.post('/heartbeat', async (req: Request, res: Response) => {
const agentId = req.headers['x-agent-id'] as string
const { status } = req.body
if (!agentId) {
return res.status(401).json({ error: 'Missing agent ID' })
}
try {
await db.update(agents)
.set({
lastHeartbeat: new Date(),
status: status || 'idle',
})
.where(eq(agents.id, agentId))
res.json({ success: true })
} catch (error) {
res.status(500).json({ error: 'Failed to update heartbeat' })
}
})
```
## WebSocket for Real-Time Updates
Alternativamente, para comunicación bidireccional en tiempo real:
```typescript
// backend: api/websocket/agents.ts
import { Server as SocketIOServer } from 'socket.io'
export function setupAgentWebSocket(io: SocketIOServer) {
const agentNamespace = io.of('/agents')
agentNamespace.on('connection', (socket) => {
const agentId = socket.handshake.query.agentId as string
console.log(`Agent connected: ${agentId}`)
// Join agent room
socket.join(agentId)
// Heartbeat
socket.on('heartbeat', async (data) => {
await db.update(agents)
.set({
lastHeartbeat: new Date(),
status: data.status,
})
.where(eq(agents.id, agentId))
})
// Task updates
socket.on('task_update', async (data) => {
await db.update(tasks)
.set({ state: data.state })
.where(eq(tasks.id, data.taskId))
// Notify frontend
io.emit('task:status_changed', {
taskId: data.taskId,
newState: data.state,
})
})
socket.on('disconnect', () => {
console.log(`Agent disconnected: ${agentId}`)
})
})
// Send task assignment to specific agent
return {
assignTask: (agentId: string, task: any) => {
agentNamespace.to(agentId).emit('task_assigned', task)
},
}
}
```
## Authentication & Security
### JWT for Agents
```typescript
// Generate agent token
import jwt from 'jsonwebtoken'
export function generateAgentToken(agentId: string) {
return jwt.sign(
{
agentId,
type: 'agent',
},
process.env.JWT_SECRET!,
{
expiresIn: '7d',
}
)
}
// Verify middleware
export function verifyAgentToken(req: Request, res: Response, next: NextFunction) {
const token = req.headers.authorization?.replace('Bearer ', '')
if (!token) {
return res.status(401).json({ error: 'No token provided' })
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!)
req.agentId = decoded.agentId
next()
} catch (error) {
return res.status(401).json({ error: 'Invalid token' })
}
}
```
### mTLS (Optional)
Para seguridad adicional, usar mTLS entre agentes y backend:
```yaml
# Agent pod with client cert
volumes:
- name: agent-certs
secret:
secretName: agent-client-certs
volumeMounts:
- name: agent-certs
mountPath: /etc/certs
readOnly: true
env:
- name: MCP_CLIENT_CERT
value: /etc/certs/client.crt
- name: MCP_CLIENT_KEY
value: /etc/certs/client.key
```
## Retry & Error Handling
```typescript
// agent/mcp-client-with-retry.ts
class MCPClientWithRetry extends MCPClient {
async callToolWithRetry(
toolName: string,
args: any,
maxRetries = 3
) {
let lastError
for (let i = 0; i < maxRetries; i++) {
try {
return await this.callTool(toolName, args)
} catch (error: any) {
lastError = error
console.error(`Attempt ${i + 1} failed:`, error.message)
if (i < maxRetries - 1) {
// Exponential backoff
await sleep(Math.pow(2, i) * 1000)
}
}
}
throw lastError
}
}
```
## Circuit Breaker
```typescript
// agent/circuit-breaker.ts
class CircuitBreaker {
private failures = 0
private lastFailureTime = 0
private state: 'closed' | 'open' | 'half-open' = 'closed'
private readonly threshold = 5
private readonly timeout = 60000 // 1 minute
async call<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === 'open') {
if (Date.now() - this.lastFailureTime > this.timeout) {
this.state = 'half-open'
} else {
throw new Error('Circuit breaker is open')
}
}
try {
const result = await fn()
if (this.state === 'half-open') {
this.state = 'closed'
this.failures = 0
}
return result
} catch (error) {
this.failures++
this.lastFailureTime = Date.now()
if (this.failures >= this.threshold) {
this.state = 'open'
}
throw error
}
}
}
// Usage
const breaker = new CircuitBreaker()
const task = await breaker.call(() =>
mcp.callTool('get_next_task', { agentId })
)
```
## Monitoring Communication
```typescript
// backend: middleware/mcp-metrics.ts
import { Request, Response, NextFunction } from 'express'
import { logger } from '../utils/logger'
const metrics = {
totalCalls: 0,
successCalls: 0,
failedCalls: 0,
callDurations: [] as number[],
}
export function mcpMetricsMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
const start = Date.now()
metrics.totalCalls++
res.on('finish', () => {
const duration = Date.now() - start
metrics.callDurations.push(duration)
if (res.statusCode < 400) {
metrics.successCalls++
} else {
metrics.failedCalls++
}
logger.debug('MCP call metrics', {
method: req.body?.method,
agentId: req.headers['x-agent-id'],
duration,
status: res.statusCode,
})
})
next()
}
// Endpoint para ver métricas
router.get('/metrics', (req, res) => {
res.json({
total: metrics.totalCalls,
success: metrics.successCalls,
failed: metrics.failedCalls,
avgDuration:
metrics.callDurations.reduce((a, b) => a + b, 0) /
metrics.callDurations.length,
})
})
```
## Testing MCP Communication
```typescript
// test/mcp-client.test.ts
import { MCPClient } from '../agent/mcp-client'
describe('MCP Client', () => {
let client: MCPClient
beforeEach(() => {
client = new MCPClient('http://localhost:3100', 'test-agent')
})
it('should list available tools', async () => {
const tools = await client.listTools()
expect(tools).toContainEqual(
expect.objectContaining({ name: 'get_next_task' })
)
})
it('should call tool successfully', async () => {
const result = await client.callTool('heartbeat', {
status: 'idle',
})
expect(result.content[0].text).toContain('success')
})
it('should handle errors', async () => {
await expect(
client.callTool('invalid_tool', {})
).rejects.toThrow()
})
})
```

452
docs/05-agents/mcp-tools.md Normal file
View File

@@ -0,0 +1,452 @@
# MCP Tools - Herramientas Disponibles para Agentes
Esta documentación detalla todas las herramientas MCP que los agentes Claude Code pueden usar para interactuar con el sistema AiWorker.
## get_next_task
Obtiene la siguiente tarea disponible de la cola y la asigna al agente.
**Input**:
```json
{
"agentId": "uuid-of-agent",
"capabilities": ["javascript", "react", "python"] // opcional
}
```
**Output**:
```json
{
"task": {
"id": "task-uuid",
"title": "Implement user authentication",
"description": "Create a JWT-based authentication system...",
"priority": "high",
"project": {
"id": "project-uuid",
"name": "My App",
"giteaRepoUrl": "http://gitea/owner/my-app",
"dockerImage": "myapp:latest"
}
}
}
```
**Ejemplo de uso**:
```typescript
// En Claude Code, el agente puede hacer:
const task = await mcp.callTool('get_next_task', {
agentId: process.env.AGENT_ID,
capabilities: ['javascript', 'typescript', 'react']
})
```
---
## update_task_status
Actualiza el estado de una tarea.
**Input**:
```json
{
"taskId": "task-uuid",
"status": "in_progress" | "needs_input" | "ready_to_test" | "completed",
"metadata": {
"durationMinutes": 45,
"linesChanged": 250
}
}
```
**Output**:
```json
{
"success": true
}
```
**Estados válidos**:
- `in_progress`: Agente trabajando activamente
- `needs_input`: Agente necesita información del usuario
- `ready_to_test`: Tarea completada, lista para testing
- `completed`: Tarea completamente finalizada
---
## ask_user_question
Solicita información al usuario cuando el agente necesita clarificación.
**Input**:
```json
{
"taskId": "task-uuid",
"question": "Which authentication library should I use: Passport.js or NextAuth?",
"context": "The task requires implementing OAuth authentication. I found two popular options..."
}
```
**Output**:
```json
{
"success": true,
"message": "Question sent to user",
"questionId": "question-uuid"
}
```
**Comportamiento**:
1. Cambia el estado de la tarea a `needs_input`
2. Notifica al frontend vía WebSocket
3. Usuario responde desde el dashboard
4. Agente puede hacer polling con `check_question_response`
---
## check_question_response
Verifica si el usuario ha respondido una pregunta.
**Input**:
```json
{
"taskId": "task-uuid"
}
```
**Output (sin respuesta)**:
```json
{
"hasResponse": false,
"message": "No response yet"
}
```
**Output (con respuesta)**:
```json
{
"hasResponse": true,
"response": "Use NextAuth, it integrates better with our Next.js stack",
"question": "Which authentication library should I use..."
}
```
---
## create_branch
Crea una nueva rama en Gitea para la tarea.
**Input**:
```json
{
"taskId": "task-uuid",
"branchName": "feature/user-auth" // opcional, se genera automático
}
```
**Output**:
```json
{
"success": true,
"branchName": "task-abc123-implement-user-authentication",
"repoUrl": "http://gitea/owner/my-app"
}
```
**Comportamiento**:
- Si no se especifica `branchName`, se genera como: `task-{shortId}-{title-slugified}`
- Se crea desde la rama default del proyecto (main/develop)
- Se actualiza el campo `branchName` en la tarea
---
## create_pull_request
Crea un Pull Request en Gitea con los cambios de la tarea.
**Input**:
```json
{
"taskId": "task-uuid",
"title": "Implement JWT-based authentication",
"description": "## Changes\n- Added JWT middleware\n- Created auth routes\n- Added tests\n\n## Test Plan\n- [ ] Test login flow\n- [ ] Test token refresh"
}
```
**Output**:
```json
{
"success": true,
"prUrl": "http://gitea/owner/my-app/pulls/42",
"prNumber": 42
}
```
**Comportamiento**:
- Crea PR desde la rama de la tarea hacia la rama default
- Actualiza campos `prNumber` y `prUrl` en la tarea
- Emite evento WebSocket `task:pr_created`
---
## trigger_preview_deploy
Despliega la tarea en un preview environment aislado en Kubernetes.
**Input**:
```json
{
"taskId": "task-uuid"
}
```
**Output**:
```json
{
"success": true,
"previewUrl": "https://task-abc123.preview.aiworker.dev",
"namespace": "preview-task-abc123"
}
```
**Comportamiento**:
1. Crea namespace en K8s: `preview-task-{shortId}`
2. Deploya la aplicación con la imagen del proyecto
3. Crea ingress con URL única
4. Actualiza tarea a estado `ready_to_test`
5. Guarda `previewUrl` y `previewNamespace` en la tarea
---
## get_task_details
Obtiene detalles completos de una tarea incluyendo preguntas pendientes.
**Input**:
```json
{
"taskId": "task-uuid"
}
```
**Output**:
```json
{
"task": {
"id": "task-uuid",
"title": "Implement user authentication",
"description": "...",
"state": "needs_input",
"branchName": "task-abc123-implement-user-authentication",
"prUrl": "http://gitea/owner/my-app/pulls/42",
"previewUrl": "https://task-abc123.preview.aiworker.dev",
"questions": [
{
"id": "q-uuid",
"question": "Which auth library?",
"status": "pending",
"askedAt": "2026-01-19T10:00:00Z"
}
],
"project": {
"name": "My App",
"giteaRepoUrl": "..."
}
}
}
```
---
## log_activity
Registra actividad del agente en los logs del sistema.
**Input**:
```json
{
"agentId": "agent-uuid",
"level": "info" | "debug" | "warn" | "error",
"message": "Starting task implementation",
"metadata": {
"taskId": "task-uuid",
"operation": "code_generation",
"filesModified": 5
}
}
```
**Output**:
```json
{
"success": true
}
```
**Niveles de log**:
- `debug`: Información detallada de debugging
- `info`: Eventos normales (default)
- `warn`: Situaciones que requieren atención
- `error`: Errores que impidieron completar una operación
---
## heartbeat
Envía señal de vida para indicar que el agente está activo.
**Input**:
```json
{
"agentId": "agent-uuid",
"status": "idle" | "busy" | "error"
}
```
**Output**:
```json
{
"success": true
}
```
**Comportamiento**:
- Actualiza `lastHeartbeat` timestamp
- Actualiza `status` del agente
- Si no se recibe heartbeat por 5 minutos, el agente se marca como `offline`
---
## Flujo Típico de una Tarea
```mermaid
sequenceDiagram
Agent->>MCP: get_next_task()
MCP-->>Agent: task details
Agent->>MCP: create_branch()
Agent->>Agent: Work on task
Agent->>?MCP: ask_user_question() (si necesita)
Agent->>Agent: Wait for response
Agent->>MCP: check_question_response()
Agent->>Agent: Continue working
Agent->>Git: commit & push
Agent->>MCP: create_pull_request()
Agent->>MCP: trigger_preview_deploy()
Agent->>MCP: update_task_status("ready_to_test")
```
## Ejemplo de Uso Completo
```typescript
// Dentro del agente Claude Code
async function processTask() {
// 1. Get task
const taskResult = await mcp.callTool('get_next_task', {
agentId: process.env.AGENT_ID
})
const task = JSON.parse(taskResult.content[0].text).task
if (!task) {
console.log('No tasks available')
return
}
console.log(`Working on: ${task.title}`)
// 2. Create branch
const branchResult = await mcp.callTool('create_branch', {
taskId: task.id
})
const { branchName } = JSON.parse(branchResult.content[0].text)
// 3. Clone and checkout
await exec(`git clone ${task.project.giteaRepoUrl} /workspace/task-${task.id}`)
await exec(`cd /workspace/task-${task.id} && git checkout ${branchName}`)
// 4. Do the work...
// (Claude Code generates and commits code)
// 5. Need clarification?
if (needsClarification) {
await mcp.callTool('ask_user_question', {
taskId: task.id,
question: 'Should I add error handling for network failures?',
context: 'The API calls can fail...'
})
// Wait for response
let response
while (!response) {
await sleep(5000)
const checkResult = await mcp.callTool('check_question_response', {
taskId: task.id
})
const check = JSON.parse(checkResult.content[0].text)
if (check.hasResponse) {
response = check.response
}
}
}
// 6. Create PR
await mcp.callTool('create_pull_request', {
taskId: task.id,
title: task.title,
description: `## Summary\nImplemented ${task.title}\n\n## Changes\n- Feature A\n- Feature B`
})
// 7. Deploy preview
await mcp.callTool('trigger_preview_deploy', {
taskId: task.id
})
// 8. Mark as done
await mcp.callTool('update_task_status', {
taskId: task.id,
status: 'ready_to_test'
})
console.log('Task completed!')
}
```
## Error Handling
Todos los tools pueden retornar errores:
```json
{
"content": [{
"type": "text",
"text": "Error: Task not found"
}],
"isError": true
}
```
El agente debe manejar estos errores apropiadamente:
```typescript
const result = await mcp.callTool('update_task_status', { ... })
if (result.isError) {
console.error('Tool failed:', result.content[0].text)
// Handle error
}
```
## Rate Limiting
Para evitar abuse, los tools tienen rate limits:
- `get_next_task`: 1 por segundo
- `ask_user_question`: 5 por minuto por tarea
- `create_pr`: 1 por minuto
- `trigger_preview_deploy`: 1 por minuto
- Otros: 10 por segundo
Si se excede el rate limit, el tool retorna error 429.