Implementar MCP Docker Server personalizado
Some checks failed
build-and-deploy / build (push) Successful in 8s
build-and-deploy / deploy (push) Failing after 2s

- Creado servidor MCP en TypeScript con @modelcontextprotocol/sdk
- Implementadas 13 herramientas Docker seguras usando dockerode:
  * docker_ps: Listar contenedores
  * docker_logs: Ver logs de contenedores
  * docker_inspect: Inspeccionar contenedor
  * docker_stats: Estadísticas de recursos
  * docker_top: Procesos del contenedor
  * docker_start/stop/restart: Gestión de contenedores
  * docker_exec: Ejecutar comandos
  * docker_images/networks/volumes: Listar recursos
  * docker_info: Información del sistema
- Configurado servidor HTTP con Express en puerto 3000
- Agregado endpoint /mcp para protocolo MCP
- Agregado health check en /health
- Actualizado docker-compose.yml para usar imagen personalizada
- Configurado GitHub Actions para build y deploy automático
- Socket Docker montado en modo solo lectura para seguridad
This commit is contained in:
2025-10-13 19:12:34 -06:00
parent aa5ac70c7c
commit a84c7b9114
8 changed files with 712 additions and 6 deletions

View File

@@ -0,0 +1,533 @@
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import express from 'express';
import { z } from 'zod';
import Docker from 'dockerode';
// Inicializar Docker
const docker = new Docker({ socketPath: '/var/run/docker.sock' });
// Crear el servidor MCP
const server = new McpServer({
name: 'docker-server',
version: '1.0.0'
});
// Tool: Listar contenedores
server.registerTool(
'docker_ps',
{
title: 'Listar Contenedores',
description: 'Lista todos los contenedores Docker (en ejecución o todos)',
inputSchema: {
all: z.boolean().optional().describe('Mostrar todos los contenedores (incluidos detenidos)')
},
outputSchema: {
containers: z.array(z.object({
id: z.string(),
name: z.string(),
image: z.string(),
state: z.string(),
status: z.string()
}))
}
},
async ({ all = false }) => {
try {
const containers = await docker.listContainers({ all });
const output = {
containers: containers.map(c => ({
id: c.Id.substring(0, 12),
name: c.Names[0].replace('/', ''),
image: c.Image,
state: c.State,
status: c.Status
}))
};
return {
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
structuredContent: output
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
return {
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
isError: true
};
}
}
);
// Tool: Ver logs de un contenedor
server.registerTool(
'docker_logs',
{
title: 'Ver Logs de Contenedor',
description: 'Obtiene los logs de un contenedor específico',
inputSchema: {
container_id: z.string().describe('ID o nombre del contenedor'),
tail: z.number().optional().describe('Número de líneas desde el final (default: 100)'),
follow: z.boolean().optional().describe('Seguir logs en tiempo real (no recomendado para MCP)')
},
outputSchema: {
logs: z.string()
}
},
async ({ container_id, tail = 100, follow = false }) => {
try {
const container = docker.getContainer(container_id);
const logs = await container.logs({
stdout: true,
stderr: true,
tail,
follow
});
const output = { logs: logs.toString('utf-8') };
return {
content: [{ type: 'text', text: output.logs }],
structuredContent: output
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
return {
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
isError: true
};
}
}
);
// Tool: Inspeccionar contenedor
server.registerTool(
'docker_inspect',
{
title: 'Inspeccionar Contenedor',
description: 'Obtiene información detallada de un contenedor',
inputSchema: {
container_id: z.string().describe('ID o nombre del contenedor')
},
outputSchema: {
info: z.any()
}
},
async ({ container_id }) => {
try {
const container = docker.getContainer(container_id);
const info = await container.inspect();
const output = { info };
return {
content: [{ type: 'text', text: JSON.stringify(info, null, 2) }],
structuredContent: output
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
return {
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
isError: true
};
}
}
);
// Tool: Ver estadísticas de un contenedor
server.registerTool(
'docker_stats',
{
title: 'Estadísticas de Contenedor',
description: 'Obtiene estadísticas de uso de recursos de un contenedor',
inputSchema: {
container_id: z.string().describe('ID o nombre del contenedor')
},
outputSchema: {
stats: z.any()
}
},
async ({ container_id }) => {
try {
const container = docker.getContainer(container_id);
const stats = await container.stats({ stream: false });
const output = { stats };
return {
content: [{ type: 'text', text: JSON.stringify(stats, null, 2) }],
structuredContent: output
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
return {
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
isError: true
};
}
}
);
// Tool: Ver procesos de un contenedor
server.registerTool(
'docker_top',
{
title: 'Procesos del Contenedor',
description: 'Lista los procesos en ejecución dentro de un contenedor',
inputSchema: {
container_id: z.string().describe('ID o nombre del contenedor')
},
outputSchema: {
processes: z.any()
}
},
async ({ container_id }) => {
try {
const container = docker.getContainer(container_id);
const processes = await container.top();
const output = { processes };
return {
content: [{ type: 'text', text: JSON.stringify(processes, null, 2) }],
structuredContent: output
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
return {
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
isError: true
};
}
}
);
// Tool: Reiniciar contenedor
server.registerTool(
'docker_restart',
{
title: 'Reiniciar Contenedor',
description: 'Reinicia un contenedor específico',
inputSchema: {
container_id: z.string().describe('ID o nombre del contenedor')
},
outputSchema: {
success: z.boolean(),
message: z.string()
}
},
async ({ container_id }) => {
try {
const container = docker.getContainer(container_id);
await container.restart();
const output = { success: true, message: `Contenedor ${container_id} reiniciado exitosamente` };
return {
content: [{ type: 'text', text: output.message }],
structuredContent: output
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
return {
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
isError: true
};
}
}
);
// Tool: Detener contenedor
server.registerTool(
'docker_stop',
{
title: 'Detener Contenedor',
description: 'Detiene un contenedor en ejecución',
inputSchema: {
container_id: z.string().describe('ID o nombre del contenedor')
},
outputSchema: {
success: z.boolean(),
message: z.string()
}
},
async ({ container_id }) => {
try {
const container = docker.getContainer(container_id);
await container.stop();
const output = { success: true, message: `Contenedor ${container_id} detenido exitosamente` };
return {
content: [{ type: 'text', text: output.message }],
structuredContent: output
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
return {
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
isError: true
};
}
}
);
// Tool: Iniciar contenedor
server.registerTool(
'docker_start',
{
title: 'Iniciar Contenedor',
description: 'Inicia un contenedor detenido',
inputSchema: {
container_id: z.string().describe('ID o nombre del contenedor')
},
outputSchema: {
success: z.boolean(),
message: z.string()
}
},
async ({ container_id }) => {
try {
const container = docker.getContainer(container_id);
await container.start();
const output = { success: true, message: `Contenedor ${container_id} iniciado exitosamente` };
return {
content: [{ type: 'text', text: output.message }],
structuredContent: output
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
return {
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
isError: true
};
}
}
);
// Tool: Listar imágenes
server.registerTool(
'docker_images',
{
title: 'Listar Imágenes',
description: 'Lista todas las imágenes Docker disponibles',
inputSchema: {},
outputSchema: {
images: z.array(z.object({
id: z.string(),
tags: z.array(z.string()),
size: z.number()
}))
}
},
async () => {
try {
const images = await docker.listImages();
const output = {
images: images.map(img => ({
id: img.Id.substring(7, 19),
tags: img.RepoTags || [],
size: img.Size
}))
};
return {
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
structuredContent: output
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
return {
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
isError: true
};
}
}
);
// Tool: Listar redes
server.registerTool(
'docker_networks',
{
title: 'Listar Redes',
description: 'Lista todas las redes Docker',
inputSchema: {},
outputSchema: {
networks: z.array(z.object({
id: z.string(),
name: z.string(),
driver: z.string()
}))
}
},
async () => {
try {
const networks = await docker.listNetworks();
const output = {
networks: networks.map(net => ({
id: net.Id.substring(0, 12),
name: net.Name,
driver: net.Driver
}))
};
return {
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
structuredContent: output
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
return {
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
isError: true
};
}
}
);
// Tool: Listar volúmenes
server.registerTool(
'docker_volumes',
{
title: 'Listar Volúmenes',
description: 'Lista todos los volúmenes Docker',
inputSchema: {},
outputSchema: {
volumes: z.array(z.object({
name: z.string(),
driver: z.string(),
mountpoint: z.string()
}))
}
},
async () => {
try {
const result = await docker.listVolumes();
const output = {
volumes: (result.Volumes || []).map(vol => ({
name: vol.Name,
driver: vol.Driver,
mountpoint: vol.Mountpoint
}))
};
return {
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
structuredContent: output
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
return {
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
isError: true
};
}
}
);
// Tool: Información del sistema Docker
server.registerTool(
'docker_info',
{
title: 'Información del Sistema',
description: 'Obtiene información general del sistema Docker',
inputSchema: {},
outputSchema: {
info: z.any()
}
},
async () => {
try {
const info = await docker.info();
const output = { info };
return {
content: [{ type: 'text', text: JSON.stringify(info, null, 2) }],
structuredContent: output
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
return {
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
isError: true
};
}
}
);
// Tool: Ejecutar comando en contenedor
server.registerTool(
'docker_exec',
{
title: 'Ejecutar Comando',
description: 'Ejecuta un comando dentro de un contenedor',
inputSchema: {
container_id: z.string().describe('ID o nombre del contenedor'),
command: z.array(z.string()).describe('Comando a ejecutar como array (ej: ["ls", "-la"])')
},
outputSchema: {
output: z.string()
}
},
async ({ container_id, command }) => {
try {
const container = docker.getContainer(container_id);
const exec = await container.exec({
Cmd: command,
AttachStdout: true,
AttachStderr: true
});
const stream = await exec.start({ Detach: false });
// Recolectar la salida del stream
const chunks: Buffer[] = [];
for await (const chunk of stream) {
chunks.push(Buffer.from(chunk));
}
const outputBuffer = Buffer.concat(chunks);
const outputText = outputBuffer.toString('utf-8');
const output = { output: outputText };
return {
content: [{ type: 'text', text: outputText }],
structuredContent: output
};
} catch (error) {
const errorMsg = error instanceof Error ? error.message : 'Error desconocido';
return {
content: [{ type: 'text', text: `Error: ${errorMsg}` }],
isError: true
};
}
}
);
// Configurar el servidor HTTP
const app = express();
app.use(express.json());
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', service: 'mcp-docker-server' });
});
// Endpoint MCP
app.post('/mcp', async (req, res) => {
try {
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: undefined,
enableJsonResponse: true
});
res.on('close', () => {
transport.close();
});
await server.connect(transport);
await transport.handleRequest(req, res, req.body);
} catch (error) {
console.error('Error handling MCP request:', error);
if (!res.headersSent) {
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32603,
message: 'Internal server error'
},
id: null
});
}
}
});
const port = parseInt(process.env.PORT || '3000');
app.listen(port, '0.0.0.0', () => {
console.log(`MCP Docker Server corriendo en http://0.0.0.0:${port}/mcp`);
console.log(`Health check disponible en http://0.0.0.0:${port}/health`);
}).on('error', error => {
console.error('Error del servidor:', error);
process.exit(1);
});