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
+3
View File
@@ -0,0 +1,3 @@
.venv
__pycache__
.cache*
-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"
+11
View File
@@ -3,5 +3,16 @@ name = "prompts"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"fastmcp>=2.10.0",
"zensical>=0.0.45",
]
[project.scripts]
personal-mcp = "personal_mcp.main:main"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/personal_mcp"]
+21 -5
View File
@@ -41,9 +41,10 @@ Produce:
1. Frame the baseline architecture.
2. Choose optional extensions (DB, AI, docs) using decision points below.
3. Map modules, dependencies, and key boundaries.
4. Define key functions/classes and configuration surfaces.
5. Produce phased checklist with rollout or migration notes when relevant.
6. Run completion checks before returning.
4. Define async behavior and UI responsiveness expectations.
5. Define key functions/classes and configuration surfaces.
6. Produce phased checklist with rollout or migration notes when relevant.
7. Run completion checks before returning.
### 1) Baseline architecture
@@ -131,15 +132,27 @@ Prefer:
Avoid reverse imports from services into API or UI modules.
### 5) Testing minimums
### 5) Async and UI responsiveness rules
- Prefer `async def` for page handlers, service methods, and integrations when the call path includes I/O.
- Use non-blocking clients/libraries where possible so long-running I/O does not freeze UI updates.
- Do not run blocking calls (`time.sleep`, blocking HTTP/database clients) in UI event handlers.
- For heavy CPU work, offload to worker/background execution and keep the UI loop free.
- Show progress states for long actions (disable action button, show spinner/progress text, re-enable on completion).
- Stream or chunk incremental results to the UI when workflows are multi-step or long-running.
- Keep cancellation and timeout behavior explicit for user-triggered long tasks.
- Ensure exceptions from async tasks are surfaced with user-friendly feedback and logged for diagnostics.
### 6) Testing minimums
- Test FastAPI health route behavior.
- Test page registration wiring.
- If DB enabled: session lifecycle and rollback behavior tests.
- If AI enabled: graph happy path and interrupt/resume coverage.
- If docs enabled: mounted docs route returns index page.
- For async flows: test long-running actions preserve UI responsiveness (loading state, completion state, and error state).
### 6) Styling architecture
### 7) Styling architecture
- Keep structure and layout in Python modules using NiceGUI class composition.
- Keep visual polish in shared CSS files, loaded once at startup.
@@ -151,6 +164,7 @@ Avoid reverse imports from services into API or UI modules.
- Pages are modularized (not single-file UI).
- Health endpoint exists on FastAPI side.
- Dependency direction is clean and one-way.
- Async-first guidance is applied where I/O exists, with explicit non-blocking UX states.
- Optional DB/AI/docs decisions are explicit and reflected in structure.
- Output includes architecture summary and package-organized checklist.
@@ -171,6 +185,8 @@ Return:
- Do not collapse all pages into one file.
- Do not use globals or implicit global side effects.
- Do not block UI event handlers with synchronous I/O or long CPU tasks.
- Always define loading/progress/error states for long user-triggered actions.
- Keep code minimal but production-minded.
- Prefer clarity and maintainability over clever abstractions.
+97
View File
@@ -0,0 +1,97 @@
---
name: python-logging-dictconfig
description: 'Set up idiomatic Python logging with logging.config.dictConfig. Use when creating or refactoring logging setup, standardizing handlers/formatters, and enforcing centralized config.'
argument-hint: 'Target context (single script, package, FastAPI app, or CLI) and desired log destinations'
---
# Idiomatic Python Logging with dictConfig
Use this skill to produce a minimal, centralized logging setup using `logging.config.dictConfig`.
Load references only when needed:
- Python logging overview and hierarchy: [./references/python-logging-docs.md](./references/python-logging-docs.md)
## When to Use
- A project configures logging ad hoc with `basicConfig` across multiple modules.
- You need one canonical logging configuration for app startup.
- You need consistent formatting and levels across console/file handlers.
- You want library modules to use named loggers without configuring logging themselves.
## Inputs To Collect
1. Runtime type: script, library, web app, worker, CLI.
2. Destinations: stdout only, file only, or both.
3. Desired default level: `INFO`, `DEBUG`, etc.
4. Whether third-party loggers should be tuned (for example `uvicorn`, `sqlalchemy`).
If missing, assume:
- stdout handler
- human-readable formatter
- root level `INFO`
- `disable_existing_loggers: False`
## Procedure
1. Define a single `LOGGING` dictionary in one startup-oriented module (for example `logging_config.py`).
2. Include `version: 1` and set `disable_existing_loggers: False` unless there is a specific reason to silence existing loggers.
3. Define formatters first, then handlers, then logger routing (`root` and optional named `loggers`).
4. Use `logging.config.dictConfig(LOGGING)` exactly once during application startup.
5. In all modules, get loggers via `logger = logging.getLogger(__name__)` and never call `basicConfig`.
6. Keep libraries configuration-free: libraries should emit logs, applications decide routing.
7. Verify behavior with a quick smoke check at multiple levels (`DEBUG`, `INFO`, `WARNING`, `ERROR`).
## Minimal Baseline Template
```python
# logging_config.py
from logging.config import dictConfig
LOGGING = {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(asctime)s %(levelname)s %(name)s: %(message)s"
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": "INFO",
"formatter": "standard",
"stream": "ext://sys.stdout",
}
},
"root": {
"level": "INFO",
"handlers": ["console"],
},
}
def configure_logging() -> None:
dictConfig(LOGGING)
```
```python
# app startup
from .logging_config import configure_logging
configure_logging()
```
```python
# any module
import logging
logger = logging.getLogger(__name__)
logger.info("module initialized")
```
## Completion Checks
1. `dictConfig` is called once at startup, not per module.
2. No `basicConfig` calls remain.
3. Modules use `getLogger(__name__)`.
4. Logs appear at expected level and destination.
5. Third-party logger noise is intentionally configured or left at defaults.
## Branching Guidance
- If structured logs are required: switch formatter output to JSON while keeping `dictConfig` topology unchanged.
- If both console and file output are needed: add a file handler and attach it to `root`.
- If a specific framework logger is too noisy: add a named logger override under `loggers`.
@@ -0,0 +1,18 @@
# Python Logging References
Use these official Python docs when applying this skill.
## Core Documentation
- Logging HOWTO: https://docs.python.org/3/howto/logging.html
- Logging Cookbook: https://docs.python.org/3/howto/logging-cookbook.html
- logging API reference: https://docs.python.org/3/library/logging.html
- logging.config reference: https://docs.python.org/3/library/logging.config.html
## dictConfig-Specific
- Dictionary schema details (`version`, formatters, handlers, loggers, root): https://docs.python.org/3/library/logging.config.html#logging-config-dictschema
- `logging.config.dictConfig` function: https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig
## Practical Notes
- Prefer app-level centralized config with one startup call to `dictConfig`.
- In modules, use `logging.getLogger(__name__)`.
- Avoid calling `basicConfig` in libraries or scattered modules.
+1
View File
@@ -0,0 +1 @@
"""Personal MCP server package."""
+22
View File
@@ -0,0 +1,22 @@
from fastmcp import FastMCP
from personal_mcp.skills.fastapi_uv_docker.server import fastapi_uv_docker_server
from personal_mcp.skills.pytest_scaffolding.server import pytest_scaffolding_server
from personal_mcp.skills.python_logging_dictconfig.server import (
python_logging_dictconfig_server,
)
mcp = FastMCP("personal-mcp")
mcp.mount(pytest_scaffolding_server, namespace="pytest_scaffolding")
mcp.mount(python_logging_dictconfig_server, namespace="python_logging_dictconfig")
mcp.mount(fastapi_uv_docker_server, namespace="fastapi_uv_docker")
def main() -> None:
"""Run the root MCP server."""
mcp.run()
if __name__ == "__main__":
main()
+1
View File
@@ -0,0 +1 @@
"""Mounted skill servers for the personal MCP app."""
@@ -0,0 +1,3 @@
from personal_mcp.skills.fastapi_uv_docker.server import fastapi_uv_docker_server
__all__ = ["fastapi_uv_docker_server"]
@@ -0,0 +1,7 @@
id: fastapi-uv-docker
name: FastAPI uv Docker
description: Provide fast migration guidance to FastAPI plus uv plus Docker.
tags:
- fastapi
- uv
- docker
@@ -0,0 +1,16 @@
from fastmcp import FastMCP
fastapi_uv_docker_server = FastMCP("fastapi-uv-docker")
@fastapi_uv_docker_server.tool()
def fastapi_uv_docker_mvp_checklist(current_state: str = "bare python project") -> list[str]:
"""Return a compact migration checklist for FastAPI + uv + Docker."""
return [
f"Current state: {current_state}",
"Create src/ package layout for the FastAPI app.",
"Manage dependencies with uv and keep uv.lock committed.",
"Use an app factory and lifespan hooks.",
"Add /healthz endpoint for operational checks.",
"Build with a multi-stage Dockerfile and run as non-root user.",
]
@@ -0,0 +1,3 @@
from personal_mcp.skills.pytest_scaffolding.server import pytest_scaffolding_server
__all__ = ["pytest_scaffolding_server"]
@@ -0,0 +1,7 @@
id: pytest-scaffolding
name: Pytest Scaffolding
description: Scaffold a maintainable pytest structure quickly.
tags:
- pytest
- testing
- python
@@ -0,0 +1,15 @@
from fastmcp import FastMCP
pytest_scaffolding_server = FastMCP("pytest-scaffolding")
@pytest_scaffolding_server.tool()
def propose_pytest_mvp_tree(target_scope: str = "src/") -> str:
"""Return a minimal test scaffold plan for a target scope."""
return (
f"MVP pytest scaffold for {target_scope}:\n"
"1. Mirror the source subtree under tests/.\n"
"2. Add one happy-path test and one edge-case test per core module.\n"
"3. Keep fast tests isolated from integration/external dependencies.\n"
"4. Use uv run pytest as the canonical runner."
)
@@ -0,0 +1,5 @@
from personal_mcp.skills.python_logging_dictconfig.server import (
python_logging_dictconfig_server,
)
__all__ = ["python_logging_dictconfig_server"]
@@ -0,0 +1,7 @@
id: python-logging-dictconfig
name: Python Logging DictConfig
description: Provide minimal logging.config.dictConfig setup guidance.
tags:
- logging
- python
- observability
@@ -0,0 +1,30 @@
from fastmcp import FastMCP
python_logging_dictconfig_server = FastMCP("python-logging-dictconfig")
@python_logging_dictconfig_server.tool()
def logging_dictconfig_template(level: str = "INFO") -> dict:
"""Return a minimal dictConfig template for application startup."""
normalized = level.upper()
return {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"standard": {
"format": "%(asctime)s %(levelname)s %(name)s: %(message)s",
}
},
"handlers": {
"console": {
"class": "logging.StreamHandler",
"level": normalized,
"formatter": "standard",
"stream": "ext://sys.stdout",
}
},
"root": {
"level": normalized,
"handlers": ["console"],
},
}
Generated
+1211 -2
View File
File diff suppressed because it is too large Load Diff