For a modern Python project in 2026, I would treat the MCP server exactly like any other deployable service: * `src/` layout * `uv` for 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][1]) I'd structure it something like: ```text 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. ```python 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][2]) --- ## Skill Metadata Every skill gets metadata. ```yaml # 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. ```python # 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: ```text One skill = One FastMCP server ``` This keeps ownership obvious. --- ## Skill Resource Resources are your discovery layer. ```python # 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][3]) --- ## Skill Prompt ```python # 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 ```python # 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. ```python catalog/ ├── server.py ├── tools.py └── resources.py ``` Example: ```python # catalog/tools.py @catalog_server.tool def list_skills(): return [ "nixos", "homelab", "knowledge_base" ] ``` ```python @catalog_server.tool def describe_skill( name: str ): ... ``` --- ## Capability Graph This is where NetworkX belongs. ```python # 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: ```python resource://skills/graph resource://skills/nixos/dependencies ``` Now agents can discover related capabilities without hardcoding them. --- ## pyproject.toml Minimal UV setup: ```toml [project] name = "personal-mcp" version = "0.1.0" dependencies = [ "fastmcp", "networkx", "pydantic-settings", "pyyaml", ] [project.scripts] personal-mcp = "personal_mcp.main:mcp" ``` --- ## Docker ```dockerfile 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: ```python class Skill: id: str metadata: SkillMetadata def create_server(self) -> FastMCP: ... ``` Then `main.py` simply discovers skills dynamically: ```python for skill in discover_skills(): mcp.mount( skill.create_server(), namespace=skill.id, ) ``` At that point adding a new skill becomes: ```text 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. [1]: https://fastmcp.wiki/en/servers/composition?utm_source=chatgpt.com "Server Composition - FastMCP" [2]: https://gofastmcp.com/servers/composition?utm_source=chatgpt.com "Composing Servers - FastMCP" [3]: https://fastmcp.mintlify.app/servers/resources?utm_source=chatgpt.com "Resources & Templates - FastMCP"