initial server

This commit is contained in:
John Lancaster
2026-06-18 19:16:06 -05:00
parent 9b02007216
commit dbaaad8df8
21 changed files with 1886 additions and 286 deletions
-168
View File
@@ -3,171 +3,3 @@ icon: lucide/rocket
---
# Get started
For full documentation visit [zensical.org](https://zensical.org/docs/).
## Commands
* [`zensical new`][new] - Create a new project
* [`zensical serve`][serve] - Start local web server
* [`zensical build`][build] - Build your site
[new]: https://zensical.org/docs/usage/new/
[serve]: https://zensical.org/docs/usage/preview/
[build]: https://zensical.org/docs/usage/build/
## Examples
### Admonitions
> Go to [documentation](https://zensical.org/docs/authoring/admonitions/)
!!! note
This is a **note** admonition. Use it to provide helpful information.
!!! warning
This is a **warning** admonition. Be careful!
### Details
> Go to [documentation](https://zensical.org/docs/authoring/admonitions/#collapsible-blocks)
??? info "Click to expand for more info"
This content is hidden until you click to expand it.
Great for FAQs or long explanations.
## Code Blocks
> Go to [documentation](https://zensical.org/docs/authoring/code-blocks/)
``` python hl_lines="2" title="Code blocks"
def greet(name):
print(f"Hello, {name}!") # (1)!
greet("Python")
```
1. > Go to [documentation](https://zensical.org/docs/authoring/code-blocks/#code-annotations)
Code annotations allow to attach notes to lines of code.
Code can also be highlighted inline: `#!python print("Hello, Python!")`.
## Content tabs
> Go to [documentation](https://zensical.org/docs/authoring/content-tabs/)
=== "Python"
``` python
print("Hello from Python!")
```
=== "Rust"
``` rs
println!("Hello from Rust!");
```
## Diagrams
> Go to [documentation](https://zensical.org/docs/authoring/diagrams/)
``` mermaid
graph LR
A[Start] --> B{Error?};
B -->|Yes| C[Hmm...];
C --> D[Debug];
D --> B;
B ---->|No| E[Yay!];
```
## Footnotes
> Go to [documentation](https://zensical.org/docs/authoring/footnotes/)
Here's a sentence with a footnote.[^1]
Hover it, to see a tooltip.
[^1]: This is the footnote.
## Formatting
> Go to [documentation](https://zensical.org/docs/authoring/formatting/)
- ==This was marked (highlight)==
- ^^This was inserted (underline)^^
- ~~This was deleted (strikethrough)~~
- H~2~O
- A^T^A
- ++ctrl+alt+del++
## Icons, Emojis
> Go to [documentation](https://zensical.org/docs/authoring/icons-emojis/)
* :sparkles: `:sparkles:`
* :rocket: `:rocket:`
* :tada: `:tada:`
* :memo: `:memo:`
* :eyes: `:eyes:`
## Maths
> Go to [documentation](https://zensical.org/docs/authoring/math/)
$$
\cos x=\sum_{k=0}^{\infty}\frac{(-1)^k}{(2k)!}x^{2k}
$$
!!! warning "Needs configuration"
Note that MathJax is included via a `script` tag on this page and is not
configured in the generated default configuration to avoid including it
in a pages that do not need it. See the documentation for details on how
to configure it on all your pages if they are more Maths-heavy than these
simple starter pages.
<script id="MathJax-script" src="https://unpkg.com/mathjax@3/es5/tex-mml-chtml.js"></script>
<script>
window.MathJax = {
tex: {
inlineMath: [["\\(", "\\)"]],
displayMath: [["\\[", "\\]"]],
processEscapes: true,
processEnvironments: true
},
options: {
ignoreHtmlClass: ".*|",
processHtmlClass: "arithmatex"
}
};
document$.subscribe(() => {
MathJax.startup.output.clearCache()
MathJax.typesetClear()
MathJax.texReset()
MathJax.typesetPromise()
})
</script>
## Task Lists
> Go to [documentation](https://zensical.org/docs/authoring/lists/#using-task-lists)
* [x] Install Zensical
* [x] Configure `zensical.toml`
* [x] Write amazing documentation
* [ ] Deploy anywhere
## Tooltips
> Go to [documentation](https://zensical.org/docs/authoring/tooltips/)
[Hover me][example]
[example]: https://example.com "I'm a tooltip!"
-111
View File
@@ -1,111 +0,0 @@
---
icon: simple/markdown
---
# Markdown in 5min
## Headers
```
# H1 Header
## H2 Header
### H3 Header
#### H4 Header
##### H5 Header
###### H6 Header
```
## Text formatting
```
**bold text**
*italic text*
***bold and italic***
~~strikethrough~~
`inline code`
```
## Links and images
```
[Link text](https://example.com)
[Link with title](https://example.com "Hover title")
![Alt text](image.jpg)
![Image with title](image.jpg "Image title")
```
## Lists
```
Unordered:
- Item 1
- Item 2
- Nested item
Ordered:
1. First item
2. Second item
3. Third item
```
## Blockquotes
```
> This is a blockquote
> Multiple lines
>> Nested quote
```
## Code blocks
````
```javascript
function hello() {
console.log("Hello, world!");
}
```
````
## Tables
```
| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Row 1 | Data | Data |
| Row 2 | Data | Data |
```
## Horizontal rule
```
---
or
***
or
___
```
## Task lists
```
- [x] Completed task
- [ ] Incomplete task
- [ ] Another task
```
## Escaping characters
```
Use backslash to escape: \* \_ \# \`
```
## Line breaks
```
End a line with two spaces
to create a line break.
Or use a blank line for a new paragraph.
```
+408
View File
@@ -0,0 +1,408 @@
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"