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.
This server cannot be deployed
Maintenance
ActivityMaintained
ResponsivenessSyncing