From 89b2b1b13d069246ca43ff9a67edd3457ca08ae2 Mon Sep 17 00:00:00 2001 From: Michal Date: Sat, 21 Feb 2026 13:34:18 +0000 Subject: [PATCH] feat: add Docker bootstrap for mcpd with auto-migration and seeding Adds Dockerfile, entrypoint, and server bootstrap so that `docker compose up` starts postgres, pushes the schema, seeds default MCP servers, and starts mcpd with all routes wired up. Co-Authored-By: Claude Opus 4.6 --- .dockerignore | 15 ++++++ deploy/Dockerfile.mcpd | 61 +++++++++++++++++++++ deploy/docker-compose.yml | 11 ++-- deploy/entrypoint.sh | 11 ++++ src/mcpd/package.json | 2 +- src/mcpd/src/main.ts | 104 ++++++++++++++++++++++++++++++++++++ src/mcpd/src/seed-runner.ts | 17 ++++++ 7 files changed, 216 insertions(+), 5 deletions(-) create mode 100644 .dockerignore create mode 100644 deploy/Dockerfile.mcpd create mode 100755 deploy/entrypoint.sh create mode 100644 src/mcpd/src/main.ts create mode 100644 src/mcpd/src/seed-runner.ts diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..f079e53 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,15 @@ +node_modules +*/node_modules +**/node_modules +dist +**/dist +.git +.taskmaster +.claude +*.md +!pnpm-workspace.yaml +.env +.env.* +deploy/docker-compose.yml +src/cli +src/local-proxy diff --git a/deploy/Dockerfile.mcpd b/deploy/Dockerfile.mcpd new file mode 100644 index 0000000..6139eae --- /dev/null +++ b/deploy/Dockerfile.mcpd @@ -0,0 +1,61 @@ +# Stage 1: Build TypeScript +FROM node:20-alpine AS builder + +RUN corepack enable && corepack prepare pnpm@9.15.0 --activate + +WORKDIR /app + +# Copy workspace config and package manifests +COPY pnpm-workspace.yaml pnpm-lock.yaml package.json tsconfig.base.json ./ +COPY src/mcpd/package.json src/mcpd/tsconfig.json src/mcpd/ +COPY src/db/package.json src/db/tsconfig.json src/db/ +COPY src/shared/package.json src/shared/tsconfig.json src/shared/ + +# Install all dependencies +RUN pnpm install --frozen-lockfile + +# Copy source code +COPY src/mcpd/src/ src/mcpd/src/ +COPY src/db/src/ src/db/src/ +COPY src/db/prisma/ src/db/prisma/ +COPY src/shared/src/ src/shared/src/ + +# Generate Prisma client and build TypeScript +RUN pnpm -F @mcpctl/db db:generate +RUN pnpm -F @mcpctl/shared build && pnpm -F @mcpctl/db build && pnpm -F @mcpctl/mcpd build + +# Stage 2: Production runtime +FROM node:20-alpine + +RUN corepack enable && corepack prepare pnpm@9.15.0 --activate + +WORKDIR /app + +# Copy workspace config, manifests, and lockfile +COPY pnpm-workspace.yaml pnpm-lock.yaml package.json ./ +COPY src/mcpd/package.json src/mcpd/ +COPY src/db/package.json src/db/ +COPY src/shared/package.json src/shared/ + +# Install all deps (prisma CLI needed at runtime for db push) +RUN pnpm install --frozen-lockfile + +# Copy prisma schema and generate client +COPY src/db/prisma/ src/db/prisma/ +RUN pnpm -F @mcpctl/db db:generate + +# Copy built output from builder +COPY --from=builder /app/src/shared/dist/ src/shared/dist/ +COPY --from=builder /app/src/db/dist/ src/db/dist/ +COPY --from=builder /app/src/mcpd/dist/ src/mcpd/dist/ + +# Copy entrypoint +COPY deploy/entrypoint.sh /entrypoint.sh +RUN chmod +x /entrypoint.sh + +EXPOSE 3100 + +HEALTHCHECK --interval=10s --timeout=5s --retries=3 --start-period=10s \ + CMD wget -q --spider http://localhost:3100/healthz || exit 1 + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index 5437c2b..cb1199b 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -27,14 +27,17 @@ services: - "3100:3100" environment: DATABASE_URL: postgresql://mcpctl:mcpctl_dev@postgres:5432/mcpctl - PORT: "3100" - HOST: "0.0.0.0" - LOG_LEVEL: info + MCPD_PORT: "3100" + MCPD_HOST: "0.0.0.0" + MCPD_LOG_LEVEL: info depends_on: postgres: condition: service_healthy volumes: - - /var/run/docker.sock:/var/run/docker.sock + # Mount container runtime socket (Docker or Podman) + # For Docker: /var/run/docker.sock + # For Podman: /run/user//podman/podman.sock + - ${CONTAINER_SOCK:-/var/run/docker.sock}:/var/run/docker.sock networks: - mcpctl - mcp-servers diff --git a/deploy/entrypoint.sh b/deploy/entrypoint.sh new file mode 100755 index 0000000..74f314c --- /dev/null +++ b/deploy/entrypoint.sh @@ -0,0 +1,11 @@ +#!/bin/sh +set -e + +echo "mcpd: pushing database schema..." +pnpm -F @mcpctl/db exec prisma db push --schema=prisma/schema.prisma --accept-data-loss 2>&1 + +echo "mcpd: seeding default data..." +node src/mcpd/dist/seed-runner.js + +echo "mcpd: starting server..." +exec node src/mcpd/dist/main.js diff --git a/src/mcpd/package.json b/src/mcpd/package.json index 1750f80..d35abb4 100644 --- a/src/mcpd/package.json +++ b/src/mcpd/package.json @@ -9,7 +9,7 @@ "build": "tsc --build", "clean": "rimraf dist", "dev": "tsx watch src/index.ts", - "start": "node dist/index.js", + "start": "node dist/main.js", "test": "vitest", "test:run": "vitest run" }, diff --git a/src/mcpd/src/main.ts b/src/mcpd/src/main.ts new file mode 100644 index 0000000..b2b2be1 --- /dev/null +++ b/src/mcpd/src/main.ts @@ -0,0 +1,104 @@ +import { PrismaClient } from '@prisma/client'; +import { seedMcpServers } from '@mcpctl/db'; +import { loadConfigFromEnv } from './config/index.js'; +import { createServer } from './server.js'; +import { setupGracefulShutdown } from './utils/index.js'; +import { + McpServerRepository, + McpProfileRepository, + McpInstanceRepository, + ProjectRepository, + AuditLogRepository, +} from './repositories/index.js'; +import { + McpServerService, + McpProfileService, + InstanceService, + ProjectService, + AuditLogService, + DockerContainerManager, + MetricsCollector, + HealthAggregator, + BackupService, + RestoreService, +} from './services/index.js'; +import { + registerMcpServerRoutes, + registerMcpProfileRoutes, + registerInstanceRoutes, + registerProjectRoutes, + registerAuditLogRoutes, + registerHealthMonitoringRoutes, + registerBackupRoutes, +} from './routes/index.js'; + +async function main(): Promise { + const config = loadConfigFromEnv(); + + // Database + const prisma = new PrismaClient({ + datasources: { db: { url: config.databaseUrl } }, + }); + await prisma.$connect(); + + // Seed default servers (upsert, safe to repeat) + await seedMcpServers(prisma); + + // Repositories + const serverRepo = new McpServerRepository(prisma); + const profileRepo = new McpProfileRepository(prisma); + const instanceRepo = new McpInstanceRepository(prisma); + const projectRepo = new ProjectRepository(prisma); + const auditLogRepo = new AuditLogRepository(prisma); + + // Orchestrator + const orchestrator = new DockerContainerManager(); + + // Services + const serverService = new McpServerService(serverRepo); + const profileService = new McpProfileService(profileRepo, serverRepo); + const instanceService = new InstanceService(instanceRepo, serverRepo, orchestrator); + const projectService = new ProjectService(projectRepo, profileRepo, serverRepo); + const auditLogService = new AuditLogService(auditLogRepo); + const metricsCollector = new MetricsCollector(); + const healthAggregator = new HealthAggregator(metricsCollector, orchestrator); + const backupService = new BackupService(serverRepo, profileRepo, projectRepo); + const restoreService = new RestoreService(serverRepo, profileRepo, projectRepo); + + // Server + const app = await createServer(config, { + health: { + checkDb: async () => { + try { + await prisma.$queryRaw`SELECT 1`; + return true; + } catch { + return false; + } + }, + }, + }); + + // Routes + registerMcpServerRoutes(app, serverService); + registerMcpProfileRoutes(app, profileService); + registerInstanceRoutes(app, instanceService); + registerProjectRoutes(app, projectService); + registerAuditLogRoutes(app, auditLogService); + registerHealthMonitoringRoutes(app, { healthAggregator, metricsCollector }); + registerBackupRoutes(app, { backupService, restoreService }); + + // Start + await app.listen({ port: config.port, host: config.host }); + app.log.info(`mcpd listening on ${config.host}:${config.port}`); + + // Graceful shutdown + setupGracefulShutdown(app, { + disconnectDb: () => prisma.$disconnect(), + }); +} + +main().catch((err) => { + console.error('Failed to start mcpd:', err); + process.exit(1); +}); diff --git a/src/mcpd/src/seed-runner.ts b/src/mcpd/src/seed-runner.ts new file mode 100644 index 0000000..4977c3c --- /dev/null +++ b/src/mcpd/src/seed-runner.ts @@ -0,0 +1,17 @@ +import { PrismaClient } from '@prisma/client'; +import { seedMcpServers } from '@mcpctl/db'; + +async function run(): Promise { + const prisma = new PrismaClient(); + try { + const count = await seedMcpServers(prisma); + console.log(`Seeded ${count} MCP servers`); + } finally { + await prisma.$disconnect(); + } +} + +run().catch((err) => { + console.error('Seed failed:', err); + process.exit(1); +});