mcpdx 0.2.0
Scaffold, test, and develop MCP servers — built for developer experience
License: MIT
Requires Python: >=3.13
Keywords: mcp, model-context-protocol, cli, scaffold, testing, developer-tools

# mcpdx

> The developer experience layer the MCP ecosystem is missing.

mcpdx is a CLI tool that makes it easy to scaffold, test, validate, and deploy [MCP](https://modelcontextprotocol.io/) (Model Context Protocol) servers. Go from zero to a working, testable MCP server in under 2 minutes.

```
mcpdx init  →  mcpdx test  →  mcpdx dev  →  mcpdx validate  →  mcpdx eval  →  mcpdx sandbox
  scaffold     test locally   dev + REPL    protocol checks    quality evals   containerized run
```

## Why?

Building MCP servers today means reading scattered docs, copying boilerplate from examples, no standardized project structure, and no way to test tools locally without a full LLM client. mcpdx fixes all of that.

- **No boilerplate** — `mcpdx init` generates a complete project with best practices baked in
- **Test without Claude** — `mcpdx test` runs your tools locally using YAML fixtures
- **Fast feedback loop** — `mcpdx dev` gives you hot reload and an interactive REPL
- **Protocol compliance** — `mcpdx validate` catches MCP violations before shipping
- **Quality checks** — `mcpdx eval` tests tool effectiveness with assertions and golden files
- **Sandboxed execution** — `mcpdx sandbox` runs tools in containers with security policies

## Install

```bash
# Option 1: Install as a global CLI tool (recommended)
uv tool install mcpdx

# Option 2: Run without installing
uvx mcpdx --help

# Option 3: pip
pip install mcpdx
```

Requires Python 3.13+ and [uv](https://docs.astral.sh/uv/) (recommended) or pip.

## Quick Start

```bash
# 1. Create a new MCP server
mcpdx init --name weather-mcp --language python --transport stdio --template minimal

# 2. Install and test it
cd weather-mcp
pip install -e .
mcpdx test

# 3. Start the dev server with interactive REPL
mcpdx dev
```

Or run `mcpdx init` without flags for an interactive experience.

## Commands

### `mcpdx init`

Create a new MCP server project with interactive prompts or CLI flags.

```bash
# Interactive mode
mcpdx init

# Non-interactive mode
mcpdx init \
  --name weather-mcp \
  --language python \
  --transport stdio \
  --template api-wrapper \
  --description "Weather data via OpenWeather API"
```

**Templates:**

| Template | Description |
|----------|-------------|
| `minimal` | Bare-bones starter with one example tool |
| `api-wrapper` | REST API integration with httpx, auth patterns |

**Languages:** Python, TypeScript
**Transports:** stdio, HTTP (streamable)

### `mcpdx test`

Run YAML test fixtures against your MCP server. No LLM client needed.

```bash
mcpdx test                    # Run all fixtures
mcpdx test -v                 # Verbose (show request/response)
mcpdx test -f "hello*"        # Filter by test name
mcpdx test -t 10000           # Custom timeout (ms)
```

Test fixtures are defined in YAML:

```yaml
tests:
  - name: "get_weather returns data"
    tool: "weather_get_current"
    input:
      city: "London"
    expect:
      status: "success"
      content_contains: "London"

  - name: "handles missing city"
    tool: "weather_get_current"
    input:
      city: ""
    expect:
      status: "error"
```

**Assertions available:**
- `status` — `"success"` or `"error"`
- `content_contains` / `content_not_contains` — Check response text
- `schema` — Validate response against JSON Schema
- `max_latency_ms` — Latency threshold

### `mcpdx dev`

Start your server with hot reload and an interactive REPL for calling tools.

```bash
mcpdx dev                     # Hot reload + REPL
mcpdx dev --no-repl           # Watch and restart only
```

REPL usage:

```
weather-mcp> weather_hello name="Alice"
Hello, Alice! Welcome to weather_mcp.

weather-mcp> weather_hello {"name": "Bob"}
Hello, Bob! Welcome to weather_mcp.

weather-mcp> tools              # List available tools
weather-mcp> help               # Show commands
weather-mcp> quit               # Exit
```

### `mcpdx validate`

Run protocol compliance checks against your MCP server. Catches naming issues, missing schemas, annotation gaps, and error handling problems.

```bash
mcpdx validate                        # Run all checks
mcpdx validate -v                     # Show passing rules too
mcpdx validate --category naming      # Only naming rules
mcpdx validate --category schema      # Only schema rules
mcpdx validate --severity error       # Only errors (hide warnings/info)
mcpdx validate --json-output          # Machine-readable JSON (for CI)
mcpdx validate -t 10000              # Custom timeout (ms)
```

**What it checks:**

| Category    | Examples                                              | Severity |
| ----------- | ----------------------------------------------------- | -------- |
| Naming      | Tool names use `snake_case` with prefix               | Warning  |
| Schema      | All tools have `inputSchema`, valid JSON Schema types | Error    |
| Annotations | Tools have `readOnlyHint`, destructive tools flagged  | Warning  |
| Errors      | Actionable error messages, no internal leaks          | Error    |
| Response    | Dual format support (JSON + Markdown)                 | Info     |

Exit code `0` if no errors. Warnings and info don't fail.

### `mcpdx eval`

Test tool _effectiveness_ with realistic scenarios — not just protocol compliance, but whether your tools return correct, useful data.

```bash
mcpdx eval                                # Run all suites
mcpdx eval --suite "core tool quality"    # Filter by suite name
mcpdx eval -t 10000                      # Custom timeout per tool call (ms)
mcpdx eval --evals-dir custom/evals/     # Custom evals directory
mcpdx eval --update-golden               # Create/update golden snapshots
mcpdx eval --compare                     # Detect regressions vs previous run
mcpdx eval --html-report report.html     # Generate HTML report
mcpdx eval --skip-validation             # Skip protocol validation pre-check
```

Eval suites live in `evals/` as YAML files:

```yaml
suite: "core tool quality"
description: "Validates core tools return correct data"

evals:
  - name: "returns valid weather object"
    tool: "weather_get_current"
    input:
      city: "London"
    assertions:
      - type: schema
        schema:
          type: object
          required: [temperature, conditions]

  - name: "temperature in reasonable range"
    tool: "weather_get_current"
    input:
      city: "London"
    assertions:
      - type: range
        path: "$.temperature"
        min: -50
        max: 60
```

**Assertion types:**

| Type          | Description                                            |
| ------------- | ------------------------------------------------------ |
| `schema`      | Validate response against a JSON Schema                |
| `range`       | Check a numeric value falls within `min`/`max` bounds  |
| `length`      | Check an array's length matches `expected`             |
| `contains`    | Check a string contains the `expected` substring       |
| `golden_file` | Compare full response against a saved snapshot         |

By default, `mcpdx eval` runs `mcpdx validate` first as a pre-check. Validation errors block evals; warnings don't.

### `mcpdx sandbox`

Run MCP tools inside containers with configurable security policies.

**Prerequisites:** An OCI-compliant container runtime — [Docker](https://www.docker.com/products/docker-desktop), [Podman](https://podman.io/getting-started/installation), or [nerdctl](https://github.com/containerd/nerdctl/releases). No Python SDK needed — mcpdx calls the CLI directly. All non-sandbox features work without a container runtime.

```bash
# Run a single tool in the sandbox
mcpdx sandbox run get_weather '{"city": "London"}'
mcpdx sandbox run get_weather '{"city": "London"}' --policy strict
mcpdx sandbox run get_weather '{"city": "London"}' --policy sandbox-policy.yaml
mcpdx sandbox run get_weather '{"city": "London"}' --build --keep --timeout 60

# Interactive shell inside the container
mcpdx sandbox shell
mcpdx sandbox shell --policy permissive --keep

# Build the container image without running
mcpdx sandbox build
mcpdx sandbox build --policy strict

# Use a specific container runtime
mcpdx sandbox run get_weather '{}' --runtime podman
mcpdx sandbox shell -r nerdctl
```

**Security policy presets:**

| Preset       | Network | Filesystem    | CPU | Memory | Timeout | Use Case               |
| ------------ | ------- | ------------- | --- | ------ | ------- | ---------------------- |
| `strict`     | None    | Read-only     | 0.5 | 256m   | 15s     | Pure computation tools |
| `standard`   | Bridge  | Limited write | 1.0 | 512m   | 30s     | API wrapper tools      |
| `permissive` | Bridge  | Full write    | 2.0 | 1g     | 120s    | Development/debugging  |

**Custom policy file** (`sandbox-policy.yaml`):

```yaml
policy: standard  # Inherit from a preset, then override

network:
  enabled: true
  allowed_domains:
    - "api.example.com"
    - "*.github.com"

filesystem:
  read_only_mounts:
    - "/app/config"
  writable_dirs:
    - "/tmp"

resources:
  cpu_limit: "0.5"
  memory_limit: "256m"
  timeout_seconds: 30
```

When `allowed_domains` is configured, mcpdx enforces the allowlist using iptables rules applied at container start. Only traffic to allowed domains is permitted.

**Runtime resolution order:**
1. `--runtime` / `-r` CLI flag
2. `container_runtime` in project config
3. Auto-detect from PATH: docker > podman > nerdctl

**Note:** Domain allowlist filtering requires a rootful container runtime (Docker, rootful Podman, nerdctl). Rootless Podman cannot apply iptables rules — the container will start but without network filtering.

Evals can also run inside the sandbox:

```bash
mcpdx eval --sandbox
mcpdx eval --policy strict
mcpdx eval --policy sandbox-policy.yaml
```

## Configuration

mcpdx reads config from `pyproject.toml` or `mcpdx.toml` (generated automatically by `mcpdx init`).

**pyproject.toml** (Python projects):

```toml
[tool.mcpdx]
server_command = "python -m weather_mcp.server"
fixtures_dir = "tests/fixtures"
container_runtime = "podman"  # Optional: docker (default), podman, or nerdctl
```

**mcpdx.toml** (TypeScript/other projects):

```toml
[server]
name = "weather-mcp"
command = "node dist/server.js"

[testing]
fixtures_dir = "tests/fixtures"

[sandbox]
runtime = "podman"  # Optional: docker (default), podman, or nerdctl
```

## Generated Project Structure

```
weather-mcp/
├── pyproject.toml          # Project config with mcpdx settings
├── README.md               # Getting started guide
├── src/
│   └── weather_mcp/
│       ├── __init__.py
│       └── server.py       # MCP server with example tool
└── tests/
    └── fixtures/
        └── test_tools.yaml # Test fixtures
```

## Writing Tools

Tools live in `src/<package>/server.py`. Here's the pattern mcpdx generates:

```python
from fastmcp import FastMCP

mcp = FastMCP("weather_mcp")

@mcp.tool(
    annotations={
        "title": "Get Current Weather",
        "readOnlyHint": True,
        "idempotentHint": True,
        "openWorldHint": True,
    }
)
def weather_get_current(city: str) -> str:
    """Get current weather for a city.

    Args:
        city: City name (e.g., "London", "New York")
    """
    # Your logic here
    return f"Weather in {city}: 72°F, sunny"
```

Key conventions:
- **Tool naming**: `{prefix}_{action}` (e.g., `weather_get_current`)
- **Annotations**: Always include `readOnlyHint`, `idempotentHint`, `openWorldHint`
- **Docstrings**: First line is the tool description, `Args:` section documents parameters
- **Type hints**: All parameters must have type annotations

## Porting an Existing MCP Server

If you already have an MCP server and want to bring it into the mcpdx workflow:

1. **Scaffold a new project** with the closest template:
   ```bash
   mcpdx init --name my-server-mcp --template minimal
   ```

2. **Copy your tool logic** into the generated `server.py`, following the conventions above

3. **Add your dependencies** to the generated `pyproject.toml`

4. **Write test fixtures** for each tool (aim for at least 2 per tool: happy path + error case)

5. **Validate**:
   ```bash
   cd my-server-mcp
   uv sync
   mcpdx test
   mcpdx dev
   ```

## Troubleshooting

### "No mcpdx configuration found"
You're not in a project directory, or the config is missing. Make sure `pyproject.toml` has a `[tool.mcpdx]` section or `mcpdx.toml` exists.

### "Server failed to start"
Check that `server_command` in your config is correct and the server can run independently:
```bash
python -m your_package.server
```

### Tests timing out
Increase the timeout: `mcpdx test -t 15000`. If a specific tool is slow, use `max_latency_ms` in your fixture to set per-test expectations.

### REPL says "Server is offline"
The server crashed during a file change. Check the error output above the REPL prompt, fix the issue, and the server will restart automatically.

### "No container runtime found"
Install Docker, Podman, or nerdctl and make sure the daemon is running. You can verify with `docker info` or `podman info`.

### Sandbox domain filtering not working
Domain allowlist enforcement requires a rootful container runtime. If you're using rootless Podman, iptables rules cannot be applied. Switch to Docker or rootful Podman for full domain filtering support.

## Development

```bash
# Clone and install
git clone https://github.com/ceasarb/mcpdx.git
cd mcpdx
uv sync

# Run tests
pytest tests/ -v

# Lint
ruff check src/
ruff format src/
```

## License

MIT