Skip to main content
Glama
j3camp

mcp-starter-kit

by j3camp
README.md
# mcp-scaffold

**A Clean Architecture MCP Server Scaffolding Toolkit.**

Generate production-ready MCP server projects, add new tools incrementally,
and derive tool scaffolding from ordinary Python functions — while keeping your
application code independent of FastMCP or any other MCP SDK.

---

## Table of Contents

1. [Project purpose](#project-purpose)
2. [Suitable use cases](#suitable-use-cases)
3. [Unsuitable use cases](#unsuitable-use-cases)
4. [Three-minute quick start](#three-minute-quick-start)
5. [Installation](#installation)
6. [CLI command reference](#cli-command-reference)
7. [Exit-code policy](#exit-code-policy)
8. [Script reference](#script-reference)
9. [Creating the first MCP server](#creating-the-first-mcp-server)
10. [Creating the first tool](#creating-the-first-tool)
11. [Inspect-driven generation](#inspect-driven-generation)
12. [Supported and unsupported Python types](#supported-and-unsupported-python-types)
13. [Generated template structure](#generated-template-structure)
14. [Generated versus handwritten files](#generated-versus-handwritten-files)
15. [SDK adapter boundary](#sdk-adapter-boundary)
16. [Architecture and diagrams](#architecture-and-diagrams)
17. [Testing](#testing)
18. [Troubleshooting](#troubleshooting)
19. [Known limitations](#known-limitations)
20. [Roadmap](#roadmap)
21. [Contributing](#contributing)

---

## Project purpose

`mcp-scaffold` creates and evolves MCP server projects so that you can focus on
business logic instead of wiring.  It generates a clean-architecture project
skeleton, adds individual tool stubs, and can derive scaffolding directly from
an existing Python function signature using `inspect`.

The generated code is **directly importable, testable, lintable, and
type-checkable** without modification.

---

## Suitable use cases

- Bootstrapping a new Python MCP server from zero.
- Adding a new tool to an existing generated project without touching unrelated
  files.
- Exploring what MCP metadata an existing Python function would produce.
- Enforcing a consistent project layout across a team.
- Demonstrating Clean Architecture with a real, working project.

---

## Unsuitable use cases

- Web-based administration UI.
- Cloud deployment automation.
- Authentication or authorization systems.
- Plugin marketplaces or dynamic runtime plugin installation.
- Distributed service discovery.
- Database-backed template registries.
- Automatic module scanning across arbitrary directories.

---

## Three-minute quick start

```bash
# 1. Install
pip install -e ".[dev]"

# 2. Create a project
mcp-scaffold new-project my-server

# 3. Enter the project and run its tests
cd my-server
python -m pytest tests/ -v

# 4. Add a tool
mcp-scaffold new-tool get-user-info --project-dir .

# 5. Validate the project structure
mcp-scaffold validate .
```

---

## Installation

### From source (development)

```bash
git clone https://github.com/j3camp/mcp-starter-kit.git
cd mcp-starter-kit
pip install -e ".[dev]"
```

### Runtime only

```bash
pip install -e .
```

### Required runtime dependencies

| Package | Purpose |
|---------|---------|
| `pydantic>=2.0` | Domain models and validation |
| `typer>=0.12` | CLI framework |
| `fastmcp>=2.0` | MCP SDK (infrastructure only) |

### Optional development dependencies

| Package | Purpose |
|---------|---------|
| `pytest>=8.0` | Test runner |
| `pytest-asyncio>=0.23` | Async test support |
| `ruff>=0.4` | Linter and formatter |
| `mypy>=1.10` | Static type checker |

---

## CLI command reference

### `mcp-scaffold new-project <project-name>`

Create a new MCP server project.

```bash
mcp-scaffold new-project my-service
mcp-scaffold new-project my-service --output-dir /projects
mcp-scaffold new-project my-service --dry-run
mcp-scaffold new-project my-service --force
```

| Option | Description |
|--------|-------------|
| `--output-dir`, `-o` | Parent directory (default: `.`) |
| `--force` | Overwrite existing files |
| `--dry-run` | Show what would be created without writing |

---

### `mcp-scaffold new-tool <tool-name>`

Add a new tool stub to an existing generated project.

```bash
mcp-scaffold new-tool get-user-info
mcp-scaffold new-tool get-user-info --project-dir ./my-service
mcp-scaffold new-tool get-user-info --dry-run
mcp-scaffold new-tool get-user-info --force
```

| Option | Description |
|--------|-------------|
| `--project-dir`, `-p` | Project root (default: `.`) |
| `--force` | Overwrite existing files |
| `--dry-run` | Show what would be created without writing |

Creates:
- `src/<pkg>/application/tools/<snake>.py` — tool function stub
- `src/<pkg>/application/tools/<snake>_registration.py` — registration helper
- `tests/test_<snake>.py` — unit test stub

---

### `mcp-scaffold inspect <module:function>`

Inspect a Python function and print normalized MCP tool metadata.

```bash
mcp-scaffold inspect my_module.tools:get_user_info
mcp-scaffold inspect my_module.tools:get_user_info --base-path .
```

| Option | Description |
|--------|-------------|
| `--base-path` | Directory prepended to `sys.path` before import (default: `.`) |

Example output:

```
Function:    get_user_info
Tool name:   get-user-info
Description: Return public information for a user.
Mode:        sync
Returns:     UserInfo
Parameters:
  user_id: str — required
```

---

### `mcp-scaffold generate <module:function>`

Inspect a Python function and generate tool scaffolding from its signature.

```bash
mcp-scaffold generate my_module.tools:get_user_info --project-dir ./my-service
mcp-scaffold generate my_module.tools:get_user_info --project-dir ./my-service --dry-run
```

| Option | Description |
|--------|-------------|
| `--project-dir`, `-p` | Target project root (default: `.`) |
| `--base-path` | Directory prepended to `sys.path` |
| `--force` | Overwrite existing files |
| `--dry-run` | Show what would be created without writing |

---

### `mcp-scaffold validate [path]`

Validate the structure of a generated project.

```bash
mcp-scaffold validate .
mcp-scaffold validate ./my-service
```

Checks:
- `pyproject.toml` exists
- `src/` directory exists
- At least one Python package under `src/`
- `tests/` directory exists

---

## Exit-code policy

| Code | Meaning |
|------|---------|
| `0` | Success |
| `1` | General execution failure |
| `2` | Invalid command usage or arguments |
| `3` | Target already exists or overwrite denied |
| `4` | Unsupported inspected type or invalid source function |
| `5` | Generated project validation failure |

All commands return non-zero codes on failure and print human-readable error
messages to stderr.  Python tracebacks are suppressed for expected user errors.

---

## Script reference

| Script (Unix) | Script (Windows) | Purpose |
|---------------|-----------------|---------|
| `scripts/setup.sh` | `scripts/setup.bat` | Install dependencies |
| `scripts/test.sh` | `scripts/test.bat` | Run test suite |
| `scripts/lint.sh` | `scripts/lint.bat` | Run linter |
| `scripts/typecheck.sh` | `scripts/typecheck.bat` | Run type checker |
| `scripts/run.sh` | `scripts/run.bat` | Start the MCP server |
| `scripts/validate-template.sh` | `scripts/validate-template.bat` | Validate project structure |

Scripts contain no business or generation logic.  They delegate entirely to
Python tools and stop on any non-zero exit code.

---

## Creating the first MCP server

```bash
# Generate the project
mcp-scaffold new-project hello-mcp --output-dir /tmp

# Inspect the generated layout
ls /tmp/hello-mcp/src/hello_mcp/

# Run the generated tests (no installation required)
cd /tmp/hello-mcp
python -m pytest tests/ -v

# Start the server
python -m hello_mcp
```

The generated `server.py` includes a composition root:

```python
def create_server() -> fastmcp.FastMCP:
    """Create and configure the MCP server."""
    mcp = fastmcp.FastMCP("mcp-server")
    adapter = FastMCPAdapter(mcp)
    adapter.register_tool(name="echo", description="...", handler=echo)
    return mcp
```

Importing this module does **not** start the server.  Only calling
`create_server()` and then `server.run()` does.

---

## Creating the first tool

```bash
mcp-scaffold new-tool search-products --project-dir /tmp/hello-mcp
```

This generates:

```
src/hello_mcp/application/tools/search_products.py
src/hello_mcp/application/tools/search_products_registration.py
tests/test_search_products.py
```

Edit the stub to implement your business logic, then call the registration
helper from `server.py`:

```python
from hello_mcp.application.tools.search_products_registration import register_search_products

def create_server() -> fastmcp.FastMCP:
    mcp = fastmcp.FastMCP("mcp-server")
    adapter = FastMCPAdapter(mcp)
    register_search_products(adapter)
    return mcp
```

---

## Inspect-driven generation

Given an ordinary Python function:

```python
# my_service/tools.py
from pydantic import BaseModel

class UserInfo(BaseModel):
    user_id: str
    display_name: str
    active: bool

def get_user_info(user_id: str) -> UserInfo:
    """Return public information for a user."""
    raise NotImplementedError
```

Inspect it:

```bash
mcp-scaffold inspect my_service.tools:get_user_info --base-path .
```

Output:

```
Function:    get_user_info
Tool name:   get-user-info
Description: Return public information for a user.
Mode:        sync
Returns:     UserInfo
Parameters:
  user_id: str — required
```

Generate scaffolding:

```bash
mcp-scaffold generate my_service.tools:get_user_info \
  --project-dir ./my-server --base-path .
```

The inspect pipeline runs in explicit stages:

```
Python function
  -> discovery         validate callable is public and annotated
  -> metadata          extract name, docstring, parameters, return type
  -> type mapping      validate each annotation
  -> normalization     produce ToolMetadata
  -> rendering model   plan files to create
  -> generated files   write to disk (skipped in --dry-run)
```

---

## Supported and unsupported Python types

### Supported

| Python type | Example |
|-------------|---------|
| `str` | `x: str` |
| `int` | `x: int` |
| `float` | `x: float` |
| `bool` | `x: bool` |
| `None` (return only) | `-> None` |
| `Optional[T]` | `x: Optional[str]` |
| `Literal[...]` | `x: Literal["a", "b"]` |
| `list[T]` | `x: list[str]` |
| `dict[K, V]` | `x: dict[str, int]` |
| Pydantic `BaseModel` subclass | `x: UserInfo` |

### Unsupported

| Python type | Behaviour |
|-------------|-----------|
| `Union[A, B]` (non-nullable) | Raises `UnsupportedTypeError` |
| `set`, `tuple`, `frozenset` | Raises `UnsupportedTypeError` |
| `*args`, `**kwargs` | Validation error recorded in metadata |
| Missing type annotation | Recorded as unsupported parameter |
| Missing return annotation | Validation error (override with `allow_missing_return=True`) |
| Private functions (`_name`) | Raises `InspectError` (override with `allow_private=True`) |
| Classes | Raises `InspectError` |

Unknown types **never** silently fall back to `str`.

---

## Generated template structure

```
<project-name>/
├── pyproject.toml               # build config, pytest, ruff, mypy
├── README.md
├── .gitignore
├── src/
│   └── <pkg>/
│       ├── __init__.py
│       ├── __main__.py          # entry point
│       ├── server.py            # composition root (create_server)
│       ├── domain/
│       │   ├── __init__.py
│       │   └── ports.py         # MCPServerPort protocol
│       ├── application/
│       │   ├── __init__.py
│       │   └── tools/
│       │       ├── __init__.py
│       │       └── example_tool.py
│       └── infrastructure/
│           ├── __init__.py
│           └── mcp_adapter.py   # FastMCP adapter
├── tests/
│   ├── __init__.py
│   ├── test_example_tool.py
│   └── test_server.py
└── scripts/
    ├── run.sh / run.bat
    ├── test.sh / test.bat
    ├── lint.sh / lint.bat
    └── typecheck.sh / typecheck.bat
```

---

## Generated versus handwritten files

| File | Type | Notes |
|------|------|-------|
| `pyproject.toml` | Generated | Regenerated by `--force` |
| `README.md` | Generated | Regenerated by `--force` |
| `.gitignore` | Generated | Regenerated by `--force` |
| `src/<pkg>/__init__.py` | Generated | Regenerated by `--force` |
| `src/<pkg>/domain/ports.py` | Generated | Regenerated by `--force` |
| `src/<pkg>/infrastructure/mcp_adapter.py` | Generated | Regenerated by `--force` |
| `src/<pkg>/server.py` | Generated (composition root) | Edit to add tool registrations |
| `src/<pkg>/application/tools/example_tool.py` | Generated | Replace with real logic |
| `src/<pkg>/application/tools/<tool>.py` | Generated stub | **Handwritten after generation** |
| `src/<pkg>/application/tools/<tool>_registration.py` | Generated | Safe to edit registration call |
| `tests/test_*.py` | Generated stub | **Handwritten after generation** |

Files marked **Handwritten after generation** are created once and never
overwritten unless `--force` is supplied.

---

## SDK adapter boundary

`mcp-scaffold` applies Dependency Inversion between the application layer and
the MCP SDK.

```
application/domain
    MCPServerPort  <-- Protocol owned by the inner layer
                       (no fastmcp import)

infrastructure
    FastMCPAdapter  --> implements MCPServerPort
                        (only file that imports fastmcp)
```

The composition root (`server.py`) wires them together:

```python
import fastmcp
from <pkg>.infrastructure.mcp_adapter import FastMCPAdapter

def create_server() -> fastmcp.FastMCP:
    mcp = fastmcp.FastMCP("name")
    adapter = FastMCPAdapter(mcp)
    adapter.register_tool(...)
    return mcp
```

**To replace FastMCP with another SDK:**

1. Create a new adapter class that implements `register_tool(*, name, description, handler)`.
2. Replace `FastMCPAdapter` in `server.py` with your adapter.
3. The application layer requires zero changes.

---

## Architecture and diagrams

### Overall architecture

```mermaid
graph TD
    CLI["CLI (Typer)"]
    App["Application Services"]
    Domain["Domain Models + Ports"]
    Infra["Infrastructure Adapters"]

    CLI --> App
    App --> Domain
    Infra --> Domain
    CLI --> Infra
```

### Dependency flow

```mermaid
graph LR
    CLI --> InspectSvc["inspect_service"]
    CLI --> GenerateSvc["generate_service"]
    CLI --> ValidateSvc["validate_service"]
    InspectSvc --> Models["domain/models.py"]
    InspectSvc --> TypeMapper["infrastructure/type_mapper.py"]
    GenerateSvc --> Models
    GenerateSvc --> Renderer["infrastructure/renderer.py"]
    FastMCPAdapter["infrastructure/fastmcp_adapter.py"] --> Ports["domain/ports.py"]
```

### New-project generation flow

```mermaid
flowchart TD
    A["mcp-scaffold new-project my-server"] --> B["generate_project()"]
    B --> C["Renderer.stage() all files"]
    C --> D{"dry_run?"}
    D -- yes --> E["Return GenerationPlan (no writes)"]
    D -- no --> F["Conflict check"]
    F --> G{"conflict?"}
    G -- "yes, no --force" --> H["RenderConflictError (exit 3)"]
    G -- "no or --force" --> I["Write files to disk"]
    I --> J["Return GenerationPlan"]
```

### New-tool generation flow

```mermaid
flowchart TD
    A["mcp-scaffold new-tool get-user"] --> B["generate_tool()"]
    B --> C["_detect_package()"]
    C --> D["Stage tool stub"]
    D --> E["Stage registration helper"]
    E --> F["Stage test stub"]
    F --> G["Renderer.commit()"]
```

### Tool request sequence

```mermaid
sequenceDiagram
    participant Client
    participant FastMCP
    participant Adapter as FastMCPAdapter
    participant Tool as Tool Function

    Client->>FastMCP: call tool "echo"
    FastMCP->>Tool: invoke handler
    Tool-->>FastMCP: return result
    FastMCP-->>Client: tool result
```

### Inspect-generation sequence

```mermaid
sequenceDiagram
    participant User
    participant CLI
    participant InspectSvc as inspect_service
    participant TypeMapper as type_mapper
    participant GenerateSvc as generate_service
    participant Renderer

    User->>CLI: mcp-scaffold generate module:fn
    CLI->>InspectSvc: inspect_from_string(spec)
    InspectSvc->>TypeMapper: map_type(annotation)
    TypeMapper-->>InspectSvc: mapped type string
    InspectSvc-->>CLI: ToolMetadata
    CLI->>GenerateSvc: generate_from_metadata(metadata)
    GenerateSvc->>Renderer: stage files
    Renderer-->>GenerateSvc: GenerationPlan
    GenerateSvc-->>CLI: GenerationPlan
    CLI-->>User: files created
```

### CLI-to-Python-generator call flow

```mermaid
flowchart LR
    CLI["cli/main.py"] --> GenSvc["generate_service.py"]
    CLI --> InspSvc["inspect_service.py"]
    CLI --> ValSvc["validate_service.py"]
    GenSvc --> Renderer["renderer.py"]
    InspSvc --> TypeMapper["type_mapper.py"]
```

---

## Testing

Run the full validation suite:

```bash
python -m pytest tests/ -v
python -m ruff check .
python -m mypy src tests
mcp-scaffold validate .
```

### Test areas covered

| Area | Tests |
|------|-------|
| Inspect discovery | Public/async/private functions, optional params, missing annotations |
| Type mapping | Primitives, Optional, Literal, list, dict, Pydantic, unsupported types |
| Renderer | Stage/commit, dry-run, conflict detection, force, LF line endings |
| Generate service | Project generation, dry-run, conflict, tool addition |
| CLI | Help, new-project, new-tool, validate, dry-run, force, error codes |
| Adapter contract | RecordingAdapter sync/async, multiple tools, name propagation |

### Generated project validation

After `mcp-scaffold new-project test-server`:

```bash
cd test-server
python -m pytest tests/ -v   # should pass with no installation
```

---

## Troubleshooting

**`ModuleNotFoundError: No module named '<pkg>'` in generated project tests**

The generated `pyproject.toml` sets `pythonpath = ["src"]` in pytest options.
If you see this error, ensure your pytest version supports `pythonpath` (pytest >= 7.0).

**`TypeError: FastMCP.add_tool() got an unexpected keyword argument 'name'`**

This occurs with fastmcp >= 2.x.  The adapter uses `Tool.from_function(fn, name=..., description=...)`.
Ensure you are using the adapter generated by this version of `mcp-scaffold`.

**`RenderConflictError: File already exists`**

Use `--force` to overwrite, or delete the conflicting file manually.

**Unsupported type error during inspect**

Check that all parameter and return type annotations use supported types (see
[Supported and unsupported Python types](#supported-and-unsupported-python-types)).
Union types with more than two members (excluding `None`) are not supported.

---

## Known limitations

- `mcp-scaffold generate` does not automatically update `server.py` to register
  the new tool.  You must add the `register_<tool>(adapter)` call manually.
- Windows batch scripts require `cmd.exe` and do not support PowerShell-only
  features.
- Paths containing `!` may cause issues with `set -e` in some sh implementations.
- Forward references (string annotations) are resolved with
  `typing.get_type_hints`; they may fail if the referenced type is not importable
  at inspect time.
- The `validate` command checks structural presence only; it does not run tests
  or verify import correctness.

---

## Roadmap

- Automatic `server.py` update after `new-tool`.
- Generated project type-check and import smoke-test in `validate`.
- Support for `Union[A, B]` (non-nullable) with explicit opt-in.
- Interactive prompts for project metadata.
- Optional `conftest.py` and fixture generation.
- GitHub Actions CI template generation.

---

## Contributing

1. Fork the repository and create a feature branch.
2. Install development dependencies: `pip install -e ".[dev]"`.
3. Run checks before opening a PR:

   ```bash
   python -m pytest tests/ -v
   python -m ruff check .
   python -m mypy src tests
   ```

4. Keep changes focused: one concern per PR.
5. Add or update tests for every changed behaviour.
6. Write commit messages in English.