zensical docs skill
This commit is contained in:
+10
-4
@@ -14,7 +14,9 @@ Use this checklist after generating a new skill under `docs/skills/<slug>/`.
|
||||
Create `src/personal_mcp/skills/<python_namespace>/` 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/<skill-id>/document`, and return `load_skill_document(skill_id=<skill-id>, skill_slug=<slug>)`.
|
||||
Follow the existing pattern: create a `FastMCP` instance, register `resource://skills/<skill-id>/document`, and return the shared loader result.
|
||||
For the default layout, load `docs/skills/<slug>/SKILL.md`.
|
||||
For special cases, set `document_path` in `metadata.yaml` to a repo-relative Markdown file and load from metadata instead of hardcoding a path in the server.
|
||||
|
||||
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/<skill-id>/document`.
|
||||
@@ -23,7 +25,7 @@ Use this checklist after generating a new skill under `docs/skills/<slug>/`.
|
||||
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/<slug>/SKILL.md`, and the catalog discovers metadata from `src/personal_mcp/skills/*/metadata.yaml` automatically.
|
||||
The document loader reads canonical Markdown from `docs/skills/<slug>/SKILL.md` by default, or from `metadata.yaml`'s optional `document_path` override when present. 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.
|
||||
@@ -39,8 +41,9 @@ To keep behavior consistent across MCP clients and Copilot session types, follow
|
||||
### Do
|
||||
|
||||
1. Add or update `metadata.yaml` fields (`id`, `description`, `tags`, `capabilities`) so catalog discovery quality stays high.
|
||||
2. Use catalog resources as the primary discovery surface.
|
||||
3. Add thin, read-only catalog tools only when client behavior needs a fallback path.
|
||||
2. Use `document_path` when a skill should expose a Markdown file outside `docs/skills/<slug>/SKILL.md`.
|
||||
3. Use catalog resources as the primary discovery surface.
|
||||
4. Add thin, read-only catalog tools only when client behavior needs a fallback path.
|
||||
|
||||
### Don't
|
||||
|
||||
@@ -92,11 +95,14 @@ description: <One sentence describing what the skill provides.>
|
||||
tags:
|
||||
- <tag-one>
|
||||
- <tag-two>
|
||||
document_path: <optional repo-relative path to a markdown file>
|
||||
capabilities:
|
||||
- resource://skills/<skill-id>/document
|
||||
depends_on: []
|
||||
```
|
||||
|
||||
Omit `document_path` when the canonical document is `docs/skills/<slug>/SKILL.md`.
|
||||
|
||||
## Root Mount Template
|
||||
|
||||
Add an import in `src/personal_mcp/mcp.py`:
|
||||
|
||||
@@ -33,6 +33,16 @@ Always start a new documentation project with `uv run zensical new`.
|
||||
- This command provides the baseline scaffolding you need for structure, configuration, and theme setup.
|
||||
- Do not hand-build a new project skeleton when this command is available.
|
||||
|
||||
## Related Skill Discovery
|
||||
|
||||
When the task is not just writing docs but creating or wiring a new MCP skill in this repository, use catalog discovery to load the bootstrap skill before drafting implementation steps.
|
||||
|
||||
1. Search the catalog with terms such as `new skill`, `skill bootstrap`, or `scaffold skill`.
|
||||
2. Fetch the `new-skill` document through the catalog tool path.
|
||||
3. Use that skill for runtime package, metadata, and mount wiring, then return here for documentation architecture and Zensical-specific authoring guidance.
|
||||
|
||||
This keeps implementation guidance and documentation guidance separated while still making both discoverable from one request.
|
||||
|
||||
## Inputs To Collect
|
||||
|
||||
Collect these before writing. If missing, make explicit assumptions.
|
||||
|
||||
@@ -147,6 +147,50 @@ Weak metadata reduces Copilot match quality and increases wrong context injectio
|
||||
|
||||
If you skip the catalog/index step, behavior is less predictable and may either miss relevant skills or pull too much context.
|
||||
|
||||
## 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 catalog tools instead.
|
||||
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` or `resource://catalog/patterns`
|
||||
2. `resource://skills/<skill-id>/document`
|
||||
|
||||
Preferred tool fallback order:
|
||||
|
||||
1. `search_patterns`
|
||||
2. `get_pattern_by_id`
|
||||
3. `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:
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any
|
||||
import yaml
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from personal_mcp.skills.document_loader import load_skill_document
|
||||
from personal_mcp.skills.document_loader import load_skill_document_from_metadata
|
||||
|
||||
catalog_server = FastMCP("catalog")
|
||||
|
||||
@@ -59,6 +59,10 @@ def _matches_query(pattern: dict[str, Any], query: str) -> bool:
|
||||
if not lowered:
|
||||
return True
|
||||
|
||||
query_terms = [term for term in lowered.replace("-", " ").split() if term]
|
||||
if not query_terms:
|
||||
return True
|
||||
|
||||
haystack = " ".join(
|
||||
[
|
||||
str(pattern.get("id", "")),
|
||||
@@ -68,7 +72,7 @@ def _matches_query(pattern: dict[str, Any], query: str) -> bool:
|
||||
" ".join(str(tag) for tag in pattern.get("tags", [])),
|
||||
]
|
||||
).lower()
|
||||
return lowered in haystack
|
||||
return all(term in haystack for term in query_terms)
|
||||
|
||||
|
||||
def _matches_tags(pattern: dict[str, Any], tags: list[str] | None) -> bool:
|
||||
@@ -83,34 +87,6 @@ def _matches_tags(pattern: dict[str, Any], tags: list[str] | None) -> bool:
|
||||
return all(tag in pattern_tags for tag in requested)
|
||||
|
||||
|
||||
def _resolve_skill_slug(*, skill_id: str, namespace: str, metadata: dict[str, Any]) -> str:
|
||||
candidates: list[str] = []
|
||||
slug = metadata.get("slug")
|
||||
if isinstance(slug, str) and slug.strip():
|
||||
candidates.append(slug.strip())
|
||||
|
||||
candidates.extend(
|
||||
[
|
||||
skill_id,
|
||||
namespace.replace("_", "-"),
|
||||
namespace,
|
||||
]
|
||||
)
|
||||
|
||||
seen: set[str] = set()
|
||||
for candidate in candidates:
|
||||
if candidate in seen:
|
||||
continue
|
||||
seen.add(candidate)
|
||||
try:
|
||||
load_skill_document(skill_id=skill_id, skill_slug=candidate)
|
||||
return candidate
|
||||
except FileNotFoundError:
|
||||
continue
|
||||
|
||||
return skill_id
|
||||
|
||||
|
||||
@catalog_server.resource("resource://catalog/skills_index")
|
||||
def skills_index() -> dict[str, Any]:
|
||||
"""Return a compact discovery index for all available pattern modules."""
|
||||
@@ -184,14 +160,13 @@ def get_skill_document_by_id(skill_id: str) -> dict[str, Any]:
|
||||
if pattern_id != skill_id:
|
||||
continue
|
||||
|
||||
skill_slug = _resolve_skill_slug(
|
||||
return {
|
||||
"found": True,
|
||||
"document": load_skill_document_from_metadata(
|
||||
skill_id=skill_id,
|
||||
namespace=namespace,
|
||||
metadata=metadata,
|
||||
)
|
||||
return {
|
||||
"found": True,
|
||||
"document": load_skill_document(skill_id=skill_id, skill_slug=skill_slug),
|
||||
),
|
||||
}
|
||||
|
||||
return {"found": False, "id": skill_id}
|
||||
|
||||
@@ -14,6 +14,7 @@ from personal_mcp.skills.pytest_scaffolding.server import pytest_scaffolding_ser
|
||||
from personal_mcp.skills.python_logging_dictconfig.server import (
|
||||
python_logging_dictconfig_server,
|
||||
)
|
||||
from personal_mcp.skills.zensical_docs.server import zensical_docs_server
|
||||
|
||||
mcp = FastMCP("personal-mcp")
|
||||
|
||||
@@ -28,3 +29,4 @@ mcp.mount(nicegui_ui_customization_server, namespace="nicegui_ui_customization")
|
||||
mcp.mount(pytest_scaffolding_server, namespace="pytest_scaffolding")
|
||||
mcp.mount(python_logging_dictconfig_server, namespace="python_logging_dictconfig")
|
||||
mcp.mount(fastapi_uv_docker_server, namespace="fastapi_uv_docker")
|
||||
mcp.mount(zensical_docs_server, namespace="zensical_docs")
|
||||
|
||||
@@ -1,10 +1,45 @@
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
def _repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[3]
|
||||
|
||||
|
||||
def resolve_skill_document_path(
|
||||
*, skill_id: str, namespace: str, metadata: dict[str, Any]
|
||||
) -> Path:
|
||||
"""Resolve the canonical Markdown document path for a skill."""
|
||||
document_path = metadata.get("document_path")
|
||||
if isinstance(document_path, str) and document_path.strip():
|
||||
return _repo_root() / document_path.strip()
|
||||
|
||||
candidates: list[str] = []
|
||||
slug = metadata.get("slug")
|
||||
if isinstance(slug, str) and slug.strip():
|
||||
candidates.append(slug.strip())
|
||||
|
||||
candidates.extend(
|
||||
[
|
||||
skill_id,
|
||||
namespace.replace("_", "-"),
|
||||
namespace,
|
||||
]
|
||||
)
|
||||
|
||||
seen: set[str] = set()
|
||||
for candidate in candidates:
|
||||
if candidate in seen:
|
||||
continue
|
||||
seen.add(candidate)
|
||||
|
||||
candidate_path = _repo_root() / "docs" / "skills" / candidate / "SKILL.md"
|
||||
if candidate_path.exists():
|
||||
return candidate_path
|
||||
|
||||
return _repo_root() / "docs" / "skills" / skill_id / "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():
|
||||
@@ -25,3 +60,15 @@ 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)
|
||||
|
||||
|
||||
def load_skill_document_from_metadata(
|
||||
*, skill_id: str, namespace: str, metadata: dict[str, Any]
|
||||
) -> dict[str, str]:
|
||||
"""Load a skill document using metadata overrides when present."""
|
||||
document_path = resolve_skill_document_path(
|
||||
skill_id=skill_id,
|
||||
namespace=namespace,
|
||||
metadata=metadata,
|
||||
)
|
||||
return load_markdown_document(skill_id=skill_id, document_path=document_path)
|
||||
|
||||
@@ -7,6 +7,7 @@ tags:
|
||||
- scaffolding
|
||||
- skills
|
||||
- mcp
|
||||
document_path: docs/new_skill.md
|
||||
capabilities:
|
||||
- resource://skills/new-skill/document
|
||||
depends_on: []
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from personal_mcp.skills.document_loader import load_markdown_document
|
||||
from personal_mcp.skills.document_loader import load_skill_document_from_metadata
|
||||
|
||||
new_skill_server = FastMCP("new-skill")
|
||||
_METADATA_PATH = Path(__file__).with_name("metadata.yaml")
|
||||
_METADATA = yaml.safe_load(_METADATA_PATH.read_text(encoding="utf-8")) or {}
|
||||
|
||||
|
||||
@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(
|
||||
return load_skill_document_from_metadata(
|
||||
skill_id="new-skill",
|
||||
document_path=document_path,
|
||||
namespace="new_skill",
|
||||
metadata=_METADATA,
|
||||
)
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""Zensical documentation authoring skill server."""
|
||||
@@ -0,0 +1,16 @@
|
||||
id: zensical-docs
|
||||
name: Zensical Documentation Authoring
|
||||
version: 1.0.0
|
||||
description: Plan, write, and improve high-quality documentation with Zensical.
|
||||
tags:
|
||||
- zensical
|
||||
- docs
|
||||
- documentation
|
||||
- information-architecture
|
||||
- skills
|
||||
- bootstrap
|
||||
- discovery
|
||||
- authoring
|
||||
capabilities:
|
||||
- resource://skills/zensical-docs/document
|
||||
depends_on: []
|
||||
@@ -0,0 +1,14 @@
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from personal_mcp.skills.document_loader import load_skill_document
|
||||
|
||||
zensical_docs_server = FastMCP("zensical-docs")
|
||||
|
||||
|
||||
@zensical_docs_server.resource("resource://skills/zensical-docs/document")
|
||||
def skill_document() -> dict[str, str]:
|
||||
"""Return the canonical Markdown document for this skill."""
|
||||
return load_skill_document(
|
||||
skill_id="zensical-docs",
|
||||
skill_slug="zensical-docs",
|
||||
)
|
||||
Reference in New Issue
Block a user