Skip to main content

Command Palette

Search for a command to run...

How to Self-Host a Docker MCP Server in Production

Copy-paste guide for self-hosting the Docker MCP server in production: health checks, auto-restart, Compose lifecycle, fleet monitoring, and log streaming.

Updated
17 min read

How to Self-Host a Docker MCP Server in Production

A copy-paste-ready guide for running the Docker MCP server in production. Built around the server's actual strengths: health checks, auto-restart, Compose lifecycle, fleet monitoring, and log streaming.


Most Docker MCP deployment guides stop at npx @supernova123/docker-mcp-server and call it a day. That works for a laptop. It does not work for a server that has to survive reboots, handle 50+ containers, and stay healthy at 3am when nobody is watching.

This guide covers what production actually looks like: Compose stacks with restart policies, systemd units for bare-metal, reverse proxies with auto-TLS, monitoring, backups, security hardening, and tuning. All configs are real. Copy, paste, edit the hostnames.

Quick Start (30 seconds):

``bash

npx @supernova123/docker-mcp-server

`

Or use Docker Compose — copy the config from Section 1, run docker compose up -d, and your self-hosted Docker MCP server is live.

One caveat up front: the Docker MCP server speaks the MCP stdio transport by default. It works with all major MCP clients — Claude Desktop, Cursor, VS Code Copilot, Claude Code, and ChatGPT (Developer Mode). If you want it reachable over the network (browser agents, remote clients, HTTP-based MCP clients), bridge it with supergateway. All the HTTP/reverse-proxy/TLS sections below assume that bridge. If you only need local stdio from a sibling process, skip straight to the systemd unit.


1. Docker Compose Setup

The most common production pattern: run the MCP server in a container, mount the Docker socket, give it restart-on-failure, and let it manage the rest of the host's containers.

docker-compose.yml: `yaml

services:

docker-mcp:

image: node:22-slim

container_name: docker-mcp-server

restart: unless-stopped

init: true

working_dir: /srv/mcp

command: ["npx", "-y", "@supernova123/docker-mcp-server@0.2.3"]

environment:

# Optional: point at a remote Docker daemon

DOCKER_HOST: unix:///var/run/docker.sock

# Logging

NODE_ENV: production

LOG_LEVEL: info

MCP_LOG_FORMAT: json

# Optional: cap how long stream_logs / watch_health will block

MCP_DEFAULT_TIMEOUT_MS: "30000"

volumes:

- /var/run/docker.sock:/var/run/docker.sock:ro

- /srv/mcp/compose:/compose:ro

- mcp-data:/srv/mcp/data

# The server uses stdio, so no port mapping is required for stdio transport.

# See Section 3 if you need HTTP/SSE exposure via supergateway.

healthcheck:

test: ["CMD", "pgrep", "-f", "docker-mcp-server"]

interval: 30s

timeout: 5s

retries: 3

start_period: 20s

deploy:

resources:

limits:

cpus: "0.50"

memory: 256M

reservations:

cpus: "0.10"

memory: 64M

logging:

driver: json-file

options:

max-size: "10m"

max-file: "5"

# Optional HTTP/SSE bridge for browser/remote MCP clients

supergateway:

image: node:22-slim

container_name: docker-mcp-gateway

restart: unless-stopped

init: true

command:

- "npx"

- "-y"

"supergateway"

- "--stdio"

- "npx -y @supernova123/docker-mcp-server@0.2.3"

- "--port"

- "8811"

- "--healthEndpoint"

- "/healthz"

environment:

DOCKER_HOST: unix:///var/run/docker.sock

volumes:

- /var/run/docker.sock:/var/run/docker.sock:ro

ports:

- "127.0.0.1:8811:8811"

depends_on:

- docker-mcp

healthcheck:

test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8811/healthz"]

interval: 30s

timeout: 5s

retries: 3

deploy:

resources:

limits:

cpus: "0.25"

memory: 128M

logging:

driver: json-file

options:

max-size: "10m"

max-file: "5"

volumes:

mcp-data:

