Implementar MCP Docker Server personalizado
- 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:
50
.github/workflows/deploy-mcp-docker.yml
vendored
Normal file
50
.github/workflows/deploy-mcp-docker.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: Deploy MCP Docker Server
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
paths:
|
||||||
|
- 'mcp-docker-server/**'
|
||||||
|
- '.github/workflows/deploy-mcp-docker.yml'
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Log in to Gitea Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ secrets.REGISTRY_URL }}
|
||||||
|
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: ./mcp-docker-server
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ secrets.REGISTRY_URL }}/${{ secrets.REPO_OWNER }}/mcp-docker-server:latest
|
||||||
|
${{ secrets.REGISTRY_URL }}/${{ secrets.REPO_OWNER }}/mcp-docker-server:${{ github.sha }}
|
||||||
|
cache-from: type=registry,ref=${{ secrets.REGISTRY_URL }}/${{ secrets.REPO_OWNER }}/mcp-docker-server:buildcache
|
||||||
|
cache-to: type=registry,ref=${{ secrets.REGISTRY_URL }}/${{ secrets.REPO_OWNER }}/mcp-docker-server:buildcache,mode=max
|
||||||
|
|
||||||
|
- name: Deploy to server
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SERVER_HOST }}
|
||||||
|
username: ${{ secrets.SERVER_USER }}
|
||||||
|
key: ${{ secrets.SERVER_SSH_KEY }}
|
||||||
|
script: |
|
||||||
|
cd ${{ secrets.DEPLOY_PATH }}
|
||||||
|
docker compose pull mcp-docker
|
||||||
|
docker compose up -d mcp-docker
|
||||||
|
docker image prune -f
|
||||||
@@ -49,10 +49,11 @@ services:
|
|||||||
- "traefik.http.middlewares.${APP_NAME}-cors.headers.addvaryheader=true"
|
- "traefik.http.middlewares.${APP_NAME}-cors.headers.addvaryheader=true"
|
||||||
|
|
||||||
mcp-docker:
|
mcp-docker:
|
||||||
image: docker:cli
|
image: ${REG}/${REPO_OWNER}/mcp-docker-server:latest
|
||||||
container_name: ${APP_NAME}-mcp-docker
|
container_name: ${APP_NAME}-mcp-docker
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: mcp gateway run --port 8080 --transport streaming
|
environment:
|
||||||
|
- PORT=3000
|
||||||
volumes:
|
volumes:
|
||||||
# Montar el socket de Docker para acceso al daemon
|
# Montar el socket de Docker para acceso al daemon
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||||
@@ -65,15 +66,15 @@ services:
|
|||||||
- "traefik.docker.network=traefik-network"
|
- "traefik.docker.network=traefik-network"
|
||||||
|
|
||||||
# Service
|
# Service
|
||||||
- "traefik.http.services.${APP_NAME}-mcp.loadbalancer.server.port=8080"
|
- "traefik.http.services.${APP_NAME}-mcp.loadbalancer.server.port=3000"
|
||||||
|
|
||||||
# Router sin autenticación
|
# Router sin autenticación con PathPrefix para /mcp
|
||||||
- "traefik.http.routers.${APP_NAME}-mcp.rule=Host(`${MCP_DOMAIN}`)"
|
- "traefik.http.routers.${APP_NAME}-mcp.rule=Host(`${MCP_DOMAIN}`) && PathPrefix(`/mcp`)"
|
||||||
- "traefik.http.routers.${APP_NAME}-mcp.entrypoints=websecure"
|
- "traefik.http.routers.${APP_NAME}-mcp.entrypoints=websecure"
|
||||||
- "traefik.http.routers.${APP_NAME}-mcp.tls.certresolver=letsencrypt"
|
- "traefik.http.routers.${APP_NAME}-mcp.tls.certresolver=letsencrypt"
|
||||||
- "traefik.http.routers.${APP_NAME}-mcp.service=${APP_NAME}-mcp"
|
- "traefik.http.routers.${APP_NAME}-mcp.service=${APP_NAME}-mcp"
|
||||||
|
|
||||||
# Headers personalizados para WebSocket y streaming
|
# Headers personalizados para MCP
|
||||||
- "traefik.http.middlewares.${APP_NAME}-mcp-headers.headers.customrequestheaders.X-Forwarded-Proto=https"
|
- "traefik.http.middlewares.${APP_NAME}-mcp-headers.headers.customrequestheaders.X-Forwarded-Proto=https"
|
||||||
- "traefik.http.routers.${APP_NAME}-mcp.middlewares=${APP_NAME}-mcp-headers"
|
- "traefik.http.routers.${APP_NAME}-mcp.middlewares=${APP_NAME}-mcp-headers"
|
||||||
|
|
||||||
|
|||||||
5
mcp-docker-server/.gitignore
vendored
Normal file
5
mcp-docker-server/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
.env
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
22
mcp-docker-server/Dockerfile
Normal file
22
mcp-docker-server/Dockerfile
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
FROM node:20-alpine
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copiar archivos de dependencias
|
||||||
|
COPY package*.json ./
|
||||||
|
COPY tsconfig.json ./
|
||||||
|
|
||||||
|
# Instalar dependencias
|
||||||
|
RUN npm ci
|
||||||
|
|
||||||
|
# Copiar código fuente
|
||||||
|
COPY src ./src
|
||||||
|
|
||||||
|
# Compilar TypeScript
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Exponer puerto
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Comando de inicio
|
||||||
|
CMD ["npm", "start"]
|
||||||
50
mcp-docker-server/README.md
Normal file
50
mcp-docker-server/README.md
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# MCP Docker Server
|
||||||
|
|
||||||
|
Servidor MCP (Model Context Protocol) para operaciones Docker seguras.
|
||||||
|
|
||||||
|
## Herramientas Disponibles
|
||||||
|
|
||||||
|
### Información y Monitoreo
|
||||||
|
- `docker_ps` - Listar contenedores
|
||||||
|
- `docker_logs` - Ver logs de contenedores
|
||||||
|
- `docker_inspect` - Inspeccionar contenedor
|
||||||
|
- `docker_stats` - Estadísticas de uso de recursos
|
||||||
|
- `docker_top` - Procesos del contenedor
|
||||||
|
- `docker_info` - Información del sistema Docker
|
||||||
|
|
||||||
|
### Gestión de Contenedores
|
||||||
|
- `docker_start` - Iniciar contenedor
|
||||||
|
- `docker_stop` - Detener contenedor
|
||||||
|
- `docker_restart` - Reiniciar contenedor
|
||||||
|
- `docker_exec` - Ejecutar comando en contenedor
|
||||||
|
|
||||||
|
### Recursos
|
||||||
|
- `docker_images` - Listar imágenes
|
||||||
|
- `docker_networks` - Listar redes
|
||||||
|
- `docker_volumes` - Listar volúmenes
|
||||||
|
|
||||||
|
## Desarrollo
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## Producción
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
## Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker build -t mcp-docker-server .
|
||||||
|
docker run -v /var/run/docker.sock:/var/run/docker.sock:ro -p 3000:3000 mcp-docker-server
|
||||||
|
```
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
- `POST /mcp` - Endpoint MCP principal
|
||||||
|
- `GET /health` - Health check
|
||||||
25
mcp-docker-server/package.json
Normal file
25
mcp-docker-server/package.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"name": "mcp-docker-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "MCP Server para operaciones Docker",
|
||||||
|
"type": "module",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"dev": "tsx src/index.ts"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@modelcontextprotocol/sdk": "^1.0.4",
|
||||||
|
"dockerode": "^4.0.2",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"zod": "^3.24.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/dockerode": "^3.3.34",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/node": "^22.10.5",
|
||||||
|
"tsx": "^4.19.2",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
533
mcp-docker-server/src/index.ts
Normal file
533
mcp-docker-server/src/index.ts
Normal 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);
|
||||||
|
});
|
||||||
20
mcp-docker-server/tsconfig.json
Normal file
20
mcp-docker-server/tsconfig.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ES2022",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user