mcp-clean-architecture
Enables building maintainable MCP servers and apps in Python using FastMCP and Clean Architecture, providing e-commerce operations like product search and cart management via MCP.
README
FastMCP Clean Architecture — MCP App UI Template
A production-oriented template for building MCP Servers and MCP Apps with Python and FastMCP, following Clean Architecture, Dependency Inversion, separation of concerns, and modern Python practices.
The project is also intended as a learning reference for developers coming from C# / .NET.
The goal is not only to build an MCP server that works, but to build one that remains maintainable, testable, extensible, and independent from external frameworks and services.
Goals
This template demonstrates how to build an MCP application with:
- Python
- FastMCP
- Streamable HTTP transport
- Stateless HTTP
- MCP Tools
- MCP Resources
- MCP Prompts
- MCP Apps / App UI
- Clean Architecture
- Dependency Inversion
- Repository Pattern
- Use Cases
- Pydantic models
- External REST API integrations
- Environment-based configuration
- Async HTTP communication
- Dependency Injection / Composition
- Centralized Error Handling
- Structured application errors
- Logging
- Unit tests
- Integration tests
The sample domain is an e-commerce application.
Products are retrieved from a public external API and exposed through MCP.
The application will evolve to support actions such as:
- Search products
- View product details
- Add products to a cart
- View the cart
- Remove products from the cart
An MCP App UI will provide an interactive experience inside compatible MCP hosts.
Architecture
The project follows Clean Architecture principles.
MCP HOST
Claude / Copilot / etc.
|
| MCP over HTTP
v
+---------------------------------------------------------+
| PRESENTATION |
| |
| FastMCP Server |
| MCP Tools |
| MCP Resources |
| MCP Prompts |
| MCP App UI |
| Error Boundary |
+---------------------------+-----------------------------+
|
v
+---------------------------------------------------------+
| APPLICATION |
| |
| Use Cases |
| |
| GetProductUseCase |
| SearchProductsUseCase |
| AddProductToCartUseCase |
| GetCartUseCase |
+---------------------------+-----------------------------+
|
v
+---------------------------------------------------------+
| DOMAIN |
| |
| Entities / Models |
| |
| Product |
| Cart |
| |
| Repository Contracts |
| |
| ProductRepository |
| CartRepository |
| |
| Domain Errors |
+---------------------------+-----------------------------+
^
|
+---------------------------+-----------------------------+
| INFRASTRUCTURE |
| |
| External API implementations |
| HTTP clients |
| Configuration |
| Persistence adapters |
| |
| DummyJsonProductRepository |
| DummyJsonCartRepository |
+---------------------------+-----------------------------+
|
v
External REST API
Dependency Rule
The most important rule is:
Presentation ---> Application ---> Domain
^
|
Infrastructure ----------+
Dependencies point toward the core application.
The Domain must never depend on:
FastMCP
HTTP libraries
Uvicorn
DummyJSON
Claude
Copilot
databases
environment variables
MCP App UI
For example:
MCP Tool
|
v
GetProductUseCase
|
v
ProductRepository
^
|
DummyJsonProductRepository
|
v
DummyJSON REST API
GetProductUseCase knows about the ProductRepository abstraction.
It does not know that products are retrieved using HTTP or DummyJSON.
This allows:
DummyJSON
to later be replaced with:
SQL Server
PostgreSQL
MongoDB
another REST API
mock repository
without changing the application use case.
Project Structure
The project will evolve toward the following structure:
mcp-clean-architecture/
|
|-- src/
| |
| |-- domain/
| | |
| | |-- entities/
| | | |-- __init__.py
| | | |-- product.py
| | | `-- cart.py
| | |
| | |-- repositories/
| | | |-- __init__.py
| | | |-- product_repository.py
| | | `-- cart_repository.py
| | |
| | `-- errors/
| | |-- __init__.py
| | `-- domain_errors.py
| |
| |-- application/
| | |
| | |-- use_cases/
| | | |-- __init__.py
| | | |-- get_product.py
| | | |-- search_products.py
| | | |-- add_product_to_cart.py
| | | `-- get_cart.py
| | |
| | `-- errors/
| | |-- __init__.py
| | `-- application_errors.py
| |
| |-- infrastructure/
| | |
| | |-- config/
| | | |-- __init__.py
| | | `-- environment.py
| | |
| | |-- http/
| | |
| | |-- repositories/
| | | |-- __init__.py
| | | |-- dummy_json_product_repository.py
| | | `-- dummy_json_cart_repository.py
| | |
| | `-- errors/
| | |-- __init__.py
| | `-- infrastructure_errors.py
| |
| `-- presentation/
| |
| `-- mcp/
| |-- __init__.py
| |-- server.py
| |
| |-- tools/
| |
| |-- resources/
| |
| |-- prompts/
| |
| `-- apps/
|
|-- tests/
| |
| |-- unit/
| `-- integration/
|
|-- .env.example
|-- .gitignore
|-- .python-version
|-- pyproject.toml
|-- uv.lock
`-- README.md
Folders should be introduced when they have a real responsibility.
The template should not create abstractions only for the sake of having more layers.
Layer Responsibilities
Domain
Contains the core business concepts and contracts.
Examples:
Product
Cart
ProductRepository
CartRepository
ProductNotFoundError
CartError
The Domain should contain business concepts without knowing how the outside world communicates with the application.
Application
Contains application-specific workflows and Use Cases.
Examples:
GetProductUseCase
SearchProductsUseCase
AddProductToCartUseCase
GetCartUseCase
A Use Case coordinates domain abstractions.
It should not directly call an external API.
Bad
class GetProductUseCase:
def execute(self, product_id: int):
requests.get(
f"https://external-api/products/{product_id}"
)
The Use Case now knows:
- HTTP exists
- which HTTP library is used
- which external provider is used
- how the provider URL works
Preferred
class GetProductUseCase:
def __init__(self, repository: ProductRepository):
self.repository = repository
def execute(self, product_id: int) -> Product:
return self.repository.get_by_id(product_id)
Now the Use Case only knows the contract:
ProductRepository
Infrastructure
Contains implementations for external technical concerns.
Examples:
HTTP clients
REST APIs
repositories
databases
cache
environment configuration
external service adapters
For example:
ProductRepository
^
|
DummyJsonProductRepository
Infrastructure implements Domain abstractions.
The Domain does not depend on Infrastructure.
Presentation
Contains MCP-specific entry points.
Examples:
FastMCP Server
MCP Tools
MCP Resources
MCP Prompts
MCP Apps
An MCP Tool should remain thin.
Its responsibility is primarily:
MCP Request
|
v
Validate / map input
|
v
Use Case
|
v
Map result
|
v
MCP Response
Business logic should not live inside MCP decorators.
MCP Architecture
MCP and FastMCP are different concepts.
MCP
|
`-- Protocol
FastMCP
|
`-- Python framework implementing MCP
The application uses MCP over Streamable HTTP.
MCP Host
|
| Streamable HTTP
v
http://localhost:8000/mcp
|
v
FastMCP Server
The server is configured to run stateless HTTP by default.
MCP Components
Tools
Actions the model can execute.
Examples:
get_product
search_products
add_product_to_cart
get_cart
remove_product_from_cart
Conceptually:
LLM
|
| tool call
v
MCP Tool
|
v
Use Case
Resources
Resources expose data or context that an MCP Host can read.
They should not become a replacement for application business logic.
Prompts
Prompts provide reusable prompt templates through MCP.
They belong to the MCP / Presentation boundary.
MCP App UI
MCP Apps allow compatible MCP hosts to display interactive UI associated with MCP functionality.
Our e-commerce example will eventually render something conceptually similar to:
+--------------------------------+
| Product |
| |
| Smartphone |
| |
| $799.99 |
| |
| [ Add to cart ] |
+---------------+----------------+
|
v
MCP Tool Call
|
v
AddProductToCartUseCase
|
v
CartRepository
The important architectural rule is:
MCP App UI is a Presentation concern.
The UI should not implement business rules.
For example, clicking:
[ Add to cart ]
should result in:
MCP App UI
|
v
MCP Tool
|
v
AddProductToCartUseCase
|
v
CartRepository
The UI does not manipulate infrastructure directly.
Environment Configuration
Runtime configuration must come from environment variables rather than being hardcoded.
Current variables:
MCP_SERVER_TRANSPORT
MCP_SERVER_HOST
MCP_SERVER_PORT
MCP_STATELESS_HTTP
Example:
$env:MCP_SERVER_PORT="9000"
The configuration flow is:
Operating System / Container
|
| Environment Variables
v
EnvironmentSettings
|
v
server.py
|
v
FastMCP
This allows the same application code to run in:
Local
Development
Test
Staging
Production
Docker
Kubernetes
Cloud environments
with different configuration.
Secrets must never be committed to Git.
Python Package Conventions
__init__.py can be used to define the public API of a Python package.
For example:
from infrastructure.config.environment import EnvironmentSettings
__all__ = [
"EnvironmentSettings",
]
Consumers can then use:
from infrastructure.config import EnvironmentSettings
instead of:
from infrastructure.config.environment import EnvironmentSettings
This reduces coupling to the internal file structure.
Conceptually, this is similar to a TypeScript:
index.ts
used as a barrel export.
__all__ defines the intended public API.
It is not an access modifier like public or private in C#.
Python / C# Reference
This project is also designed to help .NET developers learn Python.
| Python | C# concept |
|---|---|
str |
string |
int |
int |
float |
double |
bool |
bool |
None |
null |
list[T] |
List<T> |
dict[K, V] |
Dictionary<K, V> |
tuple[T1, T2] |
roughly (T1, T2) / tuple |
self |
this |
ABC |
abstract class |
@abstractmethod |
abstract method |
Repository ABC |
often used similarly to IRepository |
Product | None |
approximately Product? |
Exception |
Exception |
raise |
throw |
try / except |
try / catch |
__init__ |
constructor |
__init__.py |
package initialization / similar purpose to barrel exports |
Pydantic BaseModel |
typed model + validation/serialization |
@decorator |
conceptually similar to attributes/middleware behavior depending on usage |
When new Python concepts are introduced, their C# equivalents should be documented when useful.
Domain Models
Structured models use Pydantic where validation and serialization are useful.
Example:
from typing import Annotated
from pydantic import BaseModel
class Product(BaseModel):
id: Annotated[int, "Product identifier"]
title: Annotated[str, "Product title"]
description: Annotated[str, "Product description"]
price: Annotated[float, "Product price"]
thumbnail: Annotated[str, "Product thumbnail URL"]
Pydantic provides:
validation
type coercion
serialization
JSON-compatible output
JSON Schema generation
Repository Pattern
Repositories represent abstractions over data or external systems.
Example:
from abc import ABC, abstractmethod
from domain.entities import Product
class ProductRepository(ABC):
@abstractmethod
def get_by_id(self, product_id: int) -> Product:
pass
For a C# developer, this is conceptually similar to:
public interface IProductRepository
{
Product GetById(int productId);
}
A concrete Infrastructure implementation can then provide the actual behavior:
ProductRepository
^
|
DummyJsonProductRepository
External APIs
External APIs must be accessed from Infrastructure.
The initial implementation uses the public DummyJSON API for the e-commerce example.
The architecture prevents application use cases from depending directly on DummyJSON.
Application
|
v
ProductRepository
^
|
Infrastructure implementation
|
v
DummyJSON
This allows the external provider to be replaced later without rewriting the Application or Domain layers.
Error Handling Strategy
The project uses a centralized exception hierarchy inspired by Clean Architecture and common .NET exception-handling patterns.
The goal is to distinguish:
expected business failures
vs
technical/infrastructure failures
while providing a common structured error contract.
Error Hierarchy
AppError
|
|-- DomainError
| |
| |-- ProductNotFoundError
| `-- CartError
|
|-- ValidationError
|
`-- InfrastructureError
|
|-- ExternalAPIError
`-- ExternalAPITimeoutError
All known application errors ultimately derive from:
AppError
Base Application Error
from typing import Any
class AppError(Exception):
error_code: str = "UNKNOWN_ERROR"
def __init__(
self,
message: str,
details: dict[str, Any] | None = None,
):
self.message = message
self.details = details or {}
super().__init__(message)
def to_dict(self) -> dict:
return {
"error_code": self.error_code,
"error_type": self.__class__.__name__,
"message": self.message,
"details": self.details,
}
Conceptually, this is similar to C#:
public abstract class AppException : Exception
{
public string ErrorCode { get; }
protected AppException(
string message,
string errorCode)
: base(message)
{
ErrorCode = errorCode;
}
}
Domain Errors
Domain errors represent expected business failures.
Examples:
Product does not exist
Cart is empty
Product cannot be added to the cart
Requested quantity violates a business rule
Example:
class DomainError(AppError):
error_code = "DOMAIN_ERROR"
class ProductNotFoundError(DomainError):
error_code = "PRODUCT_NOT_FOUND"
def __init__(self, product_id: int):
super().__init__(
message=f"Product '{product_id}' was not found.",
details={
"product_id": product_id,
},
)
Conceptually similar to:
public class ProductNotFoundException : DomainException
{
public int ProductId { get; }
public ProductNotFoundException(int productId)
: base($"Product '{productId}' was not found.")
{
ProductId = productId;
}
}
Validation Errors
Validation errors represent invalid application input or violated constraints.
Examples:
Invalid product ID
Quantity must be greater than zero
Missing required input
Invalid cart operation
These are expected failures.
They should provide enough structured information for the MCP Host or LLM to understand what needs to be corrected.
Infrastructure Errors
Infrastructure errors represent failures involving technical dependencies.
Examples:
External API unavailable
HTTP timeout
Connection failure
Unexpected downstream response
Database unavailable
For example:
class InfrastructureError(AppError):
error_code = "INFRASTRUCTURE_ERROR"
class ExternalAPIError(InfrastructureError):
error_code = "EXTERNAL_API_ERROR"
The Domain must not depend on Infrastructure exceptions.
Raw library exceptions should not leak through the entire application.
For example:
httpx.TimeoutException
|
v
ExternalAPITimeoutError
|
v
Application / Presentation
instead of:
httpx.TimeoutException
|
+---------------------> MCP Host
Error Translation
Infrastructure is responsible for translating low-level technical failures when appropriate.
For example:
HTTP 404 from product provider
|
v
ProductNotFoundError
HTTP timeout
|
v
ExternalAPITimeoutError
HTTP 500
|
v
ExternalAPIError
This prevents the rest of the application from becoming coupled to a particular HTTP library.
Presentation Error Boundary
MCP Tools should not contain duplicated error handling.
Avoid:
@mcp.tool
def tool_one():
try:
...
except AppError:
...
@mcp.tool
def tool_two():
try:
...
except AppError:
...
@mcp.tool
def tool_three():
try:
...
except AppError:
...
The desired architecture is:
MCP Host
|
v
Presentation Error Boundary
|
v
MCP Tool
|
v
Use Case
|
v
Domain / Repository
Known application errors can be converted into structured MCP-friendly errors.
Unexpected exceptions should be:
logged
|
v
converted to generic internal error
|
v
returned without sensitive details
This is conceptually similar to ASP.NET Core:
Python / MCP ASP.NET Core
AppError AppException
DomainError DomainException
InfrastructureError InfrastructureException
central error boundary IExceptionHandler / Middleware
raise throw
except catch
Structured Errors
Errors should contain structured information when useful.
Example:
{
"error_code": "PRODUCT_NOT_FOUND",
"error_type": "ProductNotFoundError",
"message": "Product '123' was not found.",
"details": {
"product_id": 123
}
}
Structured errors improve:
- MCP client behavior
- LLM reasoning
- logging
- observability
- automated tests
- debugging
Error Handling Rules
- Do not expose raw infrastructure exceptions directly to MCP clients.
- Do not duplicate
try/exceptblocks across every MCP Tool. - Use specific Domain errors for expected business failures.
- Use Validation errors for invalid input and violated constraints.
- Translate external technical failures into application-specific errors.
- Preserve useful structured context through
details. - Log unexpected exceptions at the application boundary.
- Never expose secrets, tokens, stack traces, or sensitive infrastructure details to MCP clients.
- Keep error codes stable so clients and automated tests can rely on them.
- Presentation is responsible for translating application errors into MCP-friendly responses.
Dependency Injection and Composition
Dependencies should be explicit.
For example:
DummyJsonProductRepository
|
v
GetProductUseCase
|
v
MCP Tool
The composition/root wiring belongs near the application entry point, not inside the Domain.
The project should avoid hidden global dependencies when practical.
This will be introduced incrementally as the application grows.
Testing Strategy
The architecture should allow business behavior to be tested without:
starting FastMCP
calling DummyJSON
opening an HTTP port
running MCP App UI
For example:
Unit Test
|
v
GetProductUseCase
|
v
FakeProductRepository
This makes the Use Case independently testable.
Unit Tests
Unit tests should focus on:
Domain behavior
Use Cases
Validation
Error handling
using fake or mock dependencies.
Integration Tests
Integration tests can validate boundaries separately:
Infrastructure
|
v
DummyJSON API
and:
MCP Client
|
v
FastMCP Server
This separation prevents external API behavior from making every business test unreliable.
Development Setup
Requirements:
Python 3.12+
uv
Install/synchronize dependencies:
uv sync
Run the MCP server:
uv run python -m presentation.mcp.server
Default endpoint:
http://localhost:8000/mcp
Virtual Environment
The project uses:
.venv/
for isolated Python dependencies.
uv manages the project environment automatically.
Commands should generally be executed using:
uv run ...
For example:
uv run python --version
This avoids relying on globally installed project dependencies.
Development Principles
When extending this template:
- Keep MCP-specific code in Presentation.
- Keep business workflows in Application.
- Keep business models and contracts independent from frameworks where practical.
- Keep external integrations in Infrastructure.
- Depend on abstractions instead of concrete Infrastructure implementations.
- Keep MCP Tools thin.
- Do not hardcode environment-specific configuration.
- Do not commit secrets.
- Prefer typed Python.
- Validate external data at system boundaries.
- Keep external API DTOs separate from Domain models when their structures diverge.
- Make Use Cases independently testable.
- Prefer explicit dependencies over hidden global state.
- Add abstractions when they solve a real architectural problem.
- Keep the Domain independent from FastMCP.
- Translate Infrastructure failures before exposing them outside their boundary.
- Use stable structured error codes.
- Keep MCP App UI focused on presentation and interaction.
- Do not put business logic inside MCP decorators.
- Keep the external API replaceable.
Planned Learning Flow
The template is being built incrementally.
FastMCP Server
|
v
HTTP Transport
|
v
Environment Configuration
|
v
Python Package Structure
|
v
Pydantic Models
|
v
Domain Entities
|
v
Repository Contracts
|
v
Error Hierarchy
|
v
Infrastructure / External API
|
v
Application Use Cases
|
v
MCP Tools
|
v
Dependency Composition
|
v
Centralized Error Handling
|
v
MCP Resources
|
v
MCP Prompts
|
v
MCP App UI
|
v
Interactive MCP Actions
|
v
Unit Tests
|
v
Integration Tests
|
v
Claude / Copilot integration
Final Target
The final project should demonstrate the complete flow:
Claude / Copilot
|
| MCP over HTTP
v
FastMCP Server
|
v
MCP App UI
|
| user action
v
MCP Tool
|
v
Application Use Case
|
v
Domain Contract
|
v
Infrastructure Adapter
|
| HTTP
v
External Service
with errors flowing safely in the opposite direction:
External failure
|
v
Infrastructure Error
|
v
Application / Domain Error
|
v
Presentation Error Boundary
|
v
Structured MCP Error
|
v
Claude / Copilot
Purpose
This repository is intended to become a reusable template and learning reference for creating production-quality FastMCP servers and MCP Apps using Clean Architecture.
The project demonstrates how MCP can be treated as an application boundary rather than allowing MCP-specific concerns to spread throughout the codebase.
The core business logic should remain independent from:
FastMCP
MCP transport
MCP App UI
Claude
Copilot
HTTP providers
databases
external APIs
This makes the application easier to:
maintain
test
extend
replace integrations
run in different environments
connect to different MCP hosts
while preserving clear architectural boundaries.
推荐服务器
Baidu Map
百度地图核心API现已全面兼容MCP协议,是国内首家兼容MCP协议的地图服务商。
Playwright MCP Server
一个模型上下文协议服务器,它使大型语言模型能够通过结构化的可访问性快照与网页进行交互,而无需视觉模型或屏幕截图。
Magic Component Platform (MCP)
一个由人工智能驱动的工具,可以从自然语言描述生成现代化的用户界面组件,并与流行的集成开发环境(IDE)集成,从而简化用户界面开发流程。
Audiense Insights MCP Server
通过模型上下文协议启用与 Audiense Insights 账户的交互,从而促进营销洞察和受众数据的提取和分析,包括人口统计信息、行为和影响者互动。
VeyraX
一个单一的 MCP 工具,连接你所有喜爱的工具:Gmail、日历以及其他 40 多个工具。
graphlit-mcp-server
模型上下文协议 (MCP) 服务器实现了 MCP 客户端与 Graphlit 服务之间的集成。 除了网络爬取之外,还可以将任何内容(从 Slack 到 Gmail 再到播客订阅源)导入到 Graphlit 项目中,然后从 MCP 客户端检索相关内容。
Kagi MCP Server
一个 MCP 服务器,集成了 Kagi 搜索功能和 Claude AI,使 Claude 能够在回答需要最新信息的问题时执行实时网络搜索。
e2b-mcp-server
使用 MCP 通过 e2b 运行代码。
Neon MCP Server
用于与 Neon 管理 API 和数据库交互的 MCP 服务器
Exa MCP Server
模型上下文协议(MCP)服务器允许像 Claude 这样的 AI 助手使用 Exa AI 搜索 API 进行网络搜索。这种设置允许 AI 模型以安全和受控的方式获取实时的网络信息。