This commit is contained in:
John Lancaster
2026-06-18 20:01:47 -05:00
parent 485b93b3c9
commit c915c1846d
3 changed files with 279 additions and 360 deletions
+102 -360
View File
@@ -1,408 +1,150 @@
For a modern Python project in 2026, I would treat the MCP server exactly like any other deployable service:
# Static Docs Hosting Pattern
* `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
## Purpose
FastMCP's mounting/composition model is specifically intended for this sort of modular organization. ([FastMCP][1])
This document describes the completed layout and runtime pattern used to host a pre-built static documentation site from the same FastAPI app process that runs the FastMCP server.
I'd structure it something like:
This design intentionally avoids runtime docs rendering and avoids a separate docs hosting service.
## Completed-State Layout
```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
project-root/
|
|- pyproject.toml
|- uv.lock
|- zensical.toml
|
|- docs/
| |- index.md
| |- architecture.md
| |- mcp_layout.md
| |- generated/
| |- patterns/
| |- index.md
| |- pytest-scaffolding.md
| |- python-logging-dictconfig.md
| |- fastapi-uv-docker.md
|
|- site/
| |- ... static files built by zensical ...
|
|- src/
|- personal_mcp/
|- main.py
|- web/
| |- app.py
| |- docs_mount.py
|- catalog/
| |- server.py
|- skills/
|- pytest_scaffolding/
|- python_logging_dictconfig/
|- fastapi_uv_docker/
```
---
Notes:
## main.py
1. docs contains authored pages and generated markdown pages.
2. site contains static build output only.
3. MCP resources and docs generation share the same content contract.
This is intentionally boring.
## Runtime Composition
```python
from fastmcp import FastMCP
The runtime process serves two surfaces:
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
1. MCP protocol surface from FastMCP
2. Static docs surface from FastAPI static mount
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()
```mermaid
flowchart TD
A[FastMCP Root Server] --> B[MCP Transport]
A --> C[FastAPI Application]
C --> D[Static Mount /docs]
D --> E[Zensical site output directory]
```
FastMCP namespaces resources, prompts, and tools automatically when mounted. ([FastMCP][2])
## Build and Publish Flow
---
The docs flow is pre-build only.
## Skill Metadata
1. Read module resources and catalog metadata.
2. Generate docs markdown pages into docs/generated.
3. Build static site with Zensical into site.
4. Start app and serve site directory as static files.
Every skill gets metadata.
No runtime markdown conversion is required.
```yaml
# skills/nixos/metadata.yaml
## Content Merge Pattern
id: nixos
The published docs site always contains both:
name: NixOS Administration
1. Project-authored docs pages
2. Resource-derived pattern pages
description: |
Tools and guidance for managing NixOS systems.
This ensures the public docs reflect both architectural guidance and live pattern knowledge.
tags:
- nix
- linux
- systemd
## Resource-to-Docs Mapping
capabilities:
- package_management
- service_debugging
- flakes
Pattern resources map directly to docs sections.
depends_on:
- certificates
```
Example mapping model:
The catalog layer reads these files.
1. resource://skills/<id>/overview -> docs/generated/patterns/<id>.md section Overview
2. resource://skills/<id>/rules -> docs/generated/patterns/<id>.md section Rules
3. resource://skills/<id>/checklist -> docs/generated/patterns/<id>.md section Checklist
4. resource://skills/<id>/references -> docs/generated/patterns/<id>.md section References
---
Catalog resources generate index pages and navigation pages.
## Skill Server
## Why This Pattern
Each skill owns its own MCP instance.
### Operational Simplicity
```python
# skills/nixos/server.py
One application process serves both protocol and static docs surfaces.
from fastmcp import FastMCP
### Deterministic Docs
nixos_server = FastMCP(
"NixOS"
)
Published docs are immutable static assets for a given build.
from .tools.rebuild import *
from .resources.overview import *
from .prompts.expert import *
```
### Documentation Fidelity
The pattern is:
Resource-derived pages align docs with the exact methodology contracts exposed to agents.
```text
One skill
=
One FastMCP server
```
### Maintainer Experience
This keeps ownership obvious.
Authors continue to work in markdown while resource contracts remain machine-consumable.
---
## FastAPI Static Mount Expectations
## Skill Resource
The FastAPI app is expected to:
Resources are your discovery layer.
1. Mount static directory containing Zensical output.
2. Serve index and asset files from that directory.
3. Keep docs route stable across releases.
```python
# resources/overview.py
Recommended route conventions:
from .server import nixos_server
1. /docs for static site root
2. /docs/* for static assets and page routes
## Update Lifecycle
@nixos_server.resource(
"resource://skills/nixos"
)
def nixos_skill_description():
return """
NixOS administration skill.
For each documentation update:
Provides:
- flake troubleshooting
- package search
- service diagnostics
- generation management
"""
```
1. Edit authored markdown and resource content.
2. Regenerate docs markdown from resources.
3. Rebuild static site.
4. Restart runtime if needed.
Resources are exactly what MCP intends for exposing contextual information and data to clients. ([fastmcp.mintlify.app][3])
This keeps docs publication explicit and predictable.
---
## Example Source Material
## Skill Prompt
Existing reference docs remain valid content inputs in this pattern:
```python
# prompts/expert.py
1. ../skills/pytest-scaffolding/references/pytest-docs.md
2. ../skills/python-logging-dictconfig/references/python-logging-docs.md
3. ../skills/fastapi-uv-docker/references/fastapi-best-practices.md
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"
These are source documents, not deployment artifacts.