mcp-starter-kit
A scaffolding toolkit to generate clean architecture MCP servers with tool scaffolding and function inspection.
README
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
- Project purpose
- Suitable use cases
- Unsuitable use cases
- Three-minute quick start
- Installation
- CLI command reference
- Exit-code policy
- Script reference
- Creating the first MCP server
- Creating the first tool
- Inspect-driven generation
- Supported and unsupported Python types
- Generated template structure
- Generated versus handwritten files
- SDK adapter boundary
- Architecture and diagrams
- Testing
- Troubleshooting
- Known limitations
- Roadmap
- 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
# 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)
git clone https://github.com/j3camp/mcp-starter-kit.git
cd mcp-starter-kit
pip install -e ".[dev]"
Runtime only
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.
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.
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 stubsrc/<pkg>/application/tools/<snake>_registration.py— registration helpertests/test_<snake>.py— unit test stub
mcp-scaffold inspect <module:function>
Inspect a Python function and print normalized MCP tool metadata.
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.
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.
mcp-scaffold validate .
mcp-scaffold validate ./my-service
Checks:
pyproject.tomlexistssrc/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
# 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:
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
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:
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:
# 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:
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:
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:
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:
- Create a new adapter class that implements
register_tool(*, name, description, handler). - Replace
FastMCPAdapterinserver.pywith your adapter. - The application layer requires zero changes.
Architecture and diagrams
Overall architecture
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
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
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
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
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
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
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:
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:
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).
Union types with more than two members (excluding None) are not supported.
Known limitations
mcp-scaffold generatedoes not automatically updateserver.pyto register the new tool. You must add theregister_<tool>(adapter)call manually.- Windows batch scripts require
cmd.exeand do not support PowerShell-only features. - Paths containing
!may cause issues withset -ein 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
validatecommand checks structural presence only; it does not run tests or verify import correctness.
Roadmap
- Automatic
server.pyupdate afternew-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.pyand fixture generation. - GitHub Actions CI template generation.
Contributing
-
Fork the repository and create a feature branch.
-
Install development dependencies:
pip install -e ".[dev]". -
Run checks before opening a PR:
python -m pytest tests/ -v python -m ruff check . python -m mypy src tests -
Keep changes focused: one concern per PR.
-
Add or update tests for every changed behaviour.
-
Write commit messages in English.
Recommended Servers
playwright-mcp
A Model Context Protocol server that enables LLMs to interact with web pages through structured accessibility snapshots without requiring vision models or screenshots.
Magic Component Platform (MCP)
An AI-powered tool that generates modern UI components from natural language descriptions, integrating with popular IDEs to streamline UI development workflow.
Audiense Insights MCP Server
Enables interaction with Audiense Insights accounts via the Model Context Protocol, facilitating the extraction and analysis of marketing insights and audience data including demographics, behavior, and influencer engagement.
VeyraX MCP
Single MCP tool to connect all your favorite tools: Gmail, Calendar and 40 more.
graphlit-mcp-server
The Model Context Protocol (MCP) Server enables integration between MCP clients and the Graphlit service. Ingest anything from Slack to Gmail to podcast feeds, in addition to web crawling, into a Graphlit project - and then retrieve relevant contents from the MCP client.
Kagi MCP Server
An MCP server that integrates Kagi search capabilities with Claude AI, enabling Claude to perform real-time web searches when answering questions that require up-to-date information.
Neon Database
MCP server for interacting with Neon Management API and databases
E2B
Using MCP to run code via e2b.
Exa Search
A Model Context Protocol (MCP) server lets AI assistants like Claude use the Exa AI Search API for web searches. This setup allows AI models to get real-time web information in a safe and controlled way.
Qdrant Server
This repository is an example of how to create a MCP server for Qdrant, a vector search engine.