7.4 KiB
For a modern Python project in 2026, I would treat the MCP server exactly like any other deployable service:
src/layoutuvfor dependency management- skill modules as importable Python packages
- FastMCP composition via
mount() - Docker image built from the root package
- resources/prompts kept close to the skill that owns them
- avoid giant decorator files with hundreds of tools
FastMCP's mounting/composition model is specifically intended for this sort of modular organization. (FastMCP)
I'd structure it something like:
personal-mcp/
│
├── pyproject.toml
├── uv.lock
├── README.md
│
├── Dockerfile
├── .dockerignore
│
├── src/
│ └── personal_mcp/
│
│ ├── __init__.py
│ │
│ ├── main.py
│ │ # Creates the root MCP server
│ │ # Mounts all skills
│ │
│ ├── settings.py
│ │ # Pydantic settings
│ │
│ ├── graph/
│ │ ├── __init__.py
│ │ └── capability_graph.py
│ │
│ ├── catalog/
│ │ ├── __init__.py
│ │ ├── resources.py
│ │ └── tools.py
│ │
│ └── skills/
│
│ ├── __init__.py
│ │
│ ├── nixos/
│ │
│ │ ├── __init__.py
│ │ ├── server.py
│ │ │
│ │ ├── tools/
│ │ │ └── rebuild.py
│ │ │
│ │ ├── prompts/
│ │ │ └── expert.py
│ │ │
│ │ ├── resources/
│ │ │ ├── overview.py
│ │ │ └── troubleshooting.py
│ │ │
│ │ └── metadata.yaml
│ │
│ ├── homelab/
│ │
│ │ ├── __init__.py
│ │ ├── server.py
│ │ ├── tools/
│ │ ├── prompts/
│ │ ├── resources/
│ │ └── metadata.yaml
│ │
│ └── knowledge_base/
│ ├── __init__.py
│ ├── server.py
│ ├── tools/
│ ├── prompts/
│ ├── resources/
│ └── metadata.yaml
│
└── tests/
├── test_catalog.py
├── test_nixos.py
└── test_homelab.py
main.py
This is intentionally boring.
from fastmcp import FastMCP
from personal_mcp.catalog.server import catalog_server
from personal_mcp.skills.nixos.server import nixos_server
from personal_mcp.skills.homelab.server import homelab_server
mcp = FastMCP("Personal MCP")
mcp.mount(catalog_server, namespace="catalog")
mcp.mount(nixos_server, namespace="nixos")
mcp.mount(homelab_server, namespace="homelab")
if __name__ == "__main__":
mcp.run()
FastMCP namespaces resources, prompts, and tools automatically when mounted. (FastMCP)
Skill Metadata
Every skill gets metadata.
# skills/nixos/metadata.yaml
id: nixos
name: NixOS Administration
description: |
Tools and guidance for managing NixOS systems.
tags:
- nix
- linux
- systemd
capabilities:
- package_management
- service_debugging
- flakes
depends_on:
- certificates
The catalog layer reads these files.
Skill Server
Each skill owns its own MCP instance.
# skills/nixos/server.py
from fastmcp import FastMCP
nixos_server = FastMCP(
"NixOS"
)
from .tools.rebuild import *
from .resources.overview import *
from .prompts.expert import *
The pattern is:
One skill
=
One FastMCP server
This keeps ownership obvious.
Skill Resource
Resources are your discovery layer.
# resources/overview.py
from .server import nixos_server
@nixos_server.resource(
"resource://skills/nixos"
)
def nixos_skill_description():
return """
NixOS administration skill.
Provides:
- flake troubleshooting
- package search
- service diagnostics
- generation management
"""
Resources are exactly what MCP intends for exposing contextual information and data to clients. (fastmcp.mintlify.app)
Skill Prompt
# prompts/expert.py
from .server import nixos_server
@nixos_server.prompt
def nixos_expert():
return """
Act as an expert NixOS administrator.
Prefer:
- flakes
- declarative configuration
- modern systemd patterns
Avoid:
- imperative package management
"""
Skill Tool
# tools/rebuild.py
from .server import nixos_server
@nixos_server.tool
def explain_rebuild():
"""
Explain a nixos-rebuild failure.
"""
return {
"workflow": [
"Inspect journal",
"Check evaluation errors",
"Verify inputs",
"Retry build"
]
}
Catalog Layer
The catalog should be its own server.
Not a skill.
Its job is discovery.
catalog/
├── server.py
├── tools.py
└── resources.py
Example:
# catalog/tools.py
@catalog_server.tool
def list_skills():
return [
"nixos",
"homelab",
"knowledge_base"
]
@catalog_server.tool
def describe_skill(
name: str
):
...
Capability Graph
This is where NetworkX belongs.
# graph/capability_graph.py
import networkx as nx
graph = nx.DiGraph()
graph.add_edge(
"homelab",
"certificates",
relation="depends_on"
)
graph.add_edge(
"nixos",
"homelab",
relation="manages"
)
Expose it as:
resource://skills/graph
resource://skills/nixos/dependencies
Now agents can discover related capabilities without hardcoding them.
pyproject.toml
Minimal UV setup:
[project]
name = "personal-mcp"
version = "0.1.0"
dependencies = [
"fastmcp",
"networkx",
"pydantic-settings",
"pyyaml",
]
[project.scripts]
personal-mcp = "personal_mcp.main:mcp"
Docker
FROM python:3.13-slim
WORKDIR /app
COPY pyproject.toml uv.lock ./
RUN pip install uv
RUN uv sync --frozen
COPY src ./src
CMD ["uv", "run", "python", "-m", "personal_mcp.main"]
If I were building this for long-term growth, I'd make one additional change: treat every skill as a Python package with a common interface:
class Skill:
id: str
metadata: SkillMetadata
def create_server(self) -> FastMCP:
...
Then main.py simply discovers skills dynamically:
for skill in discover_skills():
mcp.mount(
skill.create_server(),
namespace=skill.id,
)
At that point adding a new skill becomes:
mkdir skills/new_skill
drop in metadata.yaml
implement Skill
and the server automatically exposes it. That's the closest thing to a plugin architecture while still remaining very Pythonic and Docker-friendly.