- Install and configure MCP bridge in agent image - Bridge connects Claude Code stdio <-> HTTP endpoints - Verification steps included Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
16 KiB
📋 Próxima Sesión - Multi-Agent + Terminal Integrado + Auth
Objetivo: Integrar terminals web en agentes, autenticación MCP, y gestión multi-agente desde UI
Tiempo estimado: 3-4 horas
Sesión anterior: past-sessions/2026-01-20-frontend-mcp-agents.md
🎯 VISIÓN DE ESTA FASE
Lo que tenemos AHORA:
- ✅ Dashboard web en app.fuq.tv
- ✅ Backend con MCP endpoints (públicos)
- ✅ Un agente deployment (claude-agent)
- ✅ Terminal web separado (claude.fuq.tv) - NO conectado al agente
Lo que queremos LOGRAR:
- 🎯 Múltiples agentes por usuario, gestionados desde UI
- 🎯 Terminal web integrado EN cada agente (no pod separado)
- 🎯 Ver terminal de cualquier agente desde app.fuq.tv (iframe/websocket)
- 🎯 MCP autenticado: cada agente usa token del usuario que lo creó
- 🎯 Agent pool management: crear, ver, eliminar agentes desde dashboard
✅ PRE-REQUISITOS
# 1. Sistema actual funcionando
curl -s https://app.fuq.tv/ | grep "AiWorker"
curl -s https://api.fuq.tv/api/health | grep "ok"
# 2. Cluster operativo
export KUBECONFIG=~/.kube/aiworker-config
kubectl get pods -n control-plane
kubectl get pods -n agents
# 3. Auth funcionando
# Prueba: registra usuario, inicia sesión, crea proyecto
🎯 PASO 0: Configurar MCP Bridge en Agente (15 min)
0.1 Incluir MCP bridge en imagen del agente
Modificar agents/Dockerfile para copiar archivos MCP:
# ... después de instalar Claude Code
# Copy MCP bridge server and config
COPY mcp-bridge-server.js /workspace/mcp-bridge-server.js
COPY claude-code-config.json /root/.config/claude/config.json
RUN chmod +x /workspace/mcp-bridge-server.js
# ...
0.2 Verificar MCP funciona
En terminal del agente:
# Entrar a claude.fuq.tv o kubectl exec al pod
claude
/mcp
# Debería mostrar: "aiworker" server con 5 tools
# Probar una herramienta
/mcp aiworker get_next_task {"agentId":"test"}
Si no funciona: Configuración manual en AGENT-ACCESS.md
🎯 PASO 1: Agregar Auth a Usuarios (30 min)
1.1 Extender schema con relación user → agents
Modificar backend/src/db/schema.ts:
// Agregar campo userId a agents table
export const agents = mysqlTable('agents', {
id: varchar('id', { length: 36 }).primaryKey(),
userId: varchar('user_id', { length: 36 }).notNull().references(() => users.id, { onDelete: 'cascade' }),
// ... resto de campos
})
// Agregar relación
export const usersRelations = relations(users, ({ many }) => ({
agents: many(agents),
projects: many(projects),
}))
export const agentsRelations = relations(agents, ({ one }) => ({
user: one(users, {
fields: [agents.userId],
references: [users.id],
}),
// ... resto
}))
Generar migración:
cd backend
bun run db:generate
# Revisar migración generada
bun run db:migrate
🎯 PASO 2: Auth en MCP Endpoints (45 min)
2.1 Crear middleware de auth
Crear backend/src/api/middleware/auth.ts:
import { db } from '../../db/client'
import { users, sessions } from '../../db/schema'
import { eq } from 'drizzle-orm'
export async function authenticateRequest(req: Request): Promise<{ userId: string } | null> {
const sessionId = getSessionIdFromCookie(req)
if (!sessionId) return null
const [session] = await db
.select()
.from(sessions)
.where(eq(sessions.id, sessionId))
.limit(1)
if (!session || session.expiresAt < new Date()) {
return null
}
return { userId: session.userId }
}
function getSessionIdFromCookie(req: Request): string | null {
const cookieHeader = req.headers.get('cookie')
if (!cookieHeader) return null
const cookies = cookieHeader.split(';').reduce((acc, cookie) => {
const [name, value] = cookie.trim().split('=')
acc[name] = value
return acc
}, {} as Record<string, string>)
return cookies.session || null
}
2.2 Proteger endpoints MCP
Modificar backend/src/api/routes/mcp.ts:
import { authenticateRequest } from '../middleware/auth'
export async function handleMCPRoutes(req: Request, url: URL): Promise<Response> {
// Autenticar request
const auth = await authenticateRequest(req)
if (!auth) {
return Response.json(
{ success: false, message: 'Unauthorized' },
{ status: 401 }
)
}
const userId = auth.userId
// Pasar userId a todos los handlers...
}
// Modificar get_next_task para filtrar por usuario
async function handleGetNextTask(req: Request, userId: string): Promise<Response> {
// ...
// Filtrar tareas de proyectos del usuario
const userProjects = await db.select().from(projects).where(eq(projects.userId, userId))
const projectIds = userProjects.map(p => p.id)
const [task] = await db
.select()
.from(tasks)
.where(and(
eq(tasks.state, 'backlog'),
inArray(tasks.projectId, projectIds)
))
.orderBy(tasks.priority, tasks.createdAt)
.limit(1)
// ...
}
🎯 PASO 3: Integrar Terminal en Imagen del Agente (1 hora)
3.1 Modificar Dockerfile del agente
Modificar agents/Dockerfile:
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
# Install system dependencies + ttyd + tmux
RUN apt-get update && apt-get install -y \
curl wget git bash unzip \
ca-certificates openssh-client \
python3 python3-pip python3-venv \
build-essential jq \
tmux \
&& rm -rf /var/lib/apt/lists/*
# Install ttyd (terminal web)
RUN curl -L https://github.com/tsl0922/ttyd/releases/download/1.7.4/ttyd.x86_64 -o /usr/local/bin/ttyd \
&& chmod +x /usr/local/bin/ttyd
# Install Node.js 24.x
RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - \
&& apt-get install -y nodejs \
&& rm -rf /var/lib/apt/lists/*
# Install Bun
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:$PATH"
# Install kubectl
RUN curl -LO "https://dl.k8s.io/release/$(curl -L -s https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl" \
&& chmod +x kubectl \
&& mv kubectl /usr/local/bin/
# Install Claude Code CLI
RUN bun install -g @anthropic-ai/claude-code
# Create workspace
WORKDIR /workspace
# Entrypoint script
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
EXPOSE 7681
CMD ["/entrypoint.sh"]
3.2 Crear entrypoint script
Crear agents/entrypoint.sh:
#!/bin/bash
set -e
# Configure git
git config --global user.name "Claude Agent"
git config --global user.email "agent@aiworker.local"
# Start tmux session
tmux new-session -d -s claude 'bash -c "cd /workspace && exec bash"'
# Start ttyd to expose tmux over HTTP on port 7681
exec ttyd -p 7681 -W tmux attach -t claude
3.3 Build y push
cd agents
git add Dockerfile entrypoint.sh
git commit -m "Integrate ttyd+tmux into agent image
- Add ttyd for web terminal access
- Add tmux for persistent sessions
- Expose port 7681 for terminal web
- Entrypoint starts tmux + ttyd automatically
Co-Authored-By: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>"
git push
# Esperar build en Gitea Actions
🎯 PASO 4: StatefulSet para Agentes (45 min)
4.1 Cambiar de Deployment a StatefulSet
Crear k8s/agents/statefulset.yaml:
apiVersion: v1
kind: Service
metadata:
name: claude-agent
namespace: agents
spec:
clusterIP: None # Headless service
selector:
app: claude-agent
ports:
- name: terminal
port: 7681
targetPort: 7681
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: claude-agent
namespace: agents
spec:
serviceName: claude-agent
replicas: 0 # Se crearán dinámicamente desde API
selector:
matchLabels:
app: claude-agent
template:
metadata:
labels:
app: claude-agent
spec:
serviceAccountName: agent-sa
imagePullSecrets:
- name: gitea-registry
containers:
- name: agent
image: git.fuq.tv/admin/aiworker-agent:latest
imagePullPolicy: Always
ports:
- containerPort: 7681
name: terminal
env:
- name: BACKEND_URL
value: "https://api.fuq.tv"
- name: MCP_ENDPOINT
value: "https://api.fuq.tv/api/mcp"
- name: GITEA_URL
value: "https://git.fuq.tv"
- name: GITEA_TOKEN
valueFrom:
secretKeyRef:
name: agent-secrets
key: gitea-token
- name: POD_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: USER_ID
value: "" # Se inyecta dinámicamente al crear el pod
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: 2000m
memory: 4Gi
volumeMounts:
- name: workspace
mountPath: /workspace
volumeClaimTemplates:
- metadata:
name: workspace
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: longhorn
resources:
requests:
storage: 10Gi
🎯 PASO 5: Backend API para Gestión de Agentes (1 hora)
5.1 Endpoints de agentes autenticados
Modificar backend/src/api/routes/agents.ts:
// POST /api/agents/launch - Lanzar nuevo agente
async function handleLaunchAgent(req: Request, userId: string): Promise<Response> {
const agentId = randomUUID()
const podName = `claude-agent-${userId.slice(0, 8)}-${Date.now()}`
// 1. Crear registro en DB
await db.insert(agents).values({
id: agentId,
userId,
podName,
k8sNamespace: 'agents',
status: 'idle',
})
// 2. Crear pod en K8s usando K8s API
const k8sConfig = KubeConfig.fromFile('~/.kube/aiworker-config')
const k8sApi = k8sConfig.makeApiClient(AppsV1Api)
// Escalar StatefulSet o crear Pod individual
await k8sApi.createNamespacedPod('agents', {
metadata: {
name: podName,
labels: {
app: 'claude-agent',
userId: userId,
agentId: agentId,
},
},
spec: {
// ... spec del pod con USER_ID env var
},
})
return Response.json({
success: true,
data: { agentId, podName },
})
}
// DELETE /api/agents/:id - Eliminar agente
async function handleDeleteAgent(agentId: string, userId: string): Promise<Response> {
const [agent] = await db
.select()
.from(agents)
.where(and(eq(agents.id, agentId), eq(agents.userId, userId)))
.limit(1)
if (!agent) {
return Response.json({ success: false, message: 'Agent not found' }, { status: 404 })
}
// Eliminar pod de K8s
await k8sApi.deleteNamespacedPod(agent.podName, 'agents')
// Eliminar de DB
await db.delete(agents).where(eq(agents.id, agentId))
return Response.json({ success: true })
}
// GET /api/agents/my - Listar mis agentes
async function handleMyAgents(userId: string): Promise<Response> {
const myAgents = await db
.select()
.from(agents)
.where(eq(agents.userId, userId))
return Response.json({
success: true,
data: myAgents,
})
}
🎯 PASO 6: Frontend - Ver Agentes y Terminals (1 hora)
6.1 Agregar botón "Launch Agent"
Modificar frontend/src/pages/Dashboard.tsx:
const [showAgentLauncher, setShowAgentLauncher] = useState(false)
// Botón en header
<button
onClick={() => setShowAgentLauncher(true)}
className="px-4 py-2 bg-green-600 text-white rounded-md"
>
Launch Agent
</button>
6.2 Crear AgentTerminalModal
Crear frontend/src/components/AgentTerminalModal.tsx:
interface AgentTerminalModalProps {
agent: Agent
isOpen: boolean
onClose: () => void
}
export default function AgentTerminalModal({ agent, isOpen, onClose }: AgentTerminalModalProps) {
if (!isOpen) return null
// URL del terminal: http://claude-agent-{podName}.agents.svc.cluster.local:7681
const terminalUrl = `/agent-terminal/${agent.id}`
return (
<div className="fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50">
<div className="bg-white rounded-lg w-full h-full max-w-7xl max-h-[90vh] flex flex-col">
<div className="p-4 border-b flex justify-between items-center">
<h2 className="text-xl font-bold">Agent Terminal - {agent.podName}</h2>
<button onClick={onClose} className="text-gray-500 hover:text-gray-700">
✕
</button>
</div>
<div className="flex-1 overflow-hidden">
<iframe
src={terminalUrl}
className="w-full h-full border-0"
allow="fullscreen"
/>
</div>
</div>
</div>
)
}
6.3 Proxy para terminals en backend
Agregar en backend/src/index.ts:
// Proxy /agent-terminal/:agentId a pod del agente
if (url.pathname.startsWith('/agent-terminal/')) {
const agentId = url.pathname.split('/')[2]
// Obtener pod name de DB
const [agent] = await db.select().from(agents).where(eq(agents.id, agentId)).limit(1)
if (!agent) return new Response('Agent not found', { status: 404 })
// Proxy a pod
const podUrl = `http://${agent.podName}.agents.svc.cluster.local:7681${url.pathname.replace(`/agent-terminal/${agentId}`, '')}`
return fetch(podUrl)
}
🎯 PASO 7: Testing End-to-End (30 min)
7.1 Test completo del flujo
# 1. Abrir dashboard
open https://app.fuq.tv
# 2. Iniciar sesión
# 3. Launch Agent (botón verde)
# Debería crear un agente
# 4. Ver en "Agents" tab
# Debería aparecer el agente con status "idle"
# 5. Click en agente → Ver terminal
# Debería abrir modal con terminal tmux
# 6. En terminal: probar Claude Code
cd /workspace
claude
# 7. Crear tarea en proyecto
# 8. En terminal del agente:
curl -X POST https://api.fuq.tv/api/mcp/get_next_task \
-H "Cookie: session=..." \
-d '{"agentId":"'$POD_NAME'"}'
# Debería retornar la tarea (solo las del usuario)
📝 NOTAS IMPORTANTES
Terminal Web por Agente
- Cada agente tiene ttyd + tmux integrado
- Puerto 7681 expuesto
- Headless service permite acceso pod-a-pod
- Frontend proxy para acceder desde navegador
Autenticación MCP
- Cookie
sessionse pasa en todos los requests - Backend valida sesión y extrae userId
- MCP filtra tareas/proyectos por userId
- Agentes solo ven tareas de su usuario
Multi-Tenancy
- Cada usuario puede lanzar N agentes
- Agentes aislados por userId en DB
- K8s pods tienen label
userId - Workspace persistente (PVC) por agente
🐛 TROUBLESHOOTING
Terminal no carga en iframe
- Verificar proxy en backend funciona
- Verificar pod tiene puerto 7681 expuesto
- Verificar ttyd está corriendo:
kubectl logs -n agents <pod>
MCP retorna "Unauthorized"
- Verificar cookie se pasa en request
- Verificar sesión no expiró
- Test con curl incluyendo cookie
Agente no se crea
- Verificar K8s API credentials
- Verificar namespace
agentsexiste - Ver logs del backend
✅ CHECKLIST DE SESIÓN
Al final de la sesión, verificar:
- Schema tiene campo
userIden agents y projects - MCP endpoints requieren autenticación
- Imagen del agente incluye ttyd + tmux
- StatefulSet configurado para agentes
- Backend API: launch, delete, my agents
- Frontend: botón launch agent
- Frontend: modal con terminal iframe
- Proxy backend a terminales de agentes
- Test: lanzar agente → ver terminal → ejecutar claude
- Test: MCP solo retorna tareas del usuario
- Código commitado
- Builds exitosos
🎉 META
Completado hasta ahora: Infraestructura + Backend + Frontend + Agente básico Esta sesión: Multi-Agent + Terminal Integrado + Auth Próximo hito: Preview Environments + CI/CD per Task + Monitoring
¡El MVP multi-usuario está casi listo! 🚀