A production-grade dynamic MCP server for Node.js that enables runtime tool creation, management, and execution in isolated execution sandboxes (docker or node).
Unlike static MCP servers that define tools at compile time, dynamic-mcp lets AI agents and operators create, update, and delete tools on the fly with full lifecycle management.
- Runtime tool management — Create, update, delete, enable/disable tools without restarts via the
dynamic.tool.*control plane - Execution backend selection —
auto(Docker preferred, Node fallback), or forcedocker/node - Dual transport — Stdio for local/CLI use, Streamable HTTP for networked deployments with per-session MCP servers
- Dual registry backend — File-based (single node) or PostgreSQL (multi-instance) with optimistic concurrency control
- Execution guard — Global concurrency and per-scope rate limiting to prevent abuse
- JWT authentication — Optional JWKS-based token verification for HTTP mode
- Audit logging — Structured JSONL logs with rotation, redaction of sensitive fields, and shutdown flush
- Experimental upstream attach — Optional feature-flagged
upstream.mcp.attachfor lazy discovery of existing MCP servers - Two profiles —
mvp(default) for core functionality,enterprisefor long-lived sandbox sessions, metrics, and ops tools - Production-ready — Health probes, Prometheus metrics, graceful shutdown, Kubernetes manifests, Docker Compose baselines
Prerequisites: Node.js >= 20 (Docker recommended)
Fastest way to run stdio (no clone/build):
npx -y dynamic-mcp --transport stdio --profile mvp
# optional: pin version for reproducibility
npx -y dynamic-mcp@<version> --transport stdio --profile mvpFrom source (local development):
# Install dependencies
pnpm install
# Run in stdio mode (default, mvp profile)
pnpm run dev
# Run in HTTP mode
pnpm run dev:http
# Run with enterprise profile
pnpm run dev:enterpriseHTTP mode default endpoint: http://127.0.0.1:8788/mcp
- Development / PoC:
mvpprofile +stdiotransport + file backend (.env.example) - Production:
enterpriseprofile +httptransport + JWT auth + PostgreSQL backend (.env.prod.example)
This project supports both MCP standard transports:
stdio(recommended for local development/CLI clients)- Streamable HTTP (recommended for remote/network deployment)
Most MCP clients launch your server as a child process in stdio mode.
Option A (recommended for quick setup): run from npm with npx:
npx -y dynamic-mcp --transport stdio --profile mvpOption B (recommended when developing this repo): build local runtime first:
pnpm install
pnpm buildThen use an absolute path to dist/index.js in client config. Example:
node /ABS/PATH/TO/dynamic-mcp/dist/index.js --transport stdio --profile mvpDynamic code execution features use the selected execution backend (docker or node).
Note: sandbox.* tools remain Docker-based; in environments without Docker, use mvp profile or avoid sandbox.*.
Execution backend can be controlled with MCP_EXECUTION_ENGINE / --execution-engine:
auto(default): use Docker when available, fallback to Node sandbox when Docker is unavailabledocker: force Dockernode: force Node sandbox (no dynamic dependency installation)
If Docker is not installed and you want the MCP server to default to Node immediately, set MCP_EXECUTION_ENGINE=node in the client config or append --execution-engine node to the launch command:
npx -y dynamic-mcp --transport stdio --profile mvp --execution-engine nodeClaude Desktop uses a local claude_desktop_config.json file with mcpServers.
macOS path:
~/Library/Application Support/Claude/claude_desktop_config.json
Windows path:
%APPDATA%\Claude\claude_desktop_config.json
Example:
{
"mcpServers": {
"dynamic-mcp": {
"command": "npx",
"args": [
"-y",
"dynamic-mcp",
"--transport",
"stdio",
"--profile",
"mvp"
],
"env": {
"MCP_DYNAMIC_BACKEND": "file",
"MCP_DYNAMIC_STORE": "/ABS/PATH/TO/dynamic-mcp/.dynamic-mcp/tools.json",
"MCP_SANDBOX_DOCKER_BIN": "docker"
}
}
}
}If Docker is not installed, configure the server to use Node explicitly:
{
"mcpServers": {
"dynamic-mcp": {
"command": "npx",
"args": [
"-y",
"dynamic-mcp",
"--transport",
"stdio",
"--profile",
"mvp"
],
"env": {
"MCP_DYNAMIC_BACKEND": "file",
"MCP_DYNAMIC_STORE": "/ABS/PATH/TO/dynamic-mcp/.dynamic-mcp/tools.json",
"MCP_EXECUTION_ENGINE": "node"
}
}
}
}Note: Claude Desktop remote MCP server management is done in app settings (Settings -> Connectors), not in claude_desktop_config.json.
Add local stdio server:
claude mcp add dynamic-mcp -- npx -y dynamic-mcp --transport stdio --profile mvpIf Docker is not installed:
claude mcp add dynamic-mcp -- npx -y dynamic-mcp --transport stdio --profile mvp --execution-engine nodeAdd remote HTTP server:
claude mcp add --transport http dynamic-mcp-http http://127.0.0.1:8788/mcpProject-level .mcp.json example (supports both local and remote server definitions):
{
"mcpServers": {
"dynamic-mcp-local": {
"command": "npx",
"args": [
"-y",
"dynamic-mcp",
"--transport",
"stdio",
"--profile",
"enterprise"
]
},
"dynamic-mcp-http": {
"type": "http",
"url": "http://127.0.0.1:8788/mcp",
"authorization_token": "${DYNAMIC_MCP_JWT_TOKEN}"
}
}
}If Docker is not installed, add "env": { "MCP_EXECUTION_ENGINE": "node" } to the local server entry or append "--execution-engine", "node" to its args.
Claude Code supports environment variable expansion in config values, including ${VAR} and ${VAR:-default}.
Use workspace config file: .vscode/mcp.json.
Local stdio example:
{
"servers": {
"dynamic-mcp": {
"command": "npx",
"args": [
"-y",
"dynamic-mcp",
"--transport",
"stdio",
"--profile",
"mvp"
],
"env": {
"MCP_DYNAMIC_BACKEND": "file",
"MCP_SANDBOX_DOCKER_BIN": "docker"
}
}
}
}If Docker is not installed, set the execution engine to Node:
{
"servers": {
"dynamic-mcp": {
"command": "npx",
"args": [
"-y",
"dynamic-mcp",
"--transport",
"stdio",
"--profile",
"mvp"
],
"env": {
"MCP_DYNAMIC_BACKEND": "file",
"MCP_EXECUTION_ENGINE": "node"
}
}
}
}Remote HTTP + JWT header example:
{
"servers": {
"dynamic-mcp-http": {
"url": "http://127.0.0.1:8788/mcp",
"headers": {
"Authorization": "Bearer ${input:dynamic_mcp_jwt}"
}
}
},
"inputs": [
{
"type": "promptString",
"id": "dynamic_mcp_jwt",
"description": "JWT Bearer token for dynamic-mcp"
}
]
}Server startup example:
pnpm run dev:http
# or:
node /ABS/PATH/TO/dynamic-mcp/dist/index.js --transport http --host 127.0.0.1 --port 8788 --path /mcp
# or:
npx -y dynamic-mcp --transport http --host 127.0.0.1 --port 8788 --path /mcpIn HTTP mode, the server runs as an independent process/container, and MCP clients connect to the configured URL.
HTTP endpoints:
POST /mcpinitialize/continue MCP sessionGET /mcpsession streamDELETE /mcpclose sessionGET /livezlivenessGET /readyzreadinessGET /metricsPrometheus metrics
JWT behavior in this repo:
- When
MCP_AUTH_MODE=jwt, authentication is enforced on MCP endpoint requests (${MCP_PATH}, default/mcp). /livez,/readyz,/metricsremain anonymous by default.
Production recommendation: keep /livez, /readyz, /metrics behind private networking, ingress allowlists, or a gateway even when JWT is enabled.
MCP_TRANSPORT=http
MCP_PROFILE=enterprise
MCP_HOST=0.0.0.0
MCP_PORT=8788
MCP_PATH=/mcp
MCP_EXECUTION_ENGINE=auto
MCP_DYNAMIC_BACKEND=postgres
MCP_REQUIRE_ADMIN_TOKEN=true
MCP_ADMIN_TOKEN=change-me
MCP_AUTH_MODE=jwt
MCP_AUTH_JWKS_URL=https://your-idp.example.com/.well-known/jwks.json
MCP_AUTH_ISSUER=https://your-idp.example.com/
MCP_AUTH_AUDIENCE=dynamic-mcp
MCP_AUTH_REQUIRED_SCOPES=mcp.invoke
# Optional experimental feature (enterprise only)
MCP_EXPERIMENTAL_UPSTREAM_MCP_ATTACH=false
MCP_EXPERIMENTAL_UPSTREAM_MCP_ATTACH_MAX=8Full variable reference: docs/configuration.md
Production baseline assets:
| Document | Description |
|---|---|
| Architecture | System design, module structure, and data flow |
| Configuration | All environment variables and CLI arguments |
| API Reference | Complete tool, resource, and prompt reference |
| Dynamic Tools Guide | How to author and manage dynamic tools |
| Security | Security model, sandbox isolation, and authentication |
| Deployment | Docker, Compose, and Kubernetes deployment guides |
| Production Runbook | Production rollout, verification, and rollback steps |
Core dynamic tool engine:
| Tool | Description |
|---|---|
dynamic.tool.create |
Register a new dynamic tool |
dynamic.tool.update |
Modify an existing tool definition |
dynamic.tool.delete |
Remove a tool |
dynamic.tool.list |
List all registered tools |
dynamic.tool.get |
Get a single tool definition |
dynamic.tool.enable |
Enable or disable a tool |
run_js_ephemeral |
One-off JavaScript execution in a sandbox |
system.health |
Server liveness and uptime |
Everything in MVP, plus:
| Tool / Resource | Description |
|---|---|
sandbox.initialize |
Create a reusable container session |
sandbox.exec |
Run shell commands in a session |
sandbox.run_js |
Run JavaScript in a session |
sandbox.stop |
Stop a session container |
sandbox.session.list |
List active sessions |
system.guard_metrics |
Concurrency/rate-limit counters |
system.runtime_config |
Sanitized config snapshot |
dynamic://metrics/guard |
Guard metrics resource |
dynamic://service/runtime-config |
Config snapshot resource |
dynamic://service/meta |
Service metadata resource |
tool-call-checklist |
Reusable pre-call checklist prompt |
upstream.mcp.attach |
Experimental upstream MCP attach* |
upstream.mcp.detach |
Experimental upstream MCP detach* |
* Registered only when MCP_EXPERIMENTAL_UPSTREAM_MCP_ATTACH=true.
Enable with feature flag (enterprise profile only):
MCP_PROFILE=enterprise
MCP_EXPERIMENTAL_UPSTREAM_MCP_ATTACH=true
MCP_EXPERIMENTAL_UPSTREAM_MCP_ATTACH_MAX=8
MCP_ADMIN_TOKEN=change-meThis registers upstream.mcp.attach and upstream.mcp.detach. attach can connect to an existing MCP server (stdio or http) and return its current listTools output.
Current scope is intentionally narrow:
- Supported: attach + tool discovery
- Supported: attach + detach + tool discovery
- Not yet supported: runtime mount/unmount/proxy of upstream tools into dynamic-mcp tool namespace
Security boundary:
transport=stdiocan spawn local processes; treat it as privilegedMCP_ADMIN_TOKENis required when this feature flag is enabled- Pair with
MCP_REQUIRE_ADMIN_TOKEN=trueandMCP_ADMIN_TOKEN=...
{
"tool": {
"name": "text.uppercase",
"description": "Convert text to uppercase",
"code": "const { text } = args;\nreturn { upper: String(text).toUpperCase() };",
"dependencies": [],
"image": "node:lts-slim",
"timeoutMs": 10000
}
}Then invoke it:
{
"args": { "text": "hello world" }
}See the Dynamic Tools Guide for full details.
docker build -t dynamic-mcp:latest .
docker run --rm -p 8788:8788 dynamic-mcp:latestWhen MCP_EXECUTION_ENGINE=auto (default), dynamic tool execution (dynamic.*, run_js_ephemeral) uses Docker when available and falls back to Node sandbox when Docker is unavailable.
sandbox.* tools remain Docker-based. For those tools in containerized deployments, the running dynamic-mcp process must have:
- A Docker CLI binary available in the container (
docker) - Connectivity and authorization to a Docker daemon (local socket or remote daemon)
Without that Docker access, sandbox.* calls fail at runtime.
Security note: exposing the host Docker socket gives the container high privilege over the host. Prefer a dedicated remote Docker daemon with network isolation and TLS for production.
docker compose -f deploy/docker-compose.postgres.yml up -d --buildkubectl apply -f deploy/k8s/dynamic-mcp-postgres.yaml
# Optional: HPA + PDB
kubectl apply -f deploy/k8s/dynamic-mcp-scalability.yaml
# Optional: Network policy
kubectl apply -f deploy/k8s/dynamic-mcp-networkpolicy.yamlpnpm run dev # stdio mode, mvp profile
pnpm run dev:mvp # explicit mvp profile
pnpm run dev:http # HTTP mode
pnpm run dev:enterprise # enterprise profile
pnpm run test # run tests
pnpm run lint # lint
pnpm run typecheck # type check
pnpm run build # compile TypeScriptMIT