Files
prompts/docs/mcp_layout.md
T
John Lancaster dbaaad8df8 initial server
2026-06-18 19:16:06 -05:00

7.4 KiB

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)

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.