` Why these choices:

  • restart: unless-stopped — Survives Docker daemon restarts and host reboots. Use always only if the server must come up even when manually stopped (rare).

  • init: true — Adds a tiny PID 1 init that reaps zombie processes. Cheap insurance for long-lived Node processes.

  • /var/run/docker.sock:ro — Read-only is intentional. The MCP server's own set_restart_policy and lifecycle tools call the Docker API, which doesn't require write access to the socket. If you find a tool that needs write, mount :rw and accept the risk.

  • /compose:ro — Mount the directory containing your docker-compose.yml files as read-only. The compose_up / compose_down tools need a project directory; this is how they find one.

  • healthcheck — The stdio server has no HTTP endpoint, so we check the process. If you run the supergateway bridge, check /healthz instead.

Bring it up:

`bash

docker compose pull

docker compose up -d

docker compose logs -f docker-mcp

`


2. systemd Service Unit (Bare-Metal)

Sometimes you want the MCP server on the host itself — no nested container, no socket mount, just node talking to local Docker. systemd handles supervision better than Compose for single-process services.

/etc/systemd/system/docker-mcp.service: `ini

[Unit]

Description=Docker MCP Server (@supernova123/docker-mcp-server)

Documentation=https://github.com/friendlygeorge/docker-mcp-server

After=network-online.target docker.service

Wants=network-online.target

Requires=docker.service

[Service]

Type=simple

User=mcp

Group=docker

WorkingDirectory=/opt/docker-mcp-server

Environment=NODE_ENV=production

Environment=LOG_LEVEL=info

Environment=MCP_LOG_FORMAT=json

Environment=MCP_DEFAULT_TIMEOUT_MS=30000

ExecStart=/usr/bin/npx -y @supernova123/docker-mcp-server@0.2.3

Restart=always

RestartSec=5

TimeoutStopSec=20

KillMode=mixed

KillSignal=SIGTERM

Hardening (see Section 7 for the full reasoning)

NoNewPrivileges=true

PrivateTmp=true

ProtectSystem=strict

ProtectHome=true

ReadWritePaths=/opt/docker-mcp-server /var/log/docker-mcp

ProtectKernelTunables=true

ProtectKernelModules=true

ProtectControlGroups=true

RestrictNamespaces=true

RestrictRealtime=true

RestrictSUIDSGID=true

LockPersonality=true

MemoryDenyWriteExecute=false

SystemCallArchitectures=native

Resource limits

LimitNOFILE=65536

LimitNPROC=4096

MemoryMax=256M

CPUQuota=50%

Logging to journald

StandardOutput=journal

StandardError=journal

SyslogIdentifier=docker-mcp

[Install]

WantedBy=multi-user.target

`

Setup:

`bash

useradd -r -s /usr/sbin/nologin -G docker mcp

mkdir -p /opt/docker-mcp-server /var/log/docker-mcp

chown -R mcp:mcp /opt/docker-mcp-server /var/log/docker-mcp

systemctl daemon-reload

systemctl enable --now docker-mcp

systemctl status docker-mcp

journalctl -u docker-mcp -f

`

The Requires=docker.service line guarantees Docker is up before MCP tries to connect. The Restart=always + RestartSec=5 pair gives the daemon a 5-second breathing room between restart attempts — short enough to recover fast, long enough to avoid tight crash loops.


3. Reverse Proxy — Caddy (Auto-TLS)

Caddy is the right choice here. It provisions and renews Let's Encrypt certificates automatically with zero configuration. The supergateway bridge listens on 127.0.0.1:8811; Caddy fronts it with TLS.

/etc/caddy/Caddyfile: `caddyfile

mcp.example.com {

encode gzip zstd

reverse_proxy 127.0.0.1:8811 {

# SSE / streamable-http needs long-lived connections

flush_interval -1

transport http {

dial_timeout 5s

response_header_timeout 0s

read_timeout 0s

write_timeout 0s

}

}

# Health endpoint for uptime monitors

handle /healthz {

reverse_proxy 127.0.0.1:8811

}

# Block anything that isn't MCP

@blocked {

not path /sse /message /healthz *

}

respond @blocked "Not Found" 404

log {

output file /var/log/caddy/mcp.log {

roll_size 50mb

roll_keep 10

}

level INFO

}

# Sane TLS defaults (Caddy already does TLS 1.2+ by default)

header {

Strict-Transport-Security "max-age=63072000; includeSubDomains; preload"

X-Content-Type-Options "nosniff"

X-Frame-Options "DENY"

Referrer-Policy "no-referrer"

# Hide the upstream

-Server

}

}

