Compare commits
45 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 660ca88e47 | |||
| e60fc4b27b | |||
| 8817d2586f | |||
| bb7508cf65 | |||
| 467e1d3c35 | |||
| 3885774e5b | |||
| f54cacd6cb | |||
| 19f3c1740a | |||
| c273ecfc54 | |||
| fa4498cb78 | |||
| adaa4177fe | |||
| 85355a8509 | |||
| 127e56692e | |||
| 85eb75d188 | |||
| 5c4de7b721 | |||
| ed6068f398 | |||
| 75b0c8d192 | |||
| 45d8beda8a | |||
| 7a9e4044f0 | |||
| 3347443ca9 | |||
| 964cd6f76d | |||
| ef3255544f | |||
| be9551c76e | |||
| 9c3fafd2fe | |||
| 36abea5940 | |||
| 07475f972f | |||
| 59c638c634 | |||
| 1254cc5432 | |||
| a4db33531e | |||
| 818de1b3f9 | |||
| 9f34e12e08 | |||
| e78383be1f | |||
| 6c5fda9c3a | |||
| 99e741f2de | |||
| c3dc66b20f | |||
| 85ae1fe8eb | |||
| b6010775ad | |||
| 4b1a261b2d | |||
| d1c80d4737 | |||
| c915c1846d | |||
| 485b93b3c9 | |||
| d54f427112 | |||
| dbaaad8df8 | |||
| 9b02007216 | |||
| 22e5357ffb |
@@ -0,0 +1,10 @@
|
||||
.git
|
||||
.github
|
||||
.venv
|
||||
__pycache__
|
||||
.cache*
|
||||
.mypy_cache
|
||||
.pytest_cache
|
||||
.ruff_cache
|
||||
site/
|
||||
.env
|
||||
@@ -0,0 +1,18 @@
|
||||
# Personal MCP HTTP runtime
|
||||
PERSONAL_MCP_HOST=127.0.0.1
|
||||
PERSONAL_MCP_PORT=8000
|
||||
PERSONAL_MCP_LOG_LEVEL=info
|
||||
PERSONAL_MCP_DEBUG=false
|
||||
|
||||
# Mounted routes
|
||||
PERSONAL_MCP_MCP_ROUTE=/mcp
|
||||
PERSONAL_MCP_DOCS_ROUTE=/docs
|
||||
|
||||
# Static docs output built by Zensical
|
||||
PERSONAL_MCP_SITE_DIR=site
|
||||
|
||||
# Example local command:
|
||||
# uv run uvicorn personal_mcp.main:app --host 127.0.0.1 --port 8000 --log-level info
|
||||
|
||||
# Example container-oriented command:
|
||||
# uv run uvicorn personal_mcp.main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips '*'
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
name: Zensical Docs Markdown Guidance
|
||||
description: Use the Zensical docs MCP resource when editing Markdown documentation in this repository.
|
||||
applyTo: '**/*.md'
|
||||
---
|
||||
|
||||
When editing Markdown files in this repository, use the Zensical docs resource `resource://skills/zensical-docs/document` for relevant documentation authoring guidance.
|
||||
|
||||
Prefer Zensical-native documentation conventions when they cover the need cleanly, while preserving expected MkDocs compatibility unless the Zensical guidance intentionally diverges.
|
||||
@@ -0,0 +1,46 @@
|
||||
## Plan: Docs-First FastMCP End State
|
||||
|
||||
Create a docs-first FastMCP architecture where all Markdown remains in docs/ as the only source of truth, each skill is Anthropic-compatible in its own directory, skill metadata lives in SKILL.md frontmatter, and packaged docs are served through importlib.resources so stdio deployments work from installed wheels.
|
||||
|
||||
**Steps**
|
||||
1. Phase 1: Define the end-state content contract. Confirm canonical structure as docs/skills/<skill-id>/SKILL.md plus docs/skills/<skill-id>/references/..., with strict per-skill ownership and no metadata.yaml sidecar. Also define stable skill-id rules (kebab-case, immutable after release). Deliverable: update the current docs/ directory with the finalized end-state content contract from this step.
|
||||
2. Phase 1: Define SKILL.md frontmatter schema with Pydantic-compatible fields: id, version, name, description, tags, capabilities, depends_on, and references manifest entries. The references manifest must map logical reference ids to relative paths so each skill can reorganize references internally without changing global server code. Depends on step 1. Deliverable: update the current docs/ directory with the finalized SKILL.md frontmatter schema from this step.
|
||||
3. Phase 1: Define URI contract with explicit break-and-replace policy. Recommend resource://catalog/skills_index, resource://catalog/skills/{skill_id}, resource://skills/{skill_id}/document, resource://skills/{skill_id}/references/{ref_id}, and resource://docs/{path*}. Evolving URIs and reference ids requires direct replacement, with no aliases or compatibility shims. Depends on steps 1-2. Deliverable: update the current docs/ directory with the finalized URI contract and break-and-replace policy from this step.
|
||||
4. Phase 2: Build a docs registry loader that reads packaged docs via importlib.resources.files(...) Traversable APIs, parses SKILL.md frontmatter, validates schema, and creates an in-memory registry keyed by skill_id. Fail fast for duplicate ids, missing files, broken reference mappings, or invalid depends_on. Depends on steps 2-3.
|
||||
5. Phase 2: Register FastMCP resources from the registry using RFC6570 templates (including wildcard paths where appropriate), read-only/idempotent annotations, explicit mime types, and on_duplicate_resources="error" for startup safety. Depends on step 4.
|
||||
6. Phase 2: Add discovery surfaces as resources first, then tool fallback. Keep catalog discovery in resources, then add ResourcesAsTools for tool-only clients. Add thin discovery tools only for parity and optional BM25/regex tool search when catalog/tool volume grows enough to affect token efficiency. Depends on step 5.
|
||||
7. Phase 3: Implement packaging so docs/ is copied into package resource space at build time (wheel + sdist) while docs/ remains canonical in source control. Use importlib.resources at runtime only; avoid direct filesystem assumptions. Depends on steps 4-6.
|
||||
8. Phase 3: Remove materialization coupling between skill source modules and docs. The website build reads docs/ directly, while MCP reads packaged docs resources from the installed package. This preserves one authored source with two distribution surfaces. Depends on step 7.
|
||||
9. Phase 4: Add validation and CI gates: frontmatter schema checks, URI uniqueness checks, reference integrity checks, docs build check, package content check, and stdio smoke checks that read representative skill/document resources from an installed wheel. Depends on steps 5-8.
|
||||
10. Phase 4: Add long-term maintainability guardrails: architecture decision record for URI and schema contracts, skill authoring checklist, and release checklist for evolving references safely within one skill. Parallel with step 9 after core architecture is stable.
|
||||
|
||||
|
||||
|
||||
**Relevant files**
|
||||
- /home/john/Documents/prompts/docs/index.md — Keep top-level docs entry and explain docs-first architecture contract.
|
||||
- /home/john/Documents/prompts/docs/skills — Canonical location for all skill content, including SKILL.md and references.
|
||||
- /home/john/Documents/prompts/pyproject.toml — Build inclusion rules for packaged markdown resources in wheel/sdist.
|
||||
- /home/john/Documents/prompts/src/personal_mcp/main.py — App/server startup wiring for resource registry initialization.
|
||||
- /home/john/Documents/prompts/src/personal_mcp/mcp.py — FastMCP instance composition and transform registration.
|
||||
- /home/john/Documents/prompts/src/personal_mcp/catalog/server.py — Catalog resource and fallback discovery behavior.
|
||||
- /home/john/Documents/prompts/src/personal_mcp/skills/document_loader.py — Replace file-path assumptions with importlib.resources docs registry loading.
|
||||
- /home/john/Documents/prompts/src/personal_mcp/web/materialize_skill_docs.py — De-scope or retire materialization once docs-first runtime is authoritative.
|
||||
|
||||
**Verification**
|
||||
1. Run uv run zensical build to verify docs/ remains valid and site output is stable.
|
||||
2. Run uv run pytest -q with tests that validate frontmatter parsing, URI generation, reference mapping, and catalog responses.
|
||||
3. Run a packaging integrity check using importlib.resources.files(...) to confirm packaged docs resources exist and are readable from an installed wheel.
|
||||
4. Run a stdio MCP smoke test that lists resources and reads at least one skill document and one reference document.
|
||||
5. Run fallback-client smoke tests verifying list_resources/read_resource tools work and return expected metadata for both static and templated resources.
|
||||
|
||||
**Decisions**
|
||||
- Anthropic compatibility: strict skill directory pattern with SKILL.md and references subtree.
|
||||
- Metadata strategy: YAML frontmatter in SKILL.md (no separate metadata file).
|
||||
- Discovery strategy: resource-first catalog with tool fallback for tool-only MCP clients.
|
||||
- Included scope: ideal end-state architecture, contracts, validation, and packaging for stdio operation.
|
||||
- Excluded scope: migration mechanics from current implementation, backward-compat shim details, and docs visual redesign.
|
||||
|
||||
**Further Considerations**
|
||||
1. Prefer recursive references support under each skill plus frontmatter manifest ids, so skill teams can reorganize internal reference folders without URI churn.
|
||||
2. Define a hard rule that skill_id and directory name must match exactly to eliminate namespace/slug drift classes of bugs.
|
||||
3. Do not provide URI aliases; client updates must track canonical URI contract changes directly.
|
||||
@@ -0,0 +1,169 @@
|
||||
**Phase 3 Results: Packaging Contract and Surface Decoupling (Wheel/sdist Resources + Docs-Only Authoring)**
|
||||
|
||||
This section finalizes Phase 3 by defining how authored docs are packaged as runtime resources, how runtime loading avoids filesystem assumptions, and how website and MCP distribution surfaces are decoupled while sharing one authored source.
|
||||
|
||||
### Greenfield Framing (Normative)
|
||||
|
||||
This Phase 3 design assumes a full refactor with intentional break-and-replace behavior:
|
||||
|
||||
1. No compatibility shims, aliases, adapter layers, or dual-read runtime paths.
|
||||
2. No runtime dependency on repository checkout layout.
|
||||
3. Runtime docs access is package-resource-only.
|
||||
4. Canonical authoring remains in `docs/` in source control.
|
||||
|
||||
### Research Baseline (Packaging + Runtime)
|
||||
|
||||
Authoritative references used for this phase:
|
||||
|
||||
1. Python `importlib.resources` docs (`files`, `Traversable`, and zip-safe behavior)
|
||||
2. Python packaging guidance for wheel/sdist data inclusion
|
||||
3. Hatchling build target configuration guidance for including non-code files
|
||||
4. Existing repository constraints from Steps 4-5 (registry-first, deterministic startup, resource-first discovery)
|
||||
|
||||
Best-practice conclusions applied to this design:
|
||||
|
||||
1. Package docs as build artifacts so runtime reads work from installed wheels.
|
||||
2. Keep docs source-of-truth in one place (`docs/`) and project into package resource space at build time.
|
||||
3. Avoid `Path(__file__)`/repo-root probing in runtime paths.
|
||||
4. Enforce parity across wheel and sdist so local/dev/prod behavior does not drift.
|
||||
|
||||
### Phase 3 Responsibilities (Normative)
|
||||
|
||||
Phase 3 MUST:
|
||||
|
||||
1. Ensure authored markdown under `docs/` is included in wheel and sdist artifacts.
|
||||
2. Ensure runtime docs registry/document reads use `importlib.resources` only.
|
||||
3. Ensure MCP runtime behavior is independent of current working directory or checkout structure.
|
||||
4. Ensure website docs build continues to consume source `docs/` directly.
|
||||
5. Remove materialization/path-probing coupling from runtime loader code.
|
||||
6. Preserve deterministic packaged docs layout for registry/resource URI generation.
|
||||
|
||||
### Packaging Contract (Wheel + sdist)
|
||||
|
||||
Canonical packaging behavior:
|
||||
|
||||
1. Source-authored docs remain at repository root: `docs/`.
|
||||
2. Build projects docs into package resource space under `personal_mcp/docs/` inside artifacts.
|
||||
3. Runtime anchor for docs loading is `importlib.resources.files("personal_mcp").joinpath("docs")`.
|
||||
4. Build artifacts MUST include:
|
||||
- top-level docs pages used by discovery/overview
|
||||
- `docs/skills/<skill-id>/SKILL.md`
|
||||
- `docs/skills/<skill-id>/references/**`
|
||||
|
||||
Parity requirements:
|
||||
|
||||
1. Wheel and sdist contain equivalent docs content for runtime use.
|
||||
2. Missing docs resources in either artifact is a hard validation failure.
|
||||
|
||||
### Build-System Plan (pyproject + build)
|
||||
|
||||
Primary target file:
|
||||
|
||||
1. `pyproject.toml`
|
||||
|
||||
Configuration goals:
|
||||
|
||||
1. Add explicit build inclusion rules so docs resources are shipped in wheel artifacts.
|
||||
2. Add explicit sdist inclusion rules so docs are present for source builds.
|
||||
3. Keep inclusion deterministic and auditable (no implicit glob side effects beyond intended docs content).
|
||||
4. Ensure packaged destination path matches runtime anchor (`personal_mcp/docs`).
|
||||
|
||||
Implementation note:
|
||||
|
||||
1. Use Hatchling-native inclusion mapping (for example force-include or equivalent target-level include mapping) to project `docs/` into package resource space.
|
||||
2. Prefer one clear packaging path over multiple fallback packaging mechanisms.
|
||||
|
||||
### Runtime Loader Contract (No Filesystem Assumptions)
|
||||
|
||||
Primary target file:
|
||||
|
||||
1. `src/personal_mcp/skills/document_loader.py`
|
||||
|
||||
Required runtime behavior:
|
||||
|
||||
1. Remove repository-root discovery helpers and path-probing candidates.
|
||||
2. Remove metadata-based document path overrides that bypass canonical skill layout.
|
||||
3. Resolve SKILL and reference documents via package-resource-relative paths only.
|
||||
4. Keep reads UTF-8 and deterministic.
|
||||
5. Raise explicit errors for missing packaged resources; no fallback probing.
|
||||
|
||||
Prohibited runtime behavior:
|
||||
|
||||
1. No `Path(__file__).resolve().parents[...]` lookup for docs.
|
||||
2. No implicit fallback to source-tree `docs/` during runtime reads.
|
||||
3. No slug-guessing or namespace substitution for path recovery.
|
||||
|
||||
### Surface Decoupling Contract (Website vs MCP)
|
||||
|
||||
Website surface:
|
||||
|
||||
1. Website build pipeline consumes source `docs/` directly (`uv run zensical build`).
|
||||
2. Static output (`site/`) remains a build artifact served by web mounting logic.
|
||||
|
||||
MCP surface:
|
||||
|
||||
1. MCP runtime serves docs from packaged resources loaded by registry/resource handlers.
|
||||
2. MCP does not read `site/` and does not depend on website build artifacts.
|
||||
|
||||
Decoupling guarantees:
|
||||
|
||||
1. One authored source (`docs/`), two distribution surfaces (website + MCP runtime).
|
||||
2. Changes to website serving do not alter MCP resource loading semantics.
|
||||
3. Changes to MCP runtime loader do not require website materialization logic.
|
||||
|
||||
### Integration Plan for Existing Modules
|
||||
|
||||
Primary integration targets:
|
||||
|
||||
1. `pyproject.toml`: add wheel/sdist docs inclusion mapping.
|
||||
2. `src/personal_mcp/skills/document_loader.py`: replace filesystem probing with package-resource loading.
|
||||
3. `src/personal_mcp/main.py`: keep startup composition deterministic once registry/resource registration is in place.
|
||||
4. `src/personal_mcp/mcp.py`: maintain registry-driven resource composition as canonical runtime surface.
|
||||
5. `src/personal_mcp/web/docs_mount.py`: continue static-site mount behavior without coupling to MCP runtime docs loading.
|
||||
|
||||
Cleanup targets:
|
||||
|
||||
1. Remove obsolete references to materialization-only modules if no longer present/used.
|
||||
2. Remove dead code paths that attempt source-tree fallback loading.
|
||||
|
||||
### Validation and Test Plan (Phase 3 Scope)
|
||||
|
||||
Build/package validation:
|
||||
|
||||
1. Build wheel and sdist in CI/local.
|
||||
2. Inspect artifacts to confirm `personal_mcp/docs/**` exists and includes representative skill/reference files.
|
||||
3. Install built wheel in isolated environment and verify resource reads via `importlib.resources.files(...)`.
|
||||
|
||||
Runtime validation:
|
||||
|
||||
1. Run MCP in an environment where repo-root docs paths are unavailable and confirm reads still succeed.
|
||||
2. Verify representative URIs resolve (skill document and reference document).
|
||||
3. Confirm startup fails clearly if required packaged docs resources are missing.
|
||||
|
||||
Decoupling validation:
|
||||
|
||||
1. Run `uv run zensical build` to verify website pipeline still consumes source `docs/`.
|
||||
2. Confirm MCP runtime does not require `site/` presence.
|
||||
3. Confirm web static serving behavior is unchanged when docs are built.
|
||||
|
||||
Expected command path in this repo:
|
||||
|
||||
1. `uv run pytest -q`
|
||||
2. `uv run zensical build`
|
||||
|
||||
### Acceptance Criteria for Phase 3 Completion
|
||||
|
||||
Phase 3 is complete when all are true:
|
||||
|
||||
1. Wheel and sdist include docs resources in deterministic package paths.
|
||||
2. Runtime docs loading works from installed artifacts using `importlib.resources` only.
|
||||
3. Runtime docs loading has no checkout-path dependency and no fallback probing.
|
||||
4. Website docs build remains source-docs-driven and independent of MCP runtime loading.
|
||||
5. No compatibility shims, aliases, or dual runtime loader paths exist.
|
||||
|
||||
### Non-goals for Phase 3
|
||||
|
||||
1. No Step 6 discovery-tool fallback implementation details.
|
||||
2. No URI aliasing or backward-compat transition mechanics.
|
||||
3. No redesign of skill frontmatter/schema contracts already finalized in earlier steps.
|
||||
4. No web UI visual redesign or docs IA overhaul.
|
||||
@@ -0,0 +1,84 @@
|
||||
**Step 1 Results: End-State Content Contract**
|
||||
|
||||
This section finalizes Step 1 by defining the canonical authored content model.
|
||||
|
||||
### Step Deliverable
|
||||
|
||||
- Update the current `docs/` directory with the finalized Step 1 content contract from this document.
|
||||
|
||||
### Canonical source of truth
|
||||
|
||||
- All authored Markdown lives under `docs/`.
|
||||
- MCP resources and static docs are two distribution surfaces of the same authored files.
|
||||
- No parallel authored markdown is allowed in `src/` or other package-only paths.
|
||||
|
||||
### Canonical skill shape (Anthropic-compatible)
|
||||
|
||||
Each skill is one directory under `docs/skills/`:
|
||||
|
||||
```text
|
||||
docs/
|
||||
skills/
|
||||
<skill-id>/
|
||||
SKILL.md
|
||||
references/
|
||||
... (one or more markdown files, optional nested folders)
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `SKILL.md` is required for every skill.
|
||||
- `references/` is the only place for skill-specific supporting docs.
|
||||
- Nested folders inside `references/` are allowed so a skill can reorganize internals without changing global architecture.
|
||||
- Skill directories are independent ownership boundaries; no cross-skill file writes.
|
||||
|
||||
### File placement and ownership boundaries
|
||||
|
||||
- Top-level project docs stay in `docs/*.md`.
|
||||
- Skill docs stay in `docs/skills/<skill-id>/...`.
|
||||
- A skill may link to other skills, but must not store content inside another skill's directory.
|
||||
- Server/runtime code may index and serve docs, but must not be the source of authored markdown.
|
||||
|
||||
### Metadata location constraint
|
||||
|
||||
- Skill metadata is embedded in YAML frontmatter in `SKILL.md`.
|
||||
- No `metadata.yaml` sidecar in the end state.
|
||||
- Reference lookup metadata (ids to relative paths) is declared from `SKILL.md` frontmatter, not inferred as a hidden global convention.
|
||||
|
||||
### Skill-id contract (change-friendly)
|
||||
|
||||
`skill-id` is the public identifier and SHOULD satisfy all rules below:
|
||||
|
||||
- Format: lowercase kebab-case only.
|
||||
- Character set: `a-z`, `0-9`, and `-`.
|
||||
- Must start with a letter.
|
||||
- No underscores, spaces, dots, or uppercase characters.
|
||||
- Directory name should equal `skill-id` in each committed revision.
|
||||
- Frontmatter `id` should equal directory name in each committed revision.
|
||||
- Treat `skill-id` as immutable after release; any rename is a breaking replacement and clients must move to the new id.
|
||||
|
||||
Example valid ids:
|
||||
|
||||
- `fastapi-uv-docker`
|
||||
- `zensical-docs`
|
||||
- `pytest-scaffolding`
|
||||
|
||||
Example invalid ids:
|
||||
|
||||
- `fastapi_uv_docker` (underscore)
|
||||
- `Zensical-Docs` (uppercase)
|
||||
- `docs.zensical` (dot)
|
||||
|
||||
### Invariants this contract guarantees
|
||||
|
||||
- One authored source tree (`docs/`) for both website and MCP.
|
||||
- One skill directory maps to one skill identity per revision.
|
||||
- Namespace/slug drift is minimized by keeping directory and frontmatter ids aligned per revision.
|
||||
- Per-skill reference structure can evolve without changing cross-skill architecture.
|
||||
- Packaging for stdio is deterministic because authored content is path-stable.
|
||||
|
||||
### Non-goals for Step 1
|
||||
|
||||
- No URI versioning policy details yet (handled in Step 3).
|
||||
- No full frontmatter schema details yet (handled in Step 2).
|
||||
- No migration instructions from current architecture (out of scope for this plan).
|
||||
@@ -0,0 +1,298 @@
|
||||
**Step 2 Results: SKILL.md Frontmatter and FastMCP Metadata Contract**
|
||||
|
||||
This section finalizes Step 2 by defining the canonical SKILL.md frontmatter schema, separating Anthropic-supported fields from repository extension fields, and mapping frontmatter to FastMCP-native metadata surfaces for resources and tools.
|
||||
|
||||
### Step Deliverable
|
||||
|
||||
- Update the current `docs/` directory with the finalized Step 2 frontmatter and metadata contract content from this document.
|
||||
|
||||
### Anthropic Frontmatter Support (Research Baseline)
|
||||
|
||||
Across Anthropic API and Agent Skills specification surfaces:
|
||||
|
||||
- Required for custom skill bundles: `name`, `description`.
|
||||
- `name` constraints (Agent Skills API docs): 1-64 chars, lowercase letters/numbers/hyphens, no XML tags, and must not use reserved words `anthropic` or `claude`.
|
||||
- `description` constraints (Agent Skills API docs): 1-1024 chars, non-empty, no XML tags.
|
||||
|
||||
Portable optional fields from the Agent Skills specification:
|
||||
|
||||
- `license`
|
||||
- `compatibility`
|
||||
- `metadata`
|
||||
- `allowed-tools` (experimental)
|
||||
|
||||
Claude Code-specific optional fields (supported by Claude Code skills docs):
|
||||
|
||||
- `when_to_use`, `argument-hint`, `arguments`
|
||||
- `disable-model-invocation`, `user-invocable`
|
||||
- `allowed-tools`, `disallowed-tools`
|
||||
- `model`, `effort`, `context`, `agent`, `hooks`, `paths`, `shell`
|
||||
|
||||
Contract decision for this repository:
|
||||
|
||||
- Treat `name` and `description` as required in all SKILL.md files, even where a client could infer defaults.
|
||||
- Keep Anthropic-facing semantics in standard fields and keep MCP indexing metadata in a namespaced extension block.
|
||||
- Preserve forward compatibility by allowing additive optional metadata fields over time.
|
||||
|
||||
### Canonical Frontmatter Schema For This Repository
|
||||
|
||||
Use this exact two-layer pattern:
|
||||
|
||||
1. Anthropic layer (portable): top-level fields intended for Anthropic/Agent Skills behavior.
|
||||
2. Repository layer (runtime indexing): one namespaced block, `x-personal-mcp`, for MCP catalog and routing metadata.
|
||||
|
||||
Canonical shape:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: <skill-id>
|
||||
description: <what this skill does and when to use it>
|
||||
|
||||
# Optional Anthropic/Agent Skills fields (use only when needed)
|
||||
when_to_use: <extra trigger guidance>
|
||||
allowed-tools: <space-separated string or YAML list>
|
||||
disable-model-invocation: false
|
||||
user-invocable: true
|
||||
license: <optional>
|
||||
compatibility: <optional>
|
||||
|
||||
# Repository-specific metadata (authoritative for MCP indexing)
|
||||
x-personal-mcp:
|
||||
id: <skill-id>
|
||||
version: <semver>
|
||||
tags:
|
||||
- <tag>
|
||||
capabilities:
|
||||
- resource://skills/<skill-id>/document
|
||||
depends_on: []
|
||||
references:
|
||||
<ref-id>:
|
||||
path: references/<file>.md
|
||||
mime_type: text/markdown
|
||||
title: <short title>
|
||||
---
|
||||
```
|
||||
|
||||
### Repository Metadata Field Rules (`x-personal-mcp`)
|
||||
|
||||
- `id` required: must follow Step 1 skill-id rules and equal directory name.
|
||||
- `version` required: semantic version string.
|
||||
- `tags` optional: list of kebab-case discovery labels.
|
||||
- `capabilities` required: list of MCP URIs this skill publishes.
|
||||
- `depends_on` optional: list of other skill ids.
|
||||
- `references` optional map:
|
||||
- key is `ref-id` (kebab-case).
|
||||
- `path` is a skill-relative markdown path and must stay inside the same skill directory.
|
||||
- nested folders under `references/` are allowed.
|
||||
- `mime_type` defaults to `text/markdown` if omitted.
|
||||
- `title` is an optional display label.
|
||||
- renaming `ref-id` values is allowed when needed; optional aliases may be used during transitions.
|
||||
|
||||
### Pydantic Models For Frontmatter Validation
|
||||
|
||||
Define the Step 2 contract with Pydantic v2 models and change-friendly validation.
|
||||
|
||||
Normative model sketch:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import PurePosixPath
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
SKILL_ID_RE = re.compile(r"^[a-z][a-z0-9-]*$")
|
||||
SEMVER_RE = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+][0-9A-Za-z.-]+)?$")
|
||||
|
||||
|
||||
class ReferenceEntry(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
|
||||
|
||||
path: str
|
||||
mime_type: str = "text/markdown"
|
||||
title: str | None = None
|
||||
|
||||
@field_validator("path")
|
||||
@classmethod
|
||||
def validate_reference_path(cls, value: str) -> str:
|
||||
p = PurePosixPath(value)
|
||||
if p.is_absolute() or ".." in p.parts:
|
||||
raise ValueError("reference path must be a relative in-skill path")
|
||||
if not str(p).startswith("references/"):
|
||||
raise ValueError("reference path must stay under references/")
|
||||
if p.suffix.lower() != ".md":
|
||||
raise ValueError("reference path must target a markdown file")
|
||||
return str(p)
|
||||
|
||||
|
||||
class PersonalMcpMetadata(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
|
||||
|
||||
id: str
|
||||
version: str
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
capabilities: list[str] = Field(min_length=1)
|
||||
depends_on: list[str] = Field(default_factory=list)
|
||||
references: dict[str, ReferenceEntry] = Field(default_factory=dict)
|
||||
|
||||
@field_validator("id")
|
||||
@classmethod
|
||||
def validate_id(cls, value: str) -> str:
|
||||
if not SKILL_ID_RE.fullmatch(value):
|
||||
raise ValueError("id must be lowercase kebab-case and start with a letter")
|
||||
return value
|
||||
|
||||
@field_validator("version")
|
||||
@classmethod
|
||||
def validate_version(cls, value: str) -> str:
|
||||
if not SEMVER_RE.fullmatch(value):
|
||||
raise ValueError("version must be semver")
|
||||
return value
|
||||
|
||||
@field_validator("depends_on")
|
||||
@classmethod
|
||||
def validate_depends_on(cls, value: list[str]) -> list[str]:
|
||||
for dep in value:
|
||||
if not SKILL_ID_RE.fullmatch(dep):
|
||||
raise ValueError(f"invalid depends_on skill id: {dep}")
|
||||
return value
|
||||
|
||||
@field_validator("references")
|
||||
@classmethod
|
||||
def validate_reference_ids(cls, value: dict[str, ReferenceEntry]) -> dict[str, ReferenceEntry]:
|
||||
for ref_id in value:
|
||||
if not SKILL_ID_RE.fullmatch(ref_id):
|
||||
raise ValueError(f"invalid reference id: {ref_id}")
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def ensure_primary_capability(self) -> "PersonalMcpMetadata":
|
||||
expected = f"resource://skills/{self.id}/document"
|
||||
if expected not in self.capabilities:
|
||||
raise ValueError(f"capabilities must include {expected}")
|
||||
return self
|
||||
|
||||
|
||||
class SkillFrontmatter(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
|
||||
|
||||
# Anthropic/Agent Skills fields
|
||||
name: str = Field(min_length=1, max_length=64)
|
||||
description: str = Field(min_length=1, max_length=1024)
|
||||
when_to_use: str | None = None
|
||||
allowed_tools: str | list[str] | None = Field(default=None, alias="allowed-tools")
|
||||
disallowed_tools: str | list[str] | None = Field(default=None, alias="disallowed-tools")
|
||||
disable_model_invocation: bool | None = Field(default=None, alias="disable-model-invocation")
|
||||
user_invocable: bool | None = Field(default=None, alias="user-invocable")
|
||||
argument_hint: str | None = Field(default=None, alias="argument-hint")
|
||||
arguments: str | list[str] | None = None
|
||||
license: str | None = None
|
||||
compatibility: str | None = None
|
||||
metadata: dict[str, str] | None = None
|
||||
|
||||
# Repository extension block
|
||||
x_personal_mcp: PersonalMcpMetadata = Field(alias="x-personal-mcp")
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, value: str) -> str:
|
||||
if not SKILL_ID_RE.fullmatch(value):
|
||||
raise ValueError("name must be lowercase kebab-case and start with a letter")
|
||||
if "anthropic" in value or "claude" in value:
|
||||
raise ValueError("name must not contain reserved words anthropic or claude")
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def cross_validate(self) -> "SkillFrontmatter":
|
||||
if self.x_personal_mcp.id != self.name:
|
||||
raise ValueError("x-personal-mcp.id must exactly match name")
|
||||
return self
|
||||
|
||||
|
||||
def validate_skill_frontmatter(raw: dict[str, Any], skill_dir_name: str) -> SkillFrontmatter:
|
||||
model = SkillFrontmatter.model_validate(raw)
|
||||
if model.name != skill_dir_name:
|
||||
raise ValueError("frontmatter name must exactly match skill directory name")
|
||||
return model
|
||||
```
|
||||
|
||||
Validation behavior contract:
|
||||
|
||||
- Validate required core fields and relationships during registry load before FastMCP resource/tool registration.
|
||||
- Allow unknown additive fields so frontmatter can evolve without blocking startup.
|
||||
- Treat hard contract violations (missing required fields, invalid ids, broken required mappings) as startup errors.
|
||||
- Treat non-critical compatibility issues as warnings when possible.
|
||||
- Error messages should include skill path and failing field for CI readability.
|
||||
|
||||
Projection mode contract (for Anthropic API upload pipelines):
|
||||
|
||||
- Parse with `SkillFrontmatter` first.
|
||||
- Emit Anthropic-safe frontmatter with standard fields only.
|
||||
- Serialize repository metadata into standard `metadata` as namespaced keys.
|
||||
- Preserve the canonical authored source in `x-personal-mcp`; projection output is a build artifact.
|
||||
|
||||
### Anthropic Upload Compatibility Rule
|
||||
|
||||
- Anthropic documentation guarantees behavior for standard frontmatter fields but does not explicitly guarantee handling of arbitrary unknown top-level keys.
|
||||
- Therefore, publishing pipelines that target strict API compatibility should support a projection mode that emits only standard frontmatter fields for upload.
|
||||
- In projection mode, repository extension metadata is serialized into the standard `metadata` field (for example as namespaced keys or JSON-encoded values), while source-of-truth authoring remains in `x-personal-mcp`.
|
||||
|
||||
### FastMCP Native Metadata Surfaces (Research Baseline)
|
||||
|
||||
Resources (`@mcp.resource` and templates) support native definition metadata:
|
||||
|
||||
- `name`, `description`, `mime_type`, `tags`
|
||||
- `annotations` (`readOnlyHint`, `idempotentHint`)
|
||||
- `icons`
|
||||
- `meta` (custom metadata passed through to the MCP client resource object)
|
||||
- `version`
|
||||
- `enabled` (deprecated in v3; prefer server-level `mcp.enable()` / `mcp.disable()`)
|
||||
|
||||
Resources support runtime metadata:
|
||||
|
||||
- `ResourceContent.meta` (item-level)
|
||||
- `ResourceResult.meta` (result-level `_meta`)
|
||||
|
||||
Tools (`@mcp.tool`) support native definition metadata:
|
||||
|
||||
- `name`, `description`, `tags`
|
||||
- `annotations` (`title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`)
|
||||
- `icons`
|
||||
- `meta` (custom metadata passed through to the MCP client tool object)
|
||||
- `version`
|
||||
- `timeout`, `output_schema`, `run_in_thread`
|
||||
- `enabled` (deprecated in v3; prefer server-level `mcp.enable()` / `mcp.disable()`)
|
||||
|
||||
Tools support runtime metadata:
|
||||
|
||||
- `ToolResult.meta` (execution-level metadata for each call)
|
||||
|
||||
### Frontmatter To FastMCP Mapping Contract
|
||||
|
||||
At server startup, map `x-personal-mcp` fields into FastMCP registration as follows:
|
||||
|
||||
- `x-personal-mcp.id` -> canonical URI namespace and identity checks.
|
||||
- `description` -> default `description` for the primary skill document resource.
|
||||
- `x-personal-mcp.tags` -> `tags` on resources/tools.
|
||||
- `x-personal-mcp.version` -> `version` on resources/tools.
|
||||
- `x-personal-mcp.capabilities` -> registered URI list plus catalog exposure.
|
||||
- `x-personal-mcp.references[*]` -> resource templates or concrete resources with:
|
||||
- `mime_type` from reference entry (or default)
|
||||
- `meta` including `skill_id`, `ref_id`, and source `path`
|
||||
- read-only annotations for documentation resources
|
||||
- `x-personal-mcp.depends_on` -> catalog dependency graph metadata and validation checks.
|
||||
|
||||
### Invariants This Contract Guarantees
|
||||
|
||||
- Anthropic-required frontmatter stays valid for custom skill upload and Claude Code loading.
|
||||
- MCP-specific metadata remains embedded in SKILL.md frontmatter, with no `metadata.yaml` sidecar.
|
||||
- FastMCP registration uses only native metadata fields for resources/tools.
|
||||
- Reference ids and metadata can evolve with low-friction updates while internal file layout under `references/` stays refactor-friendly.
|
||||
|
||||
### Non-goals For Step 2
|
||||
|
||||
- No URI versioning/deprecation rollout policy details (handled in Step 3).
|
||||
- No migration script design from existing `metadata.yaml` files.
|
||||
- No runtime caching/indexing performance tuning details.
|
||||
@@ -0,0 +1,114 @@
|
||||
**Step 3 Results: URI Contract and Compatibility Policy**
|
||||
|
||||
This section finalizes Step 3 by defining the canonical resource URI contract, template parameter rules, and explicit compatibility/versioning policy for URIs and reference ids.
|
||||
|
||||
### Step Deliverable
|
||||
|
||||
- Update the current `docs/` directory with the finalized Step 3 URI contract and compatibility policy content from this document.
|
||||
|
||||
### Canonical URI Surface (Normative)
|
||||
|
||||
The public, preferred URIs are:
|
||||
|
||||
1. `resource://catalog/skills_index`
|
||||
2. `resource://catalog/skills/{skill_id}`
|
||||
3. `resource://skills/{skill_id}/document`
|
||||
4. `resource://skills/{skill_id}/references/{ref_id}`
|
||||
5. `resource://docs/{path*}`
|
||||
|
||||
Contract intent:
|
||||
|
||||
- Catalog URIs are discovery surfaces.
|
||||
- Skill URIs are primary per-skill guidance surfaces.
|
||||
- Docs wildcard URI is a direct authored-markdown access surface under `docs/`.
|
||||
|
||||
### URI Semantics
|
||||
|
||||
`resource://catalog/skills_index`
|
||||
|
||||
- Returns a compact list of skill records for discovery.
|
||||
- One entry per `skill_id`.
|
||||
- Must include enough metadata for client-side selection (at minimum id, name, description, tags, capabilities).
|
||||
|
||||
`resource://catalog/skills/{skill_id}`
|
||||
|
||||
- Returns one normalized record for `skill_id`.
|
||||
- Must include canonical document URI and declared reference ids.
|
||||
- Returns not-found when `skill_id` does not exist.
|
||||
|
||||
`resource://skills/{skill_id}/document`
|
||||
|
||||
- Returns the canonical `SKILL.md` authored content for that skill.
|
||||
- `skill_id` must match Step 1 stable id rules.
|
||||
|
||||
`resource://skills/{skill_id}/references/{ref_id}`
|
||||
|
||||
- Returns one reference document declared in the skill frontmatter references manifest.
|
||||
- `ref_id` is the stable public handle for that reference document.
|
||||
|
||||
`resource://docs/{path*}`
|
||||
|
||||
- Returns authored markdown at a normalized relative path under `docs/`.
|
||||
- Supports nested paths via RFC6570 wildcard expansion.
|
||||
- Typical examples: `index.md`, `usage.md`, `skills/<skill-id>/SKILL.md`, `skills/<skill-id>/references/<file>.md`.
|
||||
|
||||
### Template Parameter and Validation Rules
|
||||
|
||||
`skill_id`
|
||||
|
||||
- Lowercase kebab-case.
|
||||
- Must satisfy Step 1 stable id rules.
|
||||
|
||||
`ref_id`
|
||||
|
||||
- Lowercase kebab-case.
|
||||
- Must be declared in the skill’s references manifest.
|
||||
|
||||
`path*`
|
||||
|
||||
- Relative POSIX path only.
|
||||
- No leading slash.
|
||||
- No `..` traversal segments.
|
||||
- Resolves only inside `docs/`.
|
||||
- This surface is markdown-only in end state (`.md` files).
|
||||
|
||||
### URI Versioning Policy
|
||||
|
||||
Default rule:
|
||||
|
||||
- Keep URIs unversioned by default.
|
||||
- Allow URI and payload updates when they improve clarity or implementation simplicity.
|
||||
|
||||
Breaking-change rule:
|
||||
|
||||
- Breaking changes use direct replacement of the canonical URI family.
|
||||
- No compatibility aliases or dual URI families are maintained in this greenfield phase.
|
||||
|
||||
FastMCP version metadata usage:
|
||||
|
||||
- Resource `version` metadata MAY be used for implementation/version discovery.
|
||||
- URI readability and maintainability remain the primary contract.
|
||||
|
||||
### Reference ID Compatibility Policy
|
||||
|
||||
`ref_id` is the public identifier for a reference document, separate from file path.
|
||||
|
||||
Rules:
|
||||
|
||||
- Prefer keeping `ref_id` stable when practical.
|
||||
- File paths may change without URI churn as long as the mapped `ref_id` resolves.
|
||||
- If a reference is renamed, introduce a new `ref_id` and treat the old one as retired.
|
||||
- Avoid reusing retired `ref_id` values for unrelated content.
|
||||
|
||||
### Invariants This Contract Guarantees
|
||||
|
||||
- One canonical URI pattern per core capability surface.
|
||||
- Fast, low-friction URI evolution through direct replacement of canonical URIs.
|
||||
- A single canonical catalog URI family with no alias maintenance overhead.
|
||||
- Reference mappings can evolve with minimal churn.
|
||||
|
||||
### Non-goals For Step 3
|
||||
|
||||
- No implementation-specific transform wiring details (`VersionFilter`, mounts, provider composition).
|
||||
- No migration script mechanics for auto-generating aliases.
|
||||
- No authorization policy design for URI-level access control.
|
||||
@@ -0,0 +1,248 @@
|
||||
**Step 4 Results: Docs Registry Loader Design (importlib.resources + Fail-Fast Validation)**
|
||||
|
||||
This section finalizes Step 4 by defining a production-ready docs registry loader that reads packaged docs through Python resource APIs, parses SKILL.md frontmatter, validates schema and cross-links, and builds an immutable in-memory registry keyed by skill_id.
|
||||
|
||||
### Greenfield Framing (Normative)
|
||||
|
||||
This Step 4 design is for the greenfield target state:
|
||||
|
||||
1. No legacy metadata sidecars (`metadata.yaml`) are part of the runtime contract.
|
||||
2. No dual-loader compatibility path is required.
|
||||
3. Registry loading from packaged resources is the only runtime source of truth.
|
||||
4. Compatibility shims are prohibited.
|
||||
|
||||
### Research Baseline (Python + Design Guidance)
|
||||
|
||||
Authoritative references used for this step:
|
||||
|
||||
1. Python `importlib.resources` docs (`files`, `as_file`, `Traversable` APIs)
|
||||
2. Python `importlib.resources.abc` docs (`Traversable`, path traversal semantics, joinpath compatibility notes)
|
||||
3. Pydantic v2 model/validation docs (`model_validate`, `ValidationError`, strictness and extra handling)
|
||||
4. Python packaging guidance for including package data in wheels/sdists
|
||||
|
||||
Best-practice conclusions applied to this design:
|
||||
|
||||
1. Prefer `importlib.resources.files(<package>).joinpath(...)` over filesystem assumptions so stdio deployments from installed wheels work.
|
||||
2. Treat resources as potentially non-filesystem artifacts (zip-import compatible); only use `as_file(...)` when an actual OS path is required.
|
||||
3. Validate metadata with explicit Pydantic models and fail startup on contract violations.
|
||||
4. Keep registry load deterministic (sorted traversal, stable error messages, no hidden fallback mutations).
|
||||
5. Resolve references via manifest ids declared in frontmatter, not by global file conventions.
|
||||
|
||||
### Loader Responsibilities (Normative)
|
||||
|
||||
The Step 4 loader MUST:
|
||||
|
||||
1. Read canonical docs from package resources (not repo-root paths).
|
||||
2. Discover all skill directories under `docs/skills/` in packaged resources.
|
||||
3. For each skill, read and parse `SKILL.md` frontmatter.
|
||||
4. Validate frontmatter using the Step 2 schema contract.
|
||||
5. Validate directory/id invariants from Step 1 (directory name equals frontmatter id).
|
||||
6. Validate URI/reference semantics from Step 3 assumptions.
|
||||
7. Build a single in-memory registry keyed by `skill_id`.
|
||||
8. Fail fast on any integrity error before FastMCP resource registration.
|
||||
9. Precompute compact discovery projections so index resources can be served without reading full markdown bodies at request time.
|
||||
|
||||
### Package Resource Contract
|
||||
|
||||
Runtime anchor:
|
||||
|
||||
1. The loader resolves content from an importable package anchor, for example `personal_mcp`.
|
||||
2. Docs root is located as `files(anchor).joinpath("docs")` when docs are packaged at package root, or an equivalent configured subpath.
|
||||
3. Skill root is `docs/skills`.
|
||||
|
||||
Resource assumptions:
|
||||
|
||||
1. `SKILL.md` is UTF-8 text.
|
||||
2. Reference files declared in frontmatter are UTF-8 markdown by default unless otherwise declared.
|
||||
3. Path resolution always remains inside the same skill directory.
|
||||
|
||||
### Registry Data Model
|
||||
|
||||
Build immutable runtime records with explicit structure:
|
||||
|
||||
1. `SkillRecord`
|
||||
- `skill_id`
|
||||
- `name`
|
||||
- `description`
|
||||
- `version`
|
||||
- `tags`
|
||||
- `capabilities`
|
||||
- `depends_on`
|
||||
- `document_uri`
|
||||
- `document_relpath` (canonical resource-relative path)
|
||||
- `references` map keyed by `ref_id`
|
||||
2. `ReferenceRecord`
|
||||
- `ref_id`
|
||||
- `uri`
|
||||
- `relpath`
|
||||
- `mime_type`
|
||||
- `title`
|
||||
3. `DocsRegistry`
|
||||
- `skills_by_id: dict[str, SkillRecord]`
|
||||
- `skills_in_load_order: list[str]` (deterministic ordering)
|
||||
- helper indexes for catalog payload generation
|
||||
- `skills_summary_in_load_order: list[SkillSummaryRecord]` for progressive discovery responses
|
||||
- filter indexes (for example by tag/capability) derived once at startup
|
||||
|
||||
4. `SkillSummaryRecord`
|
||||
- `skill_id`
|
||||
- `name`
|
||||
- `description`
|
||||
- `tags`
|
||||
- `capabilities`
|
||||
- `document_uri`
|
||||
- optional `version`
|
||||
|
||||
Immutability rule:
|
||||
|
||||
1. Once built, registry records are treated as read-only for the process lifetime.
|
||||
2. No runtime mutation during requests; refresh only via process restart.
|
||||
|
||||
### Frontmatter Parsing Contract
|
||||
|
||||
`SKILL.md` parse steps:
|
||||
|
||||
1. Read full markdown text from resource.
|
||||
2. Parse YAML frontmatter block at file start (between the first two `---` delimiters).
|
||||
3. Parse YAML with safe loader semantics.
|
||||
4. Validate parsed object with Step 2 Pydantic model(s).
|
||||
5. Preserve markdown body as document content payload.
|
||||
|
||||
Parsing failure behavior:
|
||||
|
||||
1. Missing frontmatter block: startup error.
|
||||
2. Invalid YAML: startup error with skill path and YAML parser detail.
|
||||
3. Missing required fields (`name`, `description`, `x-personal-mcp` contract fields): startup error.
|
||||
|
||||
### Validation Pipeline (Fail-Fast)
|
||||
|
||||
Validation happens in this order:
|
||||
|
||||
1. Structural discovery validation
|
||||
- skill directory exists under `docs/skills`
|
||||
- required `SKILL.md` exists for each discovered skill
|
||||
2. Schema validation
|
||||
- Pydantic frontmatter validation for all required and constrained fields
|
||||
3. Identity validation
|
||||
- frontmatter `name` equals `x-personal-mcp.id`
|
||||
- frontmatter id equals skill directory name
|
||||
4. Reference manifest validation
|
||||
- unique `ref_id` keys per skill
|
||||
- each manifest path is relative, in-skill, and under `references/`
|
||||
- each manifest target exists and is a file
|
||||
5. Dependency graph validation
|
||||
- every `depends_on` target exists in discovered skill set
|
||||
- no self-dependency
|
||||
- cycle detection enabled (hard error on cycle)
|
||||
6. Capability sanity checks
|
||||
- required primary capability `resource://skills/{skill_id}/document` is present
|
||||
7. Global uniqueness checks
|
||||
- no duplicate `skill_id`
|
||||
- no duplicate canonical resource URIs generated from registry
|
||||
8. Discovery payload checks
|
||||
- summary fields required by catalog index are present and non-empty
|
||||
- summary generation does not require reading markdown body content during request handling
|
||||
|
||||
### Error Model and Reporting
|
||||
|
||||
Error handling contract:
|
||||
|
||||
1. Collect errors per validation phase for clarity, then raise one startup exception containing all findings.
|
||||
2. Error messages must include:
|
||||
- skill id (when known)
|
||||
- packaged relative path
|
||||
- violated rule
|
||||
- actionable fix hint
|
||||
3. If any error exists, registry is not published and FastMCP resource registration does not proceed.
|
||||
|
||||
Recommended exception shape:
|
||||
|
||||
1. `DocsRegistryValidationError(errors: list[RegistryIssue])`
|
||||
2. `RegistryIssue` fields: `code`, `message`, `skill_id`, `path`, `hint`
|
||||
|
||||
### Determinism and Runtime Safety
|
||||
|
||||
Determinism rules:
|
||||
|
||||
1. Traverse directories in sorted order.
|
||||
2. Normalize all stored relative paths to POSIX form.
|
||||
3. Normalize ids/tags exactly once at parse boundary.
|
||||
4. Produce stable catalog ordering to reduce client churn.
|
||||
5. Produce stable summary projections and filter indexes from the same normalized source records.
|
||||
|
||||
Runtime safety rules:
|
||||
|
||||
1. No dependence on `Path(__file__)` or repository root.
|
||||
2. No ad-hoc fallback probing across multiple locations.
|
||||
3. No lazy validation deferred until first request.
|
||||
|
||||
### Integration Plan for Existing Modules
|
||||
|
||||
Primary integration target:
|
||||
|
||||
1. Implement the canonical package-resource-based registry loader in `src/personal_mcp/skills/document_loader.py` as the only supported runtime loader path.
|
||||
|
||||
Catalog integration:
|
||||
|
||||
1. Update `src/personal_mcp/catalog/server.py` to consume the shared in-memory registry as the only catalog data source.
|
||||
2. Keep catalog payload normalization deterministic and sourced from registry records only.
|
||||
|
||||
Startup wiring:
|
||||
|
||||
1. Initialize registry once during app/server startup in `src/personal_mcp/main.py` or equivalent composition point.
|
||||
2. Pass registry to resource registration step (Step 5).
|
||||
|
||||
### Proposed Loader API Surface
|
||||
|
||||
Use a small, testable API:
|
||||
|
||||
1. `load_docs_registry(*, package_anchor: str, docs_root: str = "docs") -> DocsRegistry`
|
||||
2. `read_skill_document(registry: DocsRegistry, skill_id: str) -> DocumentPayload`
|
||||
3. `read_skill_reference(registry: DocsRegistry, skill_id: str, ref_id: str) -> DocumentPayload`
|
||||
|
||||
Design constraints:
|
||||
|
||||
1. Loader functions are pure relative to package resources and input args.
|
||||
2. No global mutable singleton required for unit tests.
|
||||
3. Caching is explicit and owned by startup composition.
|
||||
|
||||
### Test and Validation Plan (Step 4 Scope)
|
||||
|
||||
Unit tests:
|
||||
|
||||
1. valid multi-skill registry load from packaged test fixtures
|
||||
2. duplicate id detection
|
||||
3. missing SKILL.md detection
|
||||
4. invalid frontmatter field constraints
|
||||
5. broken reference target detection
|
||||
6. invalid depends_on target detection
|
||||
7. cycle detection in depends_on graph
|
||||
8. deterministic output ordering across runs
|
||||
|
||||
Packaging/runtime tests:
|
||||
|
||||
1. install built wheel in isolated env
|
||||
2. load registry via `importlib.resources.files(...)`
|
||||
3. assert representative skill document/reference are readable
|
||||
|
||||
Expected command path in this repo:
|
||||
|
||||
1. `uv run pytest -q`
|
||||
|
||||
### Acceptance Criteria for Step 4 Completion
|
||||
|
||||
Step 4 is complete when all are true:
|
||||
|
||||
1. Registry loads exclusively from packaged resources.
|
||||
2. All Step 2 and Step 3 dependent validations are enforced at startup.
|
||||
3. Invalid docs state blocks startup with actionable diagnostics.
|
||||
4. Registry is deterministic and immutable for runtime use.
|
||||
5. Catalog and later resource registration can consume registry without direct filesystem scanning.
|
||||
|
||||
### Non-goals for Step 4
|
||||
|
||||
1. No FastMCP resource registration wiring details (Step 5).
|
||||
2. No discovery-tool fallback behavior design (Step 6).
|
||||
3. No final packaging/build-system migration mechanics (Step 7).
|
||||
4. No backward-compat alias rollout mechanics in the greenfield baseline.
|
||||
5. No compatibility layer of any kind (URI aliases, dual reads, adapter shims, or legacy schema bridges).
|
||||
@@ -0,0 +1,221 @@
|
||||
**Step 5 Results: Registry-Driven FastMCP Resource Registration (RFC6570 + Startup Safety)**
|
||||
|
||||
This section finalizes Step 5 by defining how FastMCP resources are registered from the Step 4 docs registry using RFC6570 URI templates, explicit metadata, and strict duplicate-registration safety.
|
||||
|
||||
### Greenfield Framing (Normative)
|
||||
|
||||
This Step 5 design is for the greenfield target state:
|
||||
|
||||
1. Registry-driven resources are the primary and authoritative discovery/read surface.
|
||||
2. No legacy per-skill hardcoded resource registration is retained.
|
||||
3. Resource contracts are defined for net-new clients and replace prior contracts without transition shims.
|
||||
4. Step 6 tool fallback layers on top of this resource contract, not as a competing source of truth.
|
||||
5. Breaking changes are intentional in this full-refactor phase.
|
||||
|
||||
### Research Baseline (FastMCP + URI Templates)
|
||||
|
||||
Authoritative references used for this step:
|
||||
|
||||
1. FastMCP Resources and Templates docs (resource decorator, template behavior)
|
||||
2. FastMCP RFC6570 support docs (simple params, wildcard params, query params)
|
||||
3. FastMCP duplicate handling docs (`on_duplicate_resources`)
|
||||
4. FastMCP annotations guidance (`readOnlyHint`, `idempotentHint`)
|
||||
|
||||
Best-practice conclusions applied to this design:
|
||||
|
||||
1. Use URI templates for parameterized resources instead of generating N static resource handlers.
|
||||
2. Use wildcard template parameters (`{path*}`) for hierarchical docs paths.
|
||||
3. Set startup duplicate policy to `on_duplicate_resources="error"` to fail fast on contract collisions.
|
||||
4. Set explicit `mime_type` and resource annotations for all docs resources.
|
||||
5. Keep registration deterministic and sourced only from the validated Step 4 registry.
|
||||
|
||||
### Registration Responsibilities (Normative)
|
||||
|
||||
The Step 5 registration layer MUST:
|
||||
|
||||
1. Consume only the validated in-memory registry produced by Step 4.
|
||||
2. Register canonical resource discovery surfaces and skill document/reference surfaces.
|
||||
3. Use RFC6570 templates where URI patterns are parameterized.
|
||||
4. Use wildcard templates where path depth is variable.
|
||||
5. Attach read-only/idempotent annotations to documentation resources.
|
||||
6. Set explicit MIME types for all registered resources.
|
||||
7. Fail startup if duplicate URI/template keys are encountered.
|
||||
|
||||
### Canonical Resource Surface (from Registry)
|
||||
|
||||
The preferred resources registered in this phase are:
|
||||
|
||||
1. `resource://catalog/skills_index`
|
||||
2. `resource://catalog/skills_index{?q,tag,capability,cursor,limit}` (optional filtered/paginated discovery template)
|
||||
3. `resource://catalog/skills/{skill_id}`
|
||||
4. `resource://skills/{skill_id}/document`
|
||||
5. `resource://skills/{skill_id}/references/{ref_id}`
|
||||
6. `resource://docs/{path*}`
|
||||
|
||||
Registration decision rules:
|
||||
|
||||
1. Use static resource registration for fixed singleton endpoints (for example `skills_index`).
|
||||
2. Use template registration for parameterized endpoints (`{skill_id}`, `{ref_id}`) and optional discovery query templates.
|
||||
3. Use wildcard template registration for hierarchical docs routing (`{path*}`).
|
||||
4. Keep the singleton and query-template discovery surfaces semantically equivalent (same schema, query template adds filtering/pagination only).
|
||||
|
||||
### Progressive Discovery Contract
|
||||
|
||||
Discovery-first behavior for Step 5 resources:
|
||||
|
||||
1. `skills_index` returns summaries only (no embedded full SKILL.md bodies).
|
||||
2. Each summary includes canonical follow-up URIs so clients can progressively fetch detail (`catalog/skills/{skill_id}` then `skills/{skill_id}/document`).
|
||||
3. Filtered/paginated discovery uses RFC6570 query params (`q`, `tag`, `capability`, `cursor`, `limit`) with deterministic ordering.
|
||||
4. Handlers should enforce bounded page size and return explicit continuation metadata when pagination is active.
|
||||
5. Errors for unsupported filter params or invalid cursor/limit are explicit and actionable.
|
||||
|
||||
### RFC6570 Template Contract
|
||||
|
||||
Path parameters:
|
||||
|
||||
1. `{skill_id}` and `{ref_id}` are single-segment template params.
|
||||
2. `{path*}` is a wildcard param and may capture multi-segment paths separated by `/`.
|
||||
|
||||
Validation contract at resource-read time:
|
||||
|
||||
1. `skill_id` must exist in registry.
|
||||
2. `ref_id` must exist in that skill’s reference manifest.
|
||||
3. wildcard `path*` must normalize to an allowed docs-relative markdown path.
|
||||
4. invalid params return explicit not-found or validation errors (no silent fallback).
|
||||
|
||||
Template function signature contract:
|
||||
|
||||
1. Required URI params must exist as function parameters.
|
||||
2. Avoid hidden implicit params not represented in template.
|
||||
3. Keep template handlers side-effect free.
|
||||
|
||||
### Metadata and Annotation Contract
|
||||
|
||||
Each docs/resource registration should specify explicit metadata:
|
||||
|
||||
1. `mime_type`
|
||||
- skill docs and references: `text/markdown`
|
||||
- catalog payloads: `application/json`
|
||||
2. `annotations`
|
||||
- `readOnlyHint: true`
|
||||
- `idempotentHint: true`
|
||||
3. `tags`
|
||||
- include stable categories such as `catalog`, `skill-doc`, `reference`, `docs`
|
||||
4. `version`
|
||||
- project-defined version from registry metadata where applicable
|
||||
5. `meta`
|
||||
- include normalized identifiers (for example `skill_id`, `ref_id`, `source_relpath`) when useful
|
||||
|
||||
### Startup Safety and Duplicate Policy
|
||||
|
||||
FastMCP initialization contract for this phase:
|
||||
|
||||
1. Construct the root server with `on_duplicate_resources="error"`.
|
||||
2. Register all Step 5 resources during startup composition before serving traffic.
|
||||
3. Treat duplicate registration as a hard startup failure.
|
||||
|
||||
Duplicate conflict classes covered:
|
||||
|
||||
1. static URI vs static URI collision
|
||||
2. static URI vs template key collision
|
||||
3. template URI vs template URI collision
|
||||
4. conflicting registrations introduced by future aliases without explicit migration handling
|
||||
|
||||
### Registration Architecture
|
||||
|
||||
Use one dedicated registration module that converts registry records into FastMCP resources.
|
||||
|
||||
Recommended API:
|
||||
|
||||
1. `register_docs_resources(mcp: FastMCP, registry: DocsRegistry) -> None`
|
||||
|
||||
Responsibilities of `register_docs_resources`:
|
||||
|
||||
1. register singleton catalog resources
|
||||
2. register parameterized catalog/detail templates
|
||||
3. register skill document and reference templates
|
||||
4. register docs wildcard template
|
||||
5. apply shared annotations and MIME defaults consistently
|
||||
|
||||
Separation of concerns:
|
||||
|
||||
1. Step 4 validates and normalizes docs state.
|
||||
2. Step 5 only registers handlers and reads from validated registry state.
|
||||
3. Request handlers do not re-discover filesystem/package structure.
|
||||
|
||||
### Handler Behavior Contract
|
||||
|
||||
Catalog handlers:
|
||||
|
||||
1. `skills_index` returns compact deterministic discovery payload (summary records only) and supports progressive follow-up links.
|
||||
2. `skills/{skill_id}` returns one normalized detail record or not-found.
|
||||
|
||||
Skill document handlers:
|
||||
|
||||
1. `skills/{skill_id}/document` returns canonical SKILL markdown content.
|
||||
2. MIME type is always `text/markdown`.
|
||||
|
||||
Reference handlers:
|
||||
|
||||
1. `skills/{skill_id}/references/{ref_id}` resolves via frontmatter manifest mapping.
|
||||
2. MIME type is explicit from manifest or defaults to `text/markdown`.
|
||||
|
||||
Wildcard docs handler:
|
||||
|
||||
1. `docs/{path*}` serves markdown docs under canonical packaged docs tree.
|
||||
2. traversal outside docs root is blocked.
|
||||
|
||||
### Integration Plan for Existing Modules
|
||||
|
||||
Primary composition updates:
|
||||
|
||||
1. Implement registry-driven registration in [src/personal_mcp/mcp.py](src/personal_mcp/mcp.py) as the canonical resource composition path.
|
||||
2. Keep [src/personal_mcp/main.py](src/personal_mcp/main.py) responsible for startup wiring order (load registry first, then register resources).
|
||||
3. Use [src/personal_mcp/catalog/server.py](src/personal_mcp/catalog/server.py) as registry-backed handlers only.
|
||||
|
||||
Lifecycle order (required):
|
||||
|
||||
1. load and validate registry (Step 4)
|
||||
2. initialize FastMCP with duplicate error policy
|
||||
3. register all Step 5 resources/templates
|
||||
4. start server
|
||||
|
||||
### Testing Plan (Step 5 Scope)
|
||||
|
||||
Unit/integration tests:
|
||||
|
||||
1. resource registration succeeds with valid registry
|
||||
2. duplicate resource registration fails at startup
|
||||
3. `skills/{skill_id}` template resolves expected record
|
||||
4. `skills/{skill_id}/document` returns markdown with correct MIME
|
||||
5. `skills/{skill_id}/references/{ref_id}` resolves manifest-mapped file
|
||||
6. `docs/{path*}` resolves nested docs paths and blocks traversal attempts
|
||||
7. all registered docs resources include `readOnlyHint` and `idempotentHint`
|
||||
8. catalog payload order is deterministic
|
||||
9. filtered/paginated `skills_index{?q,tag,capability,cursor,limit}` responses are deterministic and schema-compatible with the singleton index response
|
||||
10. catalog index payload excludes full markdown bodies and includes follow-up URIs for progressive reads
|
||||
|
||||
Smoke tests:
|
||||
|
||||
1. list resources includes singleton and template entries
|
||||
2. read representative skill doc URI and reference URI successfully
|
||||
3. read representative wildcard docs URI successfully
|
||||
|
||||
### Acceptance Criteria for Step 5 Completion
|
||||
|
||||
Step 5 is complete when all are true:
|
||||
|
||||
1. Resource registration is fully registry-driven (no per-skill hardcoded decorators required for core docs surfaces).
|
||||
2. RFC6570 templates are used for parameterized URI families, including wildcard where needed.
|
||||
3. All docs resources declare explicit MIME types and read-only/idempotent annotations.
|
||||
4. `on_duplicate_resources="error"` is enabled and verified by tests.
|
||||
5. Startup fails safely on registration conflicts.
|
||||
|
||||
### Non-goals for Step 5
|
||||
|
||||
1. No tool fallback discovery behavior implementation (Step 6).
|
||||
2. No packaging build inclusion mechanics (Step 7).
|
||||
3. No CI gate expansion details (Step 9).
|
||||
4. No migration shims for legacy URI aliases in the greenfield baseline.
|
||||
5. No ranking-strategy implementation for discovery tools beyond what is needed to preserve deterministic resource-first discovery contracts.
|
||||
6. No backward-compat resource aliases, adapter handlers, or dual registration paths.
|
||||
@@ -0,0 +1,161 @@
|
||||
**Step 6 Results: Resource-First Discovery and Tool Fallback Contract**
|
||||
|
||||
This section finalizes Step 6 by defining discovery behavior for clients that can attach MCP resources and the fallback behavior for clients or chat surfaces that must rely on MCP tools.
|
||||
|
||||
### Step Deliverable
|
||||
|
||||
- Update the current `docs/` directory with the finalized Step 6 discovery and fallback contract content from this document.
|
||||
|
||||
### Primary Source Baseline (Repository Docs)
|
||||
|
||||
Step 6 is based on the current project contracts in:
|
||||
|
||||
1. `docs/architecture.md` (resource-first architecture and catalog role)
|
||||
2. `docs/usage.md` (operating flows, bounded loading, and fallback sequence)
|
||||
3. `docs/copilot.md` (client capability lanes and practical fallback behavior)
|
||||
4. `docs/mcp_layout.md` (shared content source and thin-tool fallback position)
|
||||
5. `docs/securing.md` (read-only/public-docs security invariant)
|
||||
|
||||
Normative conclusions from those sources:
|
||||
|
||||
1. Discovery stays resource-first.
|
||||
2. Tool fallback is allowed, thin, and read-only.
|
||||
3. Resources and tools must resolve to the same canonical authored markdown.
|
||||
4. Fallback behavior should keep context bounded and deterministic.
|
||||
|
||||
### Discovery Priority Contract (Normative)
|
||||
|
||||
Preferred sequence for skill discovery and loading:
|
||||
|
||||
1. `resource://catalog/skills_index`
|
||||
2. `resource://catalog/skills/{skill_id}`
|
||||
3. `resource://skills/{skill_id}/document`
|
||||
4. `resource://skills/{skill_id}/references/{ref_id}` only when needed
|
||||
|
||||
Rules:
|
||||
|
||||
1. Start from catalog discovery before loading any skill document.
|
||||
2. Do not skip straight to broad document loading when catalog metadata can narrow choices first.
|
||||
3. Use `resource://docs/{path*}` only for direct authored-doc access outside skill-specific flows.
|
||||
|
||||
### Fallback Activation Rule
|
||||
|
||||
Fallback is used only when the active client path cannot reliably attach MCP resources (for example, tool-only chat surfaces).
|
||||
|
||||
Rules:
|
||||
|
||||
1. Keep the same discovery order semantics as the resource path.
|
||||
2. If resource attachment is available, prefer resources over tools.
|
||||
3. Tool fallback must never become a second authoritative content source.
|
||||
|
||||
### Tool Fallback Surface (Normative)
|
||||
|
||||
The fallback tool surface includes:
|
||||
|
||||
1. `list_resources`
|
||||
2. `read_resource`
|
||||
3. `search_patterns`
|
||||
4. `get_pattern_by_id`
|
||||
5. `get_skill_document_by_id`
|
||||
|
||||
Fallback order:
|
||||
|
||||
1. call `list_resources` to inspect canonical static/template resource surfaces
|
||||
2. call `read_resource` for catalog URIs and selected skill URIs
|
||||
3. use thin catalog tools only when additional metadata-first narrowing is needed
|
||||
|
||||
Tool behavior requirements:
|
||||
|
||||
1. read-only and idempotent semantics
|
||||
2. deterministic ordering and bounded pagination
|
||||
3. explicit not-found responses (`found: false` style) where applicable
|
||||
4. payloads remain schema-aligned with catalog resources
|
||||
|
||||
### Resources-As-Tools Compatibility Layer
|
||||
|
||||
Step 6 includes a resources-as-tools compatibility layer for clients that can call tools but not attach resources.
|
||||
|
||||
Rules:
|
||||
|
||||
1. It wraps canonical resource reads rather than re-implementing content transforms.
|
||||
2. It preserves canonical URIs and metadata semantics.
|
||||
3. It does not replace the minimal catalog tools listed above.
|
||||
4. It is interoperability-driven and remains read-only.
|
||||
|
||||
### Resource/Tool Parity Contract
|
||||
|
||||
Resources and fallback tools must agree on identity and routing metadata.
|
||||
|
||||
Parity requirements:
|
||||
|
||||
1. `skill_id` and `ref_id` are identical across both paths.
|
||||
2. canonical URIs in payloads match Step 3 URI rules.
|
||||
3. skill metadata (`id`, `name`, `description`, `tags`, `capabilities`, `version`) remains consistent.
|
||||
4. document payload returned by `get_skill_document_by_id` resolves to the same canonical `SKILL.md` content as `resource://skills/{skill_id}/document`.
|
||||
|
||||
### Relevance and Ranking Contract
|
||||
|
||||
Baseline matching behavior is metadata-first and deterministic.
|
||||
|
||||
Rules:
|
||||
|
||||
1. Search primarily over normalized skill metadata (id, name, description, tags).
|
||||
2. Keep deterministic ordering and deterministic pagination behavior.
|
||||
3. Keep ranking logic transparent and bounded for predictable client behavior.
|
||||
|
||||
Optional extension policy:
|
||||
|
||||
1. BM25/regex augmentation is allowed only when catalog/tool volume meaningfully harms token efficiency or precision.
|
||||
2. Any augmentation must preserve canonical ids, URIs, and deterministic tie-breaking.
|
||||
3. Any augmentation remains discovery-only and does not create alternate content payloads.
|
||||
|
||||
### Context-Bounding and Clarification Policy
|
||||
|
||||
To prevent context bloat and improve answer quality:
|
||||
|
||||
1. load only the most relevant skill document by default
|
||||
2. load at most two skill documents in one pass unless the user explicitly asks for more
|
||||
3. if confidence is low after catalog discovery, ask one clarifying question before loading additional skill documents
|
||||
4. fetch references lazily and only when required
|
||||
|
||||
### Security and Safety Constraints
|
||||
|
||||
Fallback tools must preserve the project security invariant.
|
||||
|
||||
Rules:
|
||||
|
||||
1. tool surfaces stay documentation-only and read-only
|
||||
2. no mutation, shell execution, secret access, or private filesystem exposure
|
||||
3. all returned content remains safe to publish publicly
|
||||
|
||||
### Integration Boundaries
|
||||
|
||||
Step 6 integrates with prior steps as follows:
|
||||
|
||||
1. Step 4 provides the validated in-memory registry.
|
||||
2. Step 5 provides canonical resource registration.
|
||||
3. Step 6 adds fallback discovery/read behavior that reuses the same registry and canonical markdown sources.
|
||||
|
||||
Separation-of-concerns rule:
|
||||
|
||||
1. Catalog/resource contracts remain canonical.
|
||||
2. Fallback tools are interoperability adapters, not a parallel architecture.
|
||||
|
||||
### Acceptance Criteria for Step 6 Completion
|
||||
|
||||
Step 6 is complete when all are true:
|
||||
|
||||
1. Resource-first discovery remains the documented and implemented default path.
|
||||
2. `list_resources` and `read_resource` are available for tool-only clients.
|
||||
3. Thin catalog tools remain minimal, read-only parity surfaces.
|
||||
4. Fallback tool outputs map to canonical skill identities and URIs.
|
||||
5. Context loading is bounded and clarifying-question behavior is documented for low-confidence cases.
|
||||
6. No second content source is introduced; resources and tools resolve the same authored markdown.
|
||||
|
||||
### Non-goals for Step 6
|
||||
|
||||
1. No write or side-effecting tools.
|
||||
2. No alternate authored markdown stores or duplicated skill content pipelines.
|
||||
3. No guarantee that every client session exposes MCP resource attachment UI.
|
||||
4. No packaging/build contract changes (handled in Step 7).
|
||||
5. No CI gate expansion details (handled in later validation/governance steps).
|
||||
@@ -0,0 +1,4 @@
|
||||
.venv
|
||||
__pycache__
|
||||
.cache*
|
||||
site/
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM python:3.12-slim AS builder
|
||||
|
||||
COPY --from=ghcr.io/astral-sh/uv:0.8.4 /uv /uvx /bin/
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pyproject.toml uv.lock ./
|
||||
COPY src ./src
|
||||
|
||||
RUN uv sync --frozen --no-dev
|
||||
|
||||
COPY docs ./docs
|
||||
COPY zensical.toml ./
|
||||
|
||||
RUN uv run zensical build
|
||||
|
||||
FROM python:3.12-slim AS runtime
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PATH="/app/.venv/bin:$PATH" \
|
||||
PERSONAL_MCP_HOST=0.0.0.0 \
|
||||
PERSONAL_MCP_PORT=8765
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN groupadd --system --gid 1001 appuser \
|
||||
&& useradd --system --uid 1001 --gid appuser --create-home --home-dir /home/appuser appuser
|
||||
|
||||
COPY --from=builder --chown=appuser:appuser /app /app
|
||||
|
||||
EXPOSE 8765
|
||||
|
||||
USER appuser
|
||||
|
||||
CMD ["uvicorn", "personal_mcp.main:app", "--host", "0.0.0.0", "--port", "8765"]
|
||||
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
personal-mcp:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8765:8765"
|
||||
restart: unless-stopped
|
||||
@@ -0,0 +1,236 @@
|
||||
---
|
||||
icon: lucide/library
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
## Overview
|
||||
|
||||
The platform is implemented as a resource-first MCP system with an integrated static documentation surface. The same methodology content powers both MCP resources and the published docs site.
|
||||
|
||||
An MCP server is a runtime that exposes machine-readable resources and tools through stable interfaces so AI clients can discover and consume context consistently. Here, the server's role is intentionally narrow: publish canonical methodology documents as resources, keep discovery predictable through a catalog layer, and serve the same source material as pre-built static documentation.
|
||||
|
||||
The system is complete in three layers:
|
||||
|
||||
1. Canonical methodology is maintained in Markdown skill documents.
|
||||
2. Catalog resources provide normalized discovery.
|
||||
3. Zensical builds a static site from those same Markdown sources and the FastAPI app serves it in the FastMCP runtime process.
|
||||
|
||||
This architecture is anchored by three contracts:
|
||||
|
||||
1. Docs-first authored content contract under `docs/` with strict per-skill ownership.
|
||||
2. `SKILL.md` frontmatter contract with Anthropic fields plus `x-personal-mcp` metadata.
|
||||
3. Canonical resource URI contract with break-and-replace policy for contract changes.
|
||||
|
||||
Detailed contract pages:
|
||||
|
||||
1. [Content Contract](./content.md)
|
||||
2. [Frontmatter Contract](./frontmatter.md)
|
||||
3. [URI Contract](./uris.md)
|
||||
|
||||
This architecture keeps authored content human-friendly while preserving machine-stable contracts.
|
||||
|
||||
## Intent
|
||||
|
||||
The architecture is designed to satisfy three long-term requirements:
|
||||
|
||||
1. Methodology must be editable as markdown by humans.
|
||||
2. Agents must consume stable, discoverable resource contracts, with a minimal read-only catalog tool fallback for constrained clients.
|
||||
3. Public documentation must be pre-built static output served from the application runtime without a separate docs service.
|
||||
|
||||
## System Model
|
||||
|
||||
### Pattern Modules
|
||||
|
||||
Each skill encapsulates one methodology domain in a docs-owned directory:
|
||||
|
||||
1. `docs/skills/<skill-id>/SKILL.md`
|
||||
2. `docs/skills/<skill-id>/references/...`
|
||||
|
||||
The skill document and references are the authored source of truth; runtime code indexes and serves these files without becoming a second authored source.
|
||||
|
||||
Each skill publishes resource families:
|
||||
|
||||
1. document
|
||||
|
||||
The document resource returns canonical Markdown, while clients can perform any downstream section extraction they need.
|
||||
|
||||
### Catalog Module
|
||||
|
||||
The catalog is the canonical discovery layer and publishes normalized records for all modules. It may also expose a minimal set of read-only discovery tools that resolve back to the same canonical markdown content when a client chat surface does not expose MCP resource attachment.
|
||||
|
||||
Typical catalog resources:
|
||||
|
||||
1. resource://catalog/skills_index
|
||||
2. resource://catalog/skills/{skill_id}
|
||||
|
||||
Only canonical catalog resources are part of the runtime contract in this phase.
|
||||
|
||||
### Registry Loader
|
||||
|
||||
The runtime composition includes a startup registry loader that reads packaged docs resources using `importlib.resources.files(...)` and `Traversable` APIs.
|
||||
|
||||
Loader responsibilities:
|
||||
|
||||
1. Parse SKILL.md frontmatter for each skill.
|
||||
2. Validate schema and cross-field constraints before any resource registration.
|
||||
3. Build an in-memory registry keyed by `skill_id`.
|
||||
4. Fail fast for duplicate ids, missing markdown files, broken reference mappings, and invalid `depends_on` values.
|
||||
|
||||
Registry load failure is a startup error, not a partial runtime warning.
|
||||
|
||||
### Content Sources
|
||||
|
||||
Content is authored in markdown under `docs/` and managed as long-form reference material. Skill documents and companion references now live under `docs/skills/`, while project-authored pages remain alongside them in the docs tree. Resource handlers expose the same authored documents through stable resource URIs.
|
||||
|
||||
### Static Docs Surface
|
||||
|
||||
Static docs are built directly from two markdown source streams:
|
||||
|
||||
1. Project-authored docs pages
|
||||
2. Skill and reference markdown pages
|
||||
|
||||
The merged docs tree is built by Zensical into static files and served by the FastAPI app.
|
||||
|
||||
## Data Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Authored Markdown] --> C[Resource Handlers]
|
||||
B[Pattern Metadata] --> D[Catalog Resources]
|
||||
A --> E[Zensical Static Build]
|
||||
E --> H[FastAPI Static Mount]
|
||||
H --> I[Served Docs Site]
|
||||
D --> I
|
||||
```
|
||||
|
||||
## Contracts
|
||||
|
||||
### Metadata Contract
|
||||
|
||||
Each skill declares frontmatter in `docs/skills/<skill-id>/SKILL.md`.
|
||||
|
||||
For the full field-level contract, validation model, and FastMCP metadata mapping, see [Frontmatter Contract](./frontmatter.md).
|
||||
|
||||
Anthropic-facing required fields:
|
||||
|
||||
1. name
|
||||
2. description
|
||||
|
||||
Repository indexing metadata is declared in `x-personal-mcp`:
|
||||
|
||||
1. id
|
||||
2. version
|
||||
3. tags
|
||||
4. capabilities
|
||||
5. depends_on
|
||||
6. references map (ref id to relative path and optional metadata)
|
||||
|
||||
No `metadata.yaml` sidecar is part of the end-state contract.
|
||||
|
||||
### URI Contract
|
||||
|
||||
Canonical resource URIs are:
|
||||
|
||||
For the full URI semantics, parameter validation rules, and compatibility policy, see [URI Contract](./uris.md).
|
||||
|
||||
1. resource://skills/<skill_id>/document
|
||||
2. resource://skills/<skill_id>/references/<ref_id>
|
||||
3. resource://catalog/skills_index
|
||||
4. resource://catalog/skills/{skill_id}
|
||||
5. resource://docs/{path*}
|
||||
|
||||
Validation rules:
|
||||
|
||||
1. `skill_id` is lowercase kebab-case and must satisfy the stable skill id contract.
|
||||
2. `ref_id` is lowercase kebab-case and must be declared in the skill references manifest.
|
||||
3. `path*` resolves only to normalized markdown paths under `docs/`.
|
||||
|
||||
### Resource Registration Contract
|
||||
|
||||
Resources are registered from the validated registry, not by ad hoc per-skill hardcoding.
|
||||
|
||||
Registration rules:
|
||||
|
||||
1. Use RFC6570 URI templates where appropriate.
|
||||
2. Mark documentation resources as read-only and idempotent.
|
||||
3. Set explicit mime types for resource responses.
|
||||
4. Configure duplicate URI handling with `on_duplicate="error"` for startup safety.
|
||||
|
||||
This keeps runtime behavior deterministic and prevents accidental URI collisions.
|
||||
|
||||
### Versioning Rule
|
||||
|
||||
URIs are unversioned and canonical in this phase.
|
||||
|
||||
1. Breaking URI changes are handled as direct replacement.
|
||||
2. No compatibility aliases or dual URI families are maintained.
|
||||
|
||||
## Static Hosting Pattern
|
||||
|
||||
The docs site is pre-built and served by the same FastAPI runtime process used by the MCP app.
|
||||
|
||||
Runtime behavior:
|
||||
|
||||
1. App starts.
|
||||
2. FastAPI mounts the static docs output directory.
|
||||
3. Requests to docs paths are served as static assets.
|
||||
|
||||
This provides a single deployment artifact with no runtime markdown rendering dependency.
|
||||
|
||||
## Advantages
|
||||
|
||||
### Single Source of Truth
|
||||
|
||||
Methodology is authored once and reused in both MCP resources and docs pages.
|
||||
|
||||
### High-Fidelity Agent Context
|
||||
|
||||
Resources expose the same canonical Markdown that humans author and review.
|
||||
|
||||
### Operational Simplicity
|
||||
|
||||
A single app process serves MCP and docs surfaces.
|
||||
|
||||
### Long-Term Maintainability
|
||||
|
||||
Markdown remains easy to review, while contracts remain stable for clients.
|
||||
|
||||
### Client Independence
|
||||
|
||||
Clients can use Ask, Edit, or Agent modes without requiring server-owned prompt orchestration. However, MCP affordances are still chat-surface-dependent: some clients or sessions expose resource attachment directly, while others make tool invocation the more reliable retrieval path.
|
||||
|
||||
## Authoring and Publishing Lifecycle
|
||||
|
||||
1. Update markdown reference content.
|
||||
2. Update metadata if capability surface changes.
|
||||
3. Build static docs with Zensical.
|
||||
4. Serve built output through FastAPI static mount.
|
||||
|
||||
## Scope and Non-Goals
|
||||
|
||||
In-scope:
|
||||
|
||||
1. Resource-first methodology delivery
|
||||
2. Catalog-based discovery
|
||||
3. Pre-built static docs hosting in app runtime
|
||||
|
||||
Out-of-scope:
|
||||
|
||||
1. Prompt-first orchestration as the primary interface
|
||||
2. Large tool inventories duplicating static guidance across skill modules
|
||||
3. Separate dynamic docs service at runtime
|
||||
|
||||
Allowed exception:
|
||||
|
||||
1. A small catalog-level tool layer is acceptable when it improves client interoperability without creating a second source of truth for skill content.
|
||||
|
||||
## Example Content Inputs
|
||||
|
||||
Existing markdown reference sets are valid examples of authored source material for this architecture:
|
||||
|
||||
1. docs/skills/pytest-scaffolding/references/pytest-docs.md
|
||||
2. docs/skills/python-logging-dictconfig/references/python-logging-docs.md
|
||||
3. docs/skills/fastapi-uv-docker/references/fastapi-best-practices.md
|
||||
|
||||
These inputs are treated as content sources, while resource URIs and catalog payloads remain the machine-facing contracts.
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
icon: lucide/file-text
|
||||
---
|
||||
|
||||
# Content Contract
|
||||
|
||||
This page defines the authored content contract for the docs-first MCP architecture.
|
||||
|
||||
## Canonical Source Of Truth
|
||||
|
||||
1. All authored Markdown lives under `docs/`.
|
||||
2. MCP resources and static docs are two distribution surfaces of the same authored files.
|
||||
3. No parallel authored markdown is allowed in `src/` or other package-only paths.
|
||||
|
||||
## Canonical Skill Shape
|
||||
|
||||
Each skill is one directory under `docs/skills/`:
|
||||
|
||||
```text
|
||||
docs/
|
||||
skills/
|
||||
<skill-id>/
|
||||
SKILL.md
|
||||
references/
|
||||
... (one or more markdown files, optional nested folders)
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
1. `SKILL.md` is required for every skill.
|
||||
2. `references/` is the only place for skill-specific supporting docs.
|
||||
3. Nested folders inside `references/` are allowed so a skill can reorganize internals without changing global architecture.
|
||||
4. Skill directories are independent ownership boundaries; no cross-skill file writes.
|
||||
|
||||
## File Placement And Ownership Boundaries
|
||||
|
||||
1. Top-level project docs stay in `docs/*.md`.
|
||||
2. Skill docs stay in `docs/skills/<skill-id>/...`.
|
||||
3. A skill may link to other skills, but must not store content inside another skill's directory.
|
||||
4. Server and runtime code may index and serve docs, but must not be the source of authored markdown.
|
||||
|
||||
## Metadata Location Constraint
|
||||
|
||||
1. Skill metadata is embedded in YAML frontmatter in `SKILL.md`.
|
||||
2. No `metadata.yaml` sidecar exists in the end state.
|
||||
3. Reference lookup metadata, including reference id to relative path mappings, is declared from `SKILL.md` frontmatter rather than inferred as a hidden global convention.
|
||||
|
||||
## Skill Id Contract
|
||||
|
||||
`skill-id` is the public identifier and should satisfy all rules below:
|
||||
|
||||
1. Format: lowercase kebab-case only.
|
||||
2. Character set: `a-z`, `0-9`, and `-`.
|
||||
3. Must start with a letter.
|
||||
4. No underscores, spaces, dots, or uppercase characters.
|
||||
5. Directory name should equal `skill-id` in each committed revision.
|
||||
6. Frontmatter `id` should equal directory name in each committed revision.
|
||||
7. Treat `skill-id` as immutable after release; any rename is a breaking replacement and clients must move to the new id.
|
||||
|
||||
Valid examples:
|
||||
|
||||
1. `fastapi-uv-docker`
|
||||
2. `zensical-docs`
|
||||
3. `pytest-scaffolding`
|
||||
|
||||
Invalid examples:
|
||||
|
||||
1. `fastapi_uv_docker`
|
||||
2. `Zensical-Docs`
|
||||
3. `docs.zensical`
|
||||
|
||||
## Invariants
|
||||
|
||||
This contract guarantees:
|
||||
|
||||
1. One authored source tree in `docs/` for both website and MCP.
|
||||
2. One skill directory maps to one skill identity per revision.
|
||||
3. Namespace and slug drift is minimized by keeping directory and frontmatter ids aligned per revision.
|
||||
4. Per-skill reference structure can evolve without changing cross-skill architecture.
|
||||
5. Packaging for stdio is deterministic because authored content is path-stable.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
This contract does not define:
|
||||
|
||||
1. URI versioning policy details.
|
||||
2. The full frontmatter schema.
|
||||
3. Migration instructions from the current architecture.
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
---
|
||||
icon: lucide/bot
|
||||
---
|
||||
|
||||
# Copilot MCP Mechanics
|
||||
|
||||
## Purpose
|
||||
|
||||
This page explains how the GitHub Copilot extension in VS Code behaves as an MCP client when connected to `personal-mcp`, including why tools can appear while resource attachment appears unavailable.
|
||||
|
||||
## Core Model
|
||||
|
||||
Copilot interacts with MCP servers through separate capability lanes:
|
||||
|
||||
1. tools (invoked by the model during execution)
|
||||
2. resources (attached as read-only context)
|
||||
3. prompts (server-provided prompt templates)
|
||||
|
||||
These lanes are related but independently gated in the client.
|
||||
|
||||
Reliable paths are:
|
||||
|
||||
1. attach MCP resources explicitly through `Add Context > MCP Resources` or `MCP: Browse Resources`
|
||||
2. let Copilot invoke MCP tools when the task and tool descriptions make that relevant
|
||||
3. invoke MCP prompts explicitly with `/server.prompt` when your server exposes them
|
||||
|
||||
## What Actually Happens In VS Code
|
||||
|
||||
### MCP server side
|
||||
|
||||
Your server can advertise resources and serve them correctly. In this project that includes catalog resources and skill document resources.
|
||||
|
||||
### Copilot session side
|
||||
|
||||
The chat surface exposes tools, resources, and prompts through different UI paths. In practice, you can encounter sessions where tool use is available but MCP resource attachment is not exposed in `Add Context`.
|
||||
|
||||
That is why you can sometimes see MCP tools before you see `Add Context > MCP Resources`.
|
||||
|
||||
## Why The Picker Sometimes Shows Only Tools
|
||||
|
||||
`MCP Resources...` in Add Context requires at least:
|
||||
|
||||
1. at least one connected MCP server advertises resource capability
|
||||
2. the current chat surface exposes MCP resource attachment
|
||||
|
||||
If the second condition is not met, resources can be available on the server while still being absent from the picker.
|
||||
|
||||
## Practical Workflow
|
||||
|
||||
Use this sequence to confirm behavior:
|
||||
|
||||
1. run `MCP: Browse Resources` and verify resources exist
|
||||
2. use `MCP: List Servers` to verify the server is enabled and running
|
||||
3. open Copilot Chat
|
||||
4. check `Add Context` for `MCP Resources...`
|
||||
5. if still missing, restart the server and reload VS Code window
|
||||
|
||||
## Recommended Usage Pattern
|
||||
|
||||
1. rely on canonical catalog resources for discovery (`skills_index`, then `skills/{skill_id}`)
|
||||
2. fetch only selected skill documents for context
|
||||
3. keep slash commands for deterministic fallback flows
|
||||
|
||||
When resource attachment is unavailable in the active session, use ResourcesAsTools first, then thin catalog discovery tools as parity fallback:
|
||||
|
||||
1. `list_resources`
|
||||
2. `read_resource`
|
||||
3. `search_patterns`
|
||||
4. `get_pattern_by_id`
|
||||
5. `get_skill_document_by_id`
|
||||
|
||||
The first two are generated from the canonical resource surface and should be preferred in tool-only clients.
|
||||
|
||||
These should stay read-only, minimal, and schema-aligned with catalog resources.
|
||||
|
||||
For very large tool catalogs, server operators can optionally enable tool search mode (`regex` or `bm25`) while keeping `list_resources` and `read_resource` pinned as always-visible fallback tools.
|
||||
|
||||
## What To Type In Copilot Chat
|
||||
|
||||
Use prompts that tell Copilot which MCP feature path to take.
|
||||
|
||||
### If `MCP Resources...` is available
|
||||
|
||||
Use the resource attachment UI first, then ask Copilot to work from the attached material.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
I attached the catalog resources and the FastAPI async SQLAlchemy modernization skill document. Use that context to propose a migration plan for this repo.
|
||||
```
|
||||
|
||||
If you want to keep the attachment sequence explicit, use:
|
||||
|
||||
```text
|
||||
I attached personal-mcp catalog resources first. Use them to identify the best matching skill, then work only from the selected skill document.
|
||||
```
|
||||
|
||||
### If only tools are available
|
||||
|
||||
Ask Copilot to explicitly use resource-backed tools first.
|
||||
|
||||
Example resource-backed prompt:
|
||||
|
||||
```text
|
||||
Use personal-mcp tool fallback by first calling list_resources, then read_resource for resource://catalog/skills_index and the selected resource://skills/<skill-id>/document URI. Use only that loaded skill context in your answer.
|
||||
```
|
||||
|
||||
If needed, use the thin catalog tools.
|
||||
|
||||
Example discovery prompt:
|
||||
|
||||
```text
|
||||
Use the personal-mcp catalog tools to search for the most relevant skill for FastAPI async SQLAlchemy modernization. Then load the selected skill document and use it as context for your answer.
|
||||
```
|
||||
|
||||
Example direct-load prompt:
|
||||
|
||||
```text
|
||||
Call get_skill_document_by_id for fastapi-async-sqlalchemy-modernization and use that document as the main context for this task.
|
||||
```
|
||||
|
||||
Example bounded-selection prompt:
|
||||
|
||||
```text
|
||||
Search personal-mcp skills for NiceGUI UI customization, select at most 2 strong matches, load the best skill document, and answer using only that material plus the workspace code.
|
||||
```
|
||||
|
||||
## Repo Instructions Example
|
||||
|
||||
Repo instructions are the best place to teach Copilot when MCP content is relevant and which path to prefer.
|
||||
|
||||
If you add a repo-level `copilot-instructions.md`, keep the rule simple: prefer catalog-first discovery, keep loaded skill context small, and fall back to tools when resource attachment is unavailable.
|
||||
|
||||
Instructions can strongly steer behavior, but they do not guarantee that VS Code will auto-attach MCP resources for a request. For reliable resource use, either attach resources explicitly or prompt Copilot to use the fallback tools.
|
||||
|
||||
Example:
|
||||
|
||||
```md
|
||||
# MCP Usage
|
||||
|
||||
When a task may benefit from personal-mcp skills, use this sequence:
|
||||
|
||||
1. Start with personal-mcp catalog discovery when the task appears to match documented implementation patterns.
|
||||
2. Prefer MCP resources when the chat surface exposes resource attachment.
|
||||
3. If MCP resource attachment is unavailable, use `list_resources`/`read_resource` first, then thin catalog tools if needed.
|
||||
4. Load only the most relevant skill document or at most 2 skill documents.
|
||||
5. Treat skill documents as guidance, then reconcile them with the actual repository code before making changes.
|
||||
|
||||
Preferred discovery order:
|
||||
|
||||
1. `resource://catalog/skills_index`
|
||||
2. `resource://catalog/skills/{skill_id}`
|
||||
3. `resource://skills/<skill-id>/document`
|
||||
4. `resource://skills/<skill-id>/references/<ref-id>` when needed
|
||||
|
||||
Tool fallback order:
|
||||
|
||||
1. `list_resources`
|
||||
2. `read_resource`
|
||||
3. `search_patterns`
|
||||
4. `get_pattern_by_id`
|
||||
5. `get_skill_document_by_id`
|
||||
|
||||
If confidence is low after catalog discovery, ask one clarifying question before loading more skill documents.
|
||||
```
|
||||
|
||||
That instruction style does two useful things:
|
||||
|
||||
1. it tells Copilot to prefer the MCP server when relevant without forcing it on every prompt
|
||||
2. it keeps context size bounded so skill loading does not become noisy or expensive
|
||||
|
||||
If you want stronger behavior, add one more line that names the MCP server directly:
|
||||
|
||||
```md
|
||||
Use the `personal-mcp` server for skill discovery whenever the task involves documented implementation patterns available from the catalog.
|
||||
```
|
||||
|
||||
## Known Gotcha
|
||||
|
||||
A successful `resources/list` response from the server does not guarantee the resource picker appears in every Copilot session type. UI availability is session-capability-dependent.
|
||||
|
||||
## Further Reading
|
||||
|
||||
### VS Code docs
|
||||
|
||||
1. [Add and manage MCP servers](https://code.visualstudio.com/docs/agent-customization/mcp-servers)
|
||||
2. [MCP configuration reference](https://code.visualstudio.com/docs/agents/reference/mcp-configuration)
|
||||
3. [Manage context for AI](https://code.visualstudio.com/docs/chat/copilot-chat-context)
|
||||
4. [AI features cheat sheet](https://code.visualstudio.com/docs/agents/reference/ai-features-cheat-sheet)
|
||||
|
||||
### Project docs
|
||||
|
||||
1. [Resource-First Pattern Module Architecture](./architecture.md)
|
||||
2. [Static Docs Hosting Pattern](./mcp_layout.md)
|
||||
3. [Skill Usage Mechanics](./usage.md)
|
||||
@@ -0,0 +1,316 @@
|
||||
---
|
||||
icon: lucide/braces
|
||||
---
|
||||
|
||||
# Frontmatter Contract
|
||||
|
||||
This page defines the `SKILL.md` frontmatter and FastMCP metadata contract.
|
||||
|
||||
## Anthropic Frontmatter Support
|
||||
|
||||
Across Anthropic API and Agent Skills surfaces:
|
||||
|
||||
1. Required fields for custom skill bundles are `name` and `description`.
|
||||
2. `name` must be 1-64 characters, lowercase letters, numbers, and hyphens only, with no XML tags, and must not use the reserved words `anthropic` or `claude`.
|
||||
3. `description` must be 1-1024 characters, non-empty, and contain no XML tags.
|
||||
|
||||
Portable optional fields from the Agent Skills specification:
|
||||
|
||||
1. `license`
|
||||
2. `compatibility`
|
||||
3. `metadata`
|
||||
4. `allowed-tools`
|
||||
|
||||
Claude Code-specific optional fields:
|
||||
|
||||
1. `when_to_use`
|
||||
2. `argument-hint`
|
||||
3. `arguments`
|
||||
4. `disable-model-invocation`
|
||||
5. `user-invocable`
|
||||
6. `allowed-tools`
|
||||
7. `disallowed-tools`
|
||||
8. `model`
|
||||
9. `effort`
|
||||
10. `context`
|
||||
11. `agent`
|
||||
12. `hooks`
|
||||
13. `paths`
|
||||
14. `shell`
|
||||
|
||||
Repository contract decisions:
|
||||
|
||||
1. Treat `name` and `description` as required in all `SKILL.md` files.
|
||||
2. Keep Anthropic-facing semantics in standard fields.
|
||||
3. Keep MCP indexing metadata in a namespaced extension block.
|
||||
4. Preserve forward compatibility by allowing additive optional metadata fields over time.
|
||||
|
||||
## Canonical Frontmatter Schema
|
||||
|
||||
Use this two-layer pattern:
|
||||
|
||||
1. Anthropic layer: top-level fields intended for Anthropic and Agent Skills behavior.
|
||||
2. Repository layer: one namespaced block, `x-personal-mcp`, for MCP catalog and routing metadata.
|
||||
|
||||
Canonical shape:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: <skill-id>
|
||||
description: <what this skill does and when to use it>
|
||||
|
||||
# Optional Anthropic and Agent Skills fields
|
||||
when_to_use: <extra trigger guidance>
|
||||
allowed-tools: <space-separated string or YAML list>
|
||||
disable-model-invocation: false
|
||||
user-invocable: true
|
||||
license: <optional>
|
||||
compatibility: <optional>
|
||||
|
||||
# Repository-specific metadata
|
||||
x-personal-mcp:
|
||||
id: <skill-id>
|
||||
version: <semver>
|
||||
tags:
|
||||
- <tag>
|
||||
capabilities:
|
||||
- resource://skills/<skill-id>/document
|
||||
depends_on: []
|
||||
# Optional: overrides and nested references only.
|
||||
# Top-level references/*.md are auto-discovered.
|
||||
references:
|
||||
<ref-id>:
|
||||
path: references/<file>.md
|
||||
mime_type: text/markdown
|
||||
title: <short title>
|
||||
---
|
||||
```
|
||||
|
||||
## Repository Metadata Field Rules
|
||||
|
||||
Rules for `x-personal-mcp`:
|
||||
|
||||
1. `id` is required, must follow the skill id rules from the content contract, and must equal the directory name.
|
||||
2. `version` is required and must be a semantic version string.
|
||||
3. `tags` is optional and should be a list of kebab-case discovery labels.
|
||||
4. `capabilities` is required and lists the MCP URIs the skill publishes.
|
||||
5. `depends_on` is optional and lists other skill ids.
|
||||
6. `references` is an optional map keyed by `ref-id` for overrides and nested entries.
|
||||
|
||||
Reference entry rules:
|
||||
|
||||
1. `ref-id` is lowercase kebab-case.
|
||||
2. `path` is a skill-relative markdown path and must stay inside the same skill directory.
|
||||
3. Top-level files under `references/*.md` are auto-discovered with `ref-id` derived from a normalized filename stem (lowercase kebab-case).
|
||||
4. Nested folders under `references/` are not auto-discovered and must be declared explicitly.
|
||||
5. `mime_type` defaults to `text/markdown` when omitted.
|
||||
6. `title` is an optional display label.
|
||||
7. Renaming `ref-id` values is allowed when needed; optional aliases may be used during transitions.
|
||||
|
||||
## Validation Models
|
||||
|
||||
The normative model uses Pydantic v2 with change-friendly validation:
|
||||
|
||||
```python
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import PurePosixPath
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
||||
|
||||
SKILL_ID_RE = re.compile(r"^[a-z][a-z0-9-]*$")
|
||||
SEMVER_RE = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+][0-9A-Za-z.-]+)?$")
|
||||
|
||||
|
||||
class ReferenceEntry(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
|
||||
|
||||
path: str
|
||||
mime_type: str = "text/markdown"
|
||||
title: str | None = None
|
||||
|
||||
@field_validator("path")
|
||||
@classmethod
|
||||
def validate_reference_path(cls, value: str) -> str:
|
||||
p = PurePosixPath(value)
|
||||
if p.is_absolute() or ".." in p.parts:
|
||||
raise ValueError("reference path must be a relative in-skill path")
|
||||
if not str(p).startswith("references/"):
|
||||
raise ValueError("reference path must stay under references/")
|
||||
if p.suffix.lower() != ".md":
|
||||
raise ValueError("reference path must target a markdown file")
|
||||
return str(p)
|
||||
|
||||
|
||||
class PersonalMcpMetadata(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
|
||||
|
||||
id: str
|
||||
version: str
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
capabilities: list[str] = Field(min_length=1)
|
||||
depends_on: list[str] = Field(default_factory=list)
|
||||
references: dict[str, ReferenceEntry] = Field(default_factory=dict)
|
||||
|
||||
@field_validator("id")
|
||||
@classmethod
|
||||
def validate_id(cls, value: str) -> str:
|
||||
if not SKILL_ID_RE.fullmatch(value):
|
||||
raise ValueError("id must be lowercase kebab-case and start with a letter")
|
||||
return value
|
||||
|
||||
@field_validator("version")
|
||||
@classmethod
|
||||
def validate_version(cls, value: str) -> str:
|
||||
if not SEMVER_RE.fullmatch(value):
|
||||
raise ValueError("version must be semver")
|
||||
return value
|
||||
|
||||
@field_validator("depends_on")
|
||||
@classmethod
|
||||
def validate_depends_on(cls, value: list[str]) -> list[str]:
|
||||
for dep in value:
|
||||
if not SKILL_ID_RE.fullmatch(dep):
|
||||
raise ValueError(f"invalid depends_on skill id: {dep}")
|
||||
return value
|
||||
|
||||
@field_validator("references")
|
||||
@classmethod
|
||||
def validate_reference_ids(cls, value: dict[str, ReferenceEntry]) -> dict[str, ReferenceEntry]:
|
||||
for ref_id in value:
|
||||
if not SKILL_ID_RE.fullmatch(ref_id):
|
||||
raise ValueError(f"invalid reference id: {ref_id}")
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def ensure_primary_capability(self) -> "PersonalMcpMetadata":
|
||||
expected = f"resource://skills/{self.id}/document"
|
||||
if expected not in self.capabilities:
|
||||
raise ValueError(f"capabilities must include {expected}")
|
||||
return self
|
||||
|
||||
|
||||
class SkillFrontmatter(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
|
||||
|
||||
name: str = Field(min_length=1, max_length=64)
|
||||
description: str = Field(min_length=1, max_length=1024)
|
||||
when_to_use: str | None = None
|
||||
allowed_tools: str | list[str] | None = Field(default=None, alias="allowed-tools")
|
||||
disallowed_tools: str | list[str] | None = Field(default=None, alias="disallowed-tools")
|
||||
disable_model_invocation: bool | None = Field(default=None, alias="disable-model-invocation")
|
||||
user_invocable: bool | None = Field(default=None, alias="user-invocable")
|
||||
argument_hint: str | None = Field(default=None, alias="argument-hint")
|
||||
arguments: str | list[str] | None = None
|
||||
license: str | None = None
|
||||
compatibility: str | None = None
|
||||
metadata: dict[str, str] | None = None
|
||||
|
||||
x_personal_mcp: PersonalMcpMetadata = Field(alias="x-personal-mcp")
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, value: str) -> str:
|
||||
if not SKILL_ID_RE.fullmatch(value):
|
||||
raise ValueError("name must be lowercase kebab-case and start with a letter")
|
||||
if "anthropic" in value or "claude" in value:
|
||||
raise ValueError("name must not contain reserved words anthropic or claude")
|
||||
return value
|
||||
|
||||
@model_validator(mode="after")
|
||||
def cross_validate(self) -> "SkillFrontmatter":
|
||||
if self.x_personal_mcp.id != self.name:
|
||||
raise ValueError("x-personal-mcp.id must exactly match name")
|
||||
return self
|
||||
|
||||
|
||||
def validate_skill_frontmatter(raw: dict[str, Any], skill_dir_name: str) -> SkillFrontmatter:
|
||||
model = SkillFrontmatter.model_validate(raw)
|
||||
if model.name != skill_dir_name:
|
||||
raise ValueError("frontmatter name must exactly match skill directory name")
|
||||
return model
|
||||
```
|
||||
|
||||
Validation behavior contract:
|
||||
|
||||
1. Validate required core fields and relationships during registry load before FastMCP resource or tool registration.
|
||||
2. Allow unknown additive fields so frontmatter can evolve without blocking startup.
|
||||
3. Treat hard contract violations, including missing required fields, invalid ids, and broken required mappings, as startup errors.
|
||||
4. Treat non-critical compatibility issues as warnings when possible.
|
||||
5. Error messages should include the skill path and failing field for CI readability.
|
||||
|
||||
Projection mode contract for Anthropic API upload pipelines:
|
||||
|
||||
1. Parse with `SkillFrontmatter` first.
|
||||
2. Emit Anthropic-safe frontmatter with standard fields only.
|
||||
3. Serialize repository metadata into standard `metadata` as namespaced keys.
|
||||
4. Preserve the canonical authored source in `x-personal-mcp`; projection output is a build artifact.
|
||||
|
||||
## Anthropic Upload Compatibility Rule
|
||||
|
||||
1. Anthropic documentation guarantees behavior for standard frontmatter fields but does not explicitly guarantee handling of arbitrary unknown top-level keys.
|
||||
2. Publishing pipelines that target strict API compatibility should support a projection mode that emits only standard frontmatter fields for upload.
|
||||
3. In projection mode, repository extension metadata is serialized into the standard `metadata` field as namespaced keys or JSON-encoded values, while source-of-truth authoring remains in `x-personal-mcp`.
|
||||
|
||||
## FastMCP Native Metadata Surfaces
|
||||
|
||||
Resources support native definition metadata:
|
||||
|
||||
1. `name`
|
||||
2. `description`
|
||||
3. `mime_type`
|
||||
4. `tags`
|
||||
5. `annotations`, including `readOnlyHint` and `idempotentHint`
|
||||
6. `icons`
|
||||
7. `meta`
|
||||
8. `version`
|
||||
9. `enabled`, which is deprecated in FastMCP v3 in favor of server-level enable and disable controls
|
||||
|
||||
Resources also support runtime metadata through `ResourceContent.meta` and `ResourceResult.meta`.
|
||||
|
||||
Tools support native definition metadata:
|
||||
|
||||
1. `name`
|
||||
2. `description`
|
||||
3. `tags`
|
||||
4. `annotations`, including `title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint`
|
||||
5. `icons`
|
||||
6. `meta`
|
||||
7. `version`
|
||||
8. `timeout`
|
||||
9. `output_schema`
|
||||
10. `run_in_thread`
|
||||
11. `enabled`, which is deprecated in FastMCP v3 in favor of server-level enable and disable controls
|
||||
|
||||
Tools also support runtime metadata through `ToolResult.meta`.
|
||||
|
||||
## Frontmatter To FastMCP Mapping Contract
|
||||
|
||||
At server startup, map `x-personal-mcp` into FastMCP registration as follows:
|
||||
|
||||
1. `x-personal-mcp.id` defines the canonical URI namespace and identity checks.
|
||||
2. `description` becomes the default description for the primary skill document resource.
|
||||
3. `x-personal-mcp.tags` maps to resource and tool tags.
|
||||
4. `x-personal-mcp.version` maps to resource and tool version metadata.
|
||||
5. `x-personal-mcp.capabilities` becomes the registered URI list and catalog exposure.
|
||||
6. `x-personal-mcp.references[*]` becomes resource templates or concrete resources with `mime_type`, read-only annotations, and `meta` that includes `skill_id`, `ref_id`, and source `path`.
|
||||
7. `x-personal-mcp.depends_on` becomes catalog dependency graph metadata and validation inputs.
|
||||
|
||||
## Invariants
|
||||
|
||||
This contract guarantees:
|
||||
|
||||
1. Anthropic-required frontmatter stays valid for custom skill upload and Claude Code loading.
|
||||
2. MCP-specific metadata remains embedded in `SKILL.md` frontmatter, with no `metadata.yaml` sidecar.
|
||||
3. FastMCP registration uses native metadata fields for resources and tools.
|
||||
4. Reference ids and metadata can evolve with low-friction updates while internal file layout under `references/` stays refactor-friendly.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
This contract does not define:
|
||||
|
||||
1. URI versioning and deprecation rollout policy details.
|
||||
2. Migration script design from existing `metadata.yaml` files.
|
||||
3. Runtime caching and indexing performance tuning.
|
||||
+26
-152
@@ -2,172 +2,46 @@
|
||||
icon: lucide/rocket
|
||||
---
|
||||
|
||||
# Get started
|
||||
# Personal MCP
|
||||
|
||||
For full documentation visit [zensical.org](https://zensical.org/docs/).
|
||||
This project is a document library of software patterns, best practices, and structured references to external documentation. The same markdown files are published through two equivalent surfaces, so human-readable docs and MCP resources stay aligned.
|
||||
|
||||
## Commands
|
||||
## MCP Server
|
||||
|
||||
* [`zensical new`][new] - Create a new project
|
||||
* [`zensical serve`][serve] - Start local web server
|
||||
* [`zensical build`][build] - Build your site
|
||||
An [MCP server](https://modelcontextprotocol.io/docs/getting-started/intro) at `/mcp` provides context for AI systems. The markdown files are exposed as [resources](https://modelcontextprotocol.io/docs/learn/server-concepts#resources) and are structured to be easily consumed by [MCP clients](https://modelcontextprotocol.io/docs/learn/client-concepts), such as VS Code.
|
||||
|
||||
[new]: https://zensical.org/docs/usage/new/
|
||||
[serve]: https://zensical.org/docs/usage/preview/
|
||||
[build]: https://zensical.org/docs/usage/build/
|
||||
## Docs
|
||||
|
||||
## Examples
|
||||
A website at `/docs` for humans to read and review.
|
||||
|
||||
### Admonitions
|
||||
## Quick start
|
||||
|
||||
> Go to [documentation](https://zensical.org/docs/authoring/admonitions/)
|
||||
Install dependencies first:
|
||||
|
||||
!!! 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")
|
||||
```bash
|
||||
uv sync
|
||||
```
|
||||
|
||||
1. > Go to [documentation](https://zensical.org/docs/authoring/code-blocks/#code-annotations)
|
||||
Run the app locally with the static docs rebuilt first:
|
||||
|
||||
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!];
|
||||
```bash
|
||||
uv run zensical build && uv run uvicorn personal_mcp.main:app --host 127.0.0.1 --port 8765
|
||||
```
|
||||
|
||||
## Footnotes
|
||||
Build and run the Docker image with the same exposed port:
|
||||
|
||||
> Go to [documentation](https://zensical.org/docs/authoring/footnotes/)
|
||||
```bash
|
||||
docker build -t personal-mcp . && docker run --rm -p 8765:8765 personal-mcp
|
||||
```
|
||||
|
||||
Here's a sentence with a footnote.[^1]
|
||||
When the server is running, the health check is available at `/healthz` and the generated docs are available at `/docs/`.
|
||||
|
||||
Hover it, to see a tooltip.
|
||||
## Architecture
|
||||
|
||||
[^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!"
|
||||
- [Resource-First Pattern Module Architecture](./architecture.md)
|
||||
- [Content Contract](./content.md)
|
||||
- [Frontmatter Contract](./frontmatter.md)
|
||||
- [URI Contract](./uris.md)
|
||||
- [Static Docs Hosting Pattern](./mcp_layout.md)
|
||||
- [Skill Usage Mechanics](./usage.md)
|
||||
- [Copilot MCP Mechanics](./copilot.md)
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
let mermaidPromise;
|
||||
|
||||
async function getMermaid() {
|
||||
if (!mermaidPromise) {
|
||||
mermaidPromise = import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs").then(
|
||||
(module) => {
|
||||
const mermaid = module.default ?? module;
|
||||
mermaid.initialize({ startOnLoad: false });
|
||||
return mermaid;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return mermaidPromise;
|
||||
}
|
||||
|
||||
function readDiagramSource(block) {
|
||||
const code = block.querySelector("code");
|
||||
return (code?.textContent ?? block.textContent ?? "").trim();
|
||||
}
|
||||
|
||||
async function renderBlock(block) {
|
||||
if (block.dataset.mermaidOverrideState) {
|
||||
return;
|
||||
}
|
||||
|
||||
const source = readDiagramSource(block);
|
||||
if (!source) {
|
||||
block.dataset.mermaidOverrideState = "empty";
|
||||
return;
|
||||
}
|
||||
|
||||
block.dataset.mermaidOverrideState = "pending";
|
||||
|
||||
try {
|
||||
const mermaid = await getMermaid();
|
||||
const replacement = document.createElement("div");
|
||||
replacement.className = "mermaid";
|
||||
replacement.textContent = source;
|
||||
replacement.dataset.mermaidOverrideState = "rendered";
|
||||
block.replaceWith(replacement);
|
||||
await mermaid.run({ nodes: [replacement] });
|
||||
} catch (error) {
|
||||
block.dataset.mermaidOverrideState = "failed";
|
||||
console.warn("Mermaid override failed", error);
|
||||
}
|
||||
}
|
||||
|
||||
function findBlocks(root = document) {
|
||||
const blocks = [];
|
||||
|
||||
if (root instanceof Element && root.matches("pre.mermaid")) {
|
||||
blocks.push(root);
|
||||
}
|
||||
|
||||
if (root instanceof Document || root instanceof Element) {
|
||||
blocks.push(...root.querySelectorAll("pre.mermaid"));
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
|
||||
function renderAll(root = document) {
|
||||
for (const block of findBlocks(root)) {
|
||||
void renderBlock(block);
|
||||
}
|
||||
}
|
||||
|
||||
function installObserver() {
|
||||
if (!(document.body instanceof HTMLBodyElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
for (const mutation of mutations) {
|
||||
for (const node of mutation.addedNodes) {
|
||||
if (node instanceof Element) {
|
||||
renderAll(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
renderAll();
|
||||
installObserver();
|
||||
});
|
||||
} else {
|
||||
renderAll();
|
||||
installObserver();
|
||||
}
|
||||
|
||||
window.addEventListener("pageshow", () => renderAll());
|
||||
window.addEventListener("popstate", () => renderAll());
|
||||
@@ -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")
|
||||

|
||||

|
||||
```
|
||||
|
||||
## 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.
|
||||
```
|
||||
@@ -0,0 +1,215 @@
|
||||
---
|
||||
icon: lucide/server
|
||||
---
|
||||
|
||||
# Static Docs Hosting Pattern
|
||||
|
||||
## Purpose
|
||||
|
||||
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.
|
||||
|
||||
This design intentionally avoids runtime docs rendering and avoids a separate docs hosting service.
|
||||
|
||||
It also treats Markdown as the single source of truth for both MCP resources and published docs.
|
||||
|
||||
## Completed-State Layout
|
||||
|
||||
```mermaid
|
||||
---
|
||||
config:
|
||||
treeView:
|
||||
rowIndent: 40
|
||||
lineThickness: 2
|
||||
themeVariables:
|
||||
treeView:
|
||||
labelColor: '#FFFFFF'
|
||||
lineColor: '#FFFFFF'
|
||||
---
|
||||
treeView-beta
|
||||
"project-root"
|
||||
"pyproject.toml"
|
||||
"uv.lock"
|
||||
"zensical.toml"
|
||||
"docs"
|
||||
"index.md"
|
||||
"architecture.md"
|
||||
"content.md"
|
||||
"frontmatter.md"
|
||||
"mcp_layout.md"
|
||||
"uris.md"
|
||||
"skills"
|
||||
"new-skill"
|
||||
"SKILL.md"
|
||||
"references"
|
||||
"copilot-customization"
|
||||
"SKILL.md"
|
||||
"references"
|
||||
"fastapi-async-sqlalchemy-modernization"
|
||||
"SKILL.md"
|
||||
"references"
|
||||
"fastapi-uv-docker"
|
||||
"SKILL.md"
|
||||
"references"
|
||||
"nicegui"
|
||||
"SKILL.md"
|
||||
"references"
|
||||
"nicegui-ui-customization"
|
||||
"SKILL.md"
|
||||
"references"
|
||||
"pytest-scaffolding"
|
||||
"SKILL.md"
|
||||
"references"
|
||||
"python-logging-dictconfig"
|
||||
"SKILL.md"
|
||||
"references"
|
||||
"vscode-configuration"
|
||||
"SKILL.md"
|
||||
"references"
|
||||
"zensical-docs"
|
||||
"SKILL.md"
|
||||
"references"
|
||||
"site"
|
||||
"static build output"
|
||||
"src"
|
||||
"personal_mcp"
|
||||
"main.py"
|
||||
"mcp.py"
|
||||
"web"
|
||||
"app.py"
|
||||
"docs_mount.py"
|
||||
"catalog"
|
||||
"server.py"
|
||||
"skills"
|
||||
"document_loader.py"
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
1. docs contains both project-authored pages and the canonical skill Markdown tree.
|
||||
2. site contains static build output only.
|
||||
3. docs/skills contains canonical skill Markdown and reference Markdown.
|
||||
4. MCP resources and docs site read from the same Markdown sources.
|
||||
|
||||
## Runtime Composition
|
||||
|
||||
The runtime process serves two surfaces:
|
||||
|
||||
1. MCP protocol surface from FastMCP
|
||||
2. Static docs surface from FastAPI static mount
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[Docs Registry Loader] --> B[Validated In-Memory Registry]
|
||||
B --> C[FastMCP Resource Registration]
|
||||
C --> D[MCP Transport]
|
||||
C --> E[FastAPI Application]
|
||||
E --> F[Static Mount /docs]
|
||||
F --> G[Zensical site output directory]
|
||||
```
|
||||
|
||||
Runtime guarantees:
|
||||
|
||||
1. Docs registry load and validation happen before resource exposure.
|
||||
2. Duplicate resource and template registration fails startup (`on_duplicate="error"`).
|
||||
3. Resource registration is metadata-driven from SKILL frontmatter and reference manifests.
|
||||
4. Legacy per-skill Python servers and `metadata.yaml` sidecars are not part of the runtime.
|
||||
|
||||
## Build and Publish Flow
|
||||
|
||||
The docs flow is pre-build only.
|
||||
|
||||
1. Read authored docs pages and skill markdown sources.
|
||||
2. Build static site with Zensical into site.
|
||||
3. Start app and serve site directory as static files.
|
||||
|
||||
No runtime markdown conversion is required.
|
||||
|
||||
## Content Merge Pattern
|
||||
|
||||
The published docs site always contains both:
|
||||
|
||||
1. Project-authored docs pages
|
||||
2. Skill Markdown content from docs/skills/*/SKILL.md and references
|
||||
|
||||
This ensures the public docs reflect architectural guidance and the exact Markdown served by MCP.
|
||||
|
||||
## Markdown-to-Resource Mapping
|
||||
|
||||
MCP resources map directly to canonical Markdown documents.
|
||||
|
||||
Example mapping model:
|
||||
|
||||
1. docs/skills/<skill-id>/SKILL.md -> resource://skills/<skill_id>/document
|
||||
2. docs/skills/<skill-id>/references/<file>.md -> resource://skills/<skill_id>/references/<ref_id> (via frontmatter references manifest)
|
||||
3. docs/<path>.md -> resource://docs/{path*}
|
||||
|
||||
Catalog discovery resources are:
|
||||
|
||||
1. resource://catalog/skills_index
|
||||
2. resource://catalog/skills/{skill_id}
|
||||
|
||||
Registry-backed registration details:
|
||||
|
||||
1. `resource://skills/{skill_id}/document` resolves to each skill's SKILL.md.
|
||||
2. `resource://skills/{skill_id}/references/{ref_id}` resolves through frontmatter reference manifests.
|
||||
3. `resource://docs/{path*}` resolves normalized markdown paths under `docs/`.
|
||||
4. Resource metadata includes explicit mime type and read-only/idempotent annotations.
|
||||
|
||||
When clients cannot attach MCP resources directly, thin catalog tools may retrieve the same underlying skill documents indirectly. This does not create a second content source.
|
||||
|
||||
## URI Compatibility Policy
|
||||
|
||||
1. Canonical URIs are the only supported URIs in this runtime.
|
||||
2. No backward-compatibility aliases or dual registration paths are maintained.
|
||||
3. Contract changes should update clients to canonical URIs directly.
|
||||
|
||||
## Why This Pattern
|
||||
|
||||
### Operational Simplicity
|
||||
|
||||
One application process serves both protocol and static docs surfaces.
|
||||
|
||||
### Deterministic Docs
|
||||
|
||||
Published docs are immutable static assets for a given build.
|
||||
|
||||
### Documentation Fidelity
|
||||
|
||||
The docs site and MCP resources resolve from the same Markdown sources.
|
||||
|
||||
### Maintainer Experience
|
||||
|
||||
Authors continue to work in markdown while resource contracts remain machine-consumable.
|
||||
|
||||
## FastAPI Static Mount Expectations
|
||||
|
||||
The FastAPI app is expected to:
|
||||
|
||||
1. Mount static directory containing Zensical output.
|
||||
2. Serve index and asset files from that directory.
|
||||
3. Keep docs route stable across releases.
|
||||
|
||||
Recommended route conventions:
|
||||
|
||||
1. /docs for static site root
|
||||
2. /docs/* for static assets and page routes
|
||||
|
||||
## Update Lifecycle
|
||||
|
||||
For each documentation update:
|
||||
|
||||
1. Edit authored docs and skill markdown content.
|
||||
2. Rebuild static site.
|
||||
3. Restart runtime if needed.
|
||||
|
||||
This keeps docs publication explicit and predictable.
|
||||
|
||||
## Example Source Material
|
||||
|
||||
Existing reference docs remain valid content inputs in this pattern:
|
||||
|
||||
1. docs/skills/pytest-scaffolding/references/pytest-docs.md
|
||||
2. docs/skills/python-logging-dictconfig/references/python-logging-docs.md
|
||||
3. docs/skills/fastapi-uv-docker/references/fastapi-best-practices.md
|
||||
|
||||
These are source documents, not deployment artifacts.
|
||||
@@ -0,0 +1,129 @@
|
||||
---
|
||||
icon: lucide/file-plus
|
||||
---
|
||||
|
||||
# Hooking Up a New Skill
|
||||
|
||||
Use this checklist to add a new skill in the docs-first model.
|
||||
|
||||
For the full contract details, see [Content Contract](./content.md), [Frontmatter Contract](./frontmatter.md), and [URI Contract](./uris.md).
|
||||
|
||||
## Canonical Skill Shape
|
||||
|
||||
Create one skill directory under `docs/skills/`:
|
||||
|
||||
```text
|
||||
docs/
|
||||
skills/
|
||||
<skill-id>/
|
||||
SKILL.md
|
||||
references/
|
||||
... (optional markdown files, nested folders allowed)
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
1. `SKILL.md` is required.
|
||||
2. All skill-specific supporting docs live under `references/`.
|
||||
3. Skill directories are ownership boundaries; no cross-skill writes.
|
||||
4. `skill-id` is lowercase kebab-case and should remain stable.
|
||||
|
||||
## SKILL.md Frontmatter
|
||||
|
||||
`SKILL.md` frontmatter is authoritative for metadata.
|
||||
|
||||
Required top-level fields:
|
||||
|
||||
1. `name`
|
||||
2. `description`
|
||||
3. `x-personal-mcp`
|
||||
|
||||
Required `x-personal-mcp` fields:
|
||||
|
||||
1. `id`
|
||||
2. `version`
|
||||
3. `capabilities`
|
||||
|
||||
Optional `x-personal-mcp` fields:
|
||||
|
||||
1. `tags`
|
||||
2. `depends_on`
|
||||
3. `references`
|
||||
|
||||
Canonical frontmatter template:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: <skill-id>
|
||||
description: <what this skill does and when to use it>
|
||||
|
||||
x-personal-mcp:
|
||||
id: <skill-id>
|
||||
version: 1.0.0
|
||||
tags: []
|
||||
capabilities:
|
||||
- resource://skills/<skill-id>/document
|
||||
depends_on: []
|
||||
# Optional: only for nested references or metadata overrides.
|
||||
references:
|
||||
<ref-id>:
|
||||
path: references/<file>.md
|
||||
mime_type: text/markdown
|
||||
title: <optional short title>
|
||||
---
|
||||
```
|
||||
|
||||
Reference manifest rules:
|
||||
|
||||
1. `ref-id` is lowercase kebab-case.
|
||||
2. `path` is skill-relative and must stay under `references/`.
|
||||
3. Top-level `references/*.md` files are auto-discovered, and `ref-id` is derived from a normalized filename stem.
|
||||
4. Nested `references/**` markdown files must be declared explicitly.
|
||||
5. Reference paths are markdown files.
|
||||
|
||||
No `metadata.yaml` sidecar is part of this model.
|
||||
|
||||
## URI Surface
|
||||
|
||||
Canonical resource URIs for a skill:
|
||||
|
||||
1. `resource://skills/<skill_id>/document`
|
||||
2. `resource://skills/<skill_id>/references/<ref_id>`
|
||||
|
||||
Canonical discovery URIs:
|
||||
|
||||
1. `resource://catalog/skills_index`
|
||||
2. `resource://catalog/skills/{skill_id}`
|
||||
|
||||
Docs passthrough URI:
|
||||
|
||||
1. `resource://docs/{path*}`
|
||||
|
||||
Compatibility rule:
|
||||
|
||||
1. Keep URI families unversioned by default.
|
||||
2. For breaking changes, update clients to the canonical replacement URIs directly.
|
||||
|
||||
## Checklist
|
||||
|
||||
1. Create `docs/skills/<skill-id>/SKILL.md`.
|
||||
2. Add optional references under `docs/skills/<skill-id>/references/`.
|
||||
3. Populate frontmatter with `name`, `description`, and `x-personal-mcp` metadata.
|
||||
4. Ensure `x-personal-mcp.id` equals `name` and directory `<skill-id>`.
|
||||
5. Ensure `capabilities` includes `resource://skills/<skill-id>/document`.
|
||||
6. Add supporting docs under `references/`; top-level markdown files are exposed automatically.
|
||||
7. Declare `x-personal-mcp.references` only for nested paths or to override defaults.
|
||||
|
||||
## Quick Validation
|
||||
|
||||
1. Confirm docs build succeeds:
|
||||
|
||||
```bash
|
||||
uv run zensical build
|
||||
```
|
||||
|
||||
2. Confirm tests succeed:
|
||||
|
||||
```bash
|
||||
uv run pytest -q
|
||||
```
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
icon: lucide/shield-check
|
||||
---
|
||||
|
||||
# Securing Remote Access
|
||||
|
||||
## Context
|
||||
|
||||
This project exposes two related surfaces from the same runtime:
|
||||
|
||||
1. a static documentation site under `/docs`
|
||||
2. a Streamable HTTP MCP endpoint under `/mcp`
|
||||
|
||||
The same Markdown content backs both surfaces. For the current project shape, the MCP server is resource-first and primarily exposes public skill and documentation text. It is not intended to expose secrets, private data, shell access, filesystem access, or tools with side effects.
|
||||
|
||||
The expected deployment path is:
|
||||
|
||||
```text
|
||||
Public internet
|
||||
-> Cloudflare Tunnel
|
||||
-> Caddy
|
||||
-> personal-mcp container
|
||||
```
|
||||
|
||||
## Decision
|
||||
|
||||
For the current use case, heavy application-level authentication is not required.
|
||||
|
||||
The recommended posture is:
|
||||
|
||||
1. Keep the service behind Cloudflare Tunnel and Caddy.
|
||||
2. Do not expose the container port directly to the public internet.
|
||||
3. Treat everything exposed through MCP as publishable public documentation.
|
||||
4. Add stronger authentication only if the MCP surface later includes sensitive content or tools with meaningful side effects.
|
||||
|
||||
This keeps the deployment simple while preserving a clear upgrade path.
|
||||
|
||||
## Tradeoffs
|
||||
|
||||
### Leaving `/mcp` Public
|
||||
|
||||
This is acceptable if `/mcp` exposes only the same public Markdown already available through `/docs`.
|
||||
|
||||
Benefits:
|
||||
|
||||
1. lowest operational friction
|
||||
2. fewer compatibility issues with MCP clients
|
||||
3. no need to implement OAuth, mTLS, JWT validation, or custom auth middleware
|
||||
4. consistent with the project assumption that documentation content is public
|
||||
|
||||
Risks:
|
||||
|
||||
1. random scraping, probing, or fuzzing of a machine endpoint
|
||||
2. possible bandwidth or CPU nuisance traffic
|
||||
3. accidental future exposure if new tools or private resources are added
|
||||
4. less control over who can use the MCP endpoint
|
||||
|
||||
### Protecting `/mcp` With Cloudflare Access
|
||||
|
||||
Cloudflare Access can add a lightweight gate using GitHub, Google, one-time PIN, or service tokens.
|
||||
|
||||
Benefits:
|
||||
|
||||
1. reduces random internet traffic
|
||||
2. requires little app code
|
||||
3. works well for a small trusted team
|
||||
4. provides logs and centralized access control
|
||||
|
||||
Costs:
|
||||
|
||||
1. browser-based login may not work with all MCP clients
|
||||
2. non-browser MCP clients may need Cloudflare Access service tokens
|
||||
3. adds operational configuration for a low-sensitivity endpoint
|
||||
|
||||
### Using mTLS
|
||||
|
||||
mTLS is useful when both client and server environments are tightly controlled.
|
||||
|
||||
Benefits:
|
||||
|
||||
1. strong client identity
|
||||
2. good fit for service-to-service or private infrastructure
|
||||
3. can be used between Cloudflare, Caddy, and the backend if desired
|
||||
|
||||
Costs:
|
||||
|
||||
1. harder certificate provisioning and rotation
|
||||
2. weaker compatibility with normal MCP clients
|
||||
3. unnecessary for public documentation-only content
|
||||
|
||||
For this project, mTLS is not the primary recommendation.
|
||||
|
||||
## Practical Recommendation
|
||||
|
||||
Use a simple public-docs posture unless the endpoint changes.
|
||||
|
||||
Recommended current setup:
|
||||
|
||||
```text
|
||||
/docs public
|
||||
/mcp public or lightly protected
|
||||
```
|
||||
|
||||
If `/mcp` remains public, add only basic operational safeguards:
|
||||
|
||||
1. keep Cloudflare Tunnel and Caddy in front
|
||||
2. avoid publishing `8765` directly
|
||||
3. enable Cloudflare or Caddy rate limiting if traffic becomes noisy
|
||||
4. monitor logs for unusual request volume
|
||||
5. document that MCP resources must remain safe to publish
|
||||
|
||||
A slightly stricter setup is also reasonable:
|
||||
|
||||
```text
|
||||
/docs public
|
||||
/mcp Cloudflare Access or service token
|
||||
```
|
||||
|
||||
This is the best option if the team wants to reduce drive-by MCP traffic without adding auth code to the application.
|
||||
|
||||
## Upgrade Trigger
|
||||
|
||||
Add real authentication before introducing any MCP capability that can:
|
||||
|
||||
1. read non-public files
|
||||
2. access private notes or credentials
|
||||
3. call upstream APIs
|
||||
4. mutate data
|
||||
5. run commands
|
||||
6. expose environment details
|
||||
7. perform expensive computation
|
||||
|
||||
At that point, prefer edge-level authentication first, such as Cloudflare Access, and consider proper OAuth 2.1 resource-server behavior only if broad public MCP client interoperability becomes a goal.
|
||||
|
||||
## Security Invariant
|
||||
|
||||
Everything exposed by the MCP server must be safe to publish publicly.
|
||||
|
||||
If that invariant stops being true, `/mcp` should be protected before the new capability is deployed.
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
name: copilot-customization
|
||||
description: 'Plan, create, review, and debug GitHub Copilot and VS Code agent customizations, including instructions, prompt files, skills, custom agents, hooks, MCP servers, and repo-specific personal-mcp skill integration.'
|
||||
argument-hint: 'What Copilot behavior are you customizing, and should it be workspace-scoped, personal, or exposed as an MCP skill resource?'
|
||||
x-personal-mcp:
|
||||
id: copilot-customization
|
||||
version: 1.0.0
|
||||
tags:
|
||||
- copilot
|
||||
- vscode
|
||||
- customization
|
||||
- instructions
|
||||
- prompts
|
||||
- agent-skills
|
||||
- custom-agents
|
||||
- hooks
|
||||
- mcp
|
||||
- personal-mcp
|
||||
- skills
|
||||
capabilities:
|
||||
- resource://skills/copilot-customization/document
|
||||
depends_on:
|
||||
- new-skill
|
||||
- zensical-docs
|
||||
---
|
||||
|
||||
# Copilot Customization
|
||||
|
||||
Use this skill when a task is about changing how GitHub Copilot or VS Code agents behave through customization files or MCP-backed skill resources.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Creating or updating `.github/copilot-instructions.md`, `AGENTS.md`, `CLAUDE.md`, or `*.instructions.md` files.
|
||||
- Creating prompt files, custom agents, hooks, or Agent Skills.
|
||||
- Deciding whether behavior belongs in instructions, prompts, skills, agents, hooks, MCP servers, or agent plugins.
|
||||
- Debugging why a customization is not discovered, loaded, or invoked.
|
||||
- Adding a new documentation-backed skill to this `personal-mcp` repository.
|
||||
|
||||
## Start With The Decision
|
||||
|
||||
Choose the smallest customization that matches the desired behavior:
|
||||
|
||||
1. Use always-on instructions for project-wide coding standards, architecture decisions, security rules, and documentation standards that should apply to most requests.
|
||||
2. Use file-based instructions for conventions that only apply to matching files, folders, languages, frameworks, or documentation types.
|
||||
3. Use prompt files for reusable slash commands that package a single recurring prompt.
|
||||
4. Use Agent Skills for portable, task-specific workflows that may include references, scripts, examples, or templates.
|
||||
5. Use custom agents for specialized personas, tool restrictions, model choices, or role-specific workflows.
|
||||
6. Use hooks when a deterministic lifecycle action must enforce a policy, run a command, or block unsafe behavior.
|
||||
7. Use MCP servers when the agent needs live external tools, structured resources, or discoverable data beyond static instruction files.
|
||||
8. Use agent plugins when several related customizations should ship together as an installable package.
|
||||
|
||||
If the request is ambiguous, ask only for the missing axis that changes the file type: scope, trigger, expected output, required tools, or whether it must be portable beyond VS Code.
|
||||
|
||||
## Research Map
|
||||
|
||||
Use [VS Code customization references](./references/vscode-customization.md) for official-source details about locations, frontmatter, discovery behavior, priority, and troubleshooting.
|
||||
|
||||
## Workspace Customization Workflow
|
||||
|
||||
1. Identify the customization primitive and scope.
|
||||
2. Check existing files before creating a new one.
|
||||
3. Keep the description or frontmatter trigger specific and keyword-rich.
|
||||
4. Keep instructions concise, focused, and self-contained.
|
||||
5. Add examples only when they clarify a non-obvious convention.
|
||||
6. For `*.instructions.md`, set `applyTo` only when automatic file matching is intended.
|
||||
7. For skills, make the folder name match the `name` field exactly and reference any extra files from `SKILL.md` with relative links.
|
||||
8. Validate placement, YAML frontmatter, discovery settings, and whether the customization should be workspace or user scoped.
|
||||
|
||||
## Repo Integration Workflow
|
||||
|
||||
When adding a new skill to this `personal-mcp` repo, follow the resource-first pattern:
|
||||
|
||||
1. Search the catalog for `new skill` and load `resource://skills/new-skill/document`.
|
||||
2. Create authored docs under `docs/skills/<skill-id>/SKILL.md`, with optional nested `references/` markdown files.
|
||||
3. Keep `skill-id` stable and consistent across directory name, `name`, and `x-personal-mcp.id`.
|
||||
4. Put discovery metadata in `SKILL.md` frontmatter under `x-personal-mcp`.
|
||||
5. Declare `resource://skills/<skill-id>/document` in `x-personal-mcp.capabilities`.
|
||||
6. Declare references in `x-personal-mcp.references` as `ref-id -> references/<file>.md` mappings.
|
||||
7. Validate with the registry loader and `uv run zensical build`.
|
||||
|
||||
Keep runtime implementation registry-driven in `src/personal_mcp/mcp.py`; do not add per-skill Python server modules.
|
||||
|
||||
## Quality Checks
|
||||
|
||||
Before finishing:
|
||||
|
||||
1. Confirm the customization file is in a supported location for its intended scope.
|
||||
2. Confirm required frontmatter fields are present and valid.
|
||||
3. Confirm names match directory names where VS Code requires it.
|
||||
4. Confirm descriptions include the phrases users are likely to ask for.
|
||||
5. Confirm extra skill resources are linked from `SKILL.md`.
|
||||
6. Confirm repo skill metadata exposes the correct `resource://skills/<skill-id>/document` capability.
|
||||
7. State any remaining ambiguity or user choice, such as personal vs workspace scope.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Return the concrete customization created or changed, where it lives, how to invoke or trigger it, and any validation performed.
|
||||
@@ -0,0 +1,83 @@
|
||||
# VS Code Copilot Customization References
|
||||
|
||||
Use these notes as a source map before creating or debugging Copilot customizations.
|
||||
|
||||
## Official Sources
|
||||
|
||||
!!! info "Official sources"
|
||||
- [Customization overview](https://code.visualstudio.com/docs/copilot/customization/overview)
|
||||
- [Custom instructions](https://code.visualstudio.com/docs/copilot/customization/custom-instructions)
|
||||
- [Agent skills](https://code.visualstudio.com/docs/copilot/customization/agent-skills)
|
||||
- [Prompt files](https://code.visualstudio.com/docs/copilot/customization/prompt-files)
|
||||
- [Custom agents](https://code.visualstudio.com/docs/copilot/customization/custom-agents)
|
||||
- [MCP servers](https://code.visualstudio.com/docs/copilot/customization/mcp-servers)
|
||||
- [Hooks](https://code.visualstudio.com/docs/copilot/customization/hooks)
|
||||
|
||||
## Customization Types
|
||||
|
||||
- Instructions describe standards and conventions that apply to every request or to matching files.
|
||||
- Prompt files save reusable slash-command prompts for recurring tasks.
|
||||
- Agent Skills package reusable workflows, scripts, examples, and resources that load on demand.
|
||||
- Custom agents define specialized personas, tool access, model choices, and role-specific workflows.
|
||||
- MCP servers connect the agent to external tools, resources, and data.
|
||||
- Hooks run deterministic actions at defined lifecycle points.
|
||||
- Agent plugins bundle related customization types into an installable package.
|
||||
|
||||
## Instructions
|
||||
|
||||
Use `.github/copilot-instructions.md` for workspace-wide rules that should be included in every chat request. Use `AGENTS.md` when multiple agents should share the same repository guidance, or when nested agent guidance is useful. Use `CLAUDE.md` for Claude-compatible instruction sharing.
|
||||
|
||||
Use `.github/instructions/**/*.instructions.md` for file-based or task-specific rules. Supported frontmatter fields include:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: Documentation Standards
|
||||
description: Rules for documentation writing tasks
|
||||
applyTo: '**/*.md'
|
||||
---
|
||||
```
|
||||
|
||||
`applyTo` is a workspace-relative glob. If it is omitted, the instruction file can still be manually attached but does not automatically apply by file match.
|
||||
|
||||
## Agent Skills
|
||||
|
||||
Skills live in a directory whose name must match the `name` field in `SKILL.md`. VS Code supports project skills in `.github/skills/`, `.claude/skills/`, and `.agents/skills/`, and personal skills under user-level skill folders.
|
||||
|
||||
Required skill frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: skill-name
|
||||
description: Description of what the skill does and when to use it.
|
||||
---
|
||||
```
|
||||
|
||||
Useful optional fields:
|
||||
|
||||
- `argument-hint`: shown when invoking the skill as a slash command.
|
||||
- `user-invocable`: controls whether it appears in the slash menu.
|
||||
- `disable-model-invocation`: controls whether the model can auto-load it.
|
||||
- `context`: can use `fork` for a separate subagent context when supported.
|
||||
|
||||
Skills load progressively: discovery reads frontmatter, instruction loading reads `SKILL.md`, and extra resources load only when linked from the skill document.
|
||||
|
||||
## Priority And Discovery
|
||||
|
||||
When multiple instruction sources apply, personal instructions have higher priority than repository instructions, and repository instructions have higher priority than organization instructions. If multiple instruction files exist, VS Code combines them; do not rely on ordering between instruction files.
|
||||
|
||||
For monorepos, `chat.useCustomizationsInParentRepositories` can enable discovery from a trusted parent repository root. Skill locations can also be configured with `chat.agentSkillsLocations`, and instruction locations with `chat.instructionsFilesLocations`.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If a customization is not applied:
|
||||
|
||||
1. Confirm the file is in a supported location.
|
||||
2. Confirm frontmatter is valid YAML.
|
||||
3. Confirm skill `name` matches the parent directory.
|
||||
4. Confirm `applyTo` matches the file path when using `*.instructions.md`.
|
||||
5. Confirm relevant settings are enabled, such as instruction inclusion, referenced instruction inclusion, or AGENTS/CLAUDE support.
|
||||
6. Use the Chat customization diagnostics view or Agent Debug Logs to inspect what VS Code loaded.
|
||||
|
||||
## Writing Effective Instructions
|
||||
|
||||
Keep instructions short, self-contained, and focused on non-obvious rules. Include the reason for a rule when it helps with edge cases. Prefer concrete examples over abstract preferences. Split unrelated rules into separate targeted files when they have different triggers.
|
||||
@@ -0,0 +1,252 @@
|
||||
---
|
||||
name: fastapi-async-sqlalchemy-modernization
|
||||
description: 'Create a step-by-step modernization plan for an existing FastAPI app using SQLAlchemy async patterns, context managers, and AsyncExitStack. Use when: planning migration from legacy DB setup, standardizing async engine/session lifecycles, defining transaction boundaries, and aligning with SQLAlchemy 2.x best practices.'
|
||||
argument-hint: 'What is your current FastAPI + SQLAlchemy setup (sync/async driver, session pattern, lifespan usage, and deployment model)?'
|
||||
x-personal-mcp:
|
||||
id: fastapi-async-sqlalchemy-modernization
|
||||
version: 1.0.0
|
||||
tags:
|
||||
- fastapi
|
||||
- sqlalchemy
|
||||
- async
|
||||
- modernization
|
||||
capabilities:
|
||||
- resource://skills/fastapi-async-sqlalchemy-modernization/document
|
||||
depends_on: []
|
||||
---
|
||||
|
||||
# FastAPI Async SQLAlchemy Modernization Plan
|
||||
|
||||
Create an implementation-ready plan that brings an existing FastAPI application in line with modern async SQLAlchemy practices, with explicit resource lifecycles and deterministic cleanup using async context managers and AsyncExitStack.
|
||||
|
||||
Primary targets: PostgreSQL with asyncpg and SQLite with aiosqlite.
|
||||
|
||||
## When to Use
|
||||
|
||||
- Existing FastAPI app has ad hoc database setup or mixed sync/async access.
|
||||
- Session management is inconsistent across routes/services.
|
||||
- Lifespan startup and shutdown work is spread across globals and side effects.
|
||||
- Team needs a migration plan first, not immediate large-scale rewrites.
|
||||
|
||||
## Outcome
|
||||
|
||||
Produce a practical modernization plan with:
|
||||
|
||||
- Current-state gap assessment.
|
||||
- Target architecture for engine/session/transaction lifecycle.
|
||||
- Branch-based migration path (low-risk staged rollout).
|
||||
- Quality gates and completion checks.
|
||||
- Risks, rollback strategy, and test plan.
|
||||
|
||||
## Top-Level Concepts
|
||||
|
||||
Use these concepts as the planning backbone:
|
||||
|
||||
1. Engine lifecycle and ownership:
|
||||
One AsyncEngine per process for each DB URL, created once and disposed explicitly when the app lifecycle ends.
|
||||
See the [engine lifecycle reference](references/engine.md).
|
||||
2. Session factory and scope:
|
||||
Use async_sessionmaker for configuration; create one AsyncSession per request or unit-of-work, never shared across concurrent tasks.
|
||||
See the [session management reference](references/session.md).
|
||||
3. Transaction boundaries:
|
||||
Prefer context-managed begin blocks for write units and explicit read-only sessions for queries.
|
||||
See the [transaction boundaries reference](references/transactions.md).
|
||||
4. Lifespan composition:
|
||||
Compose startup/shutdown resources with AsyncExitStack so cleanup is deterministic and ordered.
|
||||
See the [engine lifecycle reference](references/engine.md).
|
||||
5. Dependency injection:
|
||||
Provide sessions via FastAPI dependencies with async generators/context managers, not globals.
|
||||
See the [session management reference](references/session.md).
|
||||
6. Implicit I/O control in ORM:
|
||||
Avoid accidental lazy loads; use explicit eager-loading/refresh strategies for asyncio safety.
|
||||
See the [implicit I/O reference](references/implicit_io.md).
|
||||
7. Observability and resilience:
|
||||
Add pool/connection settings, logging, timeout, and health checks as first-class plan items.
|
||||
See the [observability reference](references/observability.md).
|
||||
|
||||
### Concept Reference Map
|
||||
|
||||
| Concept | Reference |
|
||||
|---|---|
|
||||
| Engine lifecycle and ownership | [Engine lifecycle reference](references/engine.md) |
|
||||
| Session factory and scope | [Session management reference](references/session.md) |
|
||||
| Transaction boundaries | [Transaction boundaries reference](references/transactions.md) |
|
||||
| Lifespan composition | [Engine lifecycle reference](references/engine.md) |
|
||||
| Dependency injection | [Session management reference](references/session.md) |
|
||||
| Implicit I/O control in ORM | [Implicit I/O reference](references/implicit_io.md) |
|
||||
| Observability and resilience | [Observability reference](references/observability.md) |
|
||||
|
||||
## Decision Points
|
||||
|
||||
Use these branching decisions before proposing migration steps.
|
||||
|
||||
| Decision | Branch A | Branch B |
|
||||
|---|---|---|
|
||||
| DB driver | Already async driver (e.g. asyncpg, aiosqlite): modernize in place | Sync driver: plan driver migration first |
|
||||
| ORM usage | Already ORM 2.x style (`select`, `session.execute`) | Legacy Query API: add compatibility stage and refactor incrementally |
|
||||
| Session scope | Request-scoped already | Global/shared sessions found: prioritize session-scope fix first |
|
||||
| Lifespan | Existing FastAPI lifespan hook | No lifespan hook: introduce lifespan before broader DB changes |
|
||||
| Concurrency | Background jobs/tasks use DB | No background DB use |
|
||||
| Transaction style | Explicit context-managed transactions | Implicit/autobegin side effects |
|
||||
|
||||
## Procedure
|
||||
|
||||
### Step 0: Audit Current State
|
||||
|
||||
Inventory the app and write a concise gap list.
|
||||
|
||||
- Engine creation location(s) and count.
|
||||
- Driver URL(s) and async compatibility.
|
||||
- Session creation patterns in routes/services/background tasks.
|
||||
- Transaction handling style (explicit begin/commit/rollback vs implicit).
|
||||
- Lifespan startup/shutdown and cleanup behavior.
|
||||
- ORM loading patterns that may trigger implicit I/O.
|
||||
|
||||
Completion check: every DB touchpoint is mapped to its engine, session, and transaction source.
|
||||
|
||||
### Step 1: Define the Target Runtime Model
|
||||
|
||||
Define one canonical model to migrate toward.
|
||||
|
||||
- Create AsyncEngine once per process.
|
||||
- Configure async_sessionmaker once.
|
||||
- Use per-request AsyncSession dependency.
|
||||
- Keep one AsyncSession per concurrent task.
|
||||
- Use context-managed transactions for writes.
|
||||
|
||||
Completion check: architecture diagram can explain where engine/session are created, used, and closed.
|
||||
|
||||
### Step 2: Plan Engine Modernization
|
||||
|
||||
Plan engine creation and pool behavior.
|
||||
|
||||
- Use `create_async_engine()` with async dialect URL.
|
||||
- Standardize pool settings and pre-ping strategy where relevant.
|
||||
- Decide isolation level strategy at engine level (avoid ad hoc per-operation switching unless justified).
|
||||
- Define explicit disposal policy for short-lived scopes and tests.
|
||||
|
||||
Completion check: engine configuration is centralized and no per-request engine creation remains.
|
||||
|
||||
### Step 3: Plan Session Lifecycle Modernization
|
||||
|
||||
Define session factory and request dependency pattern.
|
||||
|
||||
- Build `async_sessionmaker(engine, expire_on_commit=False)` unless a strict reason says otherwise.
|
||||
- Provide session via dependency that yields exactly one AsyncSession.
|
||||
- Explicitly prohibit sharing a single AsyncSession across concurrent tasks.
|
||||
- Prefer direct dependency passing over async_scoped_session for new designs.
|
||||
|
||||
Completion check: all route/service entry points receive a session from one canonical dependency.
|
||||
|
||||
### Step 4: Plan Transaction Demarcation
|
||||
|
||||
Establish consistent write and read behavior.
|
||||
|
||||
- Writes: `async with session.begin(): ...` for atomic units.
|
||||
- Reads: execute in managed session context with explicit loader options.
|
||||
- Nested/SAVEPOINT use only where required; call out backend caveats.
|
||||
- Define rollback behavior for service-layer exceptions.
|
||||
|
||||
Completion check: every mutating use case has a declared transaction boundary.
|
||||
|
||||
### Step 5: Compose Lifespan with AsyncExitStack
|
||||
|
||||
Use async context composition as the preferred orchestration pattern.
|
||||
|
||||
```python
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
async with AsyncExitStack() as stack:
|
||||
# Compose resources in acquisition order; cleanup is automatic in reverse order.
|
||||
engine = create_async_engine(settings.database_url)
|
||||
stack.push_async_callback(engine.dispose)
|
||||
|
||||
session_factory = async_sessionmaker(engine, expire_on_commit=False)
|
||||
app.state.session_factory = session_factory
|
||||
|
||||
# Add other async resources with stack.enter_async_context(...) as needed.
|
||||
yield
|
||||
```
|
||||
|
||||
Planning rules:
|
||||
|
||||
- Register every acquired resource with AsyncExitStack at acquisition time.
|
||||
- Prefer `enter_async_context()` for resources that already expose async context managers.
|
||||
- Prefer `push_async_callback()` for async cleanup callables.
|
||||
- Keep resource ownership in lifespan, not in route handlers.
|
||||
|
||||
Completion check: startup/shutdown ordering is explicit and deterministic.
|
||||
|
||||
### Step 6: Prevent Implicit ORM I/O Under Asyncio (Advisory Mode)
|
||||
|
||||
Plan for explicit loading behavior, but treat this as progressive guidance rather than a hard gate.
|
||||
|
||||
- Recommend eager-loading strategies (for example selectin-style loading) where relationship access is required.
|
||||
- For lazy/deferred attributes, define explicit awaitable or refresh paths on high-risk and high-traffic paths first.
|
||||
- Document model-level defaults and known exceptions so teams can migrate incrementally.
|
||||
|
||||
Completion check: critical request paths have explicit loading plans; non-critical paths have tracked follow-up items.
|
||||
|
||||
### Step 7: Testing and Verification Plan
|
||||
|
||||
Create modernization quality gates.
|
||||
|
||||
- Unit tests for session dependency and transaction behavior.
|
||||
- Integration tests for commit/rollback semantics.
|
||||
- Concurrency tests confirming one-session-per-task behavior.
|
||||
- Lifespan tests verifying cleanup calls and ordering.
|
||||
- Health/readiness tests including DB connectivity checks.
|
||||
|
||||
Completion check: all quality gates pass under the target async configuration.
|
||||
|
||||
### Step 8: Rollout Strategy
|
||||
|
||||
Plan low-risk migration phases.
|
||||
|
||||
1. Introduce centralized engine/session factory and lifespan orchestration.
|
||||
2. Migrate read paths to new session dependency.
|
||||
3. Migrate write paths to explicit transaction blocks.
|
||||
4. Remove legacy globals/helpers and dead code.
|
||||
5. Enable stricter linting/review checks for forbidden patterns.
|
||||
|
||||
Completion check: no legacy session/engine creation path remains in production code.
|
||||
|
||||
## Quality Criteria
|
||||
|
||||
A plan is complete only when it includes:
|
||||
|
||||
- Clear current vs target architecture.
|
||||
- Branch decisions with rationale.
|
||||
- Explicit context-manager patterns for resource ownership.
|
||||
- AsyncExitStack composition strategy.
|
||||
- Transaction policy and exception behavior.
|
||||
- Concrete tests and rollout checkpoints.
|
||||
- A documented advisory backlog for non-critical implicit I/O improvements.
|
||||
|
||||
## Anti-Patterns to Flag
|
||||
|
||||
- Creating engines inside request handlers.
|
||||
- Sharing one AsyncSession across concurrent tasks.
|
||||
- Implicit commit/rollback behavior with unclear ownership.
|
||||
- Global mutable session state.
|
||||
- Lifespan cleanup that depends on implicit garbage collection.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Return the plan as:
|
||||
|
||||
1. Current-state gap summary.
|
||||
2. Target architecture summary.
|
||||
3. Phased migration checklist with branch notes.
|
||||
4. Risk register and rollback approach.
|
||||
5. Verification matrix (tests + operational checks).
|
||||
|
||||
## References
|
||||
|
||||
!!! info "Primary sources"
|
||||
- [SQLAlchemy engine and connections](https://docs.sqlalchemy.org/en/21/core/connections.html)
|
||||
- [SQLAlchemy asyncio extension](https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html)
|
||||
- [Python async context managers and AsyncExitStack](https://docs.python.org/3/library/contextlib.html)
|
||||
@@ -0,0 +1,133 @@
|
||||
# Async SQLAlchemy Engine
|
||||
|
||||
!!! info "Primary sources"
|
||||
- [SQLAlchemy connections](https://docs.sqlalchemy.org/en/21/core/connections.html)
|
||||
- [SQLAlchemy asyncio extension](https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html)
|
||||
- [SQLAlchemy pooling and multiprocessing](https://docs.sqlalchemy.org/en/21/core/pooling.html#pooling-multiprocessing)
|
||||
- [FastAPI lifespan events](https://fastapi.tiangolo.com/advanced/events/)
|
||||
|
||||
---
|
||||
|
||||
## Engine Ownership Model
|
||||
|
||||
Create one async engine per process per database URL and keep it for the app lifetime.
|
||||
|
||||
- SQLAlchemy guidance: the engine is intended as a long-lived, concurrent registry over pooled DB connections, not a per-request object.
|
||||
- In FastAPI, app startup and shutdown ownership belongs in lifespan.
|
||||
- Use `FastAPI(lifespan=...)` (not startup/shutdown events) for modern lifecycle wiring.
|
||||
|
||||
!!! tip "Practical rule"
|
||||
- Exactly one `create_async_engine(...)` call in app bootstrap code.
|
||||
- Zero `create_async_engine(...)` calls in request handlers.
|
||||
|
||||
---
|
||||
|
||||
## Canonical Lifespan Pattern (AsyncExitStack)
|
||||
|
||||
Use `@asynccontextmanager` + `AsyncExitStack` to make teardown deterministic and composable.
|
||||
|
||||
```python
|
||||
from contextlib import AsyncExitStack, asynccontextmanager
|
||||
|
||||
from fastapi import FastAPI
|
||||
from sqlalchemy.ext.asyncio import AsyncEngine, create_async_engine
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
async with AsyncExitStack() as stack:
|
||||
engine: AsyncEngine = create_async_engine(
|
||||
app.state.settings.database_url,
|
||||
pool_pre_ping=True,
|
||||
# Optional examples:
|
||||
# echo=app.state.settings.sql_echo,
|
||||
# pool_size=10,
|
||||
# max_overflow=20,
|
||||
)
|
||||
app.state.engine = engine
|
||||
|
||||
# Ensure engine disposal always runs at shutdown.
|
||||
stack.push_async_callback(engine.dispose)
|
||||
|
||||
yield
|
||||
|
||||
|
||||
app = FastAPI(lifespan=lifespan)
|
||||
```
|
||||
|
||||
Why this pattern:
|
||||
- FastAPI executes code before `yield` at startup and after `yield` at shutdown.
|
||||
- `AsyncExitStack` lets you register multiple async cleanups in one place while preserving order.
|
||||
- Explicit disposal (directly awaited or via `AsyncExitStack` callback) avoids event-loop-closed warnings when objects fall out of scope.
|
||||
|
||||
---
|
||||
|
||||
## Driver URLs (Project Requirement: asyncpg + aiosqlite)
|
||||
|
||||
Use SQLAlchemy async driver URLs:
|
||||
|
||||
- PostgreSQL: `postgresql+asyncpg://user:pass@host:5432/dbname`
|
||||
- SQLite: `sqlite+aiosqlite:///./app.db`
|
||||
|
||||
!!! warning "Driver compatibility"
|
||||
- Do not mix sync drivers, for example `psycopg2`, with `create_async_engine()`.
|
||||
- Keep URL construction centralized in settings/config, not in feature modules.
|
||||
|
||||
---
|
||||
|
||||
## Pooling Defaults and Tuning
|
||||
|
||||
Default behavior is usually correct first:
|
||||
|
||||
- Async engines use async-compatible pooling (`AsyncAdaptedQueuePool`) by default.
|
||||
- Start with defaults, then tune from observed load (`pool_size`, `max_overflow`, `pool_timeout`, `pool_recycle`).
|
||||
- Enable `pool_pre_ping=True` for safer stale-connection handling in long-running services.
|
||||
|
||||
When to switch pool strategy:
|
||||
|
||||
- `NullPool` if you explicitly need no pooling (special environments, some tests, or strict cross-loop constraints).
|
||||
- Keep in mind this increases connect/disconnect churn.
|
||||
|
||||
---
|
||||
|
||||
## Disposal Semantics
|
||||
|
||||
`engine.dispose()` replaces/disposes the pool, but only checked-in connections are immediately closed.
|
||||
|
||||
Rules:
|
||||
- Dispose when the app is shutting down.
|
||||
- Dispose before reusing an engine across event loops.
|
||||
- In forked child-process initialization, use `engine.dispose(close=False)` (sync API guidance) so child processes do not touch parent-held connections.
|
||||
|
||||
Avoid relying on garbage collection for engine cleanup in async code.
|
||||
|
||||
---
|
||||
|
||||
## Event Loop and Process Boundaries
|
||||
|
||||
Do not share pooled connections across boundaries:
|
||||
|
||||
- Multiple event loops: do not reuse the same pooled async engine across loops unless you intentionally disable pooling (`NullPool`) or dispose before handoff.
|
||||
- Multiprocessing/fork: pooled connections must not be inherited for active use across process boundaries.
|
||||
|
||||
This prevents broken socket state and cross-process connection corruption.
|
||||
|
||||
---
|
||||
|
||||
## What Not to Do
|
||||
|
||||
- Create an engine inside every request dependency.
|
||||
- Create/dispose engines inside repository methods.
|
||||
- Keep engine creation as a hidden side effect of import-time module globals.
|
||||
- Use deprecated FastAPI startup/shutdown events together with lifespan.
|
||||
|
||||
---
|
||||
|
||||
## Engine Design Checklist
|
||||
|
||||
- One engine per process per DB URL.
|
||||
- Engine created in lifespan startup.
|
||||
- Engine disposed in lifespan shutdown.
|
||||
- Async driver URL matches backend (`asyncpg` or `aiosqlite`).
|
||||
- Pooling strategy is explicit for non-default needs.
|
||||
- No request-path engine creation.
|
||||
@@ -0,0 +1,107 @@
|
||||
# Preventing Implicit ORM I/O (Asyncio)
|
||||
|
||||
!!! info "Primary sources"
|
||||
- [Preventing implicit I/O with AsyncSession](https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html#preventing-implicit-io-when-using-asyncsession)
|
||||
- [SQLAlchemy relationship loading](https://docs.sqlalchemy.org/en/21/orm/queryguide/relationships.html)
|
||||
|
||||
??? abstract "Decision metadata"
|
||||
- Status: adopted
|
||||
- Decision level: advisory
|
||||
- Applies to: api-runtime, workers, tests
|
||||
- Last reviewed: 2026-06-17
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
Minimize unexpected database round-trips caused by attribute access in async ORM code.
|
||||
|
||||
In asyncio applications, hidden lazy loads are easy to miss and can produce runtime surprises. This guide defines explicit-loading defaults and progressive enforcement practices.
|
||||
|
||||
---
|
||||
|
||||
## Scope and Non-Goals
|
||||
|
||||
- In scope: relationship loading strategy, post-commit attribute access, explicit refresh/awaitable access patterns.
|
||||
- Out of scope: full ORM performance tuning and domain-specific query architecture.
|
||||
|
||||
---
|
||||
|
||||
## Rules
|
||||
|
||||
- Prefer explicit eager loading for data required by endpoint/service outputs.
|
||||
- Avoid relying on implicit lazy-load behavior in request critical paths.
|
||||
- Keep `expire_on_commit=False` unless strict expiration behavior is intentionally required.
|
||||
- Use explicit refresh or awaitable-attribute access when loading deferred state is necessary.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Patterns
|
||||
|
||||
### Pattern A: Eager-load what you need
|
||||
|
||||
```python
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
stmt = select(User).options(selectinload(User.roles))
|
||||
users = (await session.scalars(stmt)).all()
|
||||
```
|
||||
|
||||
### Pattern B: Explicit refresh of named attributes
|
||||
|
||||
```python
|
||||
user = await session.get(User, user_id)
|
||||
await session.refresh(user, ["roles"])
|
||||
```
|
||||
|
||||
### Pattern C: Awaitable attribute access where needed
|
||||
|
||||
```python
|
||||
# Requires AsyncAttrs mixin on mapped base or class.
|
||||
roles = await user.awaitable_attrs.roles
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Practical Enforcement Model
|
||||
|
||||
Use phased enforcement:
|
||||
|
||||
1. High-traffic and latency-sensitive routes: enforce explicit eager loading.
|
||||
2. Background tasks and less critical paths: track and progressively tighten.
|
||||
3. Add review checks to prevent newly introduced implicit-load hotspots.
|
||||
|
||||
This keeps modernization pragmatic while reducing hidden I/O over time.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Returning ORM objects from handlers and triggering lazy loads during serialization.
|
||||
- Assuming post-commit attribute access will always be loaded without explicit strategy.
|
||||
- Relying on broad expiration + implicit reload behavior in async request flows.
|
||||
- Enabling relationship patterns that hide SQL behavior in critical code paths.
|
||||
|
||||
---
|
||||
|
||||
## Operational Checks
|
||||
|
||||
- Endpoint query blocks define loader options for returned related data.
|
||||
- Critical handlers do not depend on incidental lazy loads.
|
||||
- Known exceptions are documented with rationale and follow-up items.
|
||||
|
||||
---
|
||||
|
||||
## Testing Checks
|
||||
|
||||
- Integration tests cover endpoints that return related objects.
|
||||
- Tests verify expected data is present without hidden secondary query surprises.
|
||||
- Regression tests exist for routes previously affected by implicit-load failures.
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- Start advisory: target high-risk paths first.
|
||||
- As coverage improves, elevate selected rules to mandatory in code review policy.
|
||||
@@ -0,0 +1,32 @@
|
||||
# FastAPI Async SQLAlchemy References Index
|
||||
|
||||
Purpose: concept registry for modernization guidance used by this skill.
|
||||
|
||||
---
|
||||
|
||||
## Concepts
|
||||
|
||||
| Concept | File | Status | Decision Level | Owner | Last Reviewed |
|
||||
|---|---|---|---|---|---|
|
||||
| Engine lifecycle and ownership | [engine.md](engine.md) | adopted | mandatory | platform/backend | 2026-06-17 |
|
||||
| Session factory and scope | [session.md](session.md) | adopted | mandatory | platform/backend | 2026-06-17 |
|
||||
| Transaction boundaries | [transactions.md](transactions.md) | adopted | mandatory | platform/backend | 2026-06-17 |
|
||||
| Implicit ORM I/O under asyncio | [implicit_io.md](implicit_io.md) | adopted | advisory | platform/backend | 2026-06-17 |
|
||||
| Observability and resilience | [observability.md](observability.md) | adopted | mandatory | platform/backend | 2026-06-17 |
|
||||
|
||||
---
|
||||
|
||||
## How to Use This Folder
|
||||
|
||||
- `SKILL.md` defines the planning workflow and migration procedure.
|
||||
- Each concept doc defines policy-level guidance for one concern.
|
||||
- Use the template in [template.md](template.md) for new concept docs.
|
||||
- Keep references source-linked and implementation snippets minimal.
|
||||
|
||||
---
|
||||
|
||||
## Update Rules
|
||||
|
||||
- If a PR changes database lifecycle/session/ORM loading behavior, update the relevant concept file.
|
||||
- Keep `Status`, `Decision Level`, and `Last Reviewed` current.
|
||||
- Use `advisory` only when incremental rollout is intended; use `mandatory` for required runtime policy.
|
||||
@@ -0,0 +1,114 @@
|
||||
# DB Observability and Resilience
|
||||
|
||||
!!! info "Primary sources"
|
||||
- [SQLAlchemy pooling](https://docs.sqlalchemy.org/en/21/core/pooling.html)
|
||||
- [SQLAlchemy engine configuration](https://docs.sqlalchemy.org/en/21/core/engines.html)
|
||||
- [SQLAlchemy events](https://docs.sqlalchemy.org/en/21/core/events.html)
|
||||
- [FastAPI lifespan events](https://fastapi.tiangolo.com/advanced/events/)
|
||||
|
||||
??? abstract "Decision metadata"
|
||||
- Status: adopted
|
||||
- Decision level: mandatory
|
||||
- Applies to: api-runtime, workers, tests
|
||||
- Last reviewed: 2026-06-17
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
Define baseline observability and resilience practices for DB connectivity in async FastAPI + SQLAlchemy apps.
|
||||
|
||||
Goals:
|
||||
|
||||
- detect and recover from stale/disconnected connections,
|
||||
- expose useful diagnostics for pool/engine behavior,
|
||||
- make readiness/liveness signals meaningful.
|
||||
|
||||
---
|
||||
|
||||
## Scope and Non-Goals
|
||||
|
||||
- In scope: pool health, connection liveness, SQL/pool logging hygiene, readiness checks, failure handling.
|
||||
- Out of scope: full APM stack design and vendor-specific monitoring platform setup.
|
||||
|
||||
---
|
||||
|
||||
## Rules
|
||||
|
||||
- Enable connection liveness strategy (`pool_pre_ping=True`) for long-running services.
|
||||
- Keep DB health checks out of liveness; include dependency checks in readiness.
|
||||
- Centralize engine options and logging configuration.
|
||||
- Avoid noisy SQL debug logging in production defaults.
|
||||
- Treat disconnect handling as a first-class test scenario.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Baseline
|
||||
|
||||
```python
|
||||
engine = create_async_engine(
|
||||
settings.database_url,
|
||||
pool_pre_ping=True,
|
||||
# Tune only from measured behavior:
|
||||
# pool_size=10,
|
||||
# max_overflow=20,
|
||||
# pool_timeout=30,
|
||||
# pool_recycle=1800,
|
||||
)
|
||||
```
|
||||
|
||||
Operational guidance:
|
||||
|
||||
- `pool_pre_ping=True` for stale-connection resilience.
|
||||
- Introduce `pool_recycle` where backend/network idle timeout behavior warrants it.
|
||||
- Use structured app logs with request correlation and error context.
|
||||
|
||||
---
|
||||
|
||||
## Health Endpoint Policy
|
||||
|
||||
- `/healthz`: process is alive; no DB call required.
|
||||
- `/readyz`: application can currently serve traffic; include DB connectivity verification.
|
||||
|
||||
Readiness checks should be lightweight and bounded (timeouts), not heavy diagnostic queries.
|
||||
|
||||
---
|
||||
|
||||
## Failure Handling Guidance
|
||||
|
||||
- Handle transient disconnects with pool invalidation/reconnect semantics.
|
||||
- Keep one failed request from cascading into broad app instability.
|
||||
- Capture and log contextual DB errors with enough metadata for debugging.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- No readiness check for DB-dependent services.
|
||||
- Permanent debug SQL echo in production.
|
||||
- Per-handler ad hoc pool settings.
|
||||
- Assuming disconnect events are too rare to test.
|
||||
|
||||
---
|
||||
|
||||
## Operational Checks
|
||||
|
||||
- Engine creation is centralized and configured once.
|
||||
- Liveness/readiness behavior is documented and validated.
|
||||
- Pool settings are explicit, versioned, and reviewed.
|
||||
- DB-related errors produce actionable logs.
|
||||
|
||||
---
|
||||
|
||||
## Testing Checks
|
||||
|
||||
- Readiness endpoint test covers healthy and unhealthy DB states.
|
||||
- Integration test simulates disconnect/reconnect behavior.
|
||||
- Load/concurrency tests validate pool behavior under stress.
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- Start with resilient defaults (`pool_pre_ping`) and simple health policy.
|
||||
- Add deeper metrics/event hooks incrementally once baseline reliability is in place.
|
||||
@@ -0,0 +1,141 @@
|
||||
# Async SQLAlchemy Session Management
|
||||
|
||||
!!! info "Primary sources"
|
||||
- [SQLAlchemy asyncio extension](https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html)
|
||||
- [SQLAlchemy session basics](https://docs.sqlalchemy.org/en/21/orm/session_basics.html)
|
||||
- [FastAPI dependencies with yield](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/)
|
||||
|
||||
??? abstract "Decision metadata"
|
||||
- Status: adopted
|
||||
- Decision level: mandatory
|
||||
- Applies to: api-runtime, workers, tests
|
||||
- Last reviewed: 2026-06-17
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
Define one canonical session model for FastAPI + SQLAlchemy asyncio:
|
||||
|
||||
- configure one shared session factory,
|
||||
- create one AsyncSession per request or per unit-of-work,
|
||||
- never share one AsyncSession across concurrent tasks.
|
||||
|
||||
---
|
||||
|
||||
## Scope and Non-Goals
|
||||
|
||||
- In scope: session factory creation, FastAPI dependency wiring, request/task scoping, transaction demarcation.
|
||||
- Out of scope: ORM model design, query optimization strategy, schema migration tooling.
|
||||
|
||||
---
|
||||
|
||||
## Rules
|
||||
|
||||
- Create `async_sessionmaker` once from app-owned AsyncEngine.
|
||||
- Use a fresh AsyncSession for each request or explicit unit-of-work.
|
||||
- Do not share AsyncSession across `asyncio.gather()` or parallel tasks.
|
||||
- Prefer direct dependency injection over global scoped-session patterns in new code.
|
||||
- Use explicit transaction boundaries (`async with session.begin():`) for writes.
|
||||
|
||||
---
|
||||
|
||||
## Canonical FastAPI Dependency Pattern
|
||||
|
||||
```python
|
||||
from collections.abc import AsyncIterator
|
||||
|
||||
from fastapi import Depends, Request
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
|
||||
def get_session_factory(request: Request) -> async_sessionmaker[AsyncSession]:
|
||||
return request.app.state.session_factory
|
||||
|
||||
|
||||
async def get_db_session(
|
||||
session_factory: async_sessionmaker[AsyncSession] = Depends(get_session_factory),
|
||||
) -> AsyncIterator[AsyncSession]:
|
||||
async with session_factory() as session:
|
||||
yield session
|
||||
```
|
||||
|
||||
Route usage:
|
||||
|
||||
```python
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/items")
|
||||
async def create_item(session: AsyncSession = Depends(get_db_session)) -> dict:
|
||||
async with session.begin():
|
||||
# write operations here
|
||||
...
|
||||
return {"status": "ok"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Configuration Guidance
|
||||
|
||||
Typical session factory setup:
|
||||
|
||||
```python
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
|
||||
|
||||
session_factory = async_sessionmaker(
|
||||
engine,
|
||||
class_=AsyncSession,
|
||||
expire_on_commit=False,
|
||||
)
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- `expire_on_commit=False` is commonly preferred in asyncio applications to reduce accidental post-commit reload behavior.
|
||||
- `AsyncSession.refresh()` is preferred over broad expiration patterns when state refresh is needed.
|
||||
|
||||
---
|
||||
|
||||
## Concurrency Rules
|
||||
|
||||
- One session per concurrent task.
|
||||
- If work fans out into parallel tasks, each task receives its own AsyncSession.
|
||||
- Pass sessions explicitly to service functions; avoid mutable global session state.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- A singleton/global AsyncSession reused across requests.
|
||||
- Sharing one AsyncSession across parallel tasks.
|
||||
- Hidden session creation in lower repository helpers with no caller control.
|
||||
- Mixing commit/rollback ownership across layers without a declared boundary.
|
||||
|
||||
---
|
||||
|
||||
## Operational Checks
|
||||
|
||||
- Exactly one `async_sessionmaker` is registered in app lifecycle.
|
||||
- Request handlers receive sessions from one canonical dependency.
|
||||
- No code path creates AsyncSession in module import side effects.
|
||||
- Background jobs and API handlers each create task-local sessions.
|
||||
|
||||
---
|
||||
|
||||
## Testing Checks
|
||||
|
||||
- Dependency override exists for test session factory.
|
||||
- Rollback behavior is verified for failed write units.
|
||||
- Parallel-task tests verify no shared AsyncSession instances.
|
||||
- Lifespan tests confirm session factory is initialized and teardown-safe.
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- If current code uses global/shared sessions, fix scope first before refactoring query style.
|
||||
- If legacy sync patterns are present, keep session boundary rules stable while migrating incrementally.
|
||||
@@ -0,0 +1,64 @@
|
||||
# <Concept Title>
|
||||
|
||||
!!! info "Primary sources"
|
||||
- Primary source: `<primary source URL>`
|
||||
- Secondary source: `<secondary source URL>`
|
||||
|
||||
??? abstract "Decision metadata"
|
||||
- Status: draft|adopted|deprecated
|
||||
- Decision level: advisory|mandatory
|
||||
- Applies to: api-runtime|workers|tests
|
||||
- Last reviewed: YYYY-MM-DD
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
Describe what this concept governs and why it exists.
|
||||
|
||||
## Scope and Non-Goals
|
||||
|
||||
- In scope:
|
||||
- Out of scope:
|
||||
|
||||
---
|
||||
|
||||
## Rules
|
||||
|
||||
- Rule 1
|
||||
- Rule 2
|
||||
|
||||
---
|
||||
|
||||
## Recommended Pattern
|
||||
|
||||
```python
|
||||
# minimal example
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Anti-pattern 1
|
||||
- Anti-pattern 2
|
||||
|
||||
---
|
||||
|
||||
## Operational Checks
|
||||
|
||||
- Check 1
|
||||
- Check 2
|
||||
|
||||
---
|
||||
|
||||
## Testing Checks
|
||||
|
||||
- Test 1
|
||||
- Test 2
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- Staged rollout notes and compatibility caveats.
|
||||
@@ -0,0 +1,111 @@
|
||||
# Async Transaction Boundaries
|
||||
|
||||
!!! info "Primary sources"
|
||||
- [SQLAlchemy transactions](https://docs.sqlalchemy.org/en/21/orm/session_transaction.html)
|
||||
- [SQLAlchemy asyncio extension](https://docs.sqlalchemy.org/en/21/orm/extensions/asyncio.html)
|
||||
- [SQLAlchemy connections](https://docs.sqlalchemy.org/en/21/core/connections.html)
|
||||
|
||||
??? abstract "Decision metadata"
|
||||
- Status: adopted
|
||||
- Decision level: mandatory
|
||||
- Applies to: api-runtime, workers, tests
|
||||
- Last reviewed: 2026-06-17
|
||||
|
||||
---
|
||||
|
||||
## Purpose
|
||||
|
||||
Define consistent transaction demarcation for async SQLAlchemy so write behavior is predictable, rollback semantics are clear, and concurrent request flows remain safe.
|
||||
|
||||
---
|
||||
|
||||
## Scope and Non-Goals
|
||||
|
||||
- In scope: transaction ownership, write/read policy, exception and rollback behavior, nested transaction guidance.
|
||||
- Out of scope: business-domain validation rules and cross-service distributed transactions.
|
||||
|
||||
---
|
||||
|
||||
## Rules
|
||||
|
||||
- Every mutating use case must run inside an explicit transaction boundary.
|
||||
- Prefer `async with session.begin():` for write units.
|
||||
- Keep transaction ownership at service/use-case boundary, not deep in helper internals.
|
||||
- Read paths should not auto-upgrade into hidden write behavior.
|
||||
- On exception in a transaction block, rely on rollback semantics and propagate or map exceptions intentionally.
|
||||
|
||||
---
|
||||
|
||||
## Recommended Patterns
|
||||
|
||||
### Pattern A: Single write unit
|
||||
|
||||
```python
|
||||
async def create_order(session: AsyncSession, payload: OrderIn) -> Order:
|
||||
async with session.begin():
|
||||
order = Order(...)
|
||||
session.add(order)
|
||||
# additional writes...
|
||||
return order
|
||||
```
|
||||
|
||||
### Pattern B: Explicit read flow
|
||||
|
||||
```python
|
||||
async def get_order(session: AsyncSession, order_id: UUID) -> Order | None:
|
||||
stmt = select(Order).where(Order.id == order_id)
|
||||
return await session.scalar(stmt)
|
||||
```
|
||||
|
||||
### Pattern C: Nested transaction (only when required)
|
||||
|
||||
```python
|
||||
async with session.begin():
|
||||
# outer transaction
|
||||
async with session.begin_nested():
|
||||
# savepoint-scoped operation
|
||||
...
|
||||
```
|
||||
|
||||
Use nested transactions only when partial failure semantics are explicitly required.
|
||||
|
||||
---
|
||||
|
||||
## Exception and Rollback Policy
|
||||
|
||||
- Write block fails: transaction context rolls back.
|
||||
- Caller decides whether to translate exception (for example to domain/API errors).
|
||||
- Do not swallow DB exceptions silently; map or re-raise intentionally.
|
||||
|
||||
---
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Multiple commits scattered across one logical use case.
|
||||
- Helper functions that commit/rollback without caller awareness.
|
||||
- Mixing implicit and explicit transaction styles in confusing ways.
|
||||
- Using savepoints as a default pattern rather than a targeted tool.
|
||||
|
||||
---
|
||||
|
||||
## Operational Checks
|
||||
|
||||
- All mutating service functions declare one clear transaction boundary.
|
||||
- No repository/helper performs hidden commit calls.
|
||||
- Transaction style is consistent across handlers and workers.
|
||||
|
||||
---
|
||||
|
||||
## Testing Checks
|
||||
|
||||
- Success path test verifies expected durable writes.
|
||||
- Failure path test verifies rollback behavior.
|
||||
- Tests cover concurrency-sensitive write flows.
|
||||
- Savepoint usage (if present) has dedicated behavior tests.
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
- First stabilize session scope, then normalize transaction ownership.
|
||||
- Replace ad hoc commit patterns incrementally with bounded write units.
|
||||
@@ -2,6 +2,16 @@
|
||||
name: fastapi-uv-docker
|
||||
description: 'Audit and migrate an existing Python project to best practices for a cloud-native ASGI FastAPI app managed with uv and run with uvicorn in Docker. Use when: conforming a project to production standards, setting up src layout, configuring pyproject.toml, writing multi-stage Dockerfiles, wiring lifespan and settings, adding health endpoints, enforcing non-root container user, migrating from requirements.txt to uv.'
|
||||
argument-hint: 'What is the current state of the project (bare Python, requirements.txt, pip, etc.)?'
|
||||
x-personal-mcp:
|
||||
id: fastapi-uv-docker
|
||||
version: 1.0.0
|
||||
tags:
|
||||
- fastapi
|
||||
- uv
|
||||
- docker
|
||||
capabilities:
|
||||
- resource://skills/fastapi-uv-docker/document
|
||||
depends_on: []
|
||||
---
|
||||
|
||||
# FastAPI Project Best Practices
|
||||
@@ -22,10 +32,10 @@ Bring an existing Python project into full conformance with cloud-native best pr
|
||||
|
||||
Load these references only when needed:
|
||||
|
||||
- FastAPI patterns and app structure: [./references/fastapi-best-practices.md](./references/fastapi-best-practices.md)
|
||||
- uv project layout and dependency management: [./references/uv-project-layout.md](./references/uv-project-layout.md)
|
||||
- uvicorn CLI settings reference: [./references/uvicorn-settings.md](./references/uvicorn-settings.md)
|
||||
- Docker and cloud-native patterns: [./references/docker-cloud-native.md](./references/docker-cloud-native.md)
|
||||
- FastAPI patterns and app structure: [FastAPI best practices](./references/fastapi-best-practices.md)
|
||||
- uv project layout and dependency management: [uv project layout](./references/uv-project-layout.md)
|
||||
- uvicorn CLI settings reference: [uvicorn settings](./references/uvicorn-settings.md)
|
||||
- Docker and cloud-native patterns: [Docker cloud-native patterns](./references/docker-cloud-native.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -44,9 +54,9 @@ Before making changes, map the current state across six areas. Produce a short g
|
||||
| **Container** | Is there a `Dockerfile`? Multi-stage? Non-root user? `.dockerignore` present? |
|
||||
| **Cloud-native** | Is there a `/healthz` endpoint? Graceful shutdown? Structured logs? |
|
||||
|
||||
Load [./references/fastapi-best-practices.md](./references/fastapi-best-practices.md) for structure rules.
|
||||
Load [./references/uv-project-layout.md](./references/uv-project-layout.md) for uv migration rules.
|
||||
Load [./references/uvicorn-settings.md](./references/uvicorn-settings.md) for uvicorn CLI reference.
|
||||
Load the [FastAPI best practices reference](./references/fastapi-best-practices.md) for structure rules.
|
||||
Load the [uv project layout reference](./references/uv-project-layout.md) for uv migration rules.
|
||||
Load the [uvicorn settings reference](./references/uvicorn-settings.md) for uvicorn CLI reference.
|
||||
|
||||
Completion check: You can name every gap before touching any file.
|
||||
|
||||
@@ -145,7 +155,7 @@ Completion check: `uv run python -m my_app` starts the server.
|
||||
|
||||
### Step 3: Wire the FastAPI App Factory
|
||||
|
||||
Load [./references/fastapi-best-practices.md](./references/fastapi-best-practices.md) for the full patterns. Key rules:
|
||||
Load the [FastAPI best practices reference](./references/fastapi-best-practices.md) for the full patterns. Key rules:
|
||||
|
||||
**`src/my_app/main.py`:**
|
||||
|
||||
@@ -211,7 +221,7 @@ Completion check: `uv run uvicorn my_app.main:app --reload` starts with no impor
|
||||
|
||||
### Step 4: uvicorn Production Configuration
|
||||
|
||||
Load [./references/uvicorn-settings.md](./references/uvicorn-settings.md) for the full settings reference.
|
||||
Load the [uvicorn settings reference](./references/uvicorn-settings.md) for the full settings reference.
|
||||
|
||||
**Never** configure uvicorn inside application code. Pass all settings via CLI or environment variables (`UVICORN_*` prefix).
|
||||
|
||||
@@ -270,7 +280,7 @@ Completion check: `curl http://localhost:8000/healthz` returns `{"status":"ok"}`
|
||||
|
||||
### Step 5: Write the Dockerfile
|
||||
|
||||
Load [./references/docker-cloud-native.md](./references/docker-cloud-native.md) for the full template and cloud-native rules. Key requirements:
|
||||
Load the [Docker cloud-native patterns reference](./references/docker-cloud-native.md) for the full template and cloud-native rules. Key requirements:
|
||||
|
||||
- Multi-stage build: `builder` stage installs deps; `runtime` stage is slim.
|
||||
- Pin uv version (copy from official image, not `latest`).
|
||||
+5
-1
@@ -1,6 +1,10 @@
|
||||
# Docker and Cloud-Native Patterns
|
||||
|
||||
Source: https://docs.docker.com/build/building/best-practices/ | https://docs.astral.sh/uv/guides/integration/docker/ | https://fastapi.tiangolo.com/deployment/docker/ | https://uvicorn.dev/deployment/
|
||||
!!! info "Primary sources"
|
||||
- [Docker build best practices](https://docs.docker.com/build/building/best-practices/)
|
||||
- [uv Docker integration](https://docs.astral.sh/uv/guides/integration/docker/)
|
||||
- [FastAPI Docker deployment](https://fastapi.tiangolo.com/deployment/docker/)
|
||||
- [uvicorn deployment](https://uvicorn.dev/deployment/)
|
||||
|
||||
---
|
||||
|
||||
+5
-2
@@ -1,6 +1,8 @@
|
||||
# FastAPI Best Practices
|
||||
|
||||
Source: https://fastapi.tiangolo.com/deployment/ | https://fastapi.tiangolo.com/advanced/events/
|
||||
!!! info "Primary sources"
|
||||
- [FastAPI deployment](https://fastapi.tiangolo.com/deployment/)
|
||||
- [FastAPI lifespan events](https://fastapi.tiangolo.com/advanced/events/)
|
||||
|
||||
---
|
||||
|
||||
@@ -44,7 +46,8 @@ def create_app(settings: Settings | None = None) -> FastAPI:
|
||||
app = create_app()
|
||||
```
|
||||
|
||||
**Never use `@app.on_event("startup")` / `@app.on_event("shutdown")`** — these are deprecated. The `asynccontextmanager` lifespan is the canonical approach since FastAPI 0.95.
|
||||
!!! warning "Prefer lifespan handlers"
|
||||
Never use `@app.on_event("startup")` / `@app.on_event("shutdown")`. These are deprecated. The `asynccontextmanager` lifespan is the canonical approach since FastAPI 0.95.
|
||||
|
||||
---
|
||||
|
||||
+6
-2
@@ -1,6 +1,9 @@
|
||||
# uv Project Layout and Dependency Management
|
||||
|
||||
Source: https://docs.astral.sh/uv/guides/projects/ | https://docs.astral.sh/uv/concepts/projects/layout/ | https://docs.astral.sh/uv/guides/integration/docker/
|
||||
!!! info "Primary sources"
|
||||
- [uv project guide](https://docs.astral.sh/uv/guides/projects/)
|
||||
- [uv project layout](https://docs.astral.sh/uv/concepts/projects/layout/)
|
||||
- [uv Docker integration](https://docs.astral.sh/uv/guides/integration/docker/)
|
||||
|
||||
---
|
||||
|
||||
@@ -13,7 +16,8 @@ Source: https://docs.astral.sh/uv/guides/projects/ | https://docs.astral.sh/uv/c
|
||||
| `.python-version` | Default Python version for the project | Yes |
|
||||
| `.venv/` | Local virtual environment | No (`.gitignore`) |
|
||||
|
||||
**`uv.lock` must be committed.** It is the source of truth for reproducible installs in CI and Docker. Never edit it by hand.
|
||||
!!! warning "Commit the lockfile"
|
||||
`uv.lock` must be committed. It is the source of truth for reproducible installs in CI and Docker. Never edit it by hand.
|
||||
|
||||
---
|
||||
|
||||
+5
-2
@@ -1,6 +1,8 @@
|
||||
# uvicorn Settings Reference
|
||||
|
||||
Source: https://uvicorn.dev/settings/ | https://uvicorn.dev/deployment/
|
||||
!!! info "Primary sources"
|
||||
- [uvicorn settings](https://uvicorn.dev/settings/)
|
||||
- [uvicorn deployment](https://uvicorn.dev/deployment/)
|
||||
|
||||
---
|
||||
|
||||
@@ -21,7 +23,8 @@ uvicorn main:app
|
||||
uvicorn.run("main:app", host="0.0.0.0", port=8000)
|
||||
```
|
||||
|
||||
**Note:** `UVICORN_*` env vars cannot be used from within an `--env-file`. The `--env-file` flag is for the ASGI *application's* config, not uvicorn's own config.
|
||||
!!! note "Environment file scope"
|
||||
`UVICORN_*` env vars cannot be used from within an `--env-file`. The `--env-file` flag is for the ASGI *application's* config, not uvicorn's own config.
|
||||
|
||||
---
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
name: new-skill
|
||||
description: Provide a practical checklist and baseline template for creating a new docs-first MCP skill in this repository.
|
||||
argument-hint: What skill are you creating, and what problem should it solve?
|
||||
x-personal-mcp:
|
||||
id: new-skill
|
||||
version: 1.0.0
|
||||
tags:
|
||||
- fastmcp
|
||||
- bootstrap
|
||||
- scaffolding
|
||||
- skills
|
||||
- mcp
|
||||
capabilities:
|
||||
- resource://skills/new-skill/document
|
||||
depends_on: []
|
||||
references: {}
|
||||
---
|
||||
|
||||
# New Skill Bootstrap
|
||||
|
||||
Use this skill to bootstrap a new skill in the docs-first architecture.
|
||||
|
||||
## Scope
|
||||
|
||||
1. Create docs under docs/skills/<skill-id>/.
|
||||
2. Define SKILL frontmatter with Anthropic and x-personal-mcp fields.
|
||||
3. Declare references via x-personal-mcp.references when needed.
|
||||
4. Validate the docs build and MCP resource reads.
|
||||
|
||||
## Authoring Checklist
|
||||
|
||||
1. Create docs/skills/<skill-id>/SKILL.md.
|
||||
2. Add docs/skills/<skill-id>/references/ files as needed.
|
||||
3. Keep skill id and directory name aligned.
|
||||
4. Keep frontmatter name equal to x-personal-mcp.id.
|
||||
5. Include resource://skills/<skill-id>/document in capabilities.
|
||||
|
||||
## Validation
|
||||
|
||||
1. uv run zensical build
|
||||
2. uv run pytest -q
|
||||
+14
-3
@@ -2,6 +2,17 @@
|
||||
name: nicegui-ui-customization
|
||||
description: 'Design and implement production NiceGUI UIs with reusable components, Tailwind-first styling, event-driven interactions, and troubleshooting for uploads, state, and static assets. Use when building or refactoring NiceGUI pages and interaction flows.'
|
||||
argument-hint: 'What UI outcome should this workflow produce?'
|
||||
x-personal-mcp:
|
||||
id: nicegui-ui-customization
|
||||
version: 1.0.0
|
||||
tags:
|
||||
- nicegui
|
||||
- ui
|
||||
- customization
|
||||
- frontend
|
||||
capabilities:
|
||||
- resource://skills/nicegui-ui-customization/document
|
||||
depends_on: []
|
||||
---
|
||||
|
||||
# NiceGUI UI Customization Workflow
|
||||
@@ -29,9 +40,9 @@ Deliver a responsive, accessible UI flow that:
|
||||
|
||||
Load these references only when needed:
|
||||
|
||||
- Architecture and styling rules: [./references/architecture-and-styling.md](./references/architecture-and-styling.md)
|
||||
- Event and state interaction patterns: [./references/interaction-patterns.md](./references/interaction-patterns.md)
|
||||
- Troubleshooting and release gates: [./references/troubleshooting-and-quality-gates.md](./references/troubleshooting-and-quality-gates.md)
|
||||
- Architecture and styling rules: [architecture and styling](./references/architecture-and-styling.md)
|
||||
- Event and state interaction patterns: [interaction patterns](./references/interaction-patterns.md)
|
||||
- Troubleshooting and release gates: [troubleshooting and quality gates](./references/troubleshooting-and-quality-gates.md)
|
||||
|
||||
## Procedure
|
||||
|
||||
+5
-4
@@ -70,7 +70,8 @@ ui.add_css(open("src/app/static/css/base.css").read())
|
||||
|
||||
## Links
|
||||
|
||||
- NiceGUI elements: https://nicegui.io/documentation/element
|
||||
- NiceGUI binding: https://nicegui.io/documentation/section_binding_properties
|
||||
- Tailwind: https://tailwindcss.com/docs/utility-first
|
||||
- Quasar components: https://quasar.dev/vue-components
|
||||
!!! info "Primary sources"
|
||||
- [NiceGUI elements](https://nicegui.io/documentation/element)
|
||||
- [NiceGUI binding properties](https://nicegui.io/documentation/section_binding_properties)
|
||||
- [Tailwind utility-first styling](https://tailwindcss.com/docs/utility-first)
|
||||
- [Quasar components](https://quasar.dev/vue-components)
|
||||
+4
-3
@@ -104,6 +104,7 @@ ui.button("Refresh").on_click(lambda: item_list.refresh())
|
||||
|
||||
## Links
|
||||
|
||||
- NiceGUI action events: https://nicegui.io/documentation/section_action_events
|
||||
- FastAPI SSE: https://fastapi.tiangolo.com/advanced/server-sent-events/
|
||||
- FastAPI WebSockets: https://fastapi.tiangolo.com/advanced/websockets/
|
||||
!!! info "Primary sources"
|
||||
- [NiceGUI action events](https://nicegui.io/documentation/section_action_events)
|
||||
- [FastAPI server-sent events](https://fastapi.tiangolo.com/advanced/server-sent-events/)
|
||||
- [FastAPI WebSockets](https://fastapi.tiangolo.com/advanced/websockets/)
|
||||
@@ -2,6 +2,17 @@
|
||||
name: nicegui
|
||||
description: 'Design and scaffold a production-ready NiceGUI + FastAPI application architecture. Use for multi-page app planning, package boundaries, optional DB/LangGraph/docs integration, and implementation checklists.'
|
||||
argument-hint: 'What should this app include (pages, DB, AI, docs, constraints)?'
|
||||
x-personal-mcp:
|
||||
id: nicegui
|
||||
version: 1.0.0
|
||||
tags:
|
||||
- nicegui
|
||||
- fastapi
|
||||
- ui
|
||||
- architecture
|
||||
capabilities:
|
||||
- resource://skills/nicegui/document
|
||||
depends_on: []
|
||||
---
|
||||
|
||||
# NiceGUI
|
||||
@@ -41,9 +52,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 +143,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 +175,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 +196,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.
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
# Source Documentation
|
||||
|
||||
Use these links for framework-specific details.
|
||||
|
||||
## FastAPI
|
||||
|
||||
!!! info "FastAPI sources"
|
||||
- [Lifespan events](https://fastapi.tiangolo.com/advanced/events/)
|
||||
- [Settings and environment variables](https://fastapi.tiangolo.com/advanced/settings/)
|
||||
- [Dependencies with yield](https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/)
|
||||
- [SQL databases tutorial](https://fastapi.tiangolo.com/tutorial/sql-databases/)
|
||||
|
||||
## SQLAlchemy and Alembic
|
||||
|
||||
!!! info "Persistence sources"
|
||||
- [SQLAlchemy engine configuration and pooling](https://docs.sqlalchemy.org/en/20/core/engines.html)
|
||||
- [SQLAlchemy session lifecycle basics](https://docs.sqlalchemy.org/en/20/orm/session_basics.html)
|
||||
- [Alembic tutorial](https://alembic.sqlalchemy.org/en/latest/tutorial.html)
|
||||
|
||||
## Pydantic
|
||||
|
||||
!!! info "Pydantic source"
|
||||
- [Pydantic settings management](https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/)
|
||||
|
||||
## NiceGUI
|
||||
|
||||
!!! info "NiceGUI sources"
|
||||
- [Pages, routing, and FastAPI integration](https://www.nicegui.io/documentation/section_pages_routing)
|
||||
- [Security best practices](https://www.nicegui.io/documentation/section_security)
|
||||
|
||||
## LangGraph
|
||||
|
||||
!!! info "LangGraph sources"
|
||||
- [Overview](https://docs.langchain.com/oss/python/langgraph/overview)
|
||||
- [Quickstart](https://docs.langchain.com/oss/python/langgraph/quickstart)
|
||||
- [Workflows and agents](https://docs.langchain.com/oss/python/langgraph/workflows-agents)
|
||||
- [Persistence](https://docs.langchain.com/oss/python/langgraph/persistence)
|
||||
- [Memory concepts](https://docs.langchain.com/oss/python/concepts/memory)
|
||||
- [Streaming](https://docs.langchain.com/oss/python/langgraph/streaming)
|
||||
- [Interrupts and human-in-the-loop](https://docs.langchain.com/oss/python/langgraph/interrupts)
|
||||
@@ -0,0 +1,126 @@
|
||||
---
|
||||
name: pytest-scaffolding
|
||||
description: "Scaffold a maintainable, hierarchical pytest suite with fast defaults and clear escalation paths for FastAPI and SQLAlchemy tests. Use when creating or reorganizing tests, defining fixture/marker boundaries, or making test strategy progressively discoverable."
|
||||
argument-hint: "Target scope plus stack details (pure Python, FastAPI, SQLAlchemy sync, SQLAlchemy async, or mixed)"
|
||||
x-personal-mcp:
|
||||
id: pytest-scaffolding
|
||||
version: 1.0.0
|
||||
tags:
|
||||
- pytest
|
||||
- testing
|
||||
- python
|
||||
capabilities:
|
||||
- resource://skills/pytest-scaffolding/document
|
||||
depends_on: []
|
||||
---
|
||||
|
||||
# Pytest Scaffolding
|
||||
|
||||
Create test scaffolding that stays fast for daily work and scales safely as dependencies increase.
|
||||
|
||||
This skill is optimized for progressive discoverability:
|
||||
1. Start with the shortest path in this file.
|
||||
2. Load exactly one deeper reference only when a decision requires it.
|
||||
3. Continue only as far as needed for the current task.
|
||||
|
||||
Repository defaults:
|
||||
- `uv run pytest` is the canonical invocation.
|
||||
- pytest settings live in `pyproject.toml` under `[tool.pytest.ini_options]`.
|
||||
- strict marker checking is expected (`--strict-markers`).
|
||||
|
||||
## Discovery Ladder
|
||||
|
||||
### Level 0: Scope And Stack Triage (always)
|
||||
Collect:
|
||||
1. Target scope (repo, package, module).
|
||||
2. Stack shape (pure Python, FastAPI, SQLAlchemy sync, SQLAlchemy async, or mixed).
|
||||
3. Speed target (what must stay instant).
|
||||
4. CI gate policy (which marker groups block merge).
|
||||
|
||||
If any are missing, ask concise clarifying questions before scaffolding.
|
||||
|
||||
### Level 1: Core pytest scaffold (default)
|
||||
Use this for all stacks first:
|
||||
1. Mirror `src/` into `tests/` with one starter file per core module.
|
||||
2. Classify test intent by cost:
|
||||
- `unit`: no DB/network/filesystem side effects.
|
||||
- `integration`: framework, DB, or multi-layer contracts.
|
||||
- `smoke`: thin critical-path checks.
|
||||
3. Scaffold each new module with:
|
||||
- one happy-path test,
|
||||
- one failure/edge test,
|
||||
- TODO anchors for deeper assertions.
|
||||
4. Keep fixtures layered:
|
||||
- global lightweight fixtures in `tests/conftest.py`,
|
||||
- domain fixtures in subtree `conftest.py` only when needed.
|
||||
5. Register markers early: `unit`, `integration`, `smoke`, `slow`, `external`.
|
||||
6. Validate in order:
|
||||
- `uv run pytest --collect-only -q`
|
||||
- `uv run pytest -m unit -q`
|
||||
- `uv run pytest -q` when dependencies are available.
|
||||
|
||||
Load next reference only if needed:
|
||||
- Baseline details and rationale: [pytest-docs.md](./references/pytest-docs.md)
|
||||
|
||||
### Level 2: FastAPI branch (only for HTTP/dependency/lifespan concerns)
|
||||
Escalate here when testing API routes, dependency injection boundaries, or app lifespan behavior.
|
||||
|
||||
Apply these defaults:
|
||||
1. Prefer `TestClient` with sync `def` tests for route behavior.
|
||||
2. Use `AsyncClient` + `@pytest.mark.anyio` only when test logic must await other async work.
|
||||
3. Prefer `app.dependency_overrides` over patching internals.
|
||||
4. Reset dependency overrides in teardown after every test/fixture.
|
||||
5. For startup/shutdown semantics:
|
||||
- use `TestClient` as context manager, or
|
||||
- use `LifespanManager` with async client.
|
||||
|
||||
Marker intent in FastAPI-heavy suites:
|
||||
- `unit`: service logic without HTTP/DB.
|
||||
- `integration`: route + DI + DB contract checks.
|
||||
- `smoke`: one request per critical user path.
|
||||
|
||||
Reference: [fastapi-testing.md](./references/fastapi-testing.md)
|
||||
|
||||
### Level 3: SQLAlchemy branch (only for DB transaction/session design)
|
||||
Escalate here when session lifecycle, transaction isolation, or async ORM behavior matters.
|
||||
|
||||
Apply these defaults:
|
||||
1. Create engine once per test session.
|
||||
2. Open connection + outer transaction per test.
|
||||
3. Bind session with `join_transaction_mode="create_savepoint"`.
|
||||
4. Allow code under test to call `commit()` safely; rollback outer transaction at test end.
|
||||
5. Keep unit tests DB-free; DB tests belong under `integration`.
|
||||
|
||||
Async additions:
|
||||
- use async fixtures and `@pytest.mark.anyio`.
|
||||
- set `expire_on_commit=False` for `AsyncSession`.
|
||||
- avoid implicit lazy IO; use eager loading (`selectinload`) or explicit refresh.
|
||||
|
||||
SQLite in-memory with threaded test clients:
|
||||
- use `StaticPool` when required by thread/connection sharing.
|
||||
|
||||
Reference: [sqlalchemy-testing.md](./references/sqlalchemy-testing.md)
|
||||
|
||||
## Branching Logic Summary
|
||||
- If pure logic can be faked cleanly, keep in `unit`.
|
||||
- If framework/DB contract is the behavior under test, use `integration`.
|
||||
- If external service credentials/network is required, gate behind `external`.
|
||||
- If suite slows down, split by marker before broadening fixture scope.
|
||||
- If async relationship access raises `MissingGreenlet`, switch to eager loading strategy.
|
||||
|
||||
## Completion Checks
|
||||
A scaffold pass is complete when all are true:
|
||||
1. Core source areas map to clear test modules.
|
||||
2. Fast path (`-m unit`) is deterministic and quick.
|
||||
3. Integration and external paths are isolated by fixtures and markers.
|
||||
4. No unregistered-marker failures occur.
|
||||
5. Structure is understandable without extra oral context.
|
||||
6. Clear TODO extension points exist for deeper assertions.
|
||||
|
||||
## Output Contract
|
||||
When this skill is applied, return:
|
||||
1. Proposed test tree diff.
|
||||
2. Marker and fixture plan.
|
||||
3. Exact fast-path and full-path commands.
|
||||
4. Which reference level was loaded and why.
|
||||
5. Risks or open questions before expanding assertions.
|
||||
@@ -0,0 +1,236 @@
|
||||
# [FastAPI Testing](https://fastapi.tiangolo.com/tutorial/testing/)
|
||||
|
||||
Best practices for testing FastAPI applications with pytest.
|
||||
|
||||
## Agent Quick Path
|
||||
Use this sequence before reading the full reference:
|
||||
|
||||
1. If test is pure route behavior, use `TestClient` and plain `def` tests.
|
||||
2. If test must `await` other async work, use `AsyncClient` + `@pytest.mark.anyio`.
|
||||
3. Prefer `app.dependency_overrides` over `mock.patch`.
|
||||
4. Reset overrides after each test/fixture teardown.
|
||||
5. For startup/shutdown logic, use `TestClient` as context manager or `LifespanManager` with async client.
|
||||
|
||||
Decision rules:
|
||||
- Need DB contract verification: choose integration tests and override `get_db`/`get_session`.
|
||||
- Need pure business logic checks: keep tests HTTP-free (`unit`).
|
||||
- Need one critical path sanity check: one endpoint per `smoke` test.
|
||||
|
||||
---
|
||||
|
||||
## Tools
|
||||
|
||||
| Tool | Purpose |
|
||||
|------|---------|
|
||||
| `fastapi.testclient.TestClient` | Synchronous HTTP test client (wraps HTTPX, built on Starlette) |
|
||||
| `httpx.AsyncClient` + `ASGITransport` | Async client for tests that `await` other async code |
|
||||
| `app.dependency_overrides` | Replace any `Depends()` dependency for the duration of a test |
|
||||
| `anyio` / `pytest-anyio` | Run async test functions with `@pytest.mark.anyio` |
|
||||
|
||||
Install deps: `httpx`, `anyio` (or `pytest-anyio`).
|
||||
|
||||
---
|
||||
|
||||
## Synchronous Tests (Preferred Default)
|
||||
|
||||
Use `TestClient` for route tests that don't need to `await` anything else.
|
||||
Test functions are plain `def` — no `async def`, no `await`.
|
||||
|
||||
```python
|
||||
from fastapi.testclient import TestClient
|
||||
from myapp.main import app
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
def test_read_item_returns_200():
|
||||
response = client.get("/items/foo", headers={"X-Token": "secret"})
|
||||
assert response.status_code == 200
|
||||
assert response.json()["id"] == "foo"
|
||||
|
||||
def test_read_item_bad_token_returns_400():
|
||||
response = client.get("/items/foo", headers={"X-Token": "wrong"})
|
||||
assert response.status_code == 400
|
||||
```
|
||||
|
||||
Rules:
|
||||
- One `TestClient` per test module is fine (stateless between calls).
|
||||
- Pass headers, query params, JSON body, or form data the same way as HTTPX/requests.
|
||||
- Do not pass Pydantic models directly; use `.model_dump()` or `jsonable_encoder`.
|
||||
|
||||
---
|
||||
|
||||
## Async Tests
|
||||
|
||||
Use `AsyncClient` only when the test itself needs to `await` other coroutines
|
||||
(e.g. querying a real async DB after an API call to verify side effects).
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from httpx import ASGITransport, AsyncClient
|
||||
from myapp.main import app
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_root_async():
|
||||
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
||||
response = await ac.get("/")
|
||||
assert response.status_code == 200
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Mark with `@pytest.mark.anyio`; register the `anyio` marker in `pyproject.toml`.
|
||||
- `AsyncClient` does **not** trigger lifespan events by default; use `asgi-lifespan`'s
|
||||
`LifespanManager` when startup/shutdown matters.
|
||||
- Instantiate objects that require an event loop (e.g. async DB clients) inside
|
||||
async functions, not at module level.
|
||||
|
||||
---
|
||||
|
||||
## Dependency Overrides (Preferred Over Mocking)
|
||||
|
||||
`app.dependency_overrides` is the idiomatic FastAPI seam — use it instead of
|
||||
patching internals with `unittest.mock`.
|
||||
|
||||
```python
|
||||
from fastapi.testclient import TestClient
|
||||
from myapp.main import app
|
||||
from myapp.deps import get_current_user
|
||||
|
||||
client = TestClient(app)
|
||||
|
||||
def fake_user():
|
||||
return {"id": 1, "name": "Test User"}
|
||||
|
||||
def test_protected_route_with_fake_user():
|
||||
app.dependency_overrides[get_current_user] = fake_user
|
||||
response = client.get("/me")
|
||||
app.dependency_overrides = {} # always reset after the test
|
||||
assert response.status_code == 200
|
||||
assert response.json()["name"] == "Test User"
|
||||
```
|
||||
|
||||
Or reset cleanly with `autouse=False` fixture teardown:
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from myapp.main import app
|
||||
|
||||
@pytest.fixture()
|
||||
def override_user():
|
||||
app.dependency_overrides[get_current_user] = lambda: {"id": 1, "name": "Test User"}
|
||||
yield
|
||||
app.dependency_overrides = {}
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Override at the lowest-level dependency that owns the external boundary
|
||||
(e.g. `get_db`, `get_current_user`, `get_settings`).
|
||||
- Always reset `app.dependency_overrides` after each test or fixture teardown.
|
||||
- Prefer a real in-process fake (e.g. in-memory SQLite session) over a mock object.
|
||||
|
||||
---
|
||||
|
||||
## Database Testing
|
||||
|
||||
The preferred pattern is a real SQLite (or test Postgres) session injected via
|
||||
dependency override, not a mock.
|
||||
|
||||
```python
|
||||
# tests/conftest.py
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from fastapi.testclient import TestClient
|
||||
from myapp.main import app
|
||||
from myapp.db.session import get_db
|
||||
from myapp.db.models import Base
|
||||
|
||||
TEST_DB_URL = "sqlite:///./test.db"
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def engine():
|
||||
e = create_engine(TEST_DB_URL, connect_args={"check_same_thread": False})
|
||||
Base.metadata.create_all(bind=e)
|
||||
yield e
|
||||
Base.metadata.drop_all(bind=e)
|
||||
|
||||
@pytest.fixture()
|
||||
def db_session(engine):
|
||||
Session = sessionmaker(bind=engine)
|
||||
session = Session()
|
||||
yield session
|
||||
session.rollback()
|
||||
session.close()
|
||||
|
||||
@pytest.fixture()
|
||||
def client(db_session):
|
||||
app.dependency_overrides[get_db] = lambda: db_session
|
||||
yield TestClient(app)
|
||||
app.dependency_overrides = {}
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Prefer `session`-scoped engine creation; `function`-scoped session with rollback per test.
|
||||
- Keep unit tests DB-free; use this pattern only in `integration`-marked tests.
|
||||
- For async SQLAlchemy, mirror the same pattern using `AsyncEngine` / `AsyncSession`.
|
||||
|
||||
---
|
||||
|
||||
## Lifespan and Startup Events
|
||||
|
||||
`TestClient` triggers lifespan events (startup/shutdown) when used as a context manager:
|
||||
|
||||
```python
|
||||
def test_with_lifespan():
|
||||
with TestClient(app) as client:
|
||||
response = client.get("/health")
|
||||
assert response.status_code == 200
|
||||
```
|
||||
|
||||
For `AsyncClient`, use `asgi-lifespan`:
|
||||
|
||||
```python
|
||||
from asgi_lifespan import LifespanManager
|
||||
|
||||
@pytest.mark.anyio
|
||||
async def test_with_async_lifespan():
|
||||
async with LifespanManager(app):
|
||||
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
||||
response = await ac.get("/health")
|
||||
assert response.status_code == 200
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Marker Strategy for FastAPI Tests
|
||||
|
||||
| Marker | When to use |
|
||||
|--------|------------|
|
||||
| `unit` | Pure service/utility logic with no HTTP or DB calls |
|
||||
| `integration` | `TestClient` + real DB session via dependency override |
|
||||
| `smoke` | One `TestClient` call per critical user path, no DB reset |
|
||||
| `external` | Tests that call real third-party APIs (skip in CI by default) |
|
||||
|
||||
---
|
||||
|
||||
## Quick Reference: Sending Data
|
||||
|
||||
| What to send | Parameter |
|
||||
|---|---|
|
||||
| Path / query param | Part of the URL string |
|
||||
| JSON body | `json={"key": "value"}` |
|
||||
| Form data | `data={"field": "value"}` |
|
||||
| Headers | `headers={"X-Token": "..."}` |
|
||||
| Cookies | `cookies={"session": "..."}` |
|
||||
| File upload | `files={"file": ("name.txt", b"content", "text/plain")}` |
|
||||
|
||||
---
|
||||
|
||||
## Official Docs
|
||||
|
||||
- [Testing tutorial](https://fastapi.tiangolo.com/tutorial/testing/)
|
||||
- [Async tests](https://fastapi.tiangolo.com/advanced/async-tests/)
|
||||
- [Testing dependencies with overrides](https://fastapi.tiangolo.com/advanced/testing-dependencies/)
|
||||
- [Testing a database (SQLModel)](https://fastapi.tiangolo.com/how-to/testing-database/)
|
||||
- [Testing lifespan events](https://fastapi.tiangolo.com/advanced/testing-events/)
|
||||
- [Testing WebSockets](https://fastapi.tiangolo.com/advanced/testing-websockets/)
|
||||
- [HTTPX docs](https://www.python-httpx.org/)
|
||||
@@ -0,0 +1,35 @@
|
||||
# Pytest Documentation Notes
|
||||
|
||||
!!! info "Primary sources"
|
||||
- [Good integration practices](https://docs.pytest.org/en/stable/explanation/goodpractices.html)
|
||||
- [Fixture how-to](https://docs.pytest.org/en/stable/how-to/fixtures.html)
|
||||
- [Marker examples](https://docs.pytest.org/en/stable/example/markers.html)
|
||||
- [Configuration reference](https://docs.pytest.org/en/stable/reference/customize.html)
|
||||
- [Flaky tests](https://docs.pytest.org/en/stable/explanation/flaky.html)
|
||||
|
||||
## Agent Quick Path
|
||||
Use this file when you need fast pytest scaffolding defaults without framework-specific details.
|
||||
|
||||
1. Mirror source layout under `tests/`.
|
||||
2. Keep fixtures small and explicit; default to `function` scope.
|
||||
3. Register markers up front in `pyproject.toml`.
|
||||
4. Validate structure first with `uv run pytest --collect-only -q`.
|
||||
5. Run fast lane with `uv run pytest -m unit -q`.
|
||||
|
||||
Load other references only when needed:
|
||||
- FastAPI routes/dependency injection/lifespan: `fastapi-testing.md`
|
||||
- SQLAlchemy sessions/transactions/DB fixtures: `sqlalchemy-testing.md`
|
||||
|
||||
## Practical Guidance For This Skill
|
||||
- Use src-aligned test layout and keep test discovery conventional.
|
||||
- Keep fixtures small, composable, and explicit; use `yield` for teardown.
|
||||
- Register custom markers and keep strict marker validation on.
|
||||
- Separate quick unit runs from slower integration/external runs.
|
||||
- Minimize flakiness by controlling shared state and avoiding hidden dependencies.
|
||||
- Use `--collect-only` and marker-filtered runs to validate scaffold quality early.
|
||||
|
||||
## Commands Worth Remembering
|
||||
- `uv run pytest --collect-only -q`
|
||||
- `uv run pytest -m unit -q`
|
||||
- `uv run pytest -m "not external" -q`
|
||||
- `uv run pytest -q`
|
||||
@@ -0,0 +1,246 @@
|
||||
# [SQLAlchemy 2.x Testing](https://docs.sqlalchemy.org/en/20/orm/session_transaction.html#joining-a-session-into-an-external-transaction-such-as-for-test-suites)
|
||||
|
||||
Best practices for testing SQLAlchemy ORM code (sync and async) with pytest.
|
||||
|
||||
## Agent Quick Path
|
||||
Use this path first; read deeper sections only when needed.
|
||||
|
||||
1. Create engine once per test session.
|
||||
2. Open connection + outer transaction per test function.
|
||||
3. Bind `Session`/`AsyncSession` to that connection with `join_transaction_mode="create_savepoint"`.
|
||||
4. Let code under test call `commit()` safely; rollback outer transaction after test.
|
||||
5. Inject session into FastAPI via dependency override and always clear overrides.
|
||||
|
||||
Branching logic:
|
||||
- Sync stack: use `create_engine` + `Session` fixtures.
|
||||
- Async stack: use `create_async_engine` + `AsyncSession` fixtures + `pytest.mark.anyio`.
|
||||
- SQLite in-memory with threaded client: use `StaticPool` when required by framework threading behavior.
|
||||
- Async relationship access fails (`MissingGreenlet`): eager load (`selectinload`) or explicit refresh.
|
||||
|
||||
---
|
||||
|
||||
## Core Concepts
|
||||
|
||||
| Concept | Preferred approach |
|
||||
|---|---|
|
||||
| Isolate tests from production DB | Use an in-memory SQLite engine per test |
|
||||
| Prevent data leaking between tests | Roll back at the connection level after each test |
|
||||
| Inject test DB into FastAPI | Override the `get_db` / `get_session` dependency |
|
||||
| Create schema for tests | Call `Base.metadata.create_all(engine)` once per session |
|
||||
| Avoid lazy-load errors in async | Use `expire_on_commit=False`; use `selectinload()` for relationships |
|
||||
|
||||
---
|
||||
|
||||
## Sync SQLAlchemy + FastAPI (Recommended Pattern)
|
||||
|
||||
The canonical 2.0 pattern joins a test `Session` into an external transaction on a shared `Connection`, then rolls back after each test. This means `session.commit()` calls within the code under test are "committed" to a savepoint, not the real transaction, and are fully undone after the test.
|
||||
|
||||
```python
|
||||
# tests/conftest.py
|
||||
import pytest
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import Session
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from myapp.main import app
|
||||
from myapp.db.session import get_db
|
||||
from myapp.db.models import Base
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def engine():
|
||||
e = create_engine("sqlite://", connect_args={"check_same_thread": False})
|
||||
Base.metadata.create_all(bind=e)
|
||||
yield e
|
||||
Base.metadata.drop_all(bind=e)
|
||||
|
||||
@pytest.fixture()
|
||||
def db_session(engine):
|
||||
connection = engine.connect()
|
||||
transaction = connection.begin()
|
||||
session = Session(bind=connection, join_transaction_mode="create_savepoint")
|
||||
yield session
|
||||
session.close()
|
||||
transaction.rollback()
|
||||
connection.close()
|
||||
|
||||
@pytest.fixture()
|
||||
def client(db_session):
|
||||
app.dependency_overrides[get_db] = lambda: db_session
|
||||
yield TestClient(app)
|
||||
app.dependency_overrides = {}
|
||||
```
|
||||
|
||||
Key points:
|
||||
- `join_transaction_mode="create_savepoint"` means each `session.commit()` inside code under test issues a SAVEPOINT release, not a real COMMIT — everything is rolled back when the test ends.
|
||||
- `scope="session"` engine + `scope="function"` session/connection gives fast table creation with full per-test isolation.
|
||||
- SQLite in-memory (`sqlite://`) is preferred: no files, no cleanup, fast.
|
||||
|
||||
---
|
||||
|
||||
## Async SQLAlchemy + FastAPI
|
||||
|
||||
For `AsyncSession` / `AsyncEngine`, the setup mirrors the sync version but uses async fixtures and `pytest-anyio`.
|
||||
|
||||
```python
|
||||
# tests/conftest.py
|
||||
import pytest
|
||||
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine, async_sessionmaker
|
||||
from fastapi.testclient import TestClient # sync client still works
|
||||
from httpx import AsyncClient, ASGITransport
|
||||
|
||||
from myapp.main import app
|
||||
from myapp.db.session import get_db
|
||||
from myapp.db.models import Base
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def anyio_backend():
|
||||
return "asyncio"
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
async def async_engine():
|
||||
e = create_async_engine("sqlite+aiosqlite://", connect_args={"check_same_thread": False})
|
||||
async with e.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.create_all)
|
||||
yield e
|
||||
async with e.begin() as conn:
|
||||
await conn.run_sync(Base.metadata.drop_all)
|
||||
await e.dispose()
|
||||
|
||||
@pytest.fixture()
|
||||
async def async_session(async_engine):
|
||||
async with async_engine.connect() as conn:
|
||||
await conn.begin()
|
||||
session = AsyncSession(bind=conn, join_transaction_mode="create_savepoint",
|
||||
expire_on_commit=False)
|
||||
yield session
|
||||
await session.close()
|
||||
await conn.rollback()
|
||||
|
||||
@pytest.fixture()
|
||||
async def async_client(async_session):
|
||||
async def override_get_db():
|
||||
yield async_session
|
||||
|
||||
app.dependency_overrides[get_db] = override_get_db
|
||||
async with AsyncClient(transport=ASGITransport(app=app), base_url="http://test") as ac:
|
||||
yield ac
|
||||
app.dependency_overrides = {}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Async fixtures need `@pytest.mark.anyio` on the test or `anyio_backend` session fixture.
|
||||
- `expire_on_commit=False` prevents expired-attribute access on objects after `await session.commit()`.
|
||||
- Install: `aiosqlite`, `anyio[asyncio]`, `httpx`.
|
||||
|
||||
---
|
||||
|
||||
## SQLModel Pattern (FastAPI + SQLModel)
|
||||
|
||||
SQLModel's official test pattern uses `StaticPool` + in-memory SQLite, with separate `session` and `client` pytest fixtures.
|
||||
|
||||
```python
|
||||
import pytest
|
||||
from fastapi.testclient import TestClient
|
||||
from sqlmodel import Session, SQLModel, create_engine
|
||||
from sqlmodel.pool import StaticPool
|
||||
|
||||
from myapp.main import app, get_session # get_session is the SQLModel dependency
|
||||
|
||||
@pytest.fixture(name="session")
|
||||
def session_fixture():
|
||||
engine = create_engine(
|
||||
"sqlite://",
|
||||
connect_args={"check_same_thread": False},
|
||||
poolclass=StaticPool,
|
||||
)
|
||||
SQLModel.metadata.create_all(engine)
|
||||
with Session(engine) as session:
|
||||
yield session
|
||||
|
||||
@pytest.fixture(name="client")
|
||||
def client_fixture(session: Session):
|
||||
app.dependency_overrides[get_session] = lambda: session
|
||||
yield TestClient(app)
|
||||
app.dependency_overrides.clear()
|
||||
|
||||
# --- Tests ---
|
||||
|
||||
def test_create_hero(client: TestClient):
|
||||
response = client.post("/heroes/", json={"name": "Deadpond", "secret_name": "Dive Wilson"})
|
||||
assert response.status_code == 200
|
||||
|
||||
def test_read_heroes(session: Session, client: TestClient):
|
||||
# Directly insert test data via the session — no HTTP call needed for setup
|
||||
from myapp.models import Hero
|
||||
session.add(Hero(name="Test Hero", secret_name="Hidden"))
|
||||
session.commit()
|
||||
response = client.get("/heroes/")
|
||||
assert response.status_code == 200
|
||||
assert len(response.json()) == 1
|
||||
```
|
||||
|
||||
Rules:
|
||||
- Use `StaticPool` so a single in-memory SQLite connection is shared across threads (required by `TestClient`'s threading model).
|
||||
- Both the `client` fixture and test functions can receive the same `session` — insert data directly for controlled setup rather than via the API.
|
||||
- Always call `app.dependency_overrides.clear()` in fixture teardown (after `yield`).
|
||||
|
||||
---
|
||||
|
||||
## Async Session: Avoiding Implicit I/O
|
||||
|
||||
SQLAlchemy async has strict rules about lazy loading — attributes that would trigger IO on access will raise an error.
|
||||
|
||||
```python
|
||||
# WRONG — will raise MissingGreenlet / lazy load error
|
||||
result = await session.execute(select(User))
|
||||
user = result.scalars().one()
|
||||
print(user.posts) # lazy load, fails in async context
|
||||
|
||||
# RIGHT — use selectinload() to load relationships eagerly
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
result = await session.execute(
|
||||
select(User).options(selectinload(User.posts))
|
||||
)
|
||||
user = result.scalars().one()
|
||||
print(user.posts) # already loaded, no IO needed
|
||||
```
|
||||
|
||||
Other strategies:
|
||||
- `AsyncAttrs` mixin: access any attribute as an awaitable via `await obj.awaitable_attrs.relationship_name`.
|
||||
- `write_only` relationships: never loaded implicitly; queried explicitly.
|
||||
- `await session.refresh(obj, ["attribute_name"])`: force-load a specific attribute after the fact.
|
||||
|
||||
---
|
||||
|
||||
## Fixture Scope Decision Table
|
||||
|
||||
| What to scope | `scope` | Reason |
|
||||
|---|---|---|
|
||||
| Engine + DDL (`create_all`) | `session` | Expensive; shared across all tests |
|
||||
| Connection + Transaction | `function` | Rolled back per test for isolation |
|
||||
| Session | `function` | One transaction per test |
|
||||
| TestClient / AsyncClient | `function` | Depends on session; recreated per test |
|
||||
|
||||
---
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
| Problem | Cause | Fix |
|
||||
|---|---|---|
|
||||
| `MissingGreenlet` in async | Lazy-loaded relationship accessed outside awaitable context | Use `selectinload()` or `AsyncAttrs.awaitable_attrs` |
|
||||
| `RuntimeError: Event loop is closed` | `AsyncEngine` not disposed | Call `await engine.dispose()` in fixture teardown |
|
||||
| Tests share state / data bleeds | Session not rolled back | Use `join_transaction_mode="create_savepoint"` + rollback pattern |
|
||||
| `StaticPool` not used with SQLite in-memory | TestClient spawns threads that get separate in-memory DBs | Always add `poolclass=StaticPool` for in-memory SQLite |
|
||||
| `expire_on_commit=True` (default) breaks async | Accessing attributes after commit triggers lazy IO | Set `expire_on_commit=False` on AsyncSession |
|
||||
| Not resetting `dependency_overrides` | Override persists into next test | Always clear in fixture teardown, after `yield` |
|
||||
|
||||
---
|
||||
|
||||
## Official Docs
|
||||
|
||||
- [Joining a Session into an External Transaction (test suites)](https://docs.sqlalchemy.org/en/20/orm/session_transaction.html#joining-a-session-into-an-external-transaction-such-as-for-test-suites)
|
||||
- [Asynchronous I/O (asyncio)](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html)
|
||||
- [Preventing Implicit IO when Using AsyncSession](https://docs.sqlalchemy.org/en/20/orm/extensions/asyncio.html#preventing-implicit-io-when-using-asyncsession)
|
||||
- [SQLModel: Test Applications with FastAPI](https://sqlmodel.tiangolo.com/tutorial/fastapi/tests/)
|
||||
- [FastAPI: Testing Dependencies with Overrides](https://fastapi.tiangolo.com/advanced/testing-dependencies/)
|
||||
@@ -0,0 +1,117 @@
|
||||
---
|
||||
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'
|
||||
x-personal-mcp:
|
||||
id: python-logging-dictconfig
|
||||
version: 1.0.0
|
||||
tags:
|
||||
- logging
|
||||
- python
|
||||
- observability
|
||||
capabilities:
|
||||
- resource://skills/python-logging-dictconfig/document
|
||||
depends_on: []
|
||||
---
|
||||
|
||||
# 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: [Python logging references](./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 Templates
|
||||
|
||||
### Configuration
|
||||
|
||||
!!! warning "Don't use the name `logging.py` because it will conflict
|
||||
|
||||
```python title="logging_config.py"
|
||||
import logging.config
|
||||
|
||||
LOGGING = {
|
||||
"version": 1,
|
||||
"disable_existing_loggers": False,
|
||||
"formatters": {
|
||||
"basic": {
|
||||
"format": "%(asctime)s.%(msecs)03d [%(levelname)s] %(message)s",
|
||||
"datefmt": "%Y-%m-%d %H:%M:%S",
|
||||
}
|
||||
},
|
||||
"handlers": {
|
||||
"console": {
|
||||
"class": "logging.StreamHandler",
|
||||
"formatter": "basic",
|
||||
"stream": "ext://sys.stdout",
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"level": "INFO",
|
||||
"handlers": ["console"],
|
||||
},
|
||||
}
|
||||
|
||||
def configure_logging() -> None:
|
||||
logging.config.dictConfig(LOGGING)
|
||||
```
|
||||
|
||||
```python title="app.py"
|
||||
# app startup
|
||||
from .logging_config import configure_logging
|
||||
|
||||
configure_logging()
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
The preferred way of instantiating loggers is at the top of modules like this:
|
||||
|
||||
```python
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
```
|
||||
|
||||
## 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.
|
||||
6. No module named `logging.py` in the project.
|
||||
|
||||
## 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,22 @@
|
||||
# Python Logging References
|
||||
|
||||
Use these official Python docs when applying this skill.
|
||||
|
||||
## Core Documentation
|
||||
|
||||
!!! info "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
|
||||
|
||||
!!! info "dictConfig references"
|
||||
- [Dictionary schema details](https://docs.python.org/3/library/logging.config.html#logging-config-dictschema) for `version`, formatters, handlers, loggers, and root.
|
||||
- [`logging.config.dictConfig`](https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig) function reference.
|
||||
|
||||
## 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.
|
||||
@@ -0,0 +1,109 @@
|
||||
---
|
||||
name: vscode-configuration
|
||||
description: 'Create and troubleshoot VS Code workspace configuration for Python projects, with focused patterns for launch.json debugpy/FastAPI debugging and tasks.json task automation.'
|
||||
argument-hint: 'What do you need: debug setup, FastAPI debug run profile, tasks.json automation, or all of them?'
|
||||
x-personal-mcp:
|
||||
id: vscode-configuration
|
||||
version: 1.0.0
|
||||
tags:
|
||||
- vscode
|
||||
- launch-json
|
||||
- tasks-json
|
||||
- debugpy
|
||||
- fastapi
|
||||
- python
|
||||
- skills
|
||||
capabilities:
|
||||
- resource://skills/vscode-configuration/document
|
||||
depends_on: []
|
||||
---
|
||||
|
||||
# VS Code Configuration
|
||||
|
||||
Use this skill to design or repair repeatable VS Code workspace configuration for local development workflows.
|
||||
|
||||
## When to Use
|
||||
|
||||
- You need to create or fix `.vscode/launch.json` debug profiles.
|
||||
- You need robust Python debugging with `debugpy`.
|
||||
- You need FastAPI-specific launch profiles (app module, host/port, reload options, env files).
|
||||
- You need `.vscode/tasks.json` build/test/run tasks and optional debug pre-launch integration.
|
||||
- You need consistent workspace onboarding where users can run and debug from VS Code with minimal manual setup.
|
||||
|
||||
## Progressive References
|
||||
|
||||
Load only the page that matches the current request:
|
||||
|
||||
- Launch profile mechanics and debugpy patterns: [debug launch configurations](./references/debug-launch-configurations.md)
|
||||
- FastAPI-focused debug profiles using debugpy: [FastAPI + debugpy launch patterns](./references/fastapi-debugpy-launch.md)
|
||||
- Task runner setup in VS Code: [tasks.json project tasks](./references/tasks-json-configuration.md)
|
||||
|
||||
## Procedure
|
||||
|
||||
### Step 1: Capture the Runtime Shape
|
||||
|
||||
Collect the minimum context before writing files:
|
||||
|
||||
1. Python entry shape: module path vs script path.
|
||||
2. Framework runtime: plain script, FastAPI with uvicorn, or mixed services.
|
||||
3. Required environment: env file, env vars, cwd, and PYTHONPATH needs.
|
||||
4. Task expectations: run app, run tests, lint/format, one-off setup.
|
||||
|
||||
Completion check: you can state exactly what command should run for debug and for task execution.
|
||||
|
||||
### Step 2: Create launch.json Profiles
|
||||
|
||||
1. Add at least one stable baseline profile before specialized variants.
|
||||
2. Prefer module-based launches where packaging/import paths matter.
|
||||
3. Keep debugger options explicit (`justMyCode`, `console`, `cwd`, `envFile`).
|
||||
4. Add purpose-built profiles instead of one overloaded profile.
|
||||
|
||||
For concrete patterns, open [debug launch configurations](./references/debug-launch-configurations.md).
|
||||
|
||||
Completion check: selecting each profile starts the intended process without manual edits.
|
||||
|
||||
### Step 3: Add FastAPI Profiles When Needed
|
||||
|
||||
1. Use a dedicated FastAPI profile that launches `uvicorn` via module mode.
|
||||
2. Keep host/port/reload/log-level as explicit args.
|
||||
3. Include `jinja` debugging only if templates are in scope.
|
||||
4. Add an attach profile when launching via external `debugpy` listener.
|
||||
|
||||
For complete examples, open [FastAPI + debugpy launch patterns](./references/fastapi-debugpy-launch.md).
|
||||
|
||||
Completion check: breakpoints hit in app code and startup path, and profile behavior matches dev vs non-dev expectations.
|
||||
|
||||
### Step 4: Add tasks.json for Repeated Commands
|
||||
|
||||
1. Create named tasks for run, test, lint, and docs/build steps as needed.
|
||||
2. For Python projects, keep commands consistent with the repo package manager.
|
||||
3. Use `problemMatcher` where parsers exist and background flags for long-running tasks.
|
||||
4. Link debug profiles to tasks with `preLaunchTask` only when startup sequencing is required.
|
||||
|
||||
For task schema and examples, open [tasks.json project tasks](./references/tasks-json-configuration.md).
|
||||
|
||||
Completion check: tasks run from Command Palette and can be reused by debug profiles.
|
||||
|
||||
### Step 5: Validate End-to-End
|
||||
|
||||
1. Run each launch profile once.
|
||||
2. Run each task once.
|
||||
3. Verify paths, env files, and interpreter assumptions on a clean workspace reload.
|
||||
4. Record any project-specific defaults in comments or docs if non-obvious.
|
||||
|
||||
Completion check: a teammate can clone the repo, open VS Code, and run/debug with only documented prerequisites.
|
||||
|
||||
## Decision Points
|
||||
|
||||
- If the app is imported as a package, prefer module launches over direct script paths.
|
||||
- If runtime is started outside VS Code, add attach profile instead of forcing launch mode.
|
||||
- If there are long-running dev servers, pair with background tasks.
|
||||
- If test command differs by repo convention, mirror that command in tasks exactly.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Return:
|
||||
|
||||
1. Created or updated VS Code config files and profile/task names.
|
||||
2. Any assumptions (module path, env file, command runner).
|
||||
3. Validation results and any unresolved decisions.
|
||||
@@ -0,0 +1,101 @@
|
||||
# Debug Launch Configurations in VS Code
|
||||
|
||||
This reference focuses on Python debugging through `debugpy` using `.vscode/launch.json`.
|
||||
|
||||
## Core Structure
|
||||
|
||||
A minimal launch file:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": []
|
||||
}
|
||||
```
|
||||
|
||||
Useful fields for Python configs:
|
||||
|
||||
- `type`: Use `debugpy`.
|
||||
- `request`: Usually `launch`, sometimes `attach`.
|
||||
- `name`: Friendly profile name shown in the Run and Debug panel.
|
||||
- `program`: Script path for script-based entry.
|
||||
- `module`: Module name for `python -m ...` style launches.
|
||||
- `args`: CLI arguments.
|
||||
- `cwd`: Working directory.
|
||||
- `env` / `envFile`: Environment variables.
|
||||
- `console`: `integratedTerminal` is usually most practical.
|
||||
- `justMyCode`: `true` by default; set `false` when stepping into dependencies.
|
||||
|
||||
## Launch vs Attach
|
||||
|
||||
Use `launch` when VS Code should start the process.
|
||||
Use `attach` when the process already runs with debugpy listening.
|
||||
|
||||
Attach profile example:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Python: Attach (debugpy :5678)",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 5678
|
||||
},
|
||||
"justMyCode": true
|
||||
}
|
||||
```
|
||||
|
||||
Remote process side command example:
|
||||
|
||||
```bash
|
||||
python -m debugpy --listen 5678 -m your_package.main
|
||||
```
|
||||
|
||||
## Script and Module Patterns
|
||||
|
||||
Script pattern:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Python: Script",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"program": "${workspaceFolder}/src/app.py",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true
|
||||
}
|
||||
```
|
||||
|
||||
Module pattern:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Python: Module",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "your_package.main",
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true
|
||||
}
|
||||
```
|
||||
|
||||
Prefer module mode when imports depend on package layout.
|
||||
|
||||
## Environment and Interpreter Notes
|
||||
|
||||
- Use `envFile` for shared local variables, commonly `${workspaceFolder}/.env`.
|
||||
- Keep secrets out of committed launch configs.
|
||||
- Ensure the selected VS Code interpreter matches project tooling.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If breakpoints do not hit:
|
||||
|
||||
1. Confirm the right profile is selected.
|
||||
2. Confirm the file path/module path is correct.
|
||||
3. Disable `justMyCode` temporarily to inspect call flow.
|
||||
4. Confirm no stale background process is occupying the expected port.
|
||||
5. Confirm workspace root and `cwd` align with imports.
|
||||
@@ -0,0 +1,98 @@
|
||||
# FastAPI Debug Launch with debugpy
|
||||
|
||||
This reference provides practical `.vscode/launch.json` patterns for FastAPI applications started with uvicorn.
|
||||
|
||||
## Launch FastAPI via Module
|
||||
|
||||
Preferred profile:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "FastAPI: Uvicorn (debug)",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "uvicorn",
|
||||
"args": [
|
||||
"your_package.main:app",
|
||||
"--host",
|
||||
"127.0.0.1",
|
||||
"--port",
|
||||
"8000",
|
||||
"--reload",
|
||||
"--log-level",
|
||||
"debug"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"envFile": "${workspaceFolder}/.env",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true,
|
||||
"jinja": true
|
||||
}
|
||||
```
|
||||
|
||||
Why module mode: it matches `python -m uvicorn ...` behavior and avoids path ambiguity.
|
||||
|
||||
## Launch with Factory Pattern
|
||||
|
||||
If app is created via factory function:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "FastAPI: Uvicorn factory (debug)",
|
||||
"type": "debugpy",
|
||||
"request": "launch",
|
||||
"module": "uvicorn",
|
||||
"args": [
|
||||
"your_package.main:create_app",
|
||||
"--factory",
|
||||
"--host",
|
||||
"127.0.0.1",
|
||||
"--port",
|
||||
"8000",
|
||||
"--reload"
|
||||
],
|
||||
"cwd": "${workspaceFolder}",
|
||||
"console": "integratedTerminal",
|
||||
"justMyCode": true
|
||||
}
|
||||
```
|
||||
|
||||
## Attach to an Existing FastAPI Process
|
||||
|
||||
If the app is launched externally, start with debugpy:
|
||||
|
||||
```bash
|
||||
python -m debugpy --listen 5678 -m uvicorn your_package.main:app --host 127.0.0.1 --port 8000 --reload
|
||||
```
|
||||
|
||||
Attach profile:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "FastAPI: Attach (5678)",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 5678
|
||||
},
|
||||
"justMyCode": true
|
||||
}
|
||||
```
|
||||
|
||||
## Common FastAPI Debug Pitfalls
|
||||
|
||||
1. Wrong import target in `your_package.main:app` or factory symbol.
|
||||
2. `cwd` does not match source layout.
|
||||
3. Auto-reload creating confusion about active process when breakpoints are set in startup code.
|
||||
4. Port collisions from old uvicorn processes.
|
||||
5. Environment variables not loaded because `envFile` path is wrong.
|
||||
|
||||
## Practical Quality Gate
|
||||
|
||||
A profile is considered valid when:
|
||||
|
||||
1. Server starts from VS Code Run and Debug.
|
||||
2. A breakpoint inside an endpoint is hit on request.
|
||||
3. A breakpoint in startup/lifespan logic is hit at app boot.
|
||||
4. Terminal output appears in integrated terminal with expected log level.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Configure Project Tasks in tasks.json
|
||||
|
||||
Use `.vscode/tasks.json` to define repeatable project commands and optional hooks for debugging.
|
||||
|
||||
## Minimal File
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": []
|
||||
}
|
||||
```
|
||||
|
||||
## Task Fields You Will Use Most
|
||||
|
||||
- `label`: Task name shown in VS Code.
|
||||
- `type`: Usually `shell`.
|
||||
- `command`: Executable to run.
|
||||
- `args`: Command arguments.
|
||||
- `options.cwd`: Working directory.
|
||||
- `group`: Mark default build or test tasks.
|
||||
- `problemMatcher`: Parse errors into the Problems panel.
|
||||
- `isBackground`: `true` for long-running tasks (for example dev server watch).
|
||||
|
||||
## Python Project Example
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "2.0.0",
|
||||
"tasks": [
|
||||
{
|
||||
"label": "App: Run",
|
||||
"type": "shell",
|
||||
"command": "uv",
|
||||
"args": ["run", "uvicorn", "personal_mcp.main:app", "--host", "127.0.0.1", "--port", "8000", "--reload"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"isBackground": true,
|
||||
"problemMatcher": []
|
||||
},
|
||||
{
|
||||
"label": "Tests: Pytest",
|
||||
"type": "shell",
|
||||
"command": "uv",
|
||||
"args": ["run", "pytest"],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
},
|
||||
"group": "test",
|
||||
"problemMatcher": []
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Connect Tasks to Debug Profiles
|
||||
|
||||
In `launch.json`, you can run a task first:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "FastAPI: Attach",
|
||||
"type": "debugpy",
|
||||
"request": "attach",
|
||||
"connect": {
|
||||
"host": "127.0.0.1",
|
||||
"port": 5678
|
||||
},
|
||||
"preLaunchTask": "App: Run"
|
||||
}
|
||||
```
|
||||
|
||||
Use this only when startup sequencing is needed.
|
||||
|
||||
## Task Design Guidelines
|
||||
|
||||
1. Keep labels stable and descriptive.
|
||||
2. Prefer one task per intent instead of monolithic shell commands.
|
||||
3. Keep shell portability in mind if teammates use multiple OSes.
|
||||
4. Avoid embedding secrets directly in task definitions.
|
||||
5. Mark long-running tasks with `isBackground` and keep matchers explicit.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
If a task fails unexpectedly:
|
||||
|
||||
1. Run the underlying command directly in terminal.
|
||||
2. Confirm `options.cwd` points to expected workspace root.
|
||||
3. Confirm tool availability in environment path.
|
||||
4. Confirm quoting and argument boundaries in `args`.
|
||||
5. Confirm the task is not blocked by an outdated background process.
|
||||
@@ -0,0 +1,138 @@
|
||||
---
|
||||
name: zensical-docs
|
||||
description: 'Reference skill for Zensical documentation mechanics. Use for quick lookup of docs structure, feature options, and source links. Prefer inline Markdown links to source docs and avoid bare URLs because this content is rendered as human docs and MCP resources.'
|
||||
argument-hint: 'What are you documenting, who is the audience, and what Zensical features are in scope?'
|
||||
x-personal-mcp:
|
||||
id: zensical-docs
|
||||
version: 1.0.0
|
||||
tags:
|
||||
- zensical
|
||||
- mkdocs
|
||||
- mkdocs-material
|
||||
- mkdocstrings
|
||||
- docs
|
||||
- documentation
|
||||
- information-architecture
|
||||
- skills
|
||||
- bootstrap
|
||||
- discovery
|
||||
- authoring
|
||||
capabilities:
|
||||
- resource://skills/zensical-docs/document
|
||||
depends_on: []
|
||||
---
|
||||
|
||||
# Zensical Documentation Authoring
|
||||
|
||||
Use this as a compact reference for Zensical mechanics and as the place to record evolving preferences for how this repository uses them.
|
||||
|
||||
## When to Use
|
||||
|
||||
- You need a quick reminder of Zensical features, docs structure, or configuration mechanics.
|
||||
- You want direct links back to source documentation before changing docs behavior.
|
||||
- You want one small file you can keep editing as your preferences around docs authoring become clearer.
|
||||
|
||||
## How To Use This Skill
|
||||
|
||||
1. Start here for a quick decision about what kind of docs change you are making.
|
||||
2. Open only the linked reference that matches the current task.
|
||||
3. Add or revise preference notes in this file when you decide how this repo should use a feature.
|
||||
|
||||
## Quick Reference Map
|
||||
|
||||
Open only what you need:
|
||||
|
||||
- Official docs and source map: [source map](./references/index.md)
|
||||
- Zensical feature catalog and setup links: [feature catalog](./references/zensical-features.md)
|
||||
- Theme, icons, and visual customization: [theme customization](./references/theme-customization-and-icons.md)
|
||||
- Writing quality and review criteria: [documentation quality](./references/documentation-quality.md)
|
||||
- Navigation and discoverability patterns: [discoverability and IA](./references/discoverability-and-ia.md)
|
||||
- Code-heavy docs and API reference patterns: [code-heavy docs](./references/code-heavy-docs-and-mkdocstrings.md)
|
||||
|
||||
## Common Cases
|
||||
|
||||
### New docs project
|
||||
|
||||
- Start with `uv run zensical new`.
|
||||
- Then review the [source map](./references/index.md) and [feature catalog](./references/zensical-features.md).
|
||||
|
||||
### Restructuring docs or navigation
|
||||
|
||||
- Review [discoverability and IA](./references/discoverability-and-ia.md).
|
||||
- Use it to decide overview pages, section structure, and cross-linking.
|
||||
|
||||
### Improving writing quality
|
||||
|
||||
- Review [documentation quality](./references/documentation-quality.md).
|
||||
- Use it for page quality gates, trust signals, and review criteria.
|
||||
|
||||
### Adjusting theme or UI mechanics
|
||||
|
||||
- Review [theme customization](./references/theme-customization-and-icons.md).
|
||||
- Use it for icons, color, theme extensions, and presentation choices.
|
||||
|
||||
### Documenting APIs or code-heavy systems
|
||||
|
||||
- Review [code-heavy docs](./references/code-heavy-docs-and-mkdocstrings.md).
|
||||
- Use it when generated API reference belongs alongside hand-authored docs.
|
||||
|
||||
## Preferences To Maintain Here
|
||||
|
||||
Keep this section short and revise it over time.
|
||||
|
||||
### Preferred feature choices
|
||||
|
||||
- Add the Zensical features you usually enable first.
|
||||
- Note which features are situational and why.
|
||||
- Prefer Zensical-native features and conventions when they cover the need cleanly.
|
||||
- Expect general backward compatibility with MkDocs patterns and configuration unless there is a documented reason not to.
|
||||
|
||||
### Preferred docs structure
|
||||
|
||||
- Record whether this repo prefers explicit nav, index pages, task-first docs, or another pattern.
|
||||
|
||||
### Preferred API docs approach
|
||||
|
||||
- Record whether to use mkdocstrings, how much API surface to publish, and how to link task docs back to reference pages.
|
||||
|
||||
## Source-First Rule
|
||||
|
||||
When making a recommendation, link back to the relevant reference file first, and when possible to the upstream docs linked from that reference.
|
||||
|
||||
## Link Formatting Rule
|
||||
|
||||
Because this project publishes the same markdown for both `/docs` and MCP resources, link quality is part of the content contract.
|
||||
|
||||
- Never leave a bare URL in prose or list items.
|
||||
- Prefer using in-place Markdown links with meaningful labels.
|
||||
- For external sources, prefer `[descriptive label](https://...)` over raw `https://...`.
|
||||
- For internal files, prefer relative Markdown links so rendered docs remain navigable.
|
||||
- Any mention of a library or a specific library feature should include a link to source documentation somewhere on the page.
|
||||
- If inline linking is awkward or the citation payload is too large, use a footnote or tooltip citation instead.
|
||||
|
||||
Example preferred style:
|
||||
|
||||
- `See [importlib.resources](https://docs.python.org/3/library/importlib.resources.html) for packaging details.`
|
||||
|
||||
Example to avoid:
|
||||
|
||||
- `See https://docs.python.org/3/library/importlib.resources.html for packaging details.`
|
||||
|
||||
Acceptable alternatives when inline links are not ideal:
|
||||
|
||||
- Add a footnote-style source citation at the end of the section or page.
|
||||
- Add a tooltip citation when the docs pattern supports it.
|
||||
|
||||
## Compatibility Rule
|
||||
|
||||
Prefer the Zensical-native way of doing something when it exists and is well-supported.
|
||||
Assume MkDocs compatibility is still expected for most configuration and authoring patterns, and call out any case where a Zensical recommendation intentionally diverges from standard MkDocs behavior.
|
||||
|
||||
## Output Contract
|
||||
|
||||
Return only what is useful for the current docs task:
|
||||
|
||||
1. Which reference to read next.
|
||||
2. The smallest recommended docs or config change.
|
||||
3. Any repo-specific preference this suggests should be added back into this skill.
|
||||
4. For any library or feature-level claim, include a source-doc citation somewhere (inline link preferred; footnote or tooltip acceptable).
|
||||
@@ -0,0 +1,63 @@
|
||||
# Code-Heavy Documentation with mkdocstrings
|
||||
|
||||
Use this reference when your docs include API surfaces, function/class documentation, and source-driven technical reference.
|
||||
|
||||
## Why mkdocstrings
|
||||
|
||||
mkdocstrings helps generate and maintain API reference pages directly from code and docstrings, reducing drift between implementation and docs.
|
||||
|
||||
!!! info "Primary docs"
|
||||
- [mkdocstrings home](https://mkdocstrings.github.io/)
|
||||
- [mkdocstrings Python handler](https://mkdocstrings.github.io/python/)
|
||||
- [Griffe Python parsing engine](https://mkdocstrings.github.io/griffe/)
|
||||
|
||||
## When to Use It
|
||||
|
||||
- You maintain Python modules/classes/functions that need searchable reference docs.
|
||||
- You want hand-written concept/task docs plus generated API reference pages.
|
||||
- You need consistent signatures, type hints, and docstring rendering.
|
||||
|
||||
## Recommended Documentation Split
|
||||
|
||||
1. Hand-authored docs for concepts, architecture, and tasks.
|
||||
2. Generated docs (mkdocstrings) for API details.
|
||||
3. Cross-links in both directions:
|
||||
- task pages link to specific API entries
|
||||
- API pages link to practical guides and examples
|
||||
|
||||
## Minimal Integration Pattern
|
||||
|
||||
1. Add mkdocstrings and a Python handler package to project dependencies.
|
||||
2. Configure the Zensical docs toolchain to enable mkdocstrings within the site build.
|
||||
3. Create one API index page per package/domain.
|
||||
4. Expand coverage gradually from high-value modules first.
|
||||
|
||||
!!! info "General reference examples"
|
||||
- [Zensical docs home and setup entry point](https://zensical.org/docs/)
|
||||
- [Zensical code blocks and authoring patterns](https://zensical.org/docs/authoring/code-blocks/)
|
||||
- [Zensical customization overview](https://zensical.org/docs/customization/)
|
||||
|
||||
!!! note "Compatibility"
|
||||
Zensical is generally expected to remain compatible with MkDocs-style configuration patterns, but prefer Zensical-native documentation and examples when they cover the same behavior.
|
||||
|
||||
## Authoring Guidance for Docstrings
|
||||
|
||||
- Begin with a one-line summary in imperative or descriptive form.
|
||||
- Document parameters, return values, raised exceptions, and side effects.
|
||||
- Include short examples for non-obvious usage.
|
||||
- Keep terminology aligned with task docs and architecture pages.
|
||||
|
||||
## Quality Gates for Code-Heavy Docs
|
||||
|
||||
- API pages build cleanly and include expected modules.
|
||||
- Symbols are grouped by domain, not dumped in one long page.
|
||||
- Public APIs have meaningful docstrings before publishing.
|
||||
- Generated reference pages are linked from user-facing docs.
|
||||
- Search can find both conceptual guides and concrete API entries.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- Treating generated API docs as a replacement for task documentation.
|
||||
- Publishing API pages without module-level context.
|
||||
- Letting undocumented public APIs accumulate.
|
||||
- Not reviewing generated pages after refactors.
|
||||
@@ -0,0 +1,74 @@
|
||||
# Discoverability and Information Architecture
|
||||
|
||||
Use this reference to design docs that are progressively discoverable from overview to implementation detail.
|
||||
|
||||
## IA Model
|
||||
|
||||
Organize by user intent, then by product area.
|
||||
|
||||
Recommended top-level model:
|
||||
|
||||
1. Learn (concepts, architecture, mental models)
|
||||
2. Do (task/how-to paths)
|
||||
3. Reference (API, config, command catalog)
|
||||
4. Troubleshoot (symptoms, diagnostics, fixes)
|
||||
|
||||
!!! info "IA references"
|
||||
- [Diataxis framework](https://diataxis.fr/)
|
||||
- [Divio documentation system](https://documentation.divio.com/)
|
||||
|
||||
## Progressive Discoverability Pattern
|
||||
|
||||
### Layer 1: Section Overview
|
||||
|
||||
Each section starts with an index page containing:
|
||||
|
||||
- What this section is for
|
||||
- Who should read it
|
||||
- Common journeys
|
||||
- Links to key tasks and references
|
||||
|
||||
### Layer 2: Task or Concept Pages
|
||||
|
||||
Each page includes:
|
||||
|
||||
- 1-2 sentence purpose
|
||||
- prerequisites
|
||||
- internal links to references and next steps
|
||||
|
||||
### Layer 3: Deep Reference
|
||||
|
||||
Keep deep details in dedicated reference pages and link to them from task pages when needed.
|
||||
|
||||
## Navigation Design Rules
|
||||
|
||||
1. Keep navigation labels user-facing and action-oriented.
|
||||
2. Avoid duplicate labels in separate branches.
|
||||
3. Place high-frequency tasks near the top.
|
||||
4. Keep section depth shallow where possible.
|
||||
|
||||
!!! info "Relevant Zensical configuration docs"
|
||||
- [Navigation setup](https://zensical.org/docs/setup/navigation/)
|
||||
- [Search setup](https://zensical.org/docs/setup/search/)
|
||||
- [Header setup](https://zensical.org/docs/setup/header/)
|
||||
- [Footer setup](https://zensical.org/docs/setup/footer/)
|
||||
|
||||
## Link Strategy
|
||||
|
||||
- Every deep page should have at least one inbound link from a higher-level index page.
|
||||
- Add "See also" blocks for neighboring tasks.
|
||||
- Link to source-of-truth reference pages instead of duplicating config tables.
|
||||
|
||||
## Search Optimization for Docs
|
||||
|
||||
- Put key terms in title and first paragraph.
|
||||
- Use specific H2/H3 headings that match user query language.
|
||||
- Keep repeated boilerplate minimal so snippets stay informative.
|
||||
|
||||
## Review Heuristics
|
||||
|
||||
A documentation journey is healthy when:
|
||||
|
||||
- users can identify their path within 10 seconds on a section index page
|
||||
- users can complete primary tasks without opening more than 2-3 tabs
|
||||
- users can recover from common errors without external support tickets
|
||||
@@ -0,0 +1,54 @@
|
||||
# Documentation Quality Best Practices
|
||||
|
||||
Use this reference when writing or reviewing docs for clarity, correctness, and trust.
|
||||
|
||||
## Core Writing Principles
|
||||
|
||||
1. Write for a specific audience and task.
|
||||
2. Lead with outcomes, not internal implementation details.
|
||||
3. Keep concepts, tasks, and references distinct.
|
||||
4. Make examples executable and verifiable.
|
||||
5. Prefer precise language over marketing language.
|
||||
|
||||
!!! info "Primary references"
|
||||
- [Diataxis](https://diataxis.fr/)
|
||||
- [Divio documentation system](https://documentation.divio.com/)
|
||||
- [Write the Docs guide](https://www.writethedocs.org/guide/)
|
||||
|
||||
## Style and Readability
|
||||
|
||||
- Use consistent terminology and avoid synonym drift.
|
||||
- Use short paragraphs and meaningful headings.
|
||||
- Prefer active voice and imperative instructions for task pages.
|
||||
- Add notes/warnings only for high-impact caveats.
|
||||
|
||||
!!! info "Style sources"
|
||||
- [Google developer style](https://developers.google.com/style)
|
||||
- [Microsoft Writing Style Guide](https://learn.microsoft.com/style-guide/welcome/)
|
||||
- [MDN writing guidelines](https://developer.mozilla.org/en-US/docs/MDN/Writing_guidelines)
|
||||
|
||||
## Task Page Quality Pattern
|
||||
|
||||
Each task page should include:
|
||||
|
||||
1. Goal and scope.
|
||||
2. Prerequisites (permissions, versions, environment).
|
||||
3. Step-by-step procedure.
|
||||
4. Expected result and verification command/output.
|
||||
5. Common failure modes and recovery path.
|
||||
6. Related links (concept, reference, troubleshooting).
|
||||
|
||||
## Quality Gates Before Publish
|
||||
|
||||
- Accuracy: commands and code examples are validated.
|
||||
- Completeness: no critical missing steps.
|
||||
- Discoverability: page is linked from at least one overview page.
|
||||
- Freshness: version-specific notes and dates are present where needed.
|
||||
- Accessibility: heading structure and link text are clear.
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
- Mixing conceptual explanation and long procedural flow in the same section without structure.
|
||||
- Hiding prerequisites mid-page.
|
||||
- Using screenshots as the only source of truth for commands.
|
||||
- Publishing pages with no owner and no review cadence.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Zensical Docs Skill References
|
||||
|
||||
Use this index to load only the source references needed for the current task.
|
||||
|
||||
## Zensical Official Docs
|
||||
|
||||
!!! info "Zensical official docs"
|
||||
- [New project scaffolding](https://zensical.org/docs/) for `uv run zensical new`.
|
||||
- [Home](https://zensical.org/docs/)
|
||||
- [Setup basics](https://zensical.org/docs/setup/basics/)
|
||||
- [Navigation setup](https://zensical.org/docs/setup/navigation/)
|
||||
- [Header setup and announcement bar](https://zensical.org/docs/setup/header/)
|
||||
- [Footer setup](https://zensical.org/docs/setup/footer/)
|
||||
- [Repository and content actions](https://zensical.org/docs/setup/repository/)
|
||||
- [Search setup](https://zensical.org/docs/setup/search/)
|
||||
- [Customization overview](https://zensical.org/docs/customization/)
|
||||
- [Additional CSS](https://zensical.org/docs/customization/#additional-css)
|
||||
- [Additional JavaScript](https://zensical.org/docs/customization/#additional-javascript)
|
||||
- [Theme extension and overrides](https://zensical.org/docs/customization/#extending-the-theme)
|
||||
- [Language setup](https://zensical.org/docs/setup/language/)
|
||||
- [Logo and icons](https://zensical.org/docs/setup/logo-and-icons/)
|
||||
- [Code blocks and annotations](https://zensical.org/docs/authoring/code-blocks/)
|
||||
- [Content tabs](https://zensical.org/docs/authoring/content-tabs/)
|
||||
- [Footnotes](https://zensical.org/docs/authoring/footnotes/)
|
||||
- [Tooltips](https://zensical.org/docs/authoring/tooltips/)
|
||||
|
||||
## Adjacent Documentation Quality Sources
|
||||
|
||||
!!! info "Documentation quality sources"
|
||||
- [Divio documentation system](https://documentation.divio.com/)
|
||||
- [Write the Docs guide](https://www.writethedocs.org/guide/)
|
||||
- [Google developer documentation style guide](https://developers.google.com/style)
|
||||
- [Microsoft Writing Style Guide](https://learn.microsoft.com/style-guide/welcome/)
|
||||
- [MDN writing guidelines](https://developer.mozilla.org/en-US/docs/MDN/Writing_guidelines)
|
||||
- [Diataxis framework](https://diataxis.fr/)
|
||||
|
||||
## Related Tooling References
|
||||
|
||||
!!! info "Related tooling"
|
||||
- [Markdown guide](https://www.markdownguide.org/)
|
||||
- [Zensical setup and configuration entry point](https://zensical.org/docs/)
|
||||
- [Zensical customization reference](https://zensical.org/docs/customization/)
|
||||
- [mkdocstrings](https://mkdocstrings.github.io/)
|
||||
|
||||
## Skill-Specific Deep Dives
|
||||
|
||||
- Theme customization, colors, icons: [./theme-customization-and-icons.md](./theme-customization-and-icons.md)
|
||||
- Code-heavy docs with mkdocstrings: [./code-heavy-docs-and-mkdocstrings.md](./code-heavy-docs-and-mkdocstrings.md)
|
||||
@@ -0,0 +1,62 @@
|
||||
# Theme Customization, Colors, and Icons
|
||||
|
||||
Use this reference when you want documentation that feels intentional and brand-aligned while preserving readability and accessibility.
|
||||
|
||||
## Start from the Scaffold
|
||||
|
||||
Always start new projects with `uv run zensical new` so the baseline theme/config scaffolding is in place before customization.
|
||||
|
||||
## Customization Strategy
|
||||
|
||||
1. Configure theme and feature flags in the project config first.
|
||||
2. Apply visual tokens (colors, spacing, typography) in a shared CSS layer.
|
||||
3. Add icons and logo assets with consistent naming.
|
||||
4. Use template overrides only when config/CSS cannot solve the requirement.
|
||||
|
||||
## Key Zensical Customization Surfaces
|
||||
|
||||
!!! info "Zensical sources"
|
||||
- [Customization overview](https://zensical.org/docs/customization/)
|
||||
- [Additional CSS](https://zensical.org/docs/customization/#additional-css)
|
||||
- [Additional JavaScript](https://zensical.org/docs/customization/#additional-javascript)
|
||||
- [Extending the theme](https://zensical.org/docs/customization/#extending-the-theme)
|
||||
- [Logo and icons setup](https://zensical.org/docs/setup/logo-and-icons/)
|
||||
|
||||
## Colors and Accessibility
|
||||
|
||||
- Define color variables once and reuse them for semantic roles (primary, surface, muted, success, warning).
|
||||
- Keep contrast high for body text, code blocks, and nav labels.
|
||||
- Test color changes on mobile and desktop, including search highlights and active nav states.
|
||||
|
||||
!!! info "General references"
|
||||
- [Material Design color guidance](https://m3.material.io/styles/color)
|
||||
- [WCAG overview](https://www.w3.org/WAI/standards-guidelines/wcag/)
|
||||
|
||||
## Icons: Selection and Search Landing Pages
|
||||
|
||||
If your theme supports icon sets through your docs stack, these search portals are useful:
|
||||
|
||||
- [Material Symbols search](https://fonts.google.com/icons)
|
||||
- [Font Awesome icons search](https://fontawesome.com/search)
|
||||
- [Simple Icons search](https://simpleicons.org/)
|
||||
- [Iconify icon set search](https://icon-sets.iconify.design/)
|
||||
- [Lucide icons](https://lucide.dev/icons/)
|
||||
|
||||
!!! tip "Icon family consistency"
|
||||
Pick one primary icon family for navigation and status icons, then document naming conventions.
|
||||
|
||||
## Extending the Theme Safely
|
||||
|
||||
Use overrides as a last step, not the first.
|
||||
|
||||
1. Confirm the requirement cannot be solved by config and CSS.
|
||||
2. Keep override templates minimal and focused.
|
||||
3. Track upstream changes if you override partials.
|
||||
4. Add a visual regression checklist for common pages.
|
||||
|
||||
## Review Checklist
|
||||
|
||||
- Theme changes preserve readability for long-form docs.
|
||||
- Icons are consistent in weight/style and meaningful in context.
|
||||
- Color changes do not break code-block syntax highlighting or search visibility.
|
||||
- Overrides are documented with rationale and owner.
|
||||
@@ -0,0 +1,78 @@
|
||||
# Zensical Features and Configuration Patterns
|
||||
|
||||
Use this reference when deciding which Zensical features to enable and why.
|
||||
|
||||
## Project Bootstrap
|
||||
|
||||
Always start a new docs project with `uv run zensical new`.
|
||||
|
||||
- It creates the baseline scaffolding for configuration, docs structure, and theme integration.
|
||||
- Treat this as the default starting point rather than manually assembling files.
|
||||
|
||||
## High-Value Feature Groups
|
||||
|
||||
### Navigation and Discoverability
|
||||
|
||||
- `navigation.indexes`: lets sections have index pages for overview content.
|
||||
- `navigation.path`: adds breadcrumb-like context.
|
||||
- `navigation.sections`: groups top-level sections for large doc sets.
|
||||
- `navigation.instant`: enables instant internal navigation.
|
||||
- `navigation.instant.prefetch`: prefetches likely next pages.
|
||||
- `navigation.top`: shows a back-to-top affordance.
|
||||
- `navigation.tracking`: keeps URL anchors in sync with active section.
|
||||
|
||||
!!! info "Source links"
|
||||
- [Zensical navigation setup](https://zensical.org/docs/setup/navigation/)
|
||||
|
||||
### Code-Heavy Documentation
|
||||
|
||||
- `content.code.copy`: copy button in code blocks.
|
||||
- `content.code.select`: line range selection support.
|
||||
- `content.code.annotate`: inline code annotations.
|
||||
- Prefer mkdocstrings for generated API reference pages when documenting Python code.
|
||||
- Keep generated API pages linked from hand-authored task and concept docs.
|
||||
|
||||
!!! info "Source links"
|
||||
- [Zensical code blocks](https://zensical.org/docs/authoring/code-blocks/)
|
||||
- [mkdocstrings](https://mkdocstrings.github.io/)
|
||||
|
||||
### Cross-Page UX Consistency
|
||||
|
||||
- `content.tabs.link`: keeps same-named tabs synchronized.
|
||||
- `content.tooltips`: improves tooltip behavior for links.
|
||||
- `content.footnote.tooltips`: inline footnote previews.
|
||||
|
||||
!!! info "Source links"
|
||||
- [Zensical content tabs](https://zensical.org/docs/authoring/content-tabs/)
|
||||
- [Zensical tooltips](https://zensical.org/docs/authoring/tooltips/)
|
||||
- [Zensical footnotes](https://zensical.org/docs/authoring/footnotes/)
|
||||
|
||||
### Search and Content Actions
|
||||
|
||||
- `search.highlight`: highlights matches after search navigation.
|
||||
- `content.action.edit` and `content.action.view` (if repository integration is configured).
|
||||
|
||||
!!! info "Source links"
|
||||
- [Zensical search setup](https://zensical.org/docs/setup/search/)
|
||||
- [Zensical repository setup](https://zensical.org/docs/setup/repository/)
|
||||
|
||||
## Styling and Extensibility
|
||||
|
||||
Use site-level customization when docs need stronger visual affordances.
|
||||
|
||||
- `extra_css`: add targeted style overrides.
|
||||
- `extra_javascript`: add behavior enhancements.
|
||||
- Theme override directory (`custom_dir`) for template-level changes.
|
||||
|
||||
!!! info "Source links"
|
||||
- [Zensical customization overview](https://zensical.org/docs/customization/)
|
||||
- [Additional CSS](https://zensical.org/docs/customization/#additional-css)
|
||||
- [Additional JavaScript](https://zensical.org/docs/customization/#additional-javascript)
|
||||
- [Extending the theme](https://zensical.org/docs/customization/#extending-the-theme)
|
||||
|
||||
## Practical Feature Selection Rules
|
||||
|
||||
1. Start with discoverability and clarity features first.
|
||||
2. For code-heavy docs, add copy/select/annotate first, then define mkdocstrings coverage for API reference.
|
||||
3. Avoid enabling many features at once without measurement.
|
||||
4. Track user success metrics (search success, time-to-answer, support deflection) after each change.
|
||||
@@ -0,0 +1,14 @@
|
||||
.mermaid svg text,
|
||||
.mermaid svg tspan,
|
||||
.mermaid svg foreignObject,
|
||||
.mermaid svg foreignObject div,
|
||||
.mermaid svg foreignObject span,
|
||||
.mermaid svg .nodeLabel,
|
||||
.mermaid svg .edgeLabel {
|
||||
color: var(--md-primary-bg-color, #fff);
|
||||
fill: var(--md-primary-bg-color, #fff);
|
||||
}
|
||||
|
||||
.mermaid svg .treeView-node-label {
|
||||
fill: var(--md-primary-bg-color, #fff) !important;
|
||||
}
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
---
|
||||
icon: lucide/link
|
||||
---
|
||||
|
||||
# URI Contract
|
||||
|
||||
This page defines the canonical resource URI contract, template parameter rules, and compatibility policy.
|
||||
|
||||
## Canonical URI Surface
|
||||
|
||||
The public, preferred URIs are:
|
||||
|
||||
1. `resource://catalog/skills_index`
|
||||
2. `resource://catalog/skills/{skill_id}`
|
||||
3. `resource://skills/{skill_id}/document`
|
||||
4. `resource://skills/{skill_id}/references/{ref_id}`
|
||||
5. `resource://docs/{path*}`
|
||||
|
||||
Contract intent:
|
||||
|
||||
1. Catalog URIs are discovery surfaces.
|
||||
2. Skill URIs are the primary per-skill guidance surfaces.
|
||||
3. The docs wildcard URI is a direct authored-markdown access surface under `docs/`.
|
||||
|
||||
## URI Semantics
|
||||
|
||||
### `resource://catalog/skills_index`
|
||||
|
||||
1. Returns a compact list of skill records for discovery.
|
||||
2. Contains one entry per `skill_id`.
|
||||
3. Includes enough metadata for client-side selection, at minimum `id`, `name`, `description`, `tags`, and `capabilities`.
|
||||
|
||||
### `resource://catalog/skills/{skill_id}`
|
||||
|
||||
1. Returns one normalized record for `skill_id`.
|
||||
2. Includes the canonical document URI and declared reference ids.
|
||||
3. Returns not found when `skill_id` does not exist.
|
||||
|
||||
### `resource://skills/{skill_id}/document`
|
||||
|
||||
1. Returns the canonical `SKILL.md` authored content for that skill.
|
||||
2. `skill_id` must satisfy the stable skill id rules from the content contract.
|
||||
|
||||
### `resource://skills/{skill_id}/references/{ref_id}`
|
||||
|
||||
1. Returns one reference document declared in the skill frontmatter references manifest.
|
||||
2. `ref_id` is the stable public handle for that reference document.
|
||||
|
||||
### `resource://docs/{path*}`
|
||||
|
||||
1. Returns authored markdown at a normalized relative path under `docs/`.
|
||||
2. Supports nested paths via RFC6570 wildcard expansion.
|
||||
3. Typical examples include `index.md`, `usage.md`, `skills/<skill-id>/SKILL.md`, and `skills/<skill-id>/references/<file>.md`.
|
||||
|
||||
## Template Parameter And Validation Rules
|
||||
|
||||
### `skill_id`
|
||||
|
||||
1. Lowercase kebab-case.
|
||||
2. Must satisfy the stable skill id rules from the content contract.
|
||||
|
||||
### `ref_id`
|
||||
|
||||
1. Lowercase kebab-case.
|
||||
2. Must be declared in the skill's references manifest.
|
||||
|
||||
### `path*`
|
||||
|
||||
1. Relative POSIX path only.
|
||||
2. No leading slash.
|
||||
3. No `..` traversal segments.
|
||||
4. Resolves only inside `docs/`.
|
||||
5. Markdown-only in the end state, meaning `.md` files.
|
||||
|
||||
## URI Versioning Policy
|
||||
|
||||
Default rule:
|
||||
|
||||
1. Keep URIs unversioned by default.
|
||||
2. Allow URI and payload updates when they improve clarity or implementation simplicity.
|
||||
|
||||
Breaking-change rule:
|
||||
|
||||
1. Breaking changes use direct replacement of the canonical URI family.
|
||||
2. No compatibility aliases or dual URI families are maintained.
|
||||
|
||||
FastMCP version metadata usage:
|
||||
|
||||
1. Resource `version` metadata may be used for implementation and version discovery.
|
||||
2. URI readability and maintainability remain the primary contract.
|
||||
|
||||
## Reference Id Compatibility Policy
|
||||
|
||||
`ref_id` is the public identifier for a reference document, separate from file path.
|
||||
|
||||
Rules:
|
||||
|
||||
1. Prefer keeping `ref_id` stable when practical.
|
||||
2. File paths may change without URI churn as long as the mapped `ref_id` still resolves.
|
||||
3. If a reference is renamed, introduce a new `ref_id` and treat the old one as retired.
|
||||
4. Avoid reusing retired `ref_id` values for unrelated content.
|
||||
|
||||
## Invariants
|
||||
|
||||
This contract guarantees:
|
||||
|
||||
1. One canonical URI pattern per core capability surface.
|
||||
2. Fast, low-friction URI evolution through direct replacement of canonical URIs.
|
||||
3. A single canonical catalog URI family with no alias maintenance overhead.
|
||||
4. Reference mappings can evolve with minimal churn.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
This contract does not define:
|
||||
|
||||
1. Implementation-specific transform wiring details, such as `VersionFilter`, mounts, or provider composition.
|
||||
2. Migration script mechanics for auto-generating aliases.
|
||||
3. Authorization policy design for URI-level access control.
|
||||
+257
@@ -0,0 +1,257 @@
|
||||
---
|
||||
icon: lucide/workflow
|
||||
---
|
||||
|
||||
# Skill Usage Mechanics
|
||||
|
||||
## Purpose
|
||||
|
||||
This page explains practical usage mechanics for the GitHub Copilot extension in VS Code when `personal-mcp` is configured as an MCP server:
|
||||
|
||||
1. explicit `/` command flows when you want deterministic control
|
||||
2. guided skill loading when relevance can be inferred
|
||||
|
||||
The goal is to show how Copilot behaves as a client and how to shape that behavior.
|
||||
|
||||
## Mental Model
|
||||
|
||||
In Copilot Chat, there are two distinct mechanisms:
|
||||
|
||||
1. `/` commands are user-invoked orchestration shortcuts.
|
||||
2. MCP resources are server-published knowledge units that can be attached as read-only context, while MCP tools provide an execution path for discovery and retrieval.
|
||||
|
||||
In this repository, skill guidance is exposed as MCP resources, not as server-owned prompt execution. Copilot remains the orchestrator.
|
||||
|
||||
## Background Mechanics
|
||||
|
||||
### What the server publishes
|
||||
|
||||
`personal-mcp` registers resources from the validated docs registry and exposes catalog discovery resources:
|
||||
|
||||
1. `resource://catalog/skills_index`
|
||||
2. `resource://catalog/skills/{skill_id}`
|
||||
|
||||
Each skill publishes a canonical Markdown document resource:
|
||||
|
||||
1. `resource://skills/<skill-id>/document`
|
||||
2. `resource://skills/<skill-id>/references/<ref-id>`
|
||||
|
||||
The document payload is loaded from `docs/skills/<skill-id>/SKILL.md` and returned with metadata.
|
||||
|
||||
### What Copilot does as the client
|
||||
|
||||
When connected to MCP, Copilot can do the following at runtime:
|
||||
|
||||
1. interpret the current chat request
|
||||
2. use attached MCP resources that you provide through the chat UI
|
||||
3. invoke MCP tools when the task and tool descriptions make that relevant
|
||||
4. summarize relevant sections into working context
|
||||
5. apply guidance while generating edits or recommendations
|
||||
|
||||
This behavior is shaped by the active chat surface, prompt or instruction guidance, and available MCP tools.
|
||||
|
||||
For reliable progressive discovery, use one of these sequences:
|
||||
|
||||
1. explicit resource path: attach a catalog resource first, then attach only selected skill documents
|
||||
2. tool path: call catalog tools first, then load only selected skill documents
|
||||
|
||||
### What `/` commands do
|
||||
|
||||
`/` commands in VS Code are client-side prompt entry points (for example in prompt files). They do not replace MCP resources. In Copilot, they typically:
|
||||
|
||||
1. enforce a known sequence
|
||||
2. collect missing inputs
|
||||
3. call discovery/read steps in a predictable order
|
||||
|
||||
Think of `/` commands as orchestration shortcuts on top of MCP resources.
|
||||
|
||||
### What automatic loading means here
|
||||
|
||||
In this project, "automatic loading" should be read as a preference you express through instructions and prompts, not as a guaranteed VS Code feature that auto-attaches MCP resources.
|
||||
|
||||
In practice, there are two reliable ways to make skill content available in chat:
|
||||
|
||||
1. explicit resource attachment through `Add Context > MCP Resources` or `MCP: Browse Resources`
|
||||
2. MCP tool invocation using `list_resources`/`read_resource` (ResourcesAsTools), with thin catalog tools as parity fallback
|
||||
|
||||
Instruction quality and metadata quality still matter, because they influence whether Copilot recognizes that the MCP server is relevant and chooses the tool path well.
|
||||
|
||||
## Operating Pattern
|
||||
|
||||
Use both modes intentionally in Copilot Chat.
|
||||
|
||||
### Mode A: Explicit `/` command
|
||||
|
||||
Use when you need predictable, repeatable behavior across teammates.
|
||||
|
||||
Good fits:
|
||||
|
||||
1. onboarding workflows
|
||||
2. compliance-sensitive tasks
|
||||
3. repetitive scaffolding
|
||||
|
||||
### Mode B: Guided skill loading
|
||||
|
||||
Use when requests are varied and you want lower friction during normal chat.
|
||||
|
||||
Good fits:
|
||||
|
||||
1. ad hoc implementation questions
|
||||
2. mixed-topic debugging
|
||||
3. architecture tradeoff discussions
|
||||
|
||||
### Mode C: Fallback flow
|
||||
|
||||
Start with guided loading in chat; escalate to a `/` command when:
|
||||
|
||||
1. confidence is low
|
||||
2. multiple skills conflict
|
||||
3. the user wants strict repeatability
|
||||
|
||||
## Suggested Resolution Flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A[User request in Copilot Chat] --> B{Deterministic workflow needed?}
|
||||
B -- Yes --> C[/Run slash command/]
|
||||
C --> D[Copilot fetches known catalog and skill resources]
|
||||
B -- No --> E[Copilot uses attached resources or catalog tools]
|
||||
E --> F{Confident skill match?}
|
||||
F -- Yes --> G[Copilot fetches skill documents]
|
||||
F -- No --> H[Ask clarifying question or suggest slash command]
|
||||
D --> I[Apply guidance to task]
|
||||
G --> I
|
||||
H --> I
|
||||
```
|
||||
|
||||
## Authoring Requirements For Reliable Matching
|
||||
|
||||
For resource selection or tool-based matching to work well, each skill should have:
|
||||
|
||||
1. precise `description`
|
||||
2. focused `tags`
|
||||
3. explicit `capabilities`
|
||||
4. stable `id` and slug naming
|
||||
|
||||
Weak metadata reduces Copilot match quality and increases wrong context injection.
|
||||
|
||||
## Practical Guidelines
|
||||
|
||||
1. Keep `/` commands minimal and high-value.
|
||||
2. Do not duplicate full methodology text inside command files.
|
||||
3. Keep canonical guidance in `docs/skills/*/SKILL.md`.
|
||||
4. In Copilot instructions, prefer catalog-first discovery before skill fetch.
|
||||
5. Prefer small, relevant context slices over loading every skill.
|
||||
6. Keep slash commands focused on deterministic orchestration, not content duplication.
|
||||
|
||||
If you skip the catalog/index step, behavior is less predictable and may either miss relevant skills or pull too much context.
|
||||
|
||||
## Optional Tool Search Mode
|
||||
|
||||
When tool catalogs grow, FastMCP search transforms can reduce tool-list noise for tool-only clients.
|
||||
|
||||
Runtime switches:
|
||||
|
||||
1. `PERSONAL_MCP_TOOL_SEARCH=none|regex|bm25` (default `none`)
|
||||
2. `PERSONAL_MCP_TOOL_SEARCH_MAX_RESULTS=<positive int>` (default `5`)
|
||||
|
||||
Behavior:
|
||||
|
||||
1. `regex` uses deterministic regex matching for targeted queries.
|
||||
2. `bm25` uses ranked natural-language matching.
|
||||
3. `list_resources` and `read_resource` stay visible so resource-backed fallback remains primary.
|
||||
|
||||
## Copilot Instruction Pattern
|
||||
|
||||
If you want Copilot to use `personal-mcp` skill content more reliably, the instruction file should describe three things clearly:
|
||||
|
||||
1. when MCP-backed skill guidance is relevant
|
||||
2. which retrieval path Copilot should prefer first
|
||||
3. how much skill context it should load before answering
|
||||
|
||||
That matters because instructions can strongly steer discovery behavior, but they do not force VS Code to auto-attach MCP resources. A good instruction tells Copilot to prefer the canonical MCP content path while remaining accurate about the fallback path.
|
||||
|
||||
In this repository, the right policy is:
|
||||
|
||||
1. start from catalog discovery
|
||||
2. prefer MCP resources when the current chat surface exposes resource attachment
|
||||
3. fall back to catalog tools when resource attachment is unavailable
|
||||
4. keep loaded skill context bounded
|
||||
|
||||
Suggested instruction text:
|
||||
|
||||
```md
|
||||
When a task may match a documented implementation pattern from `personal-mcp`:
|
||||
|
||||
1. Start with catalog-first discovery.
|
||||
2. Prefer MCP resources when the chat surface exposes resource attachment.
|
||||
3. If MCP resource attachment is unavailable, use `list_resources`/`read_resource` first, then thin catalog tools if needed.
|
||||
4. Load only the most relevant skill document, or at most 2 skill documents.
|
||||
5. Reconcile loaded skill guidance with the actual repository code before making changes.
|
||||
|
||||
Preferred resource order:
|
||||
|
||||
1. `resource://catalog/skills_index`
|
||||
2. `resource://catalog/skills/{skill_id}`
|
||||
3. `resource://skills/<skill-id>/document`
|
||||
4. `resource://skills/<skill-id>/references/<ref-id>` when needed
|
||||
|
||||
Preferred tool fallback order:
|
||||
|
||||
1. `list_resources`
|
||||
2. `read_resource`
|
||||
3. `search_patterns`
|
||||
4. `get_pattern_by_id`
|
||||
5. `get_skill_document_by_id`
|
||||
|
||||
If confidence is low after discovery, ask one clarifying question before loading more context.
|
||||
```
|
||||
|
||||
This is intentionally guidance, not a guarantee. It gives Copilot a strong policy for when to use resources and when to fall back to discovery tools, while preserving the resource-first architecture.
|
||||
|
||||
## Failure Modes and Recovery
|
||||
|
||||
Common failure modes:
|
||||
|
||||
1. No relevant skill selected.
|
||||
2. Too many skills selected (context bloat).
|
||||
3. Stale assumptions from old metadata.
|
||||
4. Slash command bypasses normal discovery and forces the wrong skill.
|
||||
|
||||
Recovery sequence:
|
||||
|
||||
1. re-run catalog lookup
|
||||
2. narrow by tags and intent
|
||||
3. fetch only top candidates
|
||||
4. if still ambiguous, ask one clarifying question
|
||||
5. use explicit `/` workflow for deterministic fallback
|
||||
|
||||
## Checklist
|
||||
|
||||
Use this checklist when configuring GitHub Copilot in VS Code against `personal-mcp`:
|
||||
|
||||
1. confirm server connectivity
|
||||
2. verify catalog resources are readable
|
||||
3. verify at least one `resource://skills/<id>/document` can be fetched
|
||||
4. add one deterministic `/` command for fallback
|
||||
5. add Copilot instruction: prefer catalog-first discovery, then targeted skill fetch
|
||||
6. verify context size remains bounded
|
||||
7. validate behavior in Ask/Edit/Agent-style workflows with at least one task each
|
||||
|
||||
Suggested instruction policy text:
|
||||
|
||||
1. Start with catalog-first discovery.
|
||||
2. Prefer MCP resources when the chat surface exposes resource attachment.
|
||||
3. Otherwise use tool fallback to load one or two likely skill documents.
|
||||
4. Prefer `list_resources`/`read_resource` first when operating in tool-only clients.
|
||||
5. If confidence is low, ask one clarifying question before loading more.
|
||||
|
||||
## Summary
|
||||
|
||||
The intended model is:
|
||||
|
||||
1. skills are canonical MCP resources
|
||||
2. `/` commands are explicit Copilot control shortcuts
|
||||
3. guided skill loading should be catalog-driven, bounded, and explicit about whether it is using resources or tools
|
||||
|
||||
Using all three together gives predictable control when needed and low-friction assistance by default in VS Code.
|
||||
@@ -3,5 +3,26 @@ name = "prompts"
|
||||
version = "0.1.0"
|
||||
requires-python = ">=3.12"
|
||||
dependencies = [
|
||||
"fastapi>=0.115.0",
|
||||
"fastmcp>=2.10.0",
|
||||
"pydantic-settings>=2.0.0",
|
||||
"pyyaml>=6.0.2",
|
||||
"uvicorn[standard]>=0.34.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"]
|
||||
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"pre-commit>=4.6.0",
|
||||
"ruff>=0.15.18",
|
||||
]
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
# Source Documentation
|
||||
|
||||
Use these links for framework-specific details.
|
||||
|
||||
## FastAPI
|
||||
|
||||
- FastAPI lifespan events: https://fastapi.tiangolo.com/advanced/events/
|
||||
- FastAPI settings and environment variables: https://fastapi.tiangolo.com/advanced/settings/
|
||||
- FastAPI dependencies with yield: https://fastapi.tiangolo.com/tutorial/dependencies/dependencies-with-yield/
|
||||
- FastAPI SQL databases tutorial: https://fastapi.tiangolo.com/tutorial/sql-databases/
|
||||
|
||||
## SQLAlchemy and Alembic
|
||||
|
||||
- SQLAlchemy engine configuration and pooling: https://docs.sqlalchemy.org/en/20/core/engines.html
|
||||
- SQLAlchemy session lifecycle basics: https://docs.sqlalchemy.org/en/20/orm/session_basics.html
|
||||
- Alembic tutorial: https://alembic.sqlalchemy.org/en/latest/tutorial.html
|
||||
|
||||
## Pydantic
|
||||
|
||||
- Pydantic settings management: https://pydantic.dev/docs/validation/latest/concepts/pydantic_settings/
|
||||
|
||||
## NiceGUI
|
||||
|
||||
- NiceGUI pages/routing and FastAPI integration: https://www.nicegui.io/documentation/section_pages_routing
|
||||
- NiceGUI security best practices: https://www.nicegui.io/documentation/section_security
|
||||
|
||||
## LangGraph
|
||||
|
||||
- LangGraph overview: https://docs.langchain.com/oss/python/langgraph/overview
|
||||
- LangGraph quickstart: https://docs.langchain.com/oss/python/langgraph/quickstart
|
||||
- LangGraph workflows and agents: https://docs.langchain.com/oss/python/langgraph/workflows-agents
|
||||
- LangGraph persistence: https://docs.langchain.com/oss/python/langgraph/persistence
|
||||
- LangGraph memory concepts: https://docs.langchain.com/oss/python/concepts/memory
|
||||
- LangGraph streaming: https://docs.langchain.com/oss/python/langgraph/streaming
|
||||
- LangGraph interrupts and human-in-the-loop: https://docs.langchain.com/oss/python/langgraph/interrupts
|
||||
@@ -1,136 +0,0 @@
|
||||
---
|
||||
name: pytest-scaffolding
|
||||
description: "Scaffold a maintainable, hierarchical pytest suite for core functionality first, then extend safely. Use when setting up tests, organizing fixtures by dependency, mirroring src structure in tests, or enforcing fast-by-default test runs."
|
||||
argument-hint: "Target scope (for example: app/services/job, app/ai, or full repo)"
|
||||
---
|
||||
|
||||
# Pytest Scaffolding
|
||||
|
||||
Create test scaffolding that is:
|
||||
- Hierarchical: test layout roughly mirrors source layout.
|
||||
- Fast by default: most tests run in under a second total for core units.
|
||||
- Dependency-aware: slow/external dependencies are isolated behind markers and fixture scope.
|
||||
- Extensible: minimal initial skeleton supports adding detailed tests later without refactors.
|
||||
|
||||
This repository currently uses:
|
||||
- `uv run pytest` as the canonical test invocation.
|
||||
- `pyproject.toml` pytest config under `[tool.pytest.ini_options]`.
|
||||
- strict marker checking (`--strict-markers`).
|
||||
|
||||
Load [pytest references](./references/pytest-docs.md) when you need detailed rules.
|
||||
|
||||
## When To Use
|
||||
- Bootstrapping tests for a new or existing Python repo.
|
||||
- Reorganizing tests that have become flat, slow, or difficult to extend.
|
||||
- Defining fixture boundaries before writing many assertions.
|
||||
- Creating only the first-layer scaffold for core behavior (not exhaustive coverage yet).
|
||||
|
||||
## Inputs To Collect
|
||||
1. Target test scope: full repo, package, or module.
|
||||
2. Dependency profile: pure Python, DB, network/API, filesystem, UI/browser.
|
||||
3. Runtime expectation: what must be instant vs allowed to be slower.
|
||||
4. CI policy: which marker groups must block merges.
|
||||
|
||||
If these are missing, ask concise clarifying questions before editing.
|
||||
|
||||
## Workflow
|
||||
1. Map source tree to test tree.
|
||||
2. Classify tests by dependency cost.
|
||||
3. Create minimal directories and placeholder test modules.
|
||||
4. Create fixture layers (`tests/conftest.py` plus local `conftest.py` in subtrees only when needed).
|
||||
5. Register markers and default selection behavior.
|
||||
6. Run collection and fast path tests.
|
||||
7. Report gaps and next extension points.
|
||||
|
||||
## Step 1: Map Source To Tests
|
||||
Create a mirrored structure rooted at `tests/` that follows major source concepts.
|
||||
|
||||
Example mapping pattern:
|
||||
- `src/app/services/job.py` -> `tests/app/services/test_job.py`
|
||||
- `src/app/ai/graphs/transcription.py` -> `tests/app/ai/graphs/test_transcription.py`
|
||||
- `src/app/api/routes.py` -> `tests/app/api/test_routes.py`
|
||||
|
||||
Rules:
|
||||
- One initial test module per core source module.
|
||||
- Prefer `test_<module>.py` naming.
|
||||
- Keep directory mirrors shallow first; add deeper modules only where behavior is complex.
|
||||
|
||||
## Step 2: Classify By Dependency Cost
|
||||
Assign each test module to one initial class:
|
||||
- `unit`: no DB/network/filesystem side effects; instant execution.
|
||||
- `integration`: touches DB, HTTP stack, workflow runtime, or external services.
|
||||
- `smoke`: thin end-to-end confidence checks.
|
||||
|
||||
Decision logic:
|
||||
- If logic can run with fakes/stubs, make it `unit`.
|
||||
- If contract with framework/DB is essential, make it `integration`.
|
||||
- If validating a user-critical path across layers, make it `smoke`.
|
||||
|
||||
## Step 3: Scaffold Minimal Test Modules
|
||||
For each target module, scaffold:
|
||||
- import section
|
||||
- one happy-path test function
|
||||
- one error/edge test function
|
||||
- TODO comments indicating detail expansion points
|
||||
|
||||
Keep assertions minimal but behavior-focused. Avoid large fixtures in module files.
|
||||
|
||||
## Step 4: Fixture Layering Strategy
|
||||
Use fixture scopes based on cost:
|
||||
- `function` scope by default.
|
||||
- broader scopes (`module`/`session`) only for expensive setup with clear teardown.
|
||||
|
||||
Layer fixtures by directory:
|
||||
- `tests/conftest.py`: global, lightweight fixtures only (factories, deterministic defaults).
|
||||
- subtree `conftest.py`: domain-specific fixtures (API client, DB session, AI runtime stubs).
|
||||
|
||||
Guidelines:
|
||||
- Prefer yield fixtures for setup/teardown.
|
||||
- Keep fixtures atomic (one state-changing responsibility per fixture).
|
||||
- Avoid autouse except for truly universal behavior.
|
||||
|
||||
## Step 5: Marker Taxonomy And Config
|
||||
Ensure marker names are explicit and registered in `pyproject.toml` because strict markers are enabled.
|
||||
|
||||
Recommended baseline markers:
|
||||
- `unit`
|
||||
- `integration`
|
||||
- `smoke`
|
||||
- `slow`
|
||||
- `external` (requires network/service credentials)
|
||||
|
||||
Default run strategy:
|
||||
- Fast local path: run only `unit` by default in day-to-day iteration.
|
||||
- Full validation path: run all markers in CI or pre-release checks.
|
||||
|
||||
## Step 6: Execution And Verification
|
||||
Run commands in this order:
|
||||
1. `uv run pytest --collect-only -q`
|
||||
2. `uv run pytest -m unit -q`
|
||||
3. `uv run pytest -q` (if dependencies are available)
|
||||
|
||||
Optional targeted runs:
|
||||
- by node id for one test
|
||||
- by `-k` expression for focused iteration
|
||||
|
||||
## Step 7: Completion Checks
|
||||
A scaffold pass is complete when all are true:
|
||||
1. Every core source area has at least one corresponding test module.
|
||||
2. Unit tests run quickly and deterministically.
|
||||
3. Integration/external tests are isolated by marker and fixture boundaries.
|
||||
4. No unregistered marker warnings/errors.
|
||||
5. `tests/` structure is understandable without extra documentation.
|
||||
6. A clear TODO path exists for deepening assertions later.
|
||||
|
||||
## Branching Scenarios
|
||||
- If external APIs are required: provide stubs/mocks for unit tests; guard real calls behind `external` marker.
|
||||
- If DB is required: build a dedicated integration fixture layer and keep unit tests DB-free.
|
||||
- If tests become slow: split slow tests via marker and widen fixture scope only where safe.
|
||||
- If naming conflicts appear: keep unique test module names or package test directories explicitly.
|
||||
|
||||
## Output Format
|
||||
When applying this skill, provide:
|
||||
1. Proposed test tree diff.
|
||||
2. Marker and fixture plan.
|
||||
3. Exact commands for fast path and full path.
|
||||
4. Risks/open questions before writing detailed assertions.
|
||||
@@ -1,22 +0,0 @@
|
||||
# Pytest Documentation Notes
|
||||
|
||||
Primary references used:
|
||||
- https://docs.pytest.org/en/stable/explanation/goodpractices.html
|
||||
- https://docs.pytest.org/en/stable/how-to/fixtures.html
|
||||
- https://docs.pytest.org/en/stable/example/markers.html
|
||||
- https://docs.pytest.org/en/stable/reference/customize.html
|
||||
- https://docs.pytest.org/en/stable/explanation/flaky.html
|
||||
|
||||
## Practical Guidance For This Skill
|
||||
- Use src-aligned test layout and keep test discovery conventional.
|
||||
- Keep fixtures small, composable, and explicit; use `yield` for teardown.
|
||||
- Register custom markers and keep strict marker validation on.
|
||||
- Separate quick unit runs from slower integration/external runs.
|
||||
- Minimize flakiness by controlling shared state and avoiding hidden dependencies.
|
||||
- Use `--collect-only` and marker-filtered runs to validate scaffold quality early.
|
||||
|
||||
## Commands Worth Remembering
|
||||
- `uv run pytest --collect-only -q`
|
||||
- `uv run pytest -m unit -q`
|
||||
- `uv run pytest -m "not external" -q`
|
||||
- `uv run pytest -q`
|
||||
@@ -0,0 +1 @@
|
||||
"""Personal MCP server package."""
|
||||
@@ -0,0 +1,13 @@
|
||||
from personal_mcp.catalog.server import (
|
||||
build_skill_detail_payload,
|
||||
build_skills_index_payload,
|
||||
get_pattern_by_id_payload,
|
||||
search_patterns_payload,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"build_skill_detail_payload",
|
||||
"build_skills_index_payload",
|
||||
"get_pattern_by_id_payload",
|
||||
"search_patterns_payload",
|
||||
]
|
||||
@@ -0,0 +1,173 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from personal_mcp.skills.document_loader import DocsRegistry, SkillRecord
|
||||
|
||||
DEFAULT_LIMIT = 20
|
||||
MAX_LIMIT = 100
|
||||
|
||||
|
||||
def _pattern_payload(skill: SkillRecord) -> dict[str, Any]:
|
||||
return {
|
||||
"id": skill.skill_id,
|
||||
"name": skill.name,
|
||||
"version": skill.version,
|
||||
"description": skill.description,
|
||||
"tags": list(skill.tags),
|
||||
"depends_on": list(skill.depends_on),
|
||||
"capabilities": list(skill.capabilities),
|
||||
"resources": list(skill.capabilities),
|
||||
}
|
||||
|
||||
|
||||
def _summary_payload(skill: SkillRecord) -> dict[str, Any]:
|
||||
return {
|
||||
"id": skill.skill_id,
|
||||
"name": skill.name,
|
||||
"description": skill.description,
|
||||
"tags": list(skill.tags),
|
||||
"capabilities": list(skill.capabilities),
|
||||
"version": skill.version,
|
||||
"document_uri": skill.document_uri,
|
||||
"detail_uri": f"resource://catalog/skills/{skill.skill_id}",
|
||||
"resources": {
|
||||
"document": skill.document_uri,
|
||||
"references": [
|
||||
f"resource://skills/{skill.skill_id}/references/{ref_id}"
|
||||
for ref_id in sorted(skill.references)
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _skill_matches(
|
||||
skill: SkillRecord,
|
||||
*,
|
||||
query: str | None,
|
||||
tag: str | None,
|
||||
capability: str | None,
|
||||
) -> bool:
|
||||
if query:
|
||||
lowered = query.strip().lower()
|
||||
if lowered:
|
||||
haystack = " ".join(
|
||||
[
|
||||
skill.skill_id,
|
||||
skill.name,
|
||||
skill.description,
|
||||
" ".join(skill.tags),
|
||||
]
|
||||
).lower()
|
||||
terms = [term for term in lowered.replace("-", " ").split() if term]
|
||||
if any(term not in haystack for term in terms):
|
||||
return False
|
||||
|
||||
if tag and tag not in skill.tags:
|
||||
return False
|
||||
|
||||
if capability and capability not in skill.capabilities:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def build_skills_index_payload(
|
||||
registry: DocsRegistry,
|
||||
*,
|
||||
query: str | None = None,
|
||||
tag: str | None = None,
|
||||
capability: str | None = None,
|
||||
cursor: str | None = None,
|
||||
limit: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
normalized_limit = DEFAULT_LIMIT if limit is None else max(1, min(limit, MAX_LIMIT))
|
||||
try:
|
||||
start = 0 if cursor is None else max(0, int(cursor))
|
||||
except ValueError as exc:
|
||||
raise ValueError("cursor must be an integer string") from exc
|
||||
|
||||
ordered = [
|
||||
registry.skills_by_id[skill_id] for skill_id in registry.skills_in_load_order
|
||||
]
|
||||
matches = [
|
||||
skill
|
||||
for skill in ordered
|
||||
if _skill_matches(skill, query=query, tag=tag, capability=capability)
|
||||
]
|
||||
|
||||
page = matches[start : start + normalized_limit]
|
||||
next_cursor = start + normalized_limit
|
||||
|
||||
return {
|
||||
"skills": [_summary_payload(skill) for skill in page],
|
||||
"total": len(matches),
|
||||
"cursor": str(start),
|
||||
"limit": normalized_limit,
|
||||
"next_cursor": str(next_cursor) if next_cursor < len(matches) else None,
|
||||
}
|
||||
|
||||
|
||||
def build_skill_detail_payload(registry: DocsRegistry, skill_id: str) -> dict[str, Any]:
|
||||
if skill_id not in registry.skills_by_id:
|
||||
raise KeyError(skill_id)
|
||||
|
||||
skill = registry.skills_by_id[skill_id]
|
||||
return {
|
||||
"id": skill.skill_id,
|
||||
"name": skill.name,
|
||||
"description": skill.description,
|
||||
"version": skill.version,
|
||||
"tags": list(skill.tags),
|
||||
"depends_on": list(skill.depends_on),
|
||||
"capabilities": list(skill.capabilities),
|
||||
"resources": {
|
||||
"document": skill.document_uri,
|
||||
"references": {
|
||||
ref_id: {
|
||||
"uri": ref.uri,
|
||||
"mime_type": ref.mime_type,
|
||||
"title": ref.title,
|
||||
"path": ref.relpath,
|
||||
}
|
||||
for ref_id, ref in sorted(skill.references.items())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def search_patterns_payload(
|
||||
registry: DocsRegistry,
|
||||
*,
|
||||
query: str = "",
|
||||
tags: list[str] | None = None,
|
||||
skip: int = 0,
|
||||
limit: int = DEFAULT_LIMIT,
|
||||
) -> dict[str, Any]:
|
||||
normalized_skip = max(skip, 0)
|
||||
normalized_limit = max(1, min(limit, MAX_LIMIT))
|
||||
|
||||
requested_tags = [tag.strip() for tag in (tags or []) if tag and tag.strip()]
|
||||
|
||||
matches: list[SkillRecord] = []
|
||||
for skill_id in registry.skills_in_load_order:
|
||||
skill = registry.skills_by_id[skill_id]
|
||||
if not _skill_matches(skill, query=query, tag=None, capability=None):
|
||||
continue
|
||||
if requested_tags and any(tag not in skill.tags for tag in requested_tags):
|
||||
continue
|
||||
matches.append(skill)
|
||||
|
||||
page = matches[normalized_skip : normalized_skip + normalized_limit]
|
||||
return {
|
||||
"patterns": [_pattern_payload(skill) for skill in page],
|
||||
"total": len(matches),
|
||||
"skip": normalized_skip,
|
||||
"limit": normalized_limit,
|
||||
}
|
||||
|
||||
|
||||
def get_pattern_by_id_payload(registry: DocsRegistry, skill_id: str) -> dict[str, Any]:
|
||||
if skill_id not in registry.skills_by_id:
|
||||
return {"found": False, "id": skill_id}
|
||||
return {"found": True, "pattern": _pattern_payload(registry.skills_by_id[skill_id])}
|
||||
@@ -0,0 +1,13 @@
|
||||
from personal_mcp.mcp import mcp
|
||||
from personal_mcp.web.app import app
|
||||
|
||||
__all__ = ["app", "main", "mcp"]
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Run the root MCP server."""
|
||||
mcp.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,188 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from fastmcp import FastMCP
|
||||
from fastmcp.server.transforms import ResourcesAsTools
|
||||
from fastmcp.server.transforms.search import BM25SearchTransform, RegexSearchTransform
|
||||
|
||||
from personal_mcp.catalog.server import (
|
||||
build_skill_detail_payload,
|
||||
build_skills_index_payload,
|
||||
get_pattern_by_id_payload,
|
||||
search_patterns_payload,
|
||||
)
|
||||
from personal_mcp.skills.document_loader import (
|
||||
DocsRegistry,
|
||||
load_docs_registry,
|
||||
read_docs_markdown_path,
|
||||
read_skill_document,
|
||||
read_skill_reference,
|
||||
)
|
||||
|
||||
DOCS_ROOT = os.getenv("PERSONAL_MCP_DOCS_ROOT", "../../docs")
|
||||
TOOL_SEARCH_MODE = os.getenv("PERSONAL_MCP_TOOL_SEARCH", "none").strip().lower()
|
||||
TOOL_SEARCH_MAX_RESULTS = os.getenv("PERSONAL_MCP_TOOL_SEARCH_MAX_RESULTS", "5")
|
||||
REGISTRY: DocsRegistry = load_docs_registry(
|
||||
package_anchor="personal_mcp",
|
||||
docs_root=DOCS_ROOT,
|
||||
)
|
||||
|
||||
mcp = FastMCP("personal-mcp", on_duplicate="error")
|
||||
|
||||
|
||||
def _parse_positive_int(value: str, *, env_name: str) -> int:
|
||||
try:
|
||||
parsed = int(value)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"{env_name} must be an integer") from exc
|
||||
if parsed <= 0:
|
||||
raise ValueError(f"{env_name} must be greater than zero")
|
||||
return parsed
|
||||
|
||||
|
||||
def _install_tool_fallback_transforms() -> None:
|
||||
# Expose list_resources/read_resource for tool-only clients.
|
||||
mcp.add_transform(ResourcesAsTools(mcp))
|
||||
|
||||
if TOOL_SEARCH_MODE in {"", "none"}:
|
||||
return
|
||||
|
||||
max_results = _parse_positive_int(
|
||||
TOOL_SEARCH_MAX_RESULTS,
|
||||
env_name="PERSONAL_MCP_TOOL_SEARCH_MAX_RESULTS",
|
||||
)
|
||||
kwargs: dict[str, Any] = {
|
||||
"max_results": max_results,
|
||||
"always_visible": ["list_resources", "read_resource"],
|
||||
}
|
||||
|
||||
if TOOL_SEARCH_MODE == "regex":
|
||||
mcp.add_transform(RegexSearchTransform(**kwargs))
|
||||
return
|
||||
if TOOL_SEARCH_MODE == "bm25":
|
||||
mcp.add_transform(BM25SearchTransform(**kwargs))
|
||||
return
|
||||
|
||||
raise ValueError(
|
||||
"PERSONAL_MCP_TOOL_SEARCH must be one of: none, regex, bm25"
|
||||
)
|
||||
|
||||
|
||||
def _ro_annotations() -> dict[str, bool]:
|
||||
return {
|
||||
"readOnlyHint": True,
|
||||
"idempotentHint": True,
|
||||
}
|
||||
|
||||
|
||||
@mcp.resource(
|
||||
"resource://catalog/skills_index",
|
||||
mime_type="application/json",
|
||||
tags={"catalog"},
|
||||
annotations=_ro_annotations(),
|
||||
)
|
||||
def skills_index() -> dict[str, Any]:
|
||||
return build_skills_index_payload(REGISTRY)
|
||||
|
||||
|
||||
@mcp.resource(
|
||||
"resource://catalog/skills_index{?q,tag,capability,cursor,limit}",
|
||||
mime_type="application/json",
|
||||
tags={"catalog"},
|
||||
annotations=_ro_annotations(),
|
||||
)
|
||||
def skills_index_query(
|
||||
q: str | None = None,
|
||||
tag: str | None = None,
|
||||
capability: str | None = None,
|
||||
cursor: str | None = None,
|
||||
limit: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return build_skills_index_payload(
|
||||
REGISTRY,
|
||||
query=q,
|
||||
tag=tag,
|
||||
capability=capability,
|
||||
cursor=cursor,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@mcp.resource(
|
||||
"resource://catalog/skills/{skill_id}",
|
||||
mime_type="application/json",
|
||||
tags={"catalog"},
|
||||
annotations=_ro_annotations(),
|
||||
)
|
||||
def skill_detail(skill_id: str) -> dict[str, Any]:
|
||||
return build_skill_detail_payload(REGISTRY, skill_id)
|
||||
|
||||
|
||||
@mcp.resource(
|
||||
"resource://skills/{skill_id}/document",
|
||||
mime_type="text/markdown",
|
||||
tags={"skill-doc"},
|
||||
annotations=_ro_annotations(),
|
||||
)
|
||||
def skill_document(skill_id: str) -> dict[str, str]:
|
||||
return read_skill_document(REGISTRY, skill_id)
|
||||
|
||||
|
||||
@mcp.resource(
|
||||
"resource://skills/{skill_id}/references/{ref_id}",
|
||||
mime_type="text/markdown",
|
||||
tags={"reference"},
|
||||
annotations=_ro_annotations(),
|
||||
)
|
||||
def skill_reference(skill_id: str, ref_id: str) -> dict[str, str]:
|
||||
return read_skill_reference(REGISTRY, skill_id=skill_id, ref_id=ref_id)
|
||||
|
||||
|
||||
@mcp.resource(
|
||||
"resource://docs/{path*}",
|
||||
mime_type="text/markdown",
|
||||
tags={"docs"},
|
||||
annotations=_ro_annotations(),
|
||||
)
|
||||
def docs_markdown(path: str) -> dict[str, str]:
|
||||
return read_docs_markdown_path(REGISTRY, path)
|
||||
|
||||
|
||||
@mcp.tool
|
||||
def search_patterns(
|
||||
query: str = "",
|
||||
tags: list[str] | None = None,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
) -> dict[str, Any]:
|
||||
"""Search normalized pattern metadata with optional tags and pagination."""
|
||||
return search_patterns_payload(
|
||||
REGISTRY,
|
||||
query=query,
|
||||
tags=tags,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool
|
||||
def get_pattern_by_id(id: str) -> dict[str, Any]:
|
||||
"""Return one normalized pattern by stable id."""
|
||||
return get_pattern_by_id_payload(REGISTRY, id)
|
||||
|
||||
|
||||
@mcp.tool
|
||||
def get_skill_document_by_id(skill_id: str) -> dict[str, Any]:
|
||||
"""Return the canonical skill document payload for a stable skill id."""
|
||||
if skill_id not in REGISTRY.skills_by_id:
|
||||
return {"found": False, "id": skill_id}
|
||||
|
||||
return {
|
||||
"found": True,
|
||||
"document": read_skill_document(REGISTRY, skill_id),
|
||||
}
|
||||
|
||||
|
||||
_install_tool_fallback_transforms()
|
||||
@@ -0,0 +1 @@
|
||||
"""Docs registry and markdown loading utilities for personal MCP skills."""
|
||||
@@ -0,0 +1,610 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from importlib.resources import files
|
||||
from importlib.resources.abc import Traversable
|
||||
from pathlib import PurePosixPath
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
|
||||
|
||||
SKILL_ID_RE = re.compile(r"^[a-z][a-z0-9-]*$")
|
||||
SEMVER_RE = re.compile(
|
||||
r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+][0-9A-Za-z.-]+)?$"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RegistryIssue:
|
||||
code: str
|
||||
message: str
|
||||
skill_id: str | None
|
||||
path: str
|
||||
hint: str
|
||||
|
||||
|
||||
class DocsRegistryValidationError(Exception):
|
||||
def __init__(self, errors: list[RegistryIssue]) -> None:
|
||||
self.errors = errors
|
||||
summary = "\n".join(
|
||||
[
|
||||
(
|
||||
f"{issue.code}: {issue.message} "
|
||||
f"(skill={issue.skill_id or 'unknown'}, path={issue.path})"
|
||||
)
|
||||
for issue in errors
|
||||
]
|
||||
)
|
||||
super().__init__(summary)
|
||||
|
||||
|
||||
class ReferenceEntry(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
|
||||
|
||||
path: str
|
||||
mime_type: str = "text/markdown"
|
||||
title: str | None = None
|
||||
|
||||
@field_validator("path")
|
||||
@classmethod
|
||||
def validate_reference_path(cls, value: str) -> str:
|
||||
path = PurePosixPath(value)
|
||||
if path.is_absolute() or ".." in path.parts:
|
||||
raise ValueError("reference path must be a relative in-skill path")
|
||||
if not str(path).startswith("references/"):
|
||||
raise ValueError("reference path must stay under references/")
|
||||
if path.suffix.lower() != ".md":
|
||||
raise ValueError("reference path must target a markdown file")
|
||||
return path.as_posix()
|
||||
|
||||
|
||||
class PersonalMcpMetadata(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
|
||||
|
||||
id: str
|
||||
version: str
|
||||
tags: list[str] = Field(default_factory=list)
|
||||
capabilities: list[str] = Field(min_length=1)
|
||||
depends_on: list[str] = Field(default_factory=list)
|
||||
references: dict[str, ReferenceEntry] = Field(default_factory=dict)
|
||||
|
||||
@field_validator("id")
|
||||
@classmethod
|
||||
def validate_id(cls, value: str) -> str:
|
||||
if not SKILL_ID_RE.fullmatch(value):
|
||||
raise ValueError("id must be lowercase kebab-case and start with a letter")
|
||||
return value
|
||||
|
||||
@field_validator("version")
|
||||
@classmethod
|
||||
def validate_version(cls, value: str) -> str:
|
||||
if not SEMVER_RE.fullmatch(value):
|
||||
raise ValueError("version must be semver")
|
||||
return value
|
||||
|
||||
@field_validator("tags")
|
||||
@classmethod
|
||||
def validate_tags(cls, value: list[str]) -> list[str]:
|
||||
for tag in value:
|
||||
if not SKILL_ID_RE.fullmatch(tag):
|
||||
raise ValueError(f"invalid tag: {tag}")
|
||||
return value
|
||||
|
||||
@field_validator("depends_on")
|
||||
@classmethod
|
||||
def validate_depends_on(cls, value: list[str]) -> list[str]:
|
||||
for dep in value:
|
||||
if not SKILL_ID_RE.fullmatch(dep):
|
||||
raise ValueError(f"invalid depends_on skill id: {dep}")
|
||||
return value
|
||||
|
||||
@field_validator("references")
|
||||
@classmethod
|
||||
def validate_reference_ids(
|
||||
cls, value: dict[str, ReferenceEntry]
|
||||
) -> dict[str, ReferenceEntry]:
|
||||
for ref_id in value:
|
||||
if not SKILL_ID_RE.fullmatch(ref_id):
|
||||
raise ValueError(f"invalid reference id: {ref_id}")
|
||||
return value
|
||||
|
||||
|
||||
class SkillFrontmatter(BaseModel):
|
||||
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
|
||||
|
||||
name: str = Field(min_length=1, max_length=64)
|
||||
description: str = Field(min_length=1, max_length=1024)
|
||||
when_to_use: str | None = None
|
||||
allowed_tools: str | list[str] | None = Field(default=None, alias="allowed-tools")
|
||||
disallowed_tools: str | list[str] | None = Field(
|
||||
default=None,
|
||||
alias="disallowed-tools",
|
||||
)
|
||||
disable_model_invocation: bool | None = Field(
|
||||
default=None,
|
||||
alias="disable-model-invocation",
|
||||
)
|
||||
user_invocable: bool | None = Field(default=None, alias="user-invocable")
|
||||
argument_hint: str | None = Field(default=None, alias="argument-hint")
|
||||
arguments: str | list[str] | None = None
|
||||
license: str | None = None
|
||||
compatibility: str | None = None
|
||||
metadata: dict[str, str] | None = None
|
||||
x_personal_mcp: PersonalMcpMetadata = Field(alias="x-personal-mcp")
|
||||
|
||||
@field_validator("name")
|
||||
@classmethod
|
||||
def validate_name(cls, value: str) -> str:
|
||||
if not SKILL_ID_RE.fullmatch(value):
|
||||
raise ValueError(
|
||||
"name must be lowercase kebab-case and start with a letter"
|
||||
)
|
||||
if "anthropic" in value or "claude" in value:
|
||||
raise ValueError("name must not contain reserved words anthropic or claude")
|
||||
return value
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReferenceRecord:
|
||||
ref_id: str
|
||||
uri: str
|
||||
relpath: str
|
||||
mime_type: str
|
||||
title: str | None
|
||||
content: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SkillRecord:
|
||||
skill_id: str
|
||||
name: str
|
||||
description: str
|
||||
version: str
|
||||
tags: tuple[str, ...]
|
||||
capabilities: tuple[str, ...]
|
||||
depends_on: tuple[str, ...]
|
||||
document_uri: str
|
||||
document_relpath: str
|
||||
document_content: str
|
||||
references: dict[str, ReferenceRecord]
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SkillSummaryRecord:
|
||||
skill_id: str
|
||||
name: str
|
||||
description: str
|
||||
tags: tuple[str, ...]
|
||||
capabilities: tuple[str, ...]
|
||||
document_uri: str
|
||||
version: str
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class DocsRegistry:
|
||||
skills_by_id: dict[str, SkillRecord]
|
||||
skills_in_load_order: tuple[str, ...]
|
||||
skills_summary_in_load_order: tuple[SkillSummaryRecord, ...]
|
||||
docs_markdown_by_path: dict[str, str]
|
||||
docs_markdown_path_index: tuple[str, ...]
|
||||
tag_to_skill_ids: dict[str, tuple[str, ...]]
|
||||
capability_to_skill_ids: dict[str, tuple[str, ...]]
|
||||
|
||||
|
||||
def _parse_frontmatter(markdown: str, *, path: str) -> tuple[dict[str, Any], str]:
|
||||
if not markdown.startswith("---"):
|
||||
raise ValueError(f"missing YAML frontmatter: {path}")
|
||||
|
||||
lines = markdown.splitlines()
|
||||
if len(lines) < 3 or lines[0].strip() != "---":
|
||||
raise ValueError(f"invalid YAML frontmatter start: {path}")
|
||||
|
||||
end_index: int | None = None
|
||||
for i in range(1, len(lines)):
|
||||
if lines[i].strip() == "---":
|
||||
end_index = i
|
||||
break
|
||||
|
||||
if end_index is None:
|
||||
raise ValueError(f"missing YAML frontmatter terminator: {path}")
|
||||
|
||||
raw_yaml = "\n".join(lines[1:end_index])
|
||||
body = "\n".join(lines[end_index + 1 :])
|
||||
parsed = yaml.safe_load(raw_yaml)
|
||||
if not isinstance(parsed, dict):
|
||||
raise ValueError(f"frontmatter must parse to an object: {path}")
|
||||
return parsed, body
|
||||
|
||||
|
||||
def _walk_markdown(
|
||||
node: Traversable,
|
||||
*,
|
||||
prefix: PurePosixPath = PurePosixPath(""),
|
||||
) -> list[tuple[str, Traversable]]:
|
||||
results: list[tuple[str, Traversable]] = []
|
||||
for child in sorted(node.iterdir(), key=lambda item: item.name):
|
||||
relpath = prefix.joinpath(child.name)
|
||||
if child.is_dir():
|
||||
results.extend(_walk_markdown(child, prefix=relpath))
|
||||
continue
|
||||
if not child.is_file() or not child.name.lower().endswith(".md"):
|
||||
continue
|
||||
results.append((relpath.as_posix(), child))
|
||||
return results
|
||||
|
||||
|
||||
def _validate_skill_frontmatter(
|
||||
raw: dict[str, Any], *, skill_dir_name: str
|
||||
) -> SkillFrontmatter:
|
||||
model = SkillFrontmatter.model_validate(raw)
|
||||
if model.name != skill_dir_name:
|
||||
raise ValueError("frontmatter name must exactly match skill directory name")
|
||||
if model.x_personal_mcp.id != model.name:
|
||||
raise ValueError("x-personal-mcp.id must exactly match name")
|
||||
expected_capability = f"resource://skills/{model.name}/document"
|
||||
if expected_capability not in model.x_personal_mcp.capabilities:
|
||||
raise ValueError(f"capabilities must include {expected_capability}")
|
||||
return model
|
||||
|
||||
|
||||
def _normalize_docs_path(path: str) -> str:
|
||||
normalized = PurePosixPath(path)
|
||||
if normalized.is_absolute() or ".." in normalized.parts:
|
||||
raise ValueError("path must be a normalized docs-relative path")
|
||||
if normalized.suffix.lower() != ".md":
|
||||
raise ValueError("path must point to a markdown file")
|
||||
return normalized.as_posix()
|
||||
|
||||
|
||||
def _title_from_reference_filename(filename: str) -> str:
|
||||
stem = PurePosixPath(filename).stem
|
||||
normalized = stem.replace("-", " ").replace("_", " ").split()
|
||||
if not normalized:
|
||||
return stem
|
||||
return " ".join(token.capitalize() for token in normalized)
|
||||
|
||||
|
||||
def _reference_id_from_filename(filename: str) -> str | None:
|
||||
stem = PurePosixPath(filename).stem.strip().lower().replace("_", "-")
|
||||
normalized = re.sub(r"[^a-z0-9-]+", "-", stem)
|
||||
normalized = re.sub(r"-+", "-", normalized).strip("-")
|
||||
if not normalized:
|
||||
return None
|
||||
if not SKILL_ID_RE.fullmatch(normalized):
|
||||
return None
|
||||
return normalized
|
||||
|
||||
|
||||
def _discover_top_level_references(
|
||||
*,
|
||||
skill_dir: Traversable,
|
||||
) -> dict[str, ReferenceEntry]:
|
||||
references_dir = skill_dir.joinpath("references")
|
||||
if not references_dir.is_dir():
|
||||
return {}
|
||||
|
||||
discovered: dict[str, ReferenceEntry] = {}
|
||||
for child in sorted(references_dir.iterdir(), key=lambda item: item.name):
|
||||
if child.is_dir() or not child.is_file():
|
||||
continue
|
||||
if not child.name.lower().endswith(".md"):
|
||||
continue
|
||||
|
||||
ref_id = _reference_id_from_filename(child.name)
|
||||
if ref_id is None:
|
||||
continue
|
||||
|
||||
discovered[ref_id] = ReferenceEntry(
|
||||
path=PurePosixPath("references").joinpath(child.name).as_posix(),
|
||||
title=_title_from_reference_filename(child.name),
|
||||
)
|
||||
return discovered
|
||||
|
||||
|
||||
def _ensure_no_cycles(skills_by_id: dict[str, SkillRecord]) -> list[tuple[str, str]]:
|
||||
visiting: set[str] = set()
|
||||
visited: set[str] = set()
|
||||
cycles: list[tuple[str, str]] = []
|
||||
|
||||
def walk(skill_id: str, stack: list[str]) -> None:
|
||||
if skill_id in visited:
|
||||
return
|
||||
if skill_id in visiting:
|
||||
cycle_from = stack[stack.index(skill_id) :]
|
||||
cycles.append((skill_id, " -> ".join(cycle_from + [skill_id])))
|
||||
return
|
||||
|
||||
visiting.add(skill_id)
|
||||
stack.append(skill_id)
|
||||
for dep in skills_by_id[skill_id].depends_on:
|
||||
if dep in skills_by_id:
|
||||
walk(dep, stack)
|
||||
stack.pop()
|
||||
visiting.remove(skill_id)
|
||||
visited.add(skill_id)
|
||||
|
||||
for skill_id in sorted(skills_by_id):
|
||||
walk(skill_id, [])
|
||||
return cycles
|
||||
|
||||
|
||||
def load_docs_registry(
|
||||
*,
|
||||
package_anchor: str,
|
||||
docs_root: str = "docs",
|
||||
) -> DocsRegistry:
|
||||
docs_dir = files(package_anchor).joinpath(docs_root)
|
||||
issues: list[RegistryIssue] = []
|
||||
|
||||
if not docs_dir.is_dir():
|
||||
raise DocsRegistryValidationError(
|
||||
[
|
||||
RegistryIssue(
|
||||
code="missing_docs_root",
|
||||
message="docs root directory does not exist",
|
||||
skill_id=None,
|
||||
path=docs_root,
|
||||
hint="configure docs_root to a valid packaged docs path",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
docs_markdown_by_path: dict[str, str] = {}
|
||||
for relpath, doc_file in _walk_markdown(docs_dir):
|
||||
docs_markdown_by_path[relpath] = doc_file.read_text(encoding="utf-8")
|
||||
|
||||
skills_root = docs_dir.joinpath("skills")
|
||||
if not skills_root.is_dir():
|
||||
raise DocsRegistryValidationError(
|
||||
[
|
||||
RegistryIssue(
|
||||
code="missing_skills_root",
|
||||
message="skills directory does not exist under docs root",
|
||||
skill_id=None,
|
||||
path=f"{docs_root}/skills",
|
||||
hint="ensure docs/skills is included in packaged docs",
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
skills_by_id: dict[str, SkillRecord] = {}
|
||||
summaries: list[SkillSummaryRecord] = []
|
||||
|
||||
for skill_dir in sorted(skills_root.iterdir(), key=lambda item: item.name):
|
||||
if not skill_dir.is_dir():
|
||||
continue
|
||||
|
||||
skill_dir_name = skill_dir.name
|
||||
skill_rel_root = PurePosixPath("skills").joinpath(skill_dir_name)
|
||||
skill_doc_relpath = skill_rel_root.joinpath("SKILL.md").as_posix()
|
||||
skill_doc_file = skill_dir.joinpath("SKILL.md")
|
||||
|
||||
if not skill_doc_file.is_file():
|
||||
issues.append(
|
||||
RegistryIssue(
|
||||
code="missing_skill_document",
|
||||
message="missing required SKILL.md",
|
||||
skill_id=skill_dir_name,
|
||||
path=skill_doc_relpath,
|
||||
hint="add docs/skills/<skill-id>/SKILL.md",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
skill_markdown = skill_doc_file.read_text(encoding="utf-8")
|
||||
try:
|
||||
raw_frontmatter, _ = _parse_frontmatter(
|
||||
skill_markdown,
|
||||
path=skill_doc_relpath,
|
||||
)
|
||||
frontmatter = _validate_skill_frontmatter(
|
||||
raw_frontmatter,
|
||||
skill_dir_name=skill_dir_name,
|
||||
)
|
||||
except (ValueError, ValidationError) as exc:
|
||||
issues.append(
|
||||
RegistryIssue(
|
||||
code="invalid_frontmatter",
|
||||
message=str(exc),
|
||||
skill_id=skill_dir_name,
|
||||
path=skill_doc_relpath,
|
||||
hint="fix SKILL.md YAML frontmatter to match the contract",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
effective_reference_entries = _discover_top_level_references(skill_dir=skill_dir)
|
||||
effective_reference_entries.update(frontmatter.x_personal_mcp.references)
|
||||
|
||||
references: dict[str, ReferenceRecord] = {}
|
||||
for ref_id, ref_entry in effective_reference_entries.items():
|
||||
ref_relpath = skill_rel_root.joinpath(ref_entry.path).as_posix()
|
||||
if ref_relpath not in docs_markdown_by_path:
|
||||
issues.append(
|
||||
RegistryIssue(
|
||||
code="missing_reference",
|
||||
message=f"reference target is missing for ref_id '{ref_id}'",
|
||||
skill_id=frontmatter.name,
|
||||
path=ref_relpath,
|
||||
hint="fix x-personal-mcp.references path or add the referenced markdown file",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
references[ref_id] = ReferenceRecord(
|
||||
ref_id=ref_id,
|
||||
uri=f"resource://skills/{frontmatter.name}/references/{ref_id}",
|
||||
relpath=ref_relpath,
|
||||
mime_type=ref_entry.mime_type,
|
||||
title=ref_entry.title,
|
||||
content=docs_markdown_by_path[ref_relpath],
|
||||
)
|
||||
|
||||
skill_id = frontmatter.name
|
||||
if skill_id in skills_by_id:
|
||||
issues.append(
|
||||
RegistryIssue(
|
||||
code="duplicate_skill_id",
|
||||
message="duplicate skill id discovered",
|
||||
skill_id=skill_id,
|
||||
path=skill_doc_relpath,
|
||||
hint="ensure each skill directory has a unique id",
|
||||
)
|
||||
)
|
||||
continue
|
||||
|
||||
record = SkillRecord(
|
||||
skill_id=skill_id,
|
||||
name=frontmatter.name,
|
||||
description=frontmatter.description,
|
||||
version=frontmatter.x_personal_mcp.version,
|
||||
tags=tuple(frontmatter.x_personal_mcp.tags),
|
||||
capabilities=tuple(frontmatter.x_personal_mcp.capabilities),
|
||||
depends_on=tuple(frontmatter.x_personal_mcp.depends_on),
|
||||
document_uri=f"resource://skills/{skill_id}/document",
|
||||
document_relpath=skill_doc_relpath,
|
||||
document_content=skill_markdown,
|
||||
references=references,
|
||||
)
|
||||
skills_by_id[skill_id] = record
|
||||
summaries.append(
|
||||
SkillSummaryRecord(
|
||||
skill_id=record.skill_id,
|
||||
name=record.name,
|
||||
description=record.description,
|
||||
tags=record.tags,
|
||||
capabilities=record.capabilities,
|
||||
document_uri=record.document_uri,
|
||||
version=record.version,
|
||||
)
|
||||
)
|
||||
|
||||
for skill_id, record in sorted(skills_by_id.items()):
|
||||
for dependency in record.depends_on:
|
||||
if dependency == skill_id:
|
||||
issues.append(
|
||||
RegistryIssue(
|
||||
code="self_dependency",
|
||||
message="skill must not depend on itself",
|
||||
skill_id=skill_id,
|
||||
path=record.document_relpath,
|
||||
hint="remove the skill id from depends_on",
|
||||
)
|
||||
)
|
||||
elif dependency not in skills_by_id:
|
||||
issues.append(
|
||||
RegistryIssue(
|
||||
code="missing_dependency",
|
||||
message=f"depends_on target '{dependency}' does not exist",
|
||||
skill_id=skill_id,
|
||||
path=record.document_relpath,
|
||||
hint="add the missing skill or remove it from depends_on",
|
||||
)
|
||||
)
|
||||
|
||||
for cycle_start, cycle in _ensure_no_cycles(skills_by_id):
|
||||
issues.append(
|
||||
RegistryIssue(
|
||||
code="dependency_cycle",
|
||||
message=f"depends_on cycle detected: {cycle}",
|
||||
skill_id=cycle_start,
|
||||
path=skills_by_id[cycle_start].document_relpath,
|
||||
hint="remove at least one dependency edge in the cycle",
|
||||
)
|
||||
)
|
||||
|
||||
seen_uris: set[str] = set()
|
||||
for skill_id, record in sorted(skills_by_id.items()):
|
||||
uris = [record.document_uri] + [ref.uri for ref in record.references.values()]
|
||||
for uri in uris:
|
||||
if uri in seen_uris:
|
||||
issues.append(
|
||||
RegistryIssue(
|
||||
code="duplicate_uri",
|
||||
message=f"duplicate resource URI generated: {uri}",
|
||||
skill_id=skill_id,
|
||||
path=record.document_relpath,
|
||||
hint="ensure unique skill ids and reference ids",
|
||||
)
|
||||
)
|
||||
seen_uris.add(uri)
|
||||
|
||||
if issues:
|
||||
raise DocsRegistryValidationError(issues)
|
||||
|
||||
skill_ids = tuple(sorted(skills_by_id))
|
||||
summary_by_id = {summary.skill_id: summary for summary in summaries}
|
||||
ordered_summaries = tuple(summary_by_id[skill_id] for skill_id in skill_ids)
|
||||
|
||||
tag_index: dict[str, list[str]] = {}
|
||||
capability_index: dict[str, list[str]] = {}
|
||||
for skill_id in skill_ids:
|
||||
record = skills_by_id[skill_id]
|
||||
for tag in record.tags:
|
||||
tag_index.setdefault(tag, []).append(skill_id)
|
||||
for capability in record.capabilities:
|
||||
capability_index.setdefault(capability, []).append(skill_id)
|
||||
|
||||
return DocsRegistry(
|
||||
skills_by_id=skills_by_id,
|
||||
skills_in_load_order=skill_ids,
|
||||
skills_summary_in_load_order=ordered_summaries,
|
||||
docs_markdown_by_path=docs_markdown_by_path,
|
||||
docs_markdown_path_index=tuple(sorted(docs_markdown_by_path)),
|
||||
tag_to_skill_ids={
|
||||
key: tuple(sorted(values)) for key, values in sorted(tag_index.items())
|
||||
},
|
||||
capability_to_skill_ids={
|
||||
key: tuple(sorted(values))
|
||||
for key, values in sorted(capability_index.items())
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def read_skill_document(registry: DocsRegistry, skill_id: str) -> dict[str, str]:
|
||||
if skill_id not in registry.skills_by_id:
|
||||
raise KeyError(f"unknown skill_id: {skill_id}")
|
||||
skill = registry.skills_by_id[skill_id]
|
||||
return {
|
||||
"id": skill.skill_id,
|
||||
"uri": skill.document_uri,
|
||||
"format": "markdown",
|
||||
"source_path": f"docs/{skill.document_relpath}",
|
||||
"content": skill.document_content,
|
||||
}
|
||||
|
||||
|
||||
def read_skill_reference(
|
||||
registry: DocsRegistry,
|
||||
*,
|
||||
skill_id: str,
|
||||
ref_id: str,
|
||||
) -> dict[str, str]:
|
||||
if skill_id not in registry.skills_by_id:
|
||||
raise KeyError(f"unknown skill_id: {skill_id}")
|
||||
skill = registry.skills_by_id[skill_id]
|
||||
if ref_id not in skill.references:
|
||||
raise KeyError(f"unknown ref_id '{ref_id}' for skill '{skill_id}'")
|
||||
reference = skill.references[ref_id]
|
||||
return {
|
||||
"id": ref_id,
|
||||
"skill_id": skill_id,
|
||||
"uri": reference.uri,
|
||||
"format": "markdown",
|
||||
"source_path": f"docs/{reference.relpath}",
|
||||
"content": reference.content,
|
||||
}
|
||||
|
||||
|
||||
def read_docs_markdown_path(registry: DocsRegistry, path: str) -> dict[str, str]:
|
||||
normalized_path = _normalize_docs_path(path)
|
||||
if normalized_path not in registry.docs_markdown_by_path:
|
||||
raise KeyError(f"unknown docs path: {normalized_path}")
|
||||
return {
|
||||
"uri": f"resource://docs/{normalized_path}",
|
||||
"format": "markdown",
|
||||
"source_path": f"docs/{normalized_path}",
|
||||
"content": registry.docs_markdown_by_path[normalized_path],
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
"""FastAPI web runtime for personal MCP."""
|
||||
@@ -0,0 +1,36 @@
|
||||
from fastapi import FastAPI
|
||||
|
||||
from personal_mcp.mcp import mcp
|
||||
from personal_mcp.web.config import Settings, get_settings
|
||||
from personal_mcp.web.docs_mount import mount_docs_static
|
||||
from personal_mcp.web.health import router as health_router
|
||||
|
||||
|
||||
def create_app(settings: Settings | None = None) -> FastAPI:
|
||||
runtime_settings = settings or get_settings()
|
||||
mcp_app = mcp.http_app(
|
||||
path=runtime_settings.mcp_route,
|
||||
json_response=True,
|
||||
stateless_http=True,
|
||||
transport="http",
|
||||
)
|
||||
app = FastAPI(
|
||||
debug=runtime_settings.debug,
|
||||
docs_url=None,
|
||||
redoc_url=None,
|
||||
openapi_url=None,
|
||||
lifespan=mcp_app.lifespan,
|
||||
)
|
||||
app.state.settings = runtime_settings
|
||||
|
||||
app.include_router(health_router)
|
||||
mount_docs_static(
|
||||
app,
|
||||
docs_route=runtime_settings.docs_route,
|
||||
site_dir=runtime_settings.site_dir,
|
||||
)
|
||||
app.mount("/", mcp_app, name="mcp")
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
@@ -0,0 +1,28 @@
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import Field
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
_REPO_ROOT = Path(__file__).resolve().parents[3]
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""Runtime settings for the HTTP MCP and docs server."""
|
||||
|
||||
model_config = SettingsConfigDict(
|
||||
env_file=".env",
|
||||
env_prefix="PERSONAL_MCP_",
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
host: str = "127.0.0.1"
|
||||
port: int = 8000
|
||||
debug: bool = False
|
||||
log_level: str = "info"
|
||||
docs_route: str = "/docs"
|
||||
mcp_route: str = "/mcp"
|
||||
site_dir: Path = Field(default=_REPO_ROOT / "site")
|
||||
|
||||
|
||||
def get_settings() -> Settings:
|
||||
return Settings()
|
||||
@@ -0,0 +1,54 @@
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import FastAPI, Response, status
|
||||
from fastapi.responses import RedirectResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
|
||||
def mount_docs_static(app: FastAPI, *, docs_route: str, site_dir: Path) -> None:
|
||||
"""Mount the pre-built static docs site, or expose a clear missing-build response."""
|
||||
normalized_route = docs_route.rstrip("/") or "/docs"
|
||||
docs_root = f"{normalized_route}/"
|
||||
|
||||
async def redirect_to_docs_root() -> RedirectResponse:
|
||||
return RedirectResponse(
|
||||
url=docs_root, status_code=status.HTTP_307_TEMPORARY_REDIRECT
|
||||
)
|
||||
|
||||
app.add_api_route(
|
||||
normalized_route,
|
||||
redirect_to_docs_root,
|
||||
methods=["GET", "HEAD"],
|
||||
include_in_schema=False,
|
||||
)
|
||||
|
||||
if site_dir.is_dir():
|
||||
app.mount(
|
||||
normalized_route,
|
||||
StaticFiles(directory=site_dir, html=True),
|
||||
name="docs",
|
||||
)
|
||||
return
|
||||
|
||||
async def docs_not_built() -> Response:
|
||||
return Response(
|
||||
content=(
|
||||
"Static docs have not been built yet. "
|
||||
"Run `uv run zensical build` before using this route."
|
||||
),
|
||||
media_type="text/plain",
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
)
|
||||
|
||||
app.add_api_route(
|
||||
normalized_route,
|
||||
docs_not_built,
|
||||
methods=["GET"],
|
||||
include_in_schema=False,
|
||||
)
|
||||
app.add_api_route(
|
||||
f"{normalized_route}/{{path:path}}",
|
||||
docs_not_built,
|
||||
methods=["GET"],
|
||||
include_in_schema=False,
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
from fastapi import APIRouter
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/healthz", include_in_schema=False)
|
||||
def healthz() -> dict[str, str]:
|
||||
return {"status": "ok"}
|
||||
+77
-6
@@ -44,10 +44,81 @@ Copyright © 2026 The authors
|
||||
# can be defined using TOML syntax.
|
||||
#
|
||||
# Read more: https://zensical.org/docs/setup/navigation/
|
||||
# nav = [
|
||||
# { "Get started" = "index.md" },
|
||||
# { "Markdown in 5min" = "markdown.md" },
|
||||
# ]
|
||||
nav = [
|
||||
{ "Home" = "index.md" },
|
||||
{ "Guide" = [
|
||||
{ "Arch" = "architecture.md" },
|
||||
{ "Content" = "content.md" },
|
||||
{ "Frontmatter" = "frontmatter.md" },
|
||||
{ "URIs" = "uris.md" },
|
||||
{ "MCP" = "mcp_layout.md" },
|
||||
{ "Copilot" = "copilot.md" },
|
||||
{ "Usage" = "usage.md" },
|
||||
{ "Future Work" = "future_work.md" },
|
||||
{ "New Skill" = "new_skill.md" },
|
||||
{ "Security" = "securing.md" },
|
||||
] },
|
||||
{ "Skills" = [
|
||||
{ "New Skill" = [
|
||||
{ "Overview" = "skills/new-skill/SKILL.md" },
|
||||
] },
|
||||
{ "Copilot" = [
|
||||
{ "Overview" = "skills/copilot-customization/SKILL.md" },
|
||||
{ "VS Code" = "skills/copilot-customization/references/vscode-customization.md" },
|
||||
] },
|
||||
{ "VS Code Config" = [
|
||||
{ "Overview" = "skills/vscode-configuration/SKILL.md" },
|
||||
{ "Debug Launch" = "skills/vscode-configuration/references/debug-launch-configurations.md" },
|
||||
{ "FastAPI Debug" = "skills/vscode-configuration/references/fastapi-debugpy-launch.md" },
|
||||
{ "Tasks" = "skills/vscode-configuration/references/tasks-json-configuration.md" },
|
||||
] },
|
||||
{ "FastAPI UV" = [
|
||||
{ "Overview" = "skills/fastapi-uv-docker/SKILL.md" },
|
||||
{ "Best" = "skills/fastapi-uv-docker/references/fastapi-best-practices.md" },
|
||||
{ "Layout" = "skills/fastapi-uv-docker/references/uv-project-layout.md" },
|
||||
{ "Uvicorn" = "skills/fastapi-uv-docker/references/uvicorn-settings.md" },
|
||||
{ "Docker" = "skills/fastapi-uv-docker/references/docker-cloud-native.md" },
|
||||
] },
|
||||
{ "Async SQLA" = [
|
||||
{ "Overview" = "skills/fastapi-async-sqlalchemy-modernization/SKILL.md" },
|
||||
{ "Index" = "skills/fastapi-async-sqlalchemy-modernization/references/index.md" },
|
||||
{ "Engine" = "skills/fastapi-async-sqlalchemy-modernization/references/engine.md" },
|
||||
{ "Session" = "skills/fastapi-async-sqlalchemy-modernization/references/session.md" },
|
||||
{ "Tx" = "skills/fastapi-async-sqlalchemy-modernization/references/transactions.md" },
|
||||
{ "IO" = "skills/fastapi-async-sqlalchemy-modernization/references/implicit_io.md" },
|
||||
{ "Obs" = "skills/fastapi-async-sqlalchemy-modernization/references/observability.md" },
|
||||
{ "Template" = "skills/fastapi-async-sqlalchemy-modernization/references/template.md" },
|
||||
] },
|
||||
{ "NiceGUI" = [
|
||||
{ "Overview" = "skills/nicegui/SKILL.md" },
|
||||
{ "Arch" = "skills/nicegui/references/architecture.md" },
|
||||
{ "Sources" = "skills/nicegui/references/source-documentation.md" },
|
||||
] },
|
||||
{ "NiceGUI Fine-Tuning" = [
|
||||
{ "Overview" = "skills/nicegui-ui-customization/SKILL.md" },
|
||||
{ "Style" = "skills/nicegui-ui-customization/references/architecture-and-styling.md" },
|
||||
{ "Flows" = "skills/nicegui-ui-customization/references/interaction-patterns.md" },
|
||||
{ "Quality" = "skills/nicegui-ui-customization/references/troubleshooting-and-quality-gates.md" },
|
||||
] },
|
||||
{ "Pytest" = [
|
||||
{ "Overview" = "skills/pytest-scaffolding/SKILL.md" },
|
||||
{ "Docs" = "skills/pytest-scaffolding/references/pytest-docs.md" },
|
||||
] },
|
||||
{ "Logging" = [
|
||||
{ "Overview" = "skills/python-logging-dictconfig/SKILL.md" },
|
||||
{ "Docs" = "skills/python-logging-dictconfig/references/python-logging-docs.md" },
|
||||
] },
|
||||
{ "Zensical" = [
|
||||
{ "Overview" = "skills/zensical-docs/SKILL.md" },
|
||||
{ "Map" = "skills/zensical-docs/references/index.md" },
|
||||
{ "Features" = "skills/zensical-docs/references/zensical-features.md" },
|
||||
{ "Theme" = "skills/zensical-docs/references/theme-customization-and-icons.md" },
|
||||
{ "Quality" = "skills/zensical-docs/references/documentation-quality.md" },
|
||||
{ "IA" = "skills/zensical-docs/references/discoverability-and-ia.md" },
|
||||
{ "API Docs" = "skills/zensical-docs/references/code-heavy-docs-and-mkdocstrings.md" },
|
||||
] },
|
||||
] },
|
||||
]
|
||||
|
||||
# With the "extra_css" option you can add your own CSS styling to customize
|
||||
# your Zensical project according to your needs. You can add any number of
|
||||
@@ -57,7 +128,7 @@ Copyright © 2026 The authors
|
||||
#
|
||||
# Read more: https://zensical.org/docs/customization/#additional-css
|
||||
#
|
||||
#extra_css = ["stylesheets/extra.css"]
|
||||
extra_css = ["stylesheets/mermaid-override.css"]
|
||||
|
||||
# With the `extra_javascript` option you can add your own JavaScript to your
|
||||
# project to customize the behavior according to your needs.
|
||||
@@ -65,7 +136,7 @@ Copyright © 2026 The authors
|
||||
# The path provided should be relative to the "docs_dir".
|
||||
#
|
||||
# Read more: https://zensical.org/docs/customization/#additional-javascript
|
||||
#extra_javascript = ["javascripts/extra.js"]
|
||||
extra_javascript = ["javascripts/mermaid-override.js"]
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Section for configuring theme options
|
||||
|
||||
Reference in New Issue
Block a user