Skip to main content
Glama
mcpland
by mcpland
README.md
# dynamic-mcp

![Node CI](https://github.com/mcpland/dynamic-mcp/workflows/Node%20CI/badge.svg)
[![npm](https://img.shields.io/npm/v/dynamic-mcp.svg)](https://www.npmjs.com/package/dynamic-mcp)
![license](https://img.shields.io/npm/l/dynamic-mcp)

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.

## Key Features

- **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 force `docker` / `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.attach` for lazy discovery of existing MCP servers
- **Two profiles** — `mvp` (default) for core functionality, `enterprise` for long-lived sandbox sessions, metrics, and ops tools
- **Production-ready** — Health probes, Prometheus metrics, graceful shutdown, Kubernetes manifests, Docker Compose baselines

## Quick Start

**Prerequisites:** Node.js >= 20 (Docker recommended)

Fastest way to run `stdio` (no clone/build):

```bash
npx -y dynamic-mcp --transport stdio --profile mvp
# optional: pin version for reproducibility
npx -y dynamic-mcp@<version> --transport stdio --profile mvp
```

From source (local development):

```bash
# 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:enterprise
```

HTTP mode default endpoint: `http://127.0.0.1:8788/mcp`

## Recommended Operating Modes

- Development / PoC: `mvp` profile + `stdio` transport + file backend (`.env.example`)
- Production: `enterprise` profile + `http` transport + JWT auth + PostgreSQL backend (`.env.prod.example`)

## MCP Server Configuration

This project supports both MCP standard transports:

- `stdio` (recommended for local development/CLI clients)
- Streamable HTTP (recommended for remote/network deployment)

### 1. Stdio Runtime Options

Most MCP clients launch your server as a child process in `stdio` mode.

Option A (recommended for quick setup): run from npm with `npx`:

```bash
npx -y dynamic-mcp --transport stdio --profile mvp
```

Option B (recommended when developing this repo): build local runtime first:

```bash
pnpm install
pnpm build
```

Then use an absolute path to `dist/index.js` in client config. Example:

```bash
node /ABS/PATH/TO/dynamic-mcp/dist/index.js --transport stdio --profile mvp
```

Dynamic 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 unavailable
- `docker`: force Docker
- `node`: 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:

```bash
npx -y dynamic-mcp --transport stdio --profile mvp --execution-engine node
```

### 2. Claude Desktop (Local stdio)

Claude 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:

```json
{
  "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:

```json
{
  "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`.

### 3. Claude Code

Add local stdio server:

```bash
claude mcp add dynamic-mcp -- npx -y dynamic-mcp --transport stdio --profile mvp
```

If Docker is not installed:

```bash
claude mcp add dynamic-mcp -- npx -y dynamic-mcp --transport stdio --profile mvp --execution-engine node
```

Add remote HTTP server:

```bash
claude mcp add --transport http dynamic-mcp-http http://127.0.0.1:8788/mcp
```

Project-level `.mcp.json` example (supports both local and remote server definitions):

```json
{
  "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}`.

### 4. VS Code

Use workspace config file: `.vscode/mcp.json`.

Local stdio example:

```json
{
  "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:

```json
{
  "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:

```json
{
  "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"
    }
  ]
}
```

### 5. HTTP Mode Details for This Repo

Server startup example:

```bash
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 /mcp
```

In HTTP mode, the server runs as an independent process/container, and MCP clients connect to the configured URL.

HTTP endpoints:

- `POST /mcp` initialize/continue MCP session
- `GET /mcp` session stream
- `DELETE /mcp` close session
- `GET /livez` liveness
- `GET /readyz` readiness
- `GET /metrics` Prometheus metrics

JWT behavior in this repo:

- When `MCP_AUTH_MODE=jwt`, authentication is enforced on MCP endpoint requests (`${MCP_PATH}`, default `/mcp`).
- `/livez`, `/readyz`, `/metrics` remain anonymous by default.

Production recommendation: keep `/livez`, `/readyz`, `/metrics` behind private networking, ingress allowlists, or a gateway even when JWT is enabled.

### 6. Minimal Secure Baseline (Recommended)

```bash
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=8
```

Full variable reference: [docs/configuration.md](docs/configuration.md)

Production baseline assets:

- [`.env.prod.example`](.env.prod.example)
- [`docs/production-runbook.md`](docs/production-runbook.md)

## Documentation

| Document                                         | Description                                           |
| ------------------------------------------------ | ----------------------------------------------------- |
| [Architecture](docs/architecture.md)             | System design, module structure, and data flow        |
| [Configuration](docs/configuration.md)           | All environment variables and CLI arguments           |
| [API Reference](docs/api-reference.md)           | Complete tool, resource, and prompt reference         |
| [Dynamic Tools Guide](docs/dynamic-tools.md)     | How to author and manage dynamic tools                |
| [Security](docs/security.md)                     | Security model, sandbox isolation, and authentication |
| [Deployment](docs/deployment.md)                 | Docker, Compose, and Kubernetes deployment guides     |
| [Production Runbook](docs/production-runbook.md) | Production rollout, verification, and rollback steps  |

## Profiles

### MVP (default)

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                |

### Enterprise

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`.

## Experimental Upstream MCP Attach

Enable with feature flag (enterprise profile only):

```bash
MCP_PROFILE=enterprise
MCP_EXPERIMENTAL_UPSTREAM_MCP_ATTACH=true
MCP_EXPERIMENTAL_UPSTREAM_MCP_ATTACH_MAX=8
MCP_ADMIN_TOKEN=change-me
```

This 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=stdio` can spawn local processes; treat it as privileged
- `MCP_ADMIN_TOKEN` is required when this feature flag is enabled
- Pair with `MCP_REQUIRE_ADMIN_TOKEN=true` and `MCP_ADMIN_TOKEN=...`

## Example: Creating a Dynamic Tool

```json
{
  "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:

```json
{
  "args": { "text": "hello world" }
}
```

See the [Dynamic Tools Guide](docs/dynamic-tools.md) for full details.

## Docker

```bash
docker build -t dynamic-mcp:latest .
docker run --rm -p 8788:8788 dynamic-mcp:latest
```

When `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 (with PostgreSQL)

```bash
docker compose -f deploy/docker-compose.postgres.yml up -d --build
```

## Kubernetes

```bash
kubectl 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.yaml
```

## Development

```bash
pnpm 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 TypeScript
```

## License

MIT

TDQS

B3.2/5.0

Scored across 8 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: CRUD operations for dynamic tools, one-off code execution, and system health. No functional overlap.

Naming Consistency4/5

Six tools follow a consistent 'dynamic.tool.<action>' pattern, but 'run_js_ephemeral' uses snake_case and 'system.health' uses a different namespace, creating minor inconsistency.

Tool Count5/5

8 tools is well-scoped for managing dynamic tools and executing ephemeral code, with no unnecessary bloat or deficiency.

Completeness4/5

CRUD operations and enable/disable cover the full lifecycle of dynamic tools. The ephemeral runner adds useful functionality. Missing a validation or test tool, but core needs are met.

Maintenance

ActivityInactive
ResponsivenessNo issues