diff --git a/Dockerfile b/Dockerfile index a361472..b77ba84 100644 --- a/Dockerfile +++ b/Dockerfile @@ -17,7 +17,6 @@ COPY src ./src RUN uv sync --frozen --no-dev COPY docs ./docs -COPY skills ./skills COPY zensical.toml ./ RUN uv run zensical build diff --git a/docs/new_skill.md b/docs/new_skill.md new file mode 100644 index 0000000..d023488 --- /dev/null +++ b/docs/new_skill.md @@ -0,0 +1,152 @@ +# Hooking Up a New Skill + +Use this checklist after generating a new skill under `docs/skills//`. + +## Checklist + +1. Create the authored docs content. + Add `docs/skills//SKILL.md` and any companion files under `docs/skills//references/`. + +2. Choose the three names up front. + Use a docs slug like `fastapi-uv-docker`, a resource id like `fastapi-uv-docker`, and a Python package name like `fastapi_uv_docker`. + +3. Add the runtime package. + Create `src/personal_mcp/skills//` with `__init__.py`, `server.py`, and `metadata.yaml`. + +4. Expose the document resource in `server.py`. + Follow the existing pattern: create a `FastMCP` instance, register `resource://skills//document`, and return `load_skill_document(skill_id=, skill_slug=)`. + +5. Register the catalog metadata. + In `metadata.yaml`, add the skill `id`, `name`, `version`, `description`, `tags`, `capabilities`, and `depends_on`. The `capabilities` list should include `resource://skills//document`. + +6. Mount the skill in the root server. + Import the new server in `src/personal_mcp/mcp.py` and add an `mcp.mount(...)` call with the Python namespace. + +7. Let the loader and catalog do the rest. + The document loader reads canonical Markdown from `docs/skills//SKILL.md`, and the catalog discovers metadata from `src/personal_mcp/skills/*/metadata.yaml` automatically. + +8. Rebuild and smoke-test. + Run `uv run zensical build` to publish the docs site, then run a quick Python check or start the app to confirm the new resource loads. + +## Minimal Shape + +- Docs content: `docs/skills//SKILL.md` +- Optional references: `docs/skills//references/*.md` +- Runtime package: `src/personal_mcp/skills//` +- Resource URI: `resource://skills//document` + +## Quick Validation + +1. Confirm the Markdown document resolves through the loader. + `uv run python -c "from personal_mcp.skills.document_loader import load_skill_document; print(load_skill_document(skill_id='', skill_slug='')['source_path'])"` + +2. Confirm the docs build still works. + `uv run zensical build` + +## server.py Template + +```python +from fastmcp import FastMCP + +from personal_mcp.skills.document_loader import load_skill_document + +_server = FastMCP("") + + +@_server.resource("resource://skills//document") +def skill_document() -> dict[str, str]: + """Return the canonical Markdown document for this skill.""" + return load_skill_document( + skill_id="", + skill_slug="", + ) +``` + +## metadata.yaml Template + +```yaml +id: +name: +version: 1.0.0 +description: +tags: + - + - +capabilities: + - resource://skills//document +depends_on: [] +``` + +## Root Mount Template + +Add an import in `src/personal_mcp/mcp.py`: + +```python +from personal_mcp.skills..server import _server +``` + +Add a mount call: + +```python +mcp.mount(_server, namespace="") +``` + +## Example Scaffold + +For a new skill called `sqlmodel-patterns`: + +1. Docs content lives in `docs/skills/sqlmodel-patterns/SKILL.md`. +2. The Python package lives in `src/personal_mcp/skills/sqlmodel_patterns/`. +3. The resource id is `sqlmodel-patterns`. + +Example `server.py`: + +```python +from fastmcp import FastMCP + +from personal_mcp.skills.document_loader import load_skill_document + +sqlmodel_patterns_server = FastMCP("sqlmodel-patterns") + + +@sqlmodel_patterns_server.resource("resource://skills/sqlmodel-patterns/document") +def skill_document() -> dict[str, str]: + """Return the canonical Markdown document for this skill.""" + return load_skill_document( + skill_id="sqlmodel-patterns", + skill_slug="sqlmodel-patterns", + ) +``` + +Example `metadata.yaml`: + +```yaml +id: sqlmodel-patterns +name: SQLModel Patterns +version: 1.0.0 +description: Provide reusable patterns for building apps with SQLModel. +tags: + - sqlmodel + - python + - patterns +capabilities: + - resource://skills/sqlmodel-patterns/document +depends_on: [] +``` + +Example `mcp.py` additions: + +```python +from personal_mcp.skills.sqlmodel_patterns.server import sqlmodel_patterns_server + +mcp.mount(sqlmodel_patterns_server, namespace="sqlmodel_patterns") +``` + +## Bootstrap Sequence + +1. Create `docs/skills//SKILL.md`. +2. Copy the `server.py` template into `src/personal_mcp/skills//server.py`. +3. Copy the `metadata.yaml` template into `src/personal_mcp/skills//metadata.yaml`. +4. Add `__init__.py` in the new package directory. +5. Import and mount the server in `src/personal_mcp/mcp.py`. +6. Run the validation commands above. diff --git a/src/personal_mcp/mcp.py b/src/personal_mcp/mcp.py index 5f77c65..de0b556 100644 --- a/src/personal_mcp/mcp.py +++ b/src/personal_mcp/mcp.py @@ -5,6 +5,7 @@ from personal_mcp.skills.fastapi_async_sqlalchemy_modernization.server import ( fastapi_async_sqlalchemy_modernization_server, ) from personal_mcp.skills.fastapi_uv_docker.server import fastapi_uv_docker_server +from personal_mcp.skills.new_skill.server import new_skill_server from personal_mcp.skills.nicegui.server import nicegui_server from personal_mcp.skills.nicegui_ui_customization.server import ( nicegui_ui_customization_server, @@ -21,6 +22,7 @@ mcp.mount( fastapi_async_sqlalchemy_modernization_server, namespace="fastapi_async_sqlalchemy_modernization", ) +mcp.mount(new_skill_server, namespace="new_skill") mcp.mount(nicegui_server, namespace="nicegui") mcp.mount(nicegui_ui_customization_server, namespace="nicegui_ui_customization") mcp.mount(pytest_scaffolding_server, namespace="pytest_scaffolding") diff --git a/src/personal_mcp/skills/document_loader.py b/src/personal_mcp/skills/document_loader.py index f56b64e..9fbeeb3 100644 --- a/src/personal_mcp/skills/document_loader.py +++ b/src/personal_mcp/skills/document_loader.py @@ -5,9 +5,8 @@ def _repo_root() -> Path: return Path(__file__).resolve().parents[3] -def load_skill_document(*, skill_id: str, skill_slug: str) -> dict[str, str]: - """Load the canonical skill markdown document for an MCP skill.""" - document_path = _repo_root() / "docs" / "skills" / skill_slug / "SKILL.md" +def load_markdown_document(*, skill_id: str, document_path: Path) -> dict[str, str]: + """Load an arbitrary Markdown document and expose it as a skill resource.""" if not document_path.exists(): raise FileNotFoundError( f"Missing skill document for '{skill_id}': {document_path}" @@ -20,3 +19,9 @@ def load_skill_document(*, skill_id: str, skill_slug: str) -> dict[str, str]: "source_path": str(document_path), "content": document_path.read_text(encoding="utf-8"), } + + +def load_skill_document(*, skill_id: str, skill_slug: str) -> dict[str, str]: + """Load the canonical skill markdown document for an MCP skill.""" + document_path = _repo_root() / "docs" / "skills" / skill_slug / "SKILL.md" + return load_markdown_document(skill_id=skill_id, document_path=document_path) diff --git a/src/personal_mcp/skills/new_skill/__init__.py b/src/personal_mcp/skills/new_skill/__init__.py new file mode 100644 index 0000000..60781c5 --- /dev/null +++ b/src/personal_mcp/skills/new_skill/__init__.py @@ -0,0 +1 @@ +"""Pseudo-skill exposing the new skill bootstrap guide.""" diff --git a/src/personal_mcp/skills/new_skill/metadata.yaml b/src/personal_mcp/skills/new_skill/metadata.yaml new file mode 100644 index 0000000..8673366 --- /dev/null +++ b/src/personal_mcp/skills/new_skill/metadata.yaml @@ -0,0 +1,12 @@ +id: new-skill +name: New Skill Bootstrap +version: 1.0.0 +description: Provide the bootstrap checklist and templates for creating new MCP skills. +tags: + - bootstrap + - scaffolding + - skills + - mcp +capabilities: + - resource://skills/new-skill/document +depends_on: [] diff --git a/src/personal_mcp/skills/new_skill/server.py b/src/personal_mcp/skills/new_skill/server.py new file mode 100644 index 0000000..8c3c7b0 --- /dev/null +++ b/src/personal_mcp/skills/new_skill/server.py @@ -0,0 +1,17 @@ +from pathlib import Path + +from fastmcp import FastMCP + +from personal_mcp.skills.document_loader import load_markdown_document + +new_skill_server = FastMCP("new-skill") + + +@new_skill_server.resource("resource://skills/new-skill/document") +def skill_document() -> dict[str, str]: + """Return the bootstrap guide used to scaffold new skills.""" + document_path = Path(__file__).resolve().parents[4] / "docs" / "new_skill.md" + return load_markdown_document( + skill_id="new-skill", + document_path=document_path, + )