409 lines
7.4 KiB
Markdown
409 lines
7.4 KiB
Markdown
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"
|