big rework

This commit is contained in:
John Lancaster
2026-08-29 20:10:30 -05:00
parent 73eb490537
commit de95477480
30 changed files with 558 additions and 971 deletions
@@ -1,11 +1,16 @@
---
name: Pytest Scaffolding Guidance
description: Route tests edits to the Personal MCP pytesting resource.
description: Use when working under tests/. Route test edits to pytesting guidance and never create or expand tests unless the user explicitly asks.
applyTo: 'tests/**'
---
When editing files under `tests/`, use `skill://pytesting/SKILL.md` as the primary guidance source for test scaffolding and pytest authoring decisions.
Hard rule:
- Do not create new test files, test cases, or test scaffolding unless the user explicitly asks for tests in the current request.
- If tests could help but were not requested, mention them as an optional next step instead of adding them.
Execution pattern:
1. Load `skill://pytesting/SKILL.md` first.
-1
View File
@@ -1 +0,0 @@
src/personal_mcp/docs
+94
View File
@@ -0,0 +1,94 @@
from contextlib import asynccontextmanager
from importlib.resources import as_file
from importlib.resources import files
from pathlib import Path
from fastapi import FastAPI
from fastapi import Response
from fastapi import status
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from .config import Settings
from .config import get_settings
from .mcp import create_mcp
def create_app(settings: Settings | None = None) -> FastAPI:
runtime_settings = settings if settings is not None else get_settings()
mcp_app = create_mcp().http_app(
json_response=True,
stateless_http=True,
transport="http",
)
app = FastAPI(
debug=runtime_settings.debug,
docs_url=None,
redoc_url=None,
openapi_url=None,
lifespan=app_lifespan,
)
app.state.settings = runtime_settings
app.mount(runtime_settings.mounts.mcp, mcp_app, name="mcp")
return app
@asynccontextmanager
async def app_lifespan(app: FastAPI):
from . import __name__ as package_root_name
site_resource = files(package_root_name).joinpath("site")
with as_file(site_resource) as site_dir:
mount_docs(
app,
docs_route=app.state.settings.mounts.docs,
site_dir=site_dir,
)
yield
def mount_docs(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,
)
-2
View File
@@ -2,7 +2,6 @@ from functools import cache
from pathlib import Path
from pydantic import BaseModel
from pydantic import DirectoryPath
from pydantic import Field
from pydantic_settings import BaseSettings
from pydantic_settings import SettingsConfigDict
@@ -29,7 +28,6 @@ class Settings(BaseSettings):
debug: bool = False
log_level: str = "info"
mounts: Mounts = Field(default_factory=Mounts)
site_dir: DirectoryPath = Field(default=DEFAULT_SITE_DIR)
host: str = "localhost"
port: int = 8080
reload: bool = True
@@ -0,0 +1,185 @@
#!/usr/bin/env -S uv run --script
# /// script
# dependencies = [
# "nicegui==3.16.0",
# "pandas",
# ]
# ///
from dataclasses import dataclass
import pandas as pd
from nicegui import binding
from nicegui import events
from nicegui import ui
STATUS_OPTIONS = ["draft", "active", "archived"]
EDITABLE_FIELDS = ("name", "quantity", "status")
TableValue = str | int
TableRow = dict[str, TableValue]
@binding.bindable_dataclass(bindable_fields=EDITABLE_FIELDS)
class EditableRow:
id: int
name: str
quantity: int
status: str
@dataclass(slots=True)
class EditableTableState:
rows_by_id: dict[int, EditableRow]
table_rows_by_id: dict[int, TableRow]
def table_rows(self) -> list[TableRow]:
return list(self.table_rows_by_id.values())
def dataframe_to_state(dataframe: pd.DataFrame) -> EditableTableState:
required_columns = {"id", *EDITABLE_FIELDS}
missing_columns = required_columns.difference(dataframe.columns)
if missing_columns:
raise ValueError(f"Missing columns: {sorted(missing_columns)}")
if not dataframe["id"].is_unique:
raise ValueError("The id column must contain unique row keys")
rows_by_id: dict[int, EditableRow] = {}
table_rows_by_id: dict[int, TableRow] = {}
for record in dataframe.to_dict(orient="records"):
row_state = EditableRow(
id=int(record["id"]),
name=str(record["name"]),
quantity=int(record["quantity"]),
status=str(record["status"]),
)
if row_state.status not in STATUS_OPTIONS:
raise ValueError(f"Unknown status {row_state.status!r}")
if row_state.id in rows_by_id:
raise ValueError("Row keys must remain unique after normalization")
table_row: TableRow = {
"id": row_state.id,
"name": row_state.name,
"quantity": row_state.quantity,
"status": row_state.status,
}
for field_name in EDITABLE_FIELDS:
binding.bind_to(
row_state,
field_name,
table_row,
field_name,
other_strict=True,
)
rows_by_id[row_state.id] = row_state
table_rows_by_id[row_state.id] = table_row
return EditableTableState(rows_by_id, table_rows_by_id)
def normalize_edit(field: str, raw_value: object) -> TableValue:
match field:
case "name":
if not isinstance(raw_value, str) or not (name := raw_value.strip()):
raise ValueError("Name is required")
return name
case "quantity":
if isinstance(raw_value, bool) or not isinstance(raw_value, (int, float, str)):
raise TypeError("Quantity must be an integer")
if isinstance(raw_value, float) and not raw_value.is_integer():
raise ValueError("Quantity must be an integer")
try:
quantity = int(raw_value)
except (ValueError, OverflowError) as error:
raise ValueError("Quantity must be an integer") from error
if not 0 <= quantity <= 1_000:
raise ValueError("Quantity must be between 0 and 1000")
return quantity
case "status":
if not isinstance(raw_value, str) or raw_value not in STATUS_OPTIONS:
raise ValueError("Unknown status")
return raw_value
case _:
raise ValueError(f"Field {field!r} is not editable")
def save_row(dataframe: pd.DataFrame, row_state: EditableRow) -> None:
matching_rows = dataframe["id"].eq(row_state.id)
if int(matching_rows.sum()) != 1:
raise ValueError("This row no longer exists")
dataframe.loc[matching_rows, "name"] = row_state.name
dataframe.loc[matching_rows, "quantity"] = row_state.quantity
dataframe.loc[matching_rows, "status"] = row_state.status
def render_table(dataframe: pd.DataFrame) -> EditableTableState:
state = dataframe_to_state(dataframe)
columns = [
{"name": "name", "label": "Name", "field": "name", "align": "left"},
{"name": "quantity", "label": "Quantity", "field": "quantity", "align": "right"},
{"name": "status", "label": "Status", "field": "status", "align": "left"},
]
table = ui.table(
columns=columns,
rows=state.table_rows(),
row_key="id",
selection="multiple",
).classes("w-full")
def apply_edit(event: events.GenericEventArguments) -> None:
try:
raw_row_id, raw_field, raw_value = event.args
row_id = int(raw_row_id)
field_name = str(raw_field)
row_state = state.rows_by_id.get(row_id)
if row_state is None:
raise ValueError("This row no longer exists")
normalized_value = normalize_edit(field_name, raw_value)
previous_value = getattr(row_state, field_name)
setattr(row_state, field_name, normalized_value)
try:
save_row(dataframe, row_state)
except Exception:
setattr(row_state, field_name, previous_value)
raise
except (TypeError, ValueError) as error:
ui.notify(str(error), type="negative")
finally:
table.update_rows(state.table_rows(), clear_selection=False)
with table.add_slot("body-cell-name"), table.cell("name"):
ui.input().props(':model-value="props.value" dense borderless debounce=400').on(
"update:model-value",
handler=apply_edit,
js_handler="(value) => emit(props.row.id, props.col.name, value)",
)
with table.add_slot("body-cell-quantity"), table.cell("quantity"):
ui.number(min=0, max=1_000).props(':model-value="props.value" dense borderless debounce=400').on(
"update:model-value",
handler=apply_edit,
js_handler="(value) => emit(props.row.id, props.col.name, value)",
)
with table.add_slot("body-cell-status"), table.cell("status"):
ui.select(STATUS_OPTIONS).props(':model-value="props.value" dense borderless options-dense').on(
"update:model-value",
handler=apply_edit,
js_handler="(value) => emit(props.row.id, props.col.name, value)",
)
return state
if __name__ in {"__main__", "__mp_main__"}:
items = pd.DataFrame(
[
{"id": 101, "name": "Desk", "quantity": 4, "status": "active"},
{"id": 102, "name": "Lamp", "quantity": 12, "status": "draft"},
]
)
table_state = render_table(items)
ui.run(port=8888, reload=True)
+27 -55
View File
@@ -1,18 +1,12 @@
from __future__ import annotations
from fastmcp import FastMCP
from mcp_types import CompletionArgument
from mcp_types import CompletionContext
from mcp_types import Icon
from mcp_types import PromptReference
from personal_mcp.prompts import create_prompts_provider
from personal_mcp.prompts.models import MarkdownPrompt
from personal_mcp.prompts.provider import MarkdownPromptsProvider
from personal_mcp.registry.load import get_docs_registry
from personal_mcp.registry.load import read_docs_markdown_path
from personal_mcp.registry.models import DocsRegistry
from personal_mcp.skills import create_skills_provider
from .prompts.provider import prompt_lifespan
from .registry.load import get_docs_registry
from .registry.load import read_docs_markdown_path
from .skills import skill_lifespan
_SERVER_INSTRUCTIONS = """Personal development guidance exposed as native MCP resources and prompts.
@@ -33,15 +27,22 @@ _SERVER_ICON = Icon(
)
def _ro_annotations() -> dict[str, bool]:
return {
"readOnlyHint": True,
"idempotentHint": True,
"openWorldHint": False,
}
def run_stdio() -> None:
"""Create the MCP server and expose it over stdio"""
create_mcp().run()
def _register_components(mcp: FastMCP, registry: DocsRegistry) -> None:
def create_mcp() -> FastMCP:
mcp = FastMCP(
"personal-mcp",
instructions=_SERVER_INSTRUCTIONS,
icons=[_SERVER_ICON],
on_duplicate="error",
lifespan=skill_lifespan | prompt_lifespan,
)
registry = get_docs_registry()
@mcp.resource(
"resource://docs/{path*}",
name="docs_markdown",
@@ -49,46 +50,17 @@ def _register_components(mcp: FastMCP, registry: DocsRegistry) -> None:
description="Read a packaged documentation page by its path relative to the docs root.",
mime_type="text/markdown",
tags={"docs"},
annotations=_ro_annotations(),
annotations={
"readOnlyHint": True,
"idempotentHint": True,
"openWorldHint": False,
},
)
def docs_markdown(path: str) -> dict[str, str]:
return read_docs_markdown_path(registry, path)
def _register_prompt_completions(mcp: FastMCP, provider: MarkdownPromptsProvider) -> None:
@mcp.completion
async def complete_prompt_argument(
ref: object,
argument: CompletionArgument,
context: CompletionContext | None,
) -> list[str] | None:
del context
if not isinstance(ref, PromptReference):
return None
prompt = await provider.get_prompt(ref.name)
if not isinstance(prompt, MarkdownPrompt):
return None
definition = prompt.definitions.get(argument.name)
if definition is None or definition.choices is None:
return None
prefix = argument.value.casefold()
return [choice for choice in definition.choices if choice.casefold().startswith(prefix)]
def create_mcp() -> FastMCP:
registry = get_docs_registry()
mcp = FastMCP(
"personal-mcp",
instructions=_SERVER_INSTRUCTIONS,
icons=[_SERVER_ICON],
on_duplicate="error",
)
_register_components(mcp, registry)
prompts_provider = create_prompts_provider()
mcp.add_provider(prompts_provider)
mcp.add_provider(create_skills_provider())
_register_prompt_completions(mcp, prompts_provider)
return mcp
if __name__ == "__main__":
run_stdio()
+7 -1
View File
@@ -1,3 +1,9 @@
from .provider import complete_prompt_argument_choices
from .provider import create_prompts_provider
from .provider import prompt_lifespan
__all__ = ["create_prompts_provider"]
__all__ = [
"complete_prompt_argument_choices",
"create_prompts_provider",
"prompt_lifespan",
]
+8
View File
@@ -85,6 +85,14 @@ class MarkdownPrompt(Prompt):
definitions=metadata.arguments,
)
def complete_argument(self, argument_name: str, argument_value: str) -> list[str] | None:
definition = self.definitions.get(argument_name)
if definition is None or definition.choices is None:
return None
prefix = argument_value.casefold()
return [choice for choice in definition.choices if choice.casefold().startswith(prefix)]
async def render(self, arguments: dict[str, object] | None = None) -> str:
provided = arguments or {}
declared_names = set(self.definitions)
+41
View File
@@ -2,8 +2,14 @@ from collections.abc import Sequence
from importlib.resources import files
from importlib.resources.abc import Traversable
from fastmcp import FastMCP
from fastmcp.prompts import Prompt
from fastmcp.server.lifespan import lifespan
from fastmcp.server.providers import Provider
from mcp_types import CompletionArgument
from mcp_types import CompletionContext
from mcp_types import PromptReference
from mcp_types import ResourceTemplateReference
from .content import load_prompt_definition
from .models import MarkdownPrompt
@@ -29,6 +35,41 @@ class MarkdownPromptsProvider(Provider):
return prompts
@lifespan
async def prompt_lifespan(server: FastMCP):
provider = create_prompts_provider()
server.add_provider(provider)
@server.completion
async def complete_prompt_argument(
ref: PromptReference | ResourceTemplateReference,
argument: CompletionArgument,
context: CompletionContext | None,
) -> list[str] | None:
del context
return await complete_prompt_argument_choices(provider, ref, argument)
try:
yield {}
finally:
server.providers.remove(provider)
def create_prompts_provider(root: Traversable | None = None) -> MarkdownPromptsProvider:
prompts_root = root or files("personal_mcp").joinpath("docs", "prompts")
return MarkdownPromptsProvider(prompts_root)
async def complete_prompt_argument_choices(
prompts_provider: MarkdownPromptsProvider,
ref: object,
argument: CompletionArgument,
) -> list[str] | None:
if not isinstance(ref, PromptReference):
return None
prompt = await prompts_provider.get_prompt(ref.name)
if not isinstance(prompt, MarkdownPrompt):
return None
return prompt.complete_argument(argument.name, argument.value)
+4 -4
View File
@@ -13,10 +13,6 @@ from pydantic import field_validator
__all__ = ["DocsRegistry"]
def frozen_mapping[K, V](value: Mapping[K, V] | None = None) -> Mapping[K, V]:
return MappingProxyType(dict(value) if value is not None else {})
def parse_docs_path(value: str | PurePosixPath) -> PurePosixPath:
raw = value.as_posix() if isinstance(value, PurePosixPath) else value
if "\\" in raw:
@@ -39,6 +35,10 @@ def _empty_docs_mapping() -> Mapping[DocsPath, str]:
return frozen_mapping()
def frozen_mapping[K, V](value: Mapping[K, V] | None = None) -> Mapping[K, V]:
return MappingProxyType(dict(value) if value is not None else {})
class DocsRegistry(BaseModel):
"""In-memory index of documentation content."""
+17 -26
View File
@@ -1,34 +1,25 @@
from contextlib import ExitStack
from contextlib import contextmanager
from importlib.resources import as_file
from importlib.resources import files
from weakref import finalize
from fastmcp import FastMCP
from fastmcp.server.lifespan import lifespan
from fastmcp.server.providers.skills import SkillsDirectoryProvider
def create_skills_provider() -> SkillsDirectoryProvider:
"""Create the provider for skills packaged with personal-mcp."""
skills_resource = files("personal_mcp").joinpath("docs", "skills")
if not skills_resource.is_dir():
raise FileNotFoundError(f"packaged skills directory does not exist: {skills_resource}")
@lifespan
async def skill_lifespan(server: FastMCP):
with skills_provider() as provider:
server.add_provider(provider)
try:
yield {}
finally:
server.providers.remove(provider)
has_skills = any(
skill_dir.is_dir() and skill_dir.joinpath("SKILL.md").is_file() for skill_dir in skills_resource.iterdir()
)
if not has_skills:
raise ValueError(f"packaged skills directory contains no skills: {skills_resource}")
resources = ExitStack()
try:
skills_root = resources.enter_context(as_file(skills_resource))
provider = SkillsDirectoryProvider(
roots=skills_root,
reload=False,
supporting_files="template",
)
except BaseException:
resources.close()
raise
finalize(provider, resources.close)
return provider
@contextmanager
def skills_provider():
with as_file(files(__package__).joinpath("docs", "skills")) as skills_root:
if not skills_root.is_dir():
raise FileNotFoundError(f"packaged skills directory does not exist: {skills_root}")
yield SkillsDirectoryProvider(roots=skills_root, reload=True)
-1
View File
@@ -1 +0,0 @@
"""FastAPI web runtime for personal MCP."""
-34
View File
@@ -1,34 +0,0 @@
from fastapi import FastAPI
from ..config import Settings
from ..config import get_settings
from ..mcp import create_mcp
from .docs_mount import mount_docs_static
from .health import router as health_router
def create_app(settings: Settings | None = None) -> FastAPI:
runtime_settings = settings if settings is not None else get_settings()
mcp_app = create_mcp().http_app(
path=runtime_settings.mounts.mcp,
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.mounts.docs,
site_dir=runtime_settings.site_dir,
)
app.mount("/", mcp_app, name="mcp")
return app
-56
View File
@@ -1,56 +0,0 @@
from pathlib import Path
from fastapi import FastAPI
from fastapi import Response
from fastapi import 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,
)
-8
View File
@@ -1,8 +0,0 @@
from fastapi import APIRouter
router = APIRouter()
@router.get("/healthz", include_in_schema=False)
def healthz() -> dict[str, str]:
return {"status": "ok"}
-1
View File
@@ -1 +0,0 @@
"""Test package marker for intra-suite imports."""
-3
View File
@@ -1,3 +0,0 @@
from __future__ import annotations
# Global lightweight fixtures can be added here as the suite grows.
-57
View File
@@ -1,57 +0,0 @@
from pathlib import Path
import pytest
from personal_mcp.prompts.content import load_prompt_definition
from personal_mcp.prompts.content import render_prompt
pytestmark = pytest.mark.unit
class TestPromptContentRenderer:
def test_renders_arguments_without_frontmatter(self) -> None:
rendered = render_prompt(
"jsfiddle-page-layout",
{"domain": "public library", "layout_brief": None},
)
assert not rendered.startswith("---")
assert "`domain`: public library" in rendered
assert "`layout_brief`: Not provided" in rendered
def test_rejects_placeholder_drift(self) -> None:
with pytest.raises(ValueError, match="placeholders do not match arguments"):
render_prompt("jsfiddle-page-layout", {"domain": "public library"})
def test_rejects_invalid_prompt_id(self) -> None:
with pytest.raises(ValueError, match="lowercase kebab-case"):
render_prompt("../outside", {})
def test_rejects_missing_prompt_metadata(self, tmp_path: Path) -> None:
document = tmp_path / "PROMPT.md"
document.write_text("---\nicon: lucide/messages-square\n---\n\n# Body\n", encoding="utf-8")
with pytest.raises(ValueError, match="missing the 'prompt' block"):
load_prompt_definition("demo", document)
def test_rejects_unknown_prompt_metadata(self, tmp_path: Path) -> None:
document = tmp_path / "PROMPT.md"
document.write_text(
"---\nprompt: {version: '1', description: Demo, tags: [demo], arguments: {}, unknown: true}\n"
"---\n\n# Body\n",
encoding="utf-8",
)
with pytest.raises(ValueError, match="Extra inputs are not permitted"):
load_prompt_definition("demo", document)
def test_rejects_declared_placeholder_drift(self, tmp_path: Path) -> None:
document = tmp_path / "PROMPT.md"
document.write_text(
"---\nprompt: {version: '1', description: Demo, tags: [demo], arguments: "
"{topic: {description: Topic, required: true}}}\n---\n\n# Body\n",
encoding="utf-8",
)
with pytest.raises(ValueError, match="placeholders do not match arguments"):
load_prompt_definition("demo", document)
-121
View File
@@ -1,121 +0,0 @@
from pathlib import Path
import pytest
from fastmcp import Client
from fastmcp import FastMCP
from fastmcp.exceptions import PromptError
from personal_mcp.prompts import create_prompts_provider
pytestmark = pytest.mark.unit
EXPECTED_PROMPTS = {
"authoring",
"greenfield-architecture",
"jsfiddle-page-layout",
"mcp-consumer-repo-shim",
"nicegui-component-extraction",
"pytest-fill-scaffold",
"pytest-scaffold",
}
def write_prompt(document: Path, *, description: str, heading: str = "Demo") -> None:
document.parent.mkdir(parents=True, exist_ok=True)
document.write_text(
"---\n"
f"prompt: {{version: '1.0.0', description: {description!r}, tags: [demo], arguments: "
"{kind: {description: 'Kind to render.', required: true, choices: [first, second]}, "
"note: {description: 'Optional note.', required: false}}}\n"
"---\n\n"
f"# {heading}\n\nKind: {{{{kind}}}}\n\nNote: {{{{note}}}}\n",
encoding="utf-8",
)
class TestMarkdownPromptsProvider:
@pytest.mark.asyncio
async def test_discovers_exact_authored_set(self) -> None:
mcp = FastMCP("prompts-test")
mcp.add_provider(create_prompts_provider())
async with Client(mcp) as client:
prompts = await client.list_prompts()
assert {prompt.name for prompt in prompts} == EXPECTED_PROMPTS
assert all(prompt.description for prompt in prompts)
@pytest.mark.asyncio
async def test_exposes_typed_arguments_and_renders_markdown(self) -> None:
mcp = FastMCP("prompts-test")
mcp.add_provider(create_prompts_provider())
async with Client(mcp) as client:
prompts = await client.list_prompts()
authoring = next(prompt for prompt in prompts if prompt.name == "authoring")
result = await client.get_prompt(
"authoring",
{
"artifact_type": "skill",
"artifact_id": "demo-skill",
"goal": "Demonstrate typed prompts.",
},
)
required = {argument.name for argument in authoring.arguments or [] if argument.required}
artifact_type = next(argument for argument in authoring.arguments or [] if argument.name == "artifact_type")
assert required == {"artifact_type", "artifact_id", "goal"}
assert artifact_type.description == "Artifact type to create.\n\nAccepted values: skill, prompt, shim."
assert result.messages
assert "`artifact_id`: demo-skill" in result.messages[0].content.text
@pytest.mark.asyncio
async def test_enforces_required_arguments_and_choices(self) -> None:
provider = create_prompts_provider()
prompt = await provider.get_prompt("authoring")
assert prompt is not None
with pytest.raises(PromptError, match="Missing required arguments"):
await prompt.render({"artifact_type": "skill"})
with pytest.raises(PromptError, match="must be one of"):
await prompt.render(
{
"artifact_type": "unsupported",
"artifact_id": "demo-skill",
"goal": "Demonstrate validation.",
}
)
@pytest.mark.asyncio
async def test_live_loads_edits_without_python_components(self, tmp_path: Path) -> None:
prompts_root = tmp_path / "prompts"
prompts_root.mkdir()
provider = create_prompts_provider(prompts_root)
mcp = FastMCP("prompts-test")
mcp.add_provider(provider)
async with Client(mcp) as client:
assert await client.list_prompts() == []
document = prompts_root / "dynamic-demo" / "PROMPT.md"
write_prompt(document, description="Initial description")
prompts = await client.list_prompts()
assert [prompt.name for prompt in prompts] == ["dynamic-demo"]
assert prompts[0].description == "Initial description"
result = await client.get_prompt("dynamic-demo", {"kind": "first"})
assert "# Demo" in result.messages[0].content.text
assert "Note: Not provided" in result.messages[0].content.text
write_prompt(document, description="Updated description", heading="Updated")
prompts = await client.list_prompts()
assert prompts[0].description == "Updated description"
result = await client.get_prompt("dynamic-demo", {"kind": "second", "note": "ready"})
assert "# Updated" in result.messages[0].content.text
assert "Note: ready" in result.messages[0].content.text
document.unlink()
document.parent.rmdir()
assert await client.list_prompts() == []
@@ -1,21 +0,0 @@
from __future__ import annotations
from pathlib import PurePosixPath
import pytest
from personal_mcp.registry.load import get_docs_registry
pytestmark = pytest.mark.unit
class TestCurrentDocsIngestion:
"""Covers ingestion of the repository's current docs tree."""
def test_registry_includes_docs_and_excludes_skills(self) -> None:
"""Ensures the docs registry cannot duplicate native skill resources."""
registry = get_docs_registry()
assert PurePosixPath("index.md") in registry.docs_markdown_by_path
assert any(path.parts[0] == "prompts" for path in registry.docs_markdown_by_path)
assert all(path.parts[0] != "skills" for path in registry.docs_markdown_by_path)
@@ -1,33 +0,0 @@
from __future__ import annotations
from pathlib import PurePosixPath
import pytest
from personal_mcp.registry.models import parse_docs_path
pytestmark = pytest.mark.unit
class TestDocsPathValidation:
"""Covers canonical resource-path contracts."""
def test_parse_docs_path_returns_pure_posix_path(self) -> None:
path = parse_docs_path("guides/demo.md")
assert path == PurePosixPath("guides/demo.md")
assert isinstance(path, PurePosixPath)
@pytest.mark.parametrize(
"value",
(
"/absolute.md",
"../outside.md",
"guides\\demo.md",
"guides//demo.md",
"guides/demo.txt",
),
)
def test_parse_docs_path_rejects_invalid_paths(self, value: str) -> None:
with pytest.raises(ValueError):
parse_docs_path(value)
-27
View File
@@ -1,27 +0,0 @@
from pathlib import Path
from pathlib import PurePosixPath
import pytest
from personal_mcp.registry.load import load_markdown
pytestmark = pytest.mark.unit
class TestLoadMarkdown:
"""Covers recursive Markdown discovery and loading."""
def test_loads_markdown_in_stable_order(self, tmp_path: Path) -> None:
nested = tmp_path / "guides"
nested.mkdir()
(nested / "b.md").write_text("caf\u00e9\n", encoding="utf-8")
(nested / "a.md").write_text("alpha\n", encoding="utf-8")
(nested / "ignored.txt").write_text("ignored\n", encoding="utf-8")
docs = load_markdown(tmp_path)
assert list(docs) == [
PurePosixPath("guides/a.md"),
PurePosixPath("guides/b.md"),
]
assert docs[PurePosixPath("guides/b.md")] == "caf\u00e9\n"
-37
View File
@@ -1,37 +0,0 @@
from pathlib import PurePosixPath
import pytest
from personal_mcp.registry.load import read_docs_markdown_path
from personal_mcp.registry.models import DocsRegistry
pytestmark = pytest.mark.unit
def _make_registry() -> DocsRegistry:
index_path = PurePosixPath("index.md")
return DocsRegistry(
docs_markdown_by_path={index_path: "# index"},
docs_markdown_path_index=(index_path,),
)
def test_reads_docs_path_from_string_boundary() -> None:
payload = read_docs_markdown_path(_make_registry(), "index.md")
assert payload == {
"uri": "resource://docs/index.md",
"format": "markdown",
"source_path": "docs/index.md",
"content": "# index",
}
def test_rejects_skill_docs_path() -> None:
with pytest.raises(KeyError, match="unknown docs path"):
read_docs_markdown_path(_make_registry(), "skills/demo/SKILL.md")
def test_rejects_non_posix_docs_path() -> None:
with pytest.raises(ValueError, match="POSIX separators"):
read_docs_markdown_path(_make_registry(), "guides\\demo.md")
-67
View File
@@ -1,67 +0,0 @@
from importlib.resources import files
from importlib.resources.abc import Traversable
import pytest
import yaml
from fastmcp import Client
from fastmcp import FastMCP
from fastmcp.utilities.skills import get_skill_manifest
from fastmcp.utilities.skills import list_skills
from personal_mcp.skills import create_skills_provider
pytestmark = pytest.mark.unit
SKILLS_ROOT = files("personal_mcp").joinpath("docs", "skills")
def skill_directories() -> list[Traversable]:
return [
directory
for directory in SKILLS_ROOT.iterdir()
if directory.is_dir() and directory.joinpath("SKILL.md").is_file()
]
class TestSkillsProvider:
"""Covers native FastMCP skill discovery and retrieval."""
@pytest.mark.asyncio
async def test_discovers_authored_skills(self) -> None:
"""Ensures each authored skill is exposed with its description."""
expected_names = {directory.name for directory in skill_directories()}
mcp = FastMCP("skills-test")
mcp.add_provider(create_skills_provider())
async with Client(mcp) as client:
skills = await list_skills(client)
assert {skill.name for skill in skills} == expected_names
assert all(skill.description for skill in skills)
@pytest.mark.asyncio
async def test_reads_manifest_and_supporting_file(self) -> None:
"""Ensures manifests disclose hashed files that remain directly readable."""
mcp = FastMCP("skills-test")
mcp.add_provider(create_skills_provider())
async with Client(mcp) as client:
manifest = await get_skill_manifest(client, "mcp-details")
reference = next(file for file in manifest.files if file.path.startswith("references/"))
contents = await client.read_resource(f"skill://mcp-details/{reference.path}")
assert any(file.path == "SKILL.md" for file in manifest.files)
assert all(file.hash.startswith("sha256:") for file in manifest.files)
assert reference.size > 0
assert contents
def test_frontmatter_names_match_directories(self) -> None:
"""Ensures provider identity and authored skill names remain aligned."""
for directory in skill_directories():
skill_file = directory.joinpath("SKILL.md")
raw = skill_file.read_text(encoding="utf-8")
frontmatter = yaml.safe_load(raw.split("---", 2)[1])
assert set(frontmatter) == {"name", "description"}
assert frontmatter["name"] == directory.name
assert frontmatter["description"]
-55
View File
@@ -1,55 +0,0 @@
from __future__ import annotations
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
import pytest
import pytest_asyncio
from httpx import ASGITransport
from httpx import AsyncClient
from httpx2 import ASGITransport as McpASGITransport
from httpx2 import AsyncClient as McpAsyncClient
from mcp import ClientSession
from mcp.client.streamable_http import streamable_http_client
from personal_mcp.web.app import create_app
@pytest_asyncio.fixture
async def client() -> AsyncGenerator[AsyncClient]:
"""Provides an AsyncClient bound to a fresh application instance."""
app = create_app()
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://testserver",
timeout=10.0,
) as test_client:
yield test_client
@pytest.fixture
def mcp_session_factory():
"""Provides an in-process context manager factory for MCP SDK sessions."""
@asynccontextmanager
async def create_session(*, initialize: bool = True) -> AsyncGenerator[ClientSession]:
app = create_app()
mcp_url = f"http://testserver{app.state.settings.mounts.mcp}"
async with (
app.router.lifespan_context(app),
McpAsyncClient(
transport=McpASGITransport(app=app),
base_url="http://testserver",
timeout=10.0,
) as http_client,
streamable_http_client(
mcp_url,
http_client=http_client,
) as (read_stream, write_stream),
ClientSession(read_stream, write_stream) as session,
):
if initialize:
await session.initialize()
yield session
return create_session
-58
View File
@@ -1,58 +0,0 @@
from __future__ import annotations
import pytest
from httpx import AsyncClient
pytestmark = pytest.mark.smoke
class TestMcpHttpEndpoints:
"""Covers smoke-level HTTP checks for mounted MCP runtime endpoints."""
class TestHealthz:
"""Covers health endpoint smoke behavior."""
@pytest.mark.asyncio
async def test_returns_ok_payload(self, client: AsyncClient) -> None:
"""Ensures GET /healthz responds with a healthy status payload."""
response = await client.get("/healthz")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
class TestDocsRoute:
"""Covers static docs route smoke behavior."""
@pytest.mark.asyncio
async def test_serves_docs_entrypoint(self, client: AsyncClient) -> None:
"""Ensures GET /docs returns the docs site entrypoint response."""
response = await client.get("/docs", follow_redirects=True)
assert response.status_code == 200
assert "text/html" in response.headers["content-type"]
class TestMcpRoute:
"""Covers MCP transport endpoint smoke behavior."""
@pytest.mark.asyncio
async def test_does_not_publish_legacy_resource_bridge_tools(self, mcp_session_factory) -> None:
"""Ensures deprecated compatibility tools are not exposed on the MCP route."""
async with mcp_session_factory() as mcp_session:
tools_result = await mcp_session.list_tools()
tool_names = {tool.name for tool in tools_result.tools}
assert "search_skills" not in tool_names
assert "list_resources" not in tool_names
assert "read_resource" not in tool_names
@pytest.mark.asyncio
async def test_accepts_initialize_jsonrpc_request(
self,
mcp_session_factory,
) -> None:
"""Ensures POST /mcp accepts an initialize JSON-RPC request."""
async with mcp_session_factory(initialize=False) as mcp_session_uninitialized:
initialize_result = await mcp_session_uninitialized.initialize()
assert initialize_result.protocol_version
assert initialize_result.server_info.name
-79
View File
@@ -1,79 +0,0 @@
from __future__ import annotations
import pytest
from mcp_types import PromptReference
pytestmark = pytest.mark.smoke
EXPECTED_PROMPTS = {
"authoring",
"greenfield-architecture",
"jsfiddle-page-layout",
"mcp-consumer-repo-shim",
"nicegui-component-extraction",
"pytest-fill-scaffold",
"pytest-scaffold",
}
class TestMcpPromptSurface:
"""Covers smoke-level MCP prompt discovery and retrieval paths."""
class TestServerMetadata:
"""Covers client-facing identity and completion capabilities."""
@pytest.mark.asyncio
async def test_advertises_instructions_and_completions(self, mcp_session_factory) -> None:
"""Ensures VS Code receives server guidance and completion support."""
async with mcp_session_factory(initialize=False) as mcp_session:
result = await mcp_session.initialize()
assert result.instructions is not None
assert "skill://<name>/SKILL.md" in result.instructions
assert result.server_info.icons
assert result.server_info.icons[0].src.startswith("data:image/svg+xml;base64,")
assert result.capabilities.completions is not None
class TestPromptDiscovery:
"""Covers MCP prompts/list behavior using native prompt objects."""
@pytest.mark.asyncio
async def test_lists_prompt_objects(self, mcp_session_factory) -> None:
"""Ensures prompts/list returns native prompt objects with stable names."""
async with mcp_session_factory() as mcp_session:
result = await mcp_session.list_prompts()
assert {prompt.name for prompt in result.prompts} == EXPECTED_PROMPTS
assert all(prompt.description for prompt in result.prompts)
assert all(prompt.title for prompt in result.prompts)
@pytest.mark.asyncio
async def test_completes_authored_prompt_choices(self, mcp_session_factory) -> None:
"""Ensures finite prompt choices are available as IDE-style suggestions."""
async with mcp_session_factory() as mcp_session:
result = await mcp_session.complete(
PromptReference(type="ref/prompt", name="authoring"),
{"name": "artifact_type", "value": "pr"},
)
assert result.completion.values == ["prompt"]
class TestPromptResolution:
"""Covers MCP prompts/get behavior using native request and response objects."""
@pytest.mark.asyncio
async def test_gets_prompt_as_native_object(self, mcp_session_factory) -> None:
"""Ensures prompts/get resolves a listed prompt into structured message objects."""
async with mcp_session_factory() as mcp_session:
resolved_prompt = await mcp_session.get_prompt(
name="authoring",
arguments={
"artifact_type": "skill",
"artifact_id": "demo-skill",
"goal": "Demonstrate native prompt rendering.",
},
)
assert resolved_prompt.messages
assert all(message.content for message in resolved_prompt.messages)
assert "`artifact_id`: demo-skill" in resolved_prompt.messages[0].content.text
-59
View File
@@ -1,59 +0,0 @@
from __future__ import annotations
import json
import pytest
pytestmark = pytest.mark.smoke
class TestMcpSkillsSurface:
"""Covers native skill resources over the HTTP MCP surface."""
class TestTools:
"""Covers absence of deprecated compatibility tools."""
@pytest.mark.asyncio
async def test_does_not_expose_resource_bridge_tools(self, mcp_session_factory) -> None:
"""Ensures native resources are not projected through legacy compatibility tools."""
async with mcp_session_factory() as mcp_session:
result = await mcp_session.list_tools()
tool_names = {tool.name for tool in result.tools}
assert "search_skills" not in tool_names
assert "list_resources" not in tool_names
assert "read_resource" not in tool_names
class TestResources:
"""Covers native skill resources, manifests, and file templates."""
@pytest.mark.asyncio
async def test_lists_main_file_and_manifest(self, mcp_session_factory) -> None:
"""Ensures resources/list exposes native skill entry points."""
async with mcp_session_factory() as mcp_session:
result = await mcp_session.list_resources()
resource_uris = {str(resource.uri) for resource in result.resources}
assert "skill://mcp-details/SKILL.md" in resource_uris
assert "skill://mcp-details/_manifest" in resource_uris
@pytest.mark.asyncio
async def test_lists_resource_templates(self, mcp_session_factory) -> None:
"""Ensures supporting files use per-skill wildcard templates."""
async with mcp_session_factory() as mcp_session:
result = await mcp_session.list_resource_templates()
template_uris = {template.uri_template for template in result.resource_templates}
assert "skill://mcp-details/{path*}" in template_uris
@pytest.mark.asyncio
async def test_reads_manifest_and_supporting_file(self, mcp_session_factory) -> None:
"""Ensures manifest paths resolve through the supporting-file template."""
async with mcp_session_factory() as mcp_session:
manifest_result = await mcp_session.read_resource("skill://mcp-details/_manifest")
manifest = json.loads(manifest_result.contents[0].text)
reference = next(file["path"] for file in manifest["files"] if file["path"].startswith("references/"))
reference_result = await mcp_session.read_resource(f"skill://mcp-details/{reference}")
assert manifest["skill"] == "mcp-details"
assert reference_result.contents
Generated
+55 -55
View File
@@ -1104,6 +1104,61 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/e8/6d75ffd9784bce2e93d1ae4415649427e39a53bb172d4672b2b59c6f0a7b/pathable-0.6.0-py3-none-any.whl", hash = "sha256:82c4ca6c98c502ad12e0d4e9779b6210afee93c38990988c8c5d1b49bdcdf566", size = 18983, upload-time = "2026-05-19T18:15:10.728Z" },
]
[[package]]
name = "personal-mcp"
version = "2.0.0"
source = { editable = "." }
dependencies = [
{ name = "fastapi" },
{ name = "fastmcp" },
{ name = "pydantic-settings" },
{ name = "python-json-logger" },
{ name = "pyyaml" },
{ name = "uvicorn", extra = ["standard"] },
{ name = "zensical" },
]
[package.dev-dependencies]
dev = [
{ name = "ipywidgets" },
{ name = "pre-commit" },
{ name = "ruff" },
{ name = "ty" },
]
test = [
{ name = "httpx2" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-cov" },
{ name = "pyyaml" },
]
[package.metadata]
requires-dist = [
{ name = "fastapi", specifier = ">=0.133.0" },
{ name = "fastmcp", specifier = "==4.0.0b4" },
{ name = "pydantic-settings", specifier = ">=2" },
{ name = "python-json-logger", specifier = ">=4" },
{ name = "pyyaml", specifier = ">=6.0.2" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0" },
{ name = "zensical", specifier = ">=0.0.45" },
]
[package.metadata.requires-dev]
dev = [
{ name = "ipywidgets", specifier = ">=8.1.8" },
{ name = "pre-commit", specifier = ">=4.6.0" },
{ name = "ruff", specifier = ">=0.15.18" },
{ name = "ty", specifier = ">=0.0.51" },
]
test = [
{ name = "httpx2", specifier = ">=2.9.1" },
{ name = "pytest", specifier = ">=9.1.1" },
{ name = "pytest-asyncio", specifier = ">=1.4.0" },
{ name = "pytest-cov", specifier = ">=7.1.0" },
{ name = "pyyaml", specifier = ">=6.0.2" },
]
[[package]]
name = "pexpect"
version = "4.9.0"
@@ -1162,61 +1217,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" },
]
[[package]]
name = "prompts"
version = "2.0.0"
source = { editable = "." }
dependencies = [
{ name = "fastapi" },
{ name = "fastmcp" },
{ name = "pydantic-settings" },
{ name = "python-json-logger" },
{ name = "pyyaml" },
{ name = "uvicorn", extra = ["standard"] },
{ name = "zensical" },
]
[package.dev-dependencies]
dev = [
{ name = "ipywidgets" },
{ name = "pre-commit" },
{ name = "ruff" },
{ name = "ty" },
]
test = [
{ name = "httpx2" },
{ name = "pytest" },
{ name = "pytest-asyncio" },
{ name = "pytest-cov" },
{ name = "pyyaml" },
]
[package.metadata]
requires-dist = [
{ name = "fastapi", specifier = ">=0.133.0" },
{ name = "fastmcp", specifier = "==4.0.0b4" },
{ name = "pydantic-settings", specifier = ">=2" },
{ name = "python-json-logger", specifier = ">=4" },
{ name = "pyyaml", specifier = ">=6.0.2" },
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.34.0" },
{ name = "zensical", specifier = ">=0.0.45" },
]
[package.metadata.requires-dev]
dev = [
{ name = "ipywidgets", specifier = ">=8.1.8" },
{ name = "pre-commit", specifier = ">=4.6.0" },
{ name = "ruff", specifier = ">=0.15.18" },
{ name = "ty", specifier = ">=0.0.51" },
]
test = [
{ name = "httpx2", specifier = ">=2.9.1" },
{ name = "pytest", specifier = ">=9.1.1" },
{ name = "pytest-asyncio", specifier = ">=1.4.0" },
{ name = "pytest-cov", specifier = ">=7.1.0" },
{ name = "pyyaml", specifier = ">=6.0.2" },
]
[[package]]
name = "psutil"
version = "7.2.2"
+114 -109
View File
@@ -13,6 +13,9 @@
# Read more: https://zensical.org/docs/setup/basics/#site_name
site_name = "Documentation"
site_dir = "src/personal_mcp/site"
docs_dir = "src/personal_mcp/docs"
# The site_description is included in the HTML head and should contain a
# meaningful description of the site content for use by search engines.
#
@@ -27,7 +30,7 @@ site_author = "<your name here>"
# The site_url is the canonical URL for your site. When building online
# documentation you should set this.
# Read more: https://zensical.org/docs/setup/basics/#site_url
#site_url = "https://www.example.com/"
site_url = "https://mcp.john-stream.com/docs"
# The copyright notice appears in the page footer and can contain an HTML
# fragment.
@@ -44,114 +47,115 @@ Copyright &copy; 2026 The authors
# can be defined using TOML syntax.
#
# Read more: https://zensical.org/docs/setup/navigation/
nav = [
{ "Home" = "index.md" },
{ "Guide" = [
{ "Arch" = "architecture.md" },
{ "Contracts" = [
{ "Overview" = "contracts/index.md" },
{ "Prompt" = "contracts/prompt.md" },
{ "Skill" = "contracts/skill_contract.md" },
{ "Frontmatter" = "contracts/frontmatter.md" },
{ "URIs" = "contracts/uris.md" },
] },
{ "MCP" = "mcp_layout.md" },
{ "Copilot" = "copilot.md" },
{ "Usage" = "usage.md" },
{ "Authoring" = "authoring.md" },
{ "Future Work" = "future_work.md" },
{ "Testing" = "testing.md" },
{ "Security" = "securing.md" },
] },
{ "Prompts" = [
{ "Authoring" = "prompts/authoring/PROMPT.md" },
{ "JSFiddle Page Layout" = "prompts/jsfiddle-page-layout/PROMPT.md" },
{ "NiceGUI Component Extraction" = "prompts/nicegui-component-extraction/PROMPT.md" },
{ "Pytest Fill Scaffold" = "prompts/pytest-fill-scaffold/PROMPT.md" },
{ "Pytest Scaffold" = "prompts/pytest-scaffold/PROMPT.md" },
{ "Greenfield Architecture" = "prompts/greenfield-architecture/PROMPT.md" },
{ "MCP Consumer Repo Shim" = "prompts/mcp-consumer-repo-shim/PROMPT.md" },
] },
{ "Skills" = [
{ "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/async-fastapi-sqlmodel/SKILL.md" },
{ "Engine" = "skills/async-fastapi-sqlmodel/references/engine.md" },
{ "Session" = "skills/async-fastapi-sqlmodel/references/session.md" },
{ "FastAPI" = "skills/async-fastapi-sqlmodel/references/fastapi.md" },
{ "Tx" = "skills/async-fastapi-sqlmodel/references/transactions.md" },
{ "SQLModel" = "skills/async-fastapi-sqlmodel/references/sqlmodel.md" },
{ "CRUD" = "skills/async-fastapi-sqlmodel/references/crud.md" },
{ "Testing" = "skills/async-fastapi-sqlmodel/references/testing.md" },
{ "IO" = "skills/async-fastapi-sqlmodel/references/implicit_io.md" },
{ "Obs" = "skills/async-fastapi-sqlmodel/references/observability.md" },
{ "Template" = "skills/async-fastapi-sqlmodel/references/template.md" },
] },
{ "NiceGUI" = [
{ "Overview" = "skills/nicegui/SKILL.md" },
{ "App Architecture" = "skills/nicegui/references/architecture.md" },
{ "Startup" = "skills/nicegui/references/fastapi-uvicorn-startup.md" },
{ "Visual Styling" = "skills/nicegui/references/styling-and-customization.md" },
{ "Component Mechanics" = "skills/nicegui/references/component-mechanics-and-customization.md" },
{ "Binding" = "skills/nicegui/references/binding-dataclasses.md" },
{ "Flows" = "skills/nicegui/references/interaction-patterns.md" },
{ "Quality" = "skills/nicegui/references/troubleshooting-and-quality-gates.md" },
{ "Sources" = "skills/nicegui/references/source-documentation.md" },
] },
{ "Pytest" = [
{ "Overview" = "skills/pytesting/SKILL.md" },
{ "Docs" = "skills/pytesting/references/pytest-docs.md" },
{ "AsyncIO" = "skills/pytesting/references/asyncio-testing.md" },
] },
{ "MCP Details" = [
{ "Overview" = "skills/mcp-details/SKILL.md" },
{ "Protocol" = "skills/mcp-details/references/mcp-protocol-and-spec.md" },
{ "SDKs and FastMCP" = "skills/mcp-details/references/sdk-and-fastmcp.md" },
{ "Ecosystem" = "skills/mcp-details/references/ecosystem-and-tooling.md" },
] },
{ "Logging" = [
{ "Overview" = "skills/python-logging/SKILL.md" },
{ "Docs" = "skills/python-logging/references/python-logging-docs.md" },
{ "JSON File" = "skills/python-logging/references/json-file-logging.md" },
{ "Network" = "skills/python-logging/references/network-logging-minimal-example.md" },
{ "HTTPX" = "skills/python-logging/references/httpx-logging-handler-example.md" },
] },
{ "Pydantic Settings" = [
{ "Overview" = "skills/pydantic-settings/SKILL.md" },
{ "Source Docs" = "skills/pydantic-settings/references/source-documentation.md" },
{ "Workflow" = "skills/pydantic-settings/references/implementation-workflow.md" },
] },
{ "Ruff" = [
{ "Overview" = "skills/ruff-linting-formating/SKILL.md" },
{ "Docs" = "skills/ruff-linting-formating/references/ruff-docs.md" },
{ "Integrations" = "skills/ruff-linting-formating/references/ruff-integrations.md" },
] },
{ "Zensical" = [
{ "Overview" = "skills/zensical-docs/SKILL.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" },
] },
] },
]
# nav = [
# { "Home" = "index.md" },
# { "Guide" = [
# { "Arch" = "architecture.md" },
# { "Contracts" = [
# { "Overview" = "contracts/index.md" },
# { "Prompt" = "contracts/prompt.md" },
# { "Skill" = "contracts/skill_contract.md" },
# { "Frontmatter" = "contracts/frontmatter.md" },
# { "URIs" = "contracts/uris.md" },
# ] },
# { "MCP" = "mcp_layout.md" },
# { "Copilot" = "copilot.md" },
# { "Usage" = "usage.md" },
# { "Authoring" = "authoring.md" },
# { "Future Work" = "future_work.md" },
# { "Testing" = "testing.md" },
# { "Security" = "securing.md" },
# ] },
# { "Prompts" = [
# { "Authoring" = "prompts/authoring/PROMPT.md" },
# { "JSFiddle Page Layout" = "prompts/jsfiddle-page-layout/PROMPT.md" },
# { "NiceGUI Component Extraction" = "prompts/nicegui-component-extraction/PROMPT.md" },
# { "Pytest Fill Scaffold" = "prompts/pytest-fill-scaffold/PROMPT.md" },
# { "Pytest Scaffold" = "prompts/pytest-scaffold/PROMPT.md" },
# { "Greenfield Architecture" = "prompts/greenfield-architecture/PROMPT.md" },
# { "MCP Consumer Repo Shim" = "prompts/mcp-consumer-repo-shim/PROMPT.md" },
# ] },
# { "Skills" = [
# { "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/async-fastapi-sqlmodel/SKILL.md" },
# { "Engine" = "skills/async-fastapi-sqlmodel/references/engine.md" },
# { "Session" = "skills/async-fastapi-sqlmodel/references/session.md" },
# { "FastAPI" = "skills/async-fastapi-sqlmodel/references/fastapi.md" },
# { "Tx" = "skills/async-fastapi-sqlmodel/references/transactions.md" },
# { "SQLModel" = "skills/async-fastapi-sqlmodel/references/sqlmodel.md" },
# { "CRUD" = "skills/async-fastapi-sqlmodel/references/crud.md" },
# { "Testing" = "skills/async-fastapi-sqlmodel/references/testing.md" },
# { "IO" = "skills/async-fastapi-sqlmodel/references/implicit_io.md" },
# { "Obs" = "skills/async-fastapi-sqlmodel/references/observability.md" },
# { "Template" = "skills/async-fastapi-sqlmodel/references/template.md" },
# ] },
# { "NiceGUI" = [
# { "Overview" = "skills/nicegui/SKILL.md" },
# { "App Architecture" = "skills/nicegui/references/architecture.md" },
# { "Startup" = "skills/nicegui/references/fastapi-uvicorn-startup.md" },
# { "Visual Styling" = "skills/nicegui/references/styling-and-customization.md" },
# { "Component Mechanics" = "skills/nicegui/references/component-mechanics.md" },
# { "Tables" = "skills/nicegui/references/tables.md" },
# { "Binding" = "skills/nicegui/references/binding-dataclasses.md" },
# { "Flows" = "skills/nicegui/references/interaction-patterns.md" },
# { "Quality" = "skills/nicegui/references/troubleshooting-and-quality-gates.md" },
# { "Sources" = "skills/nicegui/references/source-documentation.md" },
# ] },
# { "Pytest" = [
# { "Overview" = "skills/pytesting/SKILL.md" },
# { "Docs" = "skills/pytesting/references/pytest-docs.md" },
# { "AsyncIO" = "skills/pytesting/references/asyncio-testing.md" },
# ] },
# { "MCP Details" = [
# { "Overview" = "skills/mcp-details/SKILL.md" },
# { "Protocol" = "skills/mcp-details/references/mcp-protocol-and-spec.md" },
# { "SDKs and FastMCP" = "skills/mcp-details/references/sdk-and-fastmcp.md" },
# { "Ecosystem" = "skills/mcp-details/references/ecosystem-and-tooling.md" },
# ] },
# { "Logging" = [
# { "Overview" = "skills/python-logging/SKILL.md" },
# { "Docs" = "skills/python-logging/references/python-logging-docs.md" },
# { "JSON File" = "skills/python-logging/references/json-file-logging.md" },
# { "Network" = "skills/python-logging/references/network-logging-minimal-example.md" },
# { "HTTPX" = "skills/python-logging/references/httpx-logging-handler-example.md" },
# ] },
# { "Pydantic Settings" = [
# { "Overview" = "skills/pydantic-settings/SKILL.md" },
# { "Source Docs" = "skills/pydantic-settings/references/source-documentation.md" },
# { "Workflow" = "skills/pydantic-settings/references/implementation-workflow.md" },
# ] },
# { "Ruff" = [
# { "Overview" = "skills/ruff-linting-formating/SKILL.md" },
# { "Docs" = "skills/ruff-linting-formating/references/ruff-docs.md" },
# { "Integrations" = "skills/ruff-linting-formating/references/ruff-integrations.md" },
# ] },
# { "Zensical" = [
# { "Overview" = "skills/zensical-docs/SKILL.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
@@ -441,6 +445,7 @@ anchor_linenums = true
line_spans = "__span"
pygments_lang_class = true
[project.markdown_extensions.pymdownx.inlinehilite]
[project.markdown_extensions.pymdownx.snippets]
[project.markdown_extensions.pymdownx.keys]
[project.markdown_extensions.pymdownx.magiclink]
[project.markdown_extensions.pymdownx.mark]