`

Reload:

`bash

systemctl reload caddy

curl -sI https://mcp.example.com/healthz

`

The trick here is flush_interval -1 and the zeroed read/write timeouts. MCP's Streamable HTTP transport holds connections open for server-sent events; default proxy timeouts will kill the stream mid-tool-call. Caddy defaults are usually fine, but pinning them is safer.

Nginx Alternative

If you must use Nginx (compliance reasons, existing infra), here's the equivalent config:

/etc/nginx/sites-available/docker-mcp.conf: `nginx

upstream docker_mcp_upstream {

server 127.0.0.1:8811;

keepalive 16;

}

server {

listen 80;

server_name mcp.example.com;

return 301 https://\(host\)request_uri;

}

server {

listen 443 ssl http2;

server_name mcp.example.com;

ssl_certificate /etc/letsencrypt/live/mcp.example.com/fullchain.pem;

ssl_certificate_key /etc/letsencrypt/live/mcp.example.com/privkey.pem;

ssl_protocols TLSv1.2 TLSv1.3;

ssl_ciphers HIGH:!aNULL:!MD5;

ssl_prefer_server_ciphers on;

add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;

add_header X-Content-Type-Options "nosniff" always;

add_header X-Frame-Options "DENY" always;

# ACME challenge

location /.well-known/acme-challenge/ {

root /var/www/html;

}

# SSE endpoint

location /sse {

proxy_pass http://docker_mcp_upstream;

proxy_http_version 1.1;

proxy_set_header Connection '';

proxy_set_header Host $host;

proxy_set_header X-Real-IP $remote_addr;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

proxy_buffering off;

proxy_cache off;

proxy_read_timeout 86400s;

proxy_send_timeout 86400s;

}

# Streamable HTTP / message endpoint

location /message {

proxy_pass http://docker_mcp_upstream;

proxy_http_version 1.1;

proxy_set_header Connection '';

proxy_set_header Host $host;

proxy_set_header X-Real-IP $remote_addr;

proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

}

location = /healthz {

proxy_pass http://docker_mcp_upstream;

access_log off;

}

location / {

return 404;

}

}

` `bash

ln -s /etc/nginx/sites-available/docker-mcp.conf /etc/nginx/sites-enabled/

nginx -t && systemctl reload nginx

`


4. TLS / HTTPS

Caddy handles this transparently — point the Caddyfile at a real DNS name and Caddy issues a cert from Let's Encrypt within seconds. Verify the chain:

`bash

echo | openssl s_client -connect mcp.example.com:443 -servername mcp.example.com 2>/dev/null | openssl x509 -noout -subject -issuer -dates

`

If you're on Nginx and using Certbot:

`bash

certbot --nginx -d mcp.example.com

certbot renew --dry-run

`

For an internal/CA-signed cert (air-gapped environments):

`bash

openssl req -x509 -nodes -days 825 -newkey rsa:2048 \

-keyout /etc/ssl/private/mcp.key \

-out /etc/ssl/certs/mcp.crt \

-subj "/CN=mcp.internal.lan" \

-addext "subjectAltName=DNS:mcp.internal.lan"

`

Then point your Caddyfile/Nginx config at those files. The 825-day cap matches the current browser/CA lifetime ceiling.


5. Health Monitoring and Alerting

Two layers: liveness (is the process running?) and semantics (is the underlying Docker fleet healthy?). The MCP server exposes tools for the second layer — use them.

Layer 1: Process liveness

Prometheus blackbox-style check against /healthz:

/etc/prometheus/rules/mcp.yml: `yaml

groups:

- name: docker-mcp

rules:

- alert: DockerMCPDown

expr: probe_success{job="docker-mcp"} == 0

for: 2m

labels:

severity: critical

annotations:

summary: "Docker MCP server unreachable"

description: "mcp.example.com has failed health checks for 2 minutes."

- alert: DockerMCPHighLatency

expr: probe_duration_seconds{job="docker-mcp"} > 1

for: 5m

labels:

severity: warning

annotations:

summary: "Docker MCP health check slow"

description: "Health probe averaging {{ $value }}s."

`

Prometheus scrape job:

`yaml

scrape_configs:

- job_name: docker-mcp

metrics_path: /healthz

scheme: https

static_configs:

- targets: ["mcp.example.com"]

relabel_configs:

- source_labels: [__address__]

target_label: __param_target

- source_labels: [__param_target]

target_label: instance

- target_label: __address__

replacement: blackbox-exporter:9115

`

Layer 2: Fleet semantics via MCP tools

The MCP server's check_thresholds and fleet_status tools are made for this. Run them on a cron, parse the JSON, alert on violations.

/opt/docker-mcp-server/monitor/fleet-check.sh: `bash

#!/usr/bin/env bash

set -euo pipefail

Call the MCP server's monitor_dashboard tool via stdio, pipe the JSON out

DASHBOARD=$(npx -y @supernova123/docker-mcp-server@0.2.3 <<'EOF' 2>/dev/null

{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"monitor_dashboard","arguments":{}}}

EOF

)

Count restart-loops and memory violations

VIOLATIONS=\((echo "\)DASHBOARD" | jq '.content[0].text | fromjson | .violations | length')

UNHEALTHY=\((echo "\)DASHBOARD" | jq '.content[0].text | fromjson | .unhealthy_count')

if [ "\(VIOLATIONS" -gt 0 ] || [ "\)UNHEALTHY" -gt 0 ]; then

curl -fsS -X POST \

-H "Content-Type: application/json" \

-d "{\"text\":\"Docker MCP alert: \(VIOLATIONS violations, \)UNHEALTHY unhealthy containers\"}" \

"$ALERT_WEBHOOK_URL"

fi

` `cron /5 * mcp /opt/docker-mcp-server/monitor/fleet-check.sh `

For richer observability, push the dashboard to a TSDB:

`bash

curl -X POST http://prometheus:9091/metrics/job/docker_mcp_fleet \

--data-binary "$DASHBOARD_METRICS"

`

This pattern matters because it uses the MCP server's own fleet intelligence to monitor itself. A naive /healthz returns 200 even when every container it manages is in a restart loop. monitor_dashboard doesn't.


6. Backup and Restore

The MCP server itself is stateless — it stores no data, has no database. What you back up is the configuration it manages: the docker-compose.yml files, any persistent volumes the underlying containers use, and the Docker daemon's metadata.

What to back up

Asset

Where

Why

Compose project files

/srv/mcp/compose/*/docker-compose.yml

Reconstruct stacks

Named volumes

/var/lib/docker/volumes/*

Persistent data of managed containers

Docker daemon config

/etc/docker/daemon.json

Network, storage driver, log driver

MCP server config

/etc/systemd/system/docker-mcp.service or docker-compose.yml

Reproduce the deployment

TLS certs

/etc/letsencrypt/live/* (or Caddy data dir)

If Caddy/Nginx, not needed if Caddy manages

Backup script

/opt/docker-mcp-server/backup/backup.sh: `bash

#!/usr/bin/env bash

set -euo pipefail

BACKUP_ROOT=/var/backups/docker-mcp

DATE=$(date -u +%Y%m%dT%H%M%SZ)

DEST="\(BACKUP_ROOT/\)DATE"

mkdir -p "$DEST"

1. Compose project files

tar czf "$DEST/compose-projects.tar.gz" /srv/mcp/compose/

2. Named volumes (export each one)

docker run --rm \

-v docker-mcp_mcp-data:/source:ro \

-v "$DEST":/backup \

alpine sh -c "tar czf /backup/mcp-data.tar.gz -C /source ."

3. Docker daemon config

cp /etc/docker/daemon.json "$DEST/daemon.json"

4. List of named volumes (for reference during restore)

docker volume ls --format '{{.Name}}' > "$DEST/volumes.list"

5. Rotate: keep 14 days

find "$BACKUP_ROOT" -maxdepth 1 -type d -mtime +14 -exec rm -rf {} \;

echo "Backup completed: $DEST"

` `cron

0 3 * root /opt/docker-mcp-server/backup/backup.sh

`

Push to S3 (or any S3-compatible store) for off-host safety:

`bash

aws s3 sync /var/backups/docker-mcp/ s3://my-bucket/docker-mcp/ --delete

`

Restore

`bash

1. Restore compose project files

tar xzf compose-projects.tar.gz -C /

2. Recreate the named volume and import

docker volume create docker-mcp_mcp-data

docker run --rm \

-v docker-mcp_mcp-data:/target \

-v $(pwd):/backup:ro \

alpine sh -c "tar xzf /backup/mcp-data.tar.gz -C /target"

3. Restart everything

systemctl restart docker-mcp

docker compose -f /srv/mcp/compose//docker-compose.yml up -d

`

Test restores quarterly. A backup you haven't restored from is a backup you don't have.


7. Security Hardening

The MCP server runs commands against the Docker daemon. That is privileged by definition. Treat the host as if it has shell on every container.

Socket access

  • Read-only socket mount (:ro) when the tool set allows. The server's own write tools (set_restart_policy, start_container, etc.) go through the Docker API, not the socket filesystem, so this is usually viable.

  • Group-based access on bare metal: only the docker group and the mcp user can reach the socket.

`bash

chown root:docker /var/run/docker.sock

chmod 660 /var/run/docker.sock

`

systemd hardening (already in the unit, with rationale)

Directive

Why

NoNewPrivileges=true

Blocks setuid binaries from gaining privilege

ProtectSystem=strict

Makes /usr, /boot, /efi read-only

ProtectHome=true

No access to /home, /root, /run/user

ReadWritePaths=...

Explicit allowlist for the few paths we need

PrivateTmp=true

Isolated /tmp namespace

ProtectKernelTunables=true

Can't write to /proc/sysctl

ProtectKernelModules=true

Can't load kernel modules

ProtectControlGroups=true

Can't manipulate cgroups

RestrictNamespaces=true

No new mount/PID/network namespaces

RestrictRealtime=true

No realtime scheduling

RestrictSUIDSGID=true

No SUID/SGID bits on temp files

LockPersonality=true

Can't change process execution domain

SystemCallArchitectures=native

Seccomp filter for non-native syscalls

The Docker socket is the privilege boundary here. Everything else is defense in depth around it.

Network exposure

  • Bind the supergateway bridge to 127.0.0.1, never 0.0.0.0. Caddy/Nginx front it.

  • Firewall the host: only 22, 80, 443 open to the world. Block 8811.

`bash

ufw default deny incoming

ufw allow 22/tcp

ufw allow 80/tcp

ufw allow 443/tcp

ufw enable

`

  • Use fail2ban for SSH at minimum.

  • Rotate TLS certs (Caddy does this automatically; Certbot does this automatically; manual certs need a cron).

Authentication on the HTTP bridge

supergateway does not authenticate by default. If your MCP endpoint is reachable, anyone can call set_restart_policy and remove_container. Front it with an auth layer: `caddyfile

mcp.example.com {

basicauth {

mcp \(2a\)14$abc... # bcrypt hash from: caddy hash-password

}

reverse_proxy 127.0.0.1:8811

}

`

For token-based auth (better for MCP clients), wrap with a small reverse-auth proxy or use Caddy's forward_auth directive pointing at an OAuth proxy like oauth2-proxy.

Secret hygiene

  • Never log the full tool output for exec_in_container calls — it may include secrets.

  • Scrub environment variables in logs:

`bash

journalctl -u docker-mcp | sed -E 's/(API_KEY|SECRET|TOKEN)=[^ ]+/\1=REDACTED/g'

`

Updates

`bash

Container

docker compose pull && docker compose up -d

Bare metal

npm install -g @supernova123/docker-mcp-server@latest

systemctl restart docker-mcp

`

Pin to specific versions in production (see the 0.2.3 in the configs above). Auto-updating a privileged service is a CVE away from an outage.


8. Performance Tuning

The MCP server itself is lightweight. The heavy lifting is the Docker API calls. Most performance issues are fleet issues, not server issues.

Server-side tuning

  • Memory: 256MB is plenty for a single-instance server managing <100 containers. Increase to 512MB if you run multiple Compose projects with frequent compose_logs streams.

  • CPU: 0.5 cores handles thousands of tool calls per minute. Bottlenecks will be the Docker daemon, not the MCP server.

  • MCP_DEFAULT_TIMEOUT_MS: Default 30s is fine for most tools. watch_health and stream_logs may need higher; pass per-call when you need them.

  • JSON logging: Set MCP_LOG_FORMAT=json in production. Easier to ship to Loki, Splunk, or CloudWatch.

Docker daemon tuning

/etc/docker/daemon.json: `json

{

"log-driver": "json-file",

"log-opts": {

"max-size": "10m",

"max-file": "5"

},

"storage-driver": "overlay2",

"default-ulimits": {

"nofile": {

"Name": "nofile",

"Hard": 65536,

"Soft": 65536

}

},

"live-restore": true,

"userland-proxy": false,

"experimental": false,

"metrics-addr": "127.0.0.1:9323",

"features": {

"containerd-snapshotter": false

}

}

`

  • live-restore: true — Lets containers survive a Docker daemon restart. Critical for uptime.

  • metrics-addr — Exposes Docker's own Prometheus metrics on 127.0.0.1:9323. Combine with the MCP server's fleet_stats for full coverage.

  • max-size + max-file — Caps container log growth. Without it, a chatty container will fill /var/lib/docker.

MCP tool usage patterns

The check_thresholds and monitor_dashboard tools are designed for repeated polling — use them, don't reimplement. Specifically:

  • Prefer monitor_dashboard over calling fleet_status + fleet_stats + watch_events separately. It batches everything in one call.

  • Use check_thresholds with explicit thresholds rather than scraping container_stats and computing in your agent. Lower token cost, faster responses.

  • Stream logs with stream_logs and a tail cap. Asking for 10,000 lines is expensive; ask for 100 and grep.

  • Set set_restart_policy proactively on critical containers. restart: always is the difference between an outage and a self-healed one.

Capacity planning

Managed containers

MCP memory

MCP CPU

Notes

< 25

128MB

0.25

Single tenant, light use

25–100

256MB

0.50

Recommended default

100–500

512MB

1.00

Multiple Compose projects

500+

1GB+

2.00

Consider sharding by host

The MCP server is stateless and can run multiple instances against the same Docker daemon, but in practice one well-tuned instance handles 500+ containers without breaking a sweat. The real scaling bottleneck is the Docker daemon's own API concurrency, not the server.


TL;DR

  • Container? Use the Compose file in Section 1 with restart: unless-stopped, :ro socket, and a healthcheck.

  • Bare metal? Drop the systemd unit in Section 2, with the hardening directives.

  • Need HTTP/SSE? Bridge with supergateway, front it with Caddy, get TLS for free.

  • Fleet intel beats process healthchecks — use monitor_dashboard and check_thresholds.

  • Back up compose files and volumes, not the MCP server — it's stateless.

  • The Docker socket is the privilege boundary — treat it as root.

The Docker MCP server's whole reason to exist is that an agent can run a fleet autonomously. Production-grade deployment is what makes that autonomy safe.


FAQ

What is a Docker MCP server?

A Docker MCP server is a Model Context Protocol server that manages Docker containers, images, and compose stacks. It lets AI agents like Claude Desktop, Cursor, and ChatGPT control your Docker infrastructure through natural language commands.

How do I install the Docker MCP server?

Run npx @supernova123/docker-mcp-server` for a quick start, or use the Docker Compose config in Section 1 for a self-hosted production deployment. The server requires Docker installed on the host machine.

Can I self-host the Docker MCP server?

Yes. See Section 1 (Docker Compose) and Section 2 (systemd) for self-hosting options. The server runs on any Linux host with Docker installed. It manages containers, images, and compose stacks on the same host.

What MCP clients are supported?

The Docker MCP server works with Claude Desktop, Claude Code, Cursor, VS Code Copilot, ChatGPT (via Developer Mode), and any MCP-compatible client. See the README for client-specific config examples.

How do I expose the Docker MCP server over HTTPS?

Use supergateway to bridge stdio to HTTP/SSE, then put Caddy or Nginx in front for auto-TLS. See Sections 3-4 for the full setup.

What makes this Docker MCP server different from alternatives?

Unlike generic Docker API wrappers, this server includes health checks, auto-restart policies, Compose lifecycle management, fleet monitoring, and log streaming — all designed for autonomous AI agent operations. It also ships with 31 tools covering container, image, network, volume, and system management.


Want this set up for you? Nova builds custom MCP servers and deploys them into production environments. Average build: 2 weeks. MIT-licensed, fully auditable, your infra.


Related Reading: