implemented steps 1-5
This commit is contained in:
+76
-18
@@ -16,6 +16,12 @@ The system is complete in three layers:
|
|||||||
2. Catalog resources provide normalized discovery.
|
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.
|
3. Zensical builds a static site from those same Markdown sources and the FastAPI app serves it in the FastMCP runtime process.
|
||||||
|
|
||||||
|
For Phase 1, this architecture is anchored by three contracts:
|
||||||
|
|
||||||
|
1. Step 1: docs-first authored content contract under `docs/` with strict per-skill ownership.
|
||||||
|
2. Step 2: SKILL.md frontmatter contract with Anthropic fields plus `x-personal-mcp` metadata.
|
||||||
|
3. Step 3: canonical resource URI contract with break-and-replace policy for contract changes.
|
||||||
|
|
||||||
This architecture keeps authored content human-friendly while preserving machine-stable contracts.
|
This architecture keeps authored content human-friendly while preserving machine-stable contracts.
|
||||||
|
|
||||||
## Intent
|
## Intent
|
||||||
@@ -30,7 +36,14 @@ The architecture is designed to satisfy three long-term requirements:
|
|||||||
|
|
||||||
### Pattern Modules
|
### Pattern Modules
|
||||||
|
|
||||||
Each module encapsulates one methodology domain and publishes resource families:
|
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
|
1. document
|
||||||
|
|
||||||
@@ -42,10 +55,23 @@ The catalog is the canonical discovery layer and publishes normalized records fo
|
|||||||
|
|
||||||
Typical catalog resources:
|
Typical catalog resources:
|
||||||
|
|
||||||
1. resource://catalog/patterns
|
1. resource://catalog/skills_index
|
||||||
2. resource://catalog/patterns_by_id
|
2. resource://catalog/skills/{skill_id}
|
||||||
3. resource://catalog/skills_index
|
|
||||||
4. resource://catalog/skills_details
|
Only canonical catalog resources are part of the runtime contract in this phase.
|
||||||
|
|
||||||
|
### Registry Loader
|
||||||
|
|
||||||
|
Phase 2 runtime composition introduces 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 Sources
|
||||||
|
|
||||||
@@ -76,27 +102,59 @@ flowchart TD
|
|||||||
|
|
||||||
### Metadata Contract
|
### Metadata Contract
|
||||||
|
|
||||||
Each pattern module declares:
|
Each skill declares frontmatter in `docs/skills/<skill-id>/SKILL.md`.
|
||||||
|
|
||||||
|
Anthropic-facing required fields:
|
||||||
|
|
||||||
|
1. name
|
||||||
|
2. description
|
||||||
|
|
||||||
|
Repository indexing metadata is declared in `x-personal-mcp`:
|
||||||
|
|
||||||
1. id
|
1. id
|
||||||
2. name
|
2. version
|
||||||
3. version
|
3. tags
|
||||||
4. description
|
4. capabilities
|
||||||
5. tags
|
5. depends_on
|
||||||
6. capabilities
|
6. references map (ref id to relative path and optional metadata)
|
||||||
7. depends_on
|
|
||||||
|
No `metadata.yaml` sidecar is part of the end-state contract.
|
||||||
|
|
||||||
### URI Contract
|
### URI Contract
|
||||||
|
|
||||||
Module resource URIs are stable and follow:
|
Canonical resource URIs are:
|
||||||
|
|
||||||
1. resource://skills/<skill_id>/document
|
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*}
|
||||||
|
|
||||||
Catalog resource URIs are stable and discovery-focused.
|
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
|
### Versioning Rule
|
||||||
|
|
||||||
Published URIs are immutable. Behavioral or schema changes are versioned in metadata and documented through additive migration notes.
|
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
|
## Static Hosting Pattern
|
||||||
|
|
||||||
@@ -161,8 +219,8 @@ Allowed exception:
|
|||||||
|
|
||||||
Existing markdown reference sets are valid examples of authored source material for this architecture:
|
Existing markdown reference sets are valid examples of authored source material for this architecture:
|
||||||
|
|
||||||
1. ../docs/skills/pytest-scaffolding/references/pytest-docs.md
|
1. docs/skills/pytest-scaffolding/references/pytest-docs.md
|
||||||
2. ../docs/skills/python-logging-dictconfig/references/python-logging-docs.md
|
2. docs/skills/python-logging-dictconfig/references/python-logging-docs.md
|
||||||
3. ../docs/skills/fastapi-uv-docker/references/fastapi-best-practices.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.
|
These inputs are treated as content sources, while resource URIs and catalog payloads remain the machine-facing contracts.
|
||||||
|
|||||||
+6
-4
@@ -18,7 +18,7 @@ Copilot interacts with MCP servers through separate capability lanes:
|
|||||||
|
|
||||||
These lanes are related but independently gated in the client.
|
These lanes are related but independently gated in the client.
|
||||||
|
|
||||||
The documented reliable paths are:
|
Reliable paths are:
|
||||||
|
|
||||||
1. attach MCP resources explicitly through `Add Context > MCP Resources` or `MCP: Browse Resources`
|
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
|
2. let Copilot invoke MCP tools when the task and tool descriptions make that relevant
|
||||||
@@ -57,7 +57,7 @@ Use this sequence to confirm behavior:
|
|||||||
|
|
||||||
## Recommended Usage Pattern
|
## Recommended Usage Pattern
|
||||||
|
|
||||||
1. rely on catalog resources for discovery (`skills_index`, `patterns`, etc.)
|
1. rely on canonical catalog resources for discovery (`skills_index`, then `skills/{skill_id}`)
|
||||||
2. fetch only selected skill documents for context
|
2. fetch only selected skill documents for context
|
||||||
3. keep slash commands for deterministic fallback flows
|
3. keep slash commands for deterministic fallback flows
|
||||||
|
|
||||||
@@ -134,8 +134,10 @@ When a task may benefit from personal-mcp skills, use this sequence:
|
|||||||
|
|
||||||
Preferred discovery order:
|
Preferred discovery order:
|
||||||
|
|
||||||
1. `resource://catalog/skills_index` or `resource://catalog/patterns`
|
1. `resource://catalog/skills_index`
|
||||||
2. `resource://skills/<skill-id>/document`
|
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:
|
Tool fallback order:
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -4,11 +4,11 @@ icon: lucide/rocket
|
|||||||
|
|
||||||
# Personal MCP
|
# Personal MCP
|
||||||
|
|
||||||
This is a document library of software patterns, best practices, and structured references to external documentation, which are available in 2, equivalent ways. The same, exact markdown files are reused for both cases, so there is never any incongruity between them.
|
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.
|
||||||
|
|
||||||
## MCP Server
|
## MCP Server
|
||||||
|
|
||||||
An [MCP server](https://modelcontextprotocol.io/docs/getting-started/intro) at `/mcp` to provide 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), like VSCode.
|
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.
|
||||||
|
|
||||||
## Docs
|
## Docs
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# FastMCP Greenfield Contracts (Steps 1-5)
|
||||||
|
|
||||||
|
## Step 1: End-State Content Contract
|
||||||
|
|
||||||
|
1. All authored markdown lives under docs/.
|
||||||
|
2. Skill docs live under docs/skills/<skill-id>/.
|
||||||
|
3. Canonical skill shape is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/
|
||||||
|
skills/
|
||||||
|
<skill-id>/
|
||||||
|
SKILL.md
|
||||||
|
references/
|
||||||
|
```
|
||||||
|
|
||||||
|
4. SKILL.md is required for every skill.
|
||||||
|
5. references/ is the only place for skill-specific supporting docs.
|
||||||
|
6. Skill directories are ownership boundaries.
|
||||||
|
7. Skill id rules:
|
||||||
|
- lowercase kebab-case
|
||||||
|
- starts with a letter
|
||||||
|
- directory name matches skill id
|
||||||
|
- SKILL frontmatter id matches directory name
|
||||||
|
|
||||||
|
## Step 2: SKILL Frontmatter and Metadata Contract
|
||||||
|
|
||||||
|
1. name and description are required top-level frontmatter fields.
|
||||||
|
2. Repository indexing metadata lives in x-personal-mcp.
|
||||||
|
3. x-personal-mcp fields:
|
||||||
|
- required: id, version, capabilities
|
||||||
|
- optional: tags, depends_on, references
|
||||||
|
4. references maps stable ref ids to skill-relative markdown paths under references/.
|
||||||
|
5. metadata.yaml sidecars are not part of the canonical model.
|
||||||
|
|
||||||
|
## Step 3: URI Contract and Compatibility Policy
|
||||||
|
|
||||||
|
Canonical URI surface:
|
||||||
|
|
||||||
|
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*}
|
||||||
|
|
||||||
|
Rules:
|
||||||
|
|
||||||
|
1. skill_id and ref_id are lowercase kebab-case.
|
||||||
|
2. docs path is markdown-only and cannot traverse outside docs/.
|
||||||
|
3. URI families are unversioned and canonical in this phase.
|
||||||
|
4. Breaking changes use direct replacement with no compatibility aliases.
|
||||||
|
|
||||||
|
## Step 4: Docs Registry Loader Contract
|
||||||
|
|
||||||
|
1. Loader uses importlib.resources.files(...) and Traversable APIs.
|
||||||
|
2. Startup validates SKILL frontmatter schema, id invariants, reference integrity, dependency graph, and URI uniqueness.
|
||||||
|
3. Registry is immutable for request-time reads.
|
||||||
|
4. Invalid docs state is a hard startup error.
|
||||||
|
|
||||||
|
## Step 5: Registry-Driven Resource Registration Contract
|
||||||
|
|
||||||
|
1. FastMCP resources are registered from the validated registry.
|
||||||
|
2. RFC6570 templates are used for parameterized routes.
|
||||||
|
3. Docs resources declare explicit MIME types.
|
||||||
|
4. Docs resources include readOnlyHint and idempotentHint annotations.
|
||||||
|
5. Duplicate registrations fail startup via strict duplicate policy.
|
||||||
+64
-15
@@ -30,14 +30,36 @@ treeView-beta
|
|||||||
"index.md"
|
"index.md"
|
||||||
"architecture.md"
|
"architecture.md"
|
||||||
"mcp_layout.md"
|
"mcp_layout.md"
|
||||||
|
"mcp_contract_steps_1_5.md"
|
||||||
"skills"
|
"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"
|
"pytest-scaffolding"
|
||||||
"SKILL.md"
|
"SKILL.md"
|
||||||
"references"
|
"references"
|
||||||
"python-logging-dictconfig"
|
"python-logging-dictconfig"
|
||||||
"SKILL.md"
|
"SKILL.md"
|
||||||
"references"
|
"references"
|
||||||
"fastapi-uv-docker"
|
"vscode-configuration"
|
||||||
|
"SKILL.md"
|
||||||
|
"references"
|
||||||
|
"zensical-docs"
|
||||||
"SKILL.md"
|
"SKILL.md"
|
||||||
"references"
|
"references"
|
||||||
"site"
|
"site"
|
||||||
@@ -45,15 +67,14 @@ treeView-beta
|
|||||||
"src"
|
"src"
|
||||||
"personal_mcp"
|
"personal_mcp"
|
||||||
"main.py"
|
"main.py"
|
||||||
|
"mcp.py"
|
||||||
"web"
|
"web"
|
||||||
"app.py"
|
"app.py"
|
||||||
"docs_mount.py"
|
"docs_mount.py"
|
||||||
"catalog"
|
"catalog"
|
||||||
"server.py"
|
"server.py"
|
||||||
"skills"
|
"skills"
|
||||||
"pytest_scaffolding"
|
"document_loader.py"
|
||||||
"python_logging_dictconfig"
|
|
||||||
"fastapi_uv_docker"
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
@@ -72,12 +93,21 @@ The runtime process serves two surfaces:
|
|||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart TD
|
flowchart TD
|
||||||
A[FastMCP Root Server] --> B[MCP Transport]
|
A[Docs Registry Loader] --> B[Validated In-Memory Registry]
|
||||||
A --> C[FastAPI Application]
|
B --> C[FastMCP Resource Registration]
|
||||||
C --> D[Static Mount /docs]
|
C --> D[MCP Transport]
|
||||||
D --> E[Zensical site output directory]
|
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
|
## Build and Publish Flow
|
||||||
|
|
||||||
The docs flow is pre-build only.
|
The docs flow is pre-build only.
|
||||||
@@ -103,12 +133,31 @@ MCP resources map directly to canonical Markdown documents.
|
|||||||
|
|
||||||
Example mapping model:
|
Example mapping model:
|
||||||
|
|
||||||
1. docs/skills/<slug>/SKILL.md -> resource://skills/<id>/document
|
1. docs/skills/<skill-id>/SKILL.md -> resource://skills/<skill_id>/document
|
||||||
2. docs/skills/<slug>/references/*.md -> referenced sections or linked companion documents
|
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 resources provide discovery metadata and stable identifiers.
|
Catalog discovery resources are:
|
||||||
|
|
||||||
When clients cannot attach MCP resources directly, catalog-level tools may retrieve the same underlying skill documents indirectly. This does not create a second content source; it is only an alternate access path to the same markdown-backed contract.
|
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
|
||||||
|
|
||||||
|
This phase is a greenfield break-and-replace baseline.
|
||||||
|
|
||||||
|
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
|
## Why This Pattern
|
||||||
|
|
||||||
@@ -155,8 +204,8 @@ This keeps docs publication explicit and predictable.
|
|||||||
|
|
||||||
Existing reference docs remain valid content inputs in this pattern:
|
Existing reference docs remain valid content inputs in this pattern:
|
||||||
|
|
||||||
1. ../docs/skills/pytest-scaffolding/references/pytest-docs.md
|
1. docs/skills/pytest-scaffolding/references/pytest-docs.md
|
||||||
2. ../docs/skills/python-logging-dictconfig/references/python-logging-docs.md
|
2. docs/skills/python-logging-dictconfig/references/python-logging-docs.md
|
||||||
3. ../docs/skills/fastapi-uv-docker/references/fastapi-best-practices.md
|
3. docs/skills/fastapi-uv-docker/references/fastapi-best-practices.md
|
||||||
|
|
||||||
These are source documents, not deployment artifacts.
|
These are source documents, not deployment artifacts.
|
||||||
|
|||||||
+96
-155
@@ -1,178 +1,119 @@
|
|||||||
# Hooking Up a New Skill
|
# Hooking Up a New Skill
|
||||||
|
|
||||||
Use this checklist after generating a new skill under `docs/skills/<slug>/`.
|
Use this checklist to add a new skill in the Phase 1 docs-first model.
|
||||||
|
|
||||||
## Checklist
|
## Step 1 Contract: Canonical Skill Shape
|
||||||
|
|
||||||
1. Create the authored docs content.
|
Create one skill directory under `docs/skills/`:
|
||||||
Add `docs/skills/<slug>/SKILL.md` and any companion files under `docs/skills/<slug>/references/`.
|
|
||||||
|
|
||||||
2. Choose the three names up front.
|
```text
|
||||||
Use a docs slug like `fastapi-uv-docker`, a resource id like `fastapi-uv-docker`, and a Python package name like `fastapi_uv_docker`.
|
docs/
|
||||||
|
skills/
|
||||||
3. Add the runtime package.
|
<skill-id>/
|
||||||
Create `src/personal_mcp/skills/<python_namespace>/` with `__init__.py`, `server.py`, and `metadata.yaml`.
|
SKILL.md
|
||||||
|
references/
|
||||||
4. Expose the document resource in `server.py`.
|
... (optional markdown files, nested folders allowed)
|
||||||
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`.
|
|
||||||
|
|
||||||
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/<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.
|
|
||||||
|
|
||||||
## Discovery Tool Policy
|
|
||||||
|
|
||||||
To keep behavior consistent across MCP clients and Copilot session types, follow this boundary:
|
|
||||||
|
|
||||||
1. Keep per-skill servers resource-only.
|
|
||||||
2. Keep discovery/query tools centralized in the catalog server.
|
|
||||||
3. Keep canonical content in `docs/skills/<slug>/SKILL.md` and expose it through `resource://skills/<skill-id>/document`.
|
|
||||||
|
|
||||||
### Do
|
|
||||||
|
|
||||||
1. Add or update `metadata.yaml` fields (`id`, `description`, `tags`, `capabilities`) so catalog discovery quality stays high.
|
|
||||||
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
|
|
||||||
|
|
||||||
1. Do not add duplicate discovery tools to each skill package.
|
|
||||||
2. Do not duplicate canonical skill guidance in tool descriptions.
|
|
||||||
3. Do not create mutating catalog tools for skill discovery.
|
|
||||||
|
|
||||||
## Minimal Shape
|
|
||||||
|
|
||||||
- Docs content: `docs/skills/<slug>/SKILL.md`
|
|
||||||
- Optional references: `docs/skills/<slug>/references/*.md`
|
|
||||||
- Runtime package: `src/personal_mcp/skills/<python_namespace>/`
|
|
||||||
- Resource URI: `resource://skills/<skill-id>/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-id>', skill_slug='<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
|
|
||||||
|
|
||||||
<python_namespace>_server = FastMCP("<skill-id>")
|
|
||||||
|
|
||||||
|
|
||||||
@<python_namespace>_server.resource("resource://skills/<skill-id>/document")
|
|
||||||
def skill_document() -> dict[str, str]:
|
|
||||||
"""Return the canonical Markdown document for this skill."""
|
|
||||||
return load_skill_document(
|
|
||||||
skill_id="<skill-id>",
|
|
||||||
skill_slug="<slug>",
|
|
||||||
)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## metadata.yaml Template
|
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.
|
||||||
|
|
||||||
|
## Step 2 Contract: 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
|
```yaml
|
||||||
|
---
|
||||||
|
name: <skill-id>
|
||||||
|
description: <what this skill does and when to use it>
|
||||||
|
|
||||||
|
x-personal-mcp:
|
||||||
id: <skill-id>
|
id: <skill-id>
|
||||||
name: <Human Readable Name>
|
|
||||||
version: 1.0.0
|
version: 1.0.0
|
||||||
description: <One sentence describing what the skill provides.>
|
tags: []
|
||||||
tags:
|
|
||||||
- <tag-one>
|
|
||||||
- <tag-two>
|
|
||||||
document_path: <optional repo-relative path to a markdown file>
|
|
||||||
capabilities:
|
capabilities:
|
||||||
- resource://skills/<skill-id>/document
|
- resource://skills/<skill-id>/document
|
||||||
depends_on: []
|
depends_on: []
|
||||||
|
references:
|
||||||
|
<ref-id>:
|
||||||
|
path: references/<file>.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: <optional short title>
|
||||||
|
---
|
||||||
```
|
```
|
||||||
|
|
||||||
Omit `document_path` when the canonical document is `docs/skills/<slug>/SKILL.md`.
|
Reference manifest rules:
|
||||||
|
|
||||||
## Root Mount Template
|
1. `ref-id` is lowercase kebab-case.
|
||||||
|
2. `path` is skill-relative and must stay under `references/`.
|
||||||
|
3. Reference paths are markdown files.
|
||||||
|
|
||||||
Add an import in `src/personal_mcp/mcp.py`:
|
No `metadata.yaml` sidecar is part of this model.
|
||||||
|
|
||||||
```python
|
## Step 3 Contract: URI Surface
|
||||||
from personal_mcp.skills.<python_namespace>.server import <python_namespace>_server
|
|
||||||
|
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. If references are exposed, declare each `ref-id` in `x-personal-mcp.references`.
|
||||||
|
|
||||||
|
## Quick Validation
|
||||||
|
|
||||||
|
1. Confirm docs build succeeds:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run zensical build
|
||||||
```
|
```
|
||||||
|
|
||||||
Add a mount call:
|
2. Confirm tests succeed:
|
||||||
|
|
||||||
```python
|
```bash
|
||||||
mcp.mount(<python_namespace>_server, namespace="<python_namespace>")
|
uv run pytest -q
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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/<slug>/SKILL.md`.
|
|
||||||
2. Copy the `server.py` template into `src/personal_mcp/skills/<python_namespace>/server.py`.
|
|
||||||
3. Copy the `metadata.yaml` template into `src/personal_mcp/skills/<python_namespace>/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.
|
|
||||||
|
|||||||
@@ -2,6 +2,31 @@
|
|||||||
name: copilot-customization
|
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.'
|
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?'
|
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
|
||||||
|
references:
|
||||||
|
vscode-customization:
|
||||||
|
path: references/vscode-customization.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: VS Code Customization
|
||||||
---
|
---
|
||||||
|
|
||||||
# Copilot Customization
|
# Copilot Customization
|
||||||
@@ -51,15 +76,14 @@ Use [VS Code customization references](./references/vscode-customization.md) for
|
|||||||
When adding a new skill to this `personal-mcp` repo, follow the resource-first pattern:
|
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`.
|
1. Search the catalog for `new skill` and load `resource://skills/new-skill/document`.
|
||||||
2. Create authored docs under `docs/skills/<slug>/SKILL.md`, with optional one-level `references/` files.
|
2. Create authored docs under `docs/skills/<skill-id>/SKILL.md`, with optional nested `references/` markdown files.
|
||||||
3. Choose consistent names: docs slug and resource id use kebab-case; Python namespace uses snake_case.
|
3. Keep `skill-id` stable and consistent across directory name, `name`, and `x-personal-mcp.id`.
|
||||||
4. Create `src/personal_mcp/skills/<python_namespace>/` with `__init__.py`, `server.py`, and `metadata.yaml`.
|
4. Put discovery metadata in `SKILL.md` frontmatter under `x-personal-mcp`.
|
||||||
5. Expose only `resource://skills/<skill-id>/document` from the per-skill server.
|
5. Declare `resource://skills/<skill-id>/document` in `x-personal-mcp.capabilities`.
|
||||||
6. Put discovery metadata in `metadata.yaml`, including `id`, `name`, `version`, `description`, `tags`, `capabilities`, and `depends_on`.
|
6. Declare references in `x-personal-mcp.references` as `ref-id -> references/<file>.md` mappings.
|
||||||
7. Mount the skill server in `src/personal_mcp/mcp.py` using the Python namespace.
|
7. Validate with the registry loader and `uv run zensical build`.
|
||||||
8. Validate with the document loader and `uv run zensical build`.
|
|
||||||
|
|
||||||
Keep per-skill servers resource-only. Catalog-level discovery is the only place for thin fallback discovery tools.
|
Keep runtime implementation registry-driven in `src/personal_mcp/mcp.py`; do not add per-skill Python server modules.
|
||||||
|
|
||||||
## Quality Checks
|
## Quality Checks
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,46 @@
|
|||||||
name: fastapi-async-sqlalchemy-modernization
|
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.'
|
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)?'
|
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: []
|
||||||
|
references:
|
||||||
|
index:
|
||||||
|
path: references/index.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Index
|
||||||
|
engine:
|
||||||
|
path: references/engine.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Engine
|
||||||
|
session:
|
||||||
|
path: references/session.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Session
|
||||||
|
transactions:
|
||||||
|
path: references/transactions.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Transactions
|
||||||
|
implicit-io:
|
||||||
|
path: references/implicit_io.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Implicit IO
|
||||||
|
observability:
|
||||||
|
path: references/observability.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Observability
|
||||||
|
template:
|
||||||
|
path: references/template.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Template
|
||||||
---
|
---
|
||||||
|
|
||||||
# FastAPI Async SQLAlchemy Modernization Plan
|
# FastAPI Async SQLAlchemy Modernization Plan
|
||||||
|
|||||||
@@ -2,6 +2,33 @@
|
|||||||
name: fastapi-uv-docker
|
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.'
|
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.)?'
|
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: []
|
||||||
|
references:
|
||||||
|
fastapi-best-practices:
|
||||||
|
path: references/fastapi-best-practices.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: FastAPI Best Practices
|
||||||
|
uv-project-layout:
|
||||||
|
path: references/uv-project-layout.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: uv Project Layout
|
||||||
|
uvicorn-settings:
|
||||||
|
path: references/uvicorn-settings.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Uvicorn Settings
|
||||||
|
docker-cloud-native:
|
||||||
|
path: references/docker-cloud-native.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Docker Cloud Native
|
||||||
---
|
---
|
||||||
|
|
||||||
# FastAPI Project Best Practices
|
# FastAPI Project Best Practices
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -2,6 +2,30 @@
|
|||||||
name: nicegui-ui-customization
|
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.'
|
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?'
|
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: []
|
||||||
|
references:
|
||||||
|
architecture-and-styling:
|
||||||
|
path: references/architecture-and-styling.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Architecture and Styling
|
||||||
|
interaction-patterns:
|
||||||
|
path: references/interaction-patterns.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Interaction Patterns
|
||||||
|
troubleshooting-and-quality-gates:
|
||||||
|
path: references/troubleshooting-and-quality-gates.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Troubleshooting and Quality Gates
|
||||||
---
|
---
|
||||||
|
|
||||||
# NiceGUI UI Customization Workflow
|
# NiceGUI UI Customization Workflow
|
||||||
|
|||||||
@@ -2,6 +2,26 @@
|
|||||||
name: nicegui
|
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.'
|
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)?'
|
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: []
|
||||||
|
references:
|
||||||
|
architecture:
|
||||||
|
path: references/architecture.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Architecture
|
||||||
|
source-documentation:
|
||||||
|
path: references/source-documentation.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Source Documentation
|
||||||
---
|
---
|
||||||
|
|
||||||
# NiceGUI
|
# NiceGUI
|
||||||
|
|||||||
@@ -2,6 +2,29 @@
|
|||||||
name: pytest-scaffolding
|
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."
|
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)"
|
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: []
|
||||||
|
references:
|
||||||
|
pytest-docs:
|
||||||
|
path: references/pytest-docs.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Pytest Docs
|
||||||
|
fastapi-testing:
|
||||||
|
path: references/fastapi-testing.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: FastAPI Testing
|
||||||
|
sqlalchemy-testing:
|
||||||
|
path: references/sqlalchemy-testing.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: SQLAlchemy Testing
|
||||||
---
|
---
|
||||||
|
|
||||||
# Pytest Scaffolding
|
# Pytest Scaffolding
|
||||||
|
|||||||
@@ -2,6 +2,21 @@
|
|||||||
name: python-logging-dictconfig
|
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.'
|
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'
|
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: []
|
||||||
|
references:
|
||||||
|
python-logging-docs:
|
||||||
|
path: references/python-logging-docs.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Python Logging Docs
|
||||||
---
|
---
|
||||||
|
|
||||||
# Idiomatic Python Logging with dictConfig
|
# Idiomatic Python Logging with dictConfig
|
||||||
|
|||||||
@@ -2,6 +2,33 @@
|
|||||||
name: vscode-configuration
|
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.'
|
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?'
|
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: []
|
||||||
|
references:
|
||||||
|
debug-launch-configurations:
|
||||||
|
path: references/debug-launch-configurations.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Debug Launch Configurations
|
||||||
|
fastapi-debugpy-launch:
|
||||||
|
path: references/fastapi-debugpy-launch.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: FastAPI Debugpy Launch
|
||||||
|
tasks-json-configuration:
|
||||||
|
path: references/tasks-json-configuration.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Tasks JSON Configuration
|
||||||
---
|
---
|
||||||
|
|
||||||
# VS Code Configuration
|
# VS Code Configuration
|
||||||
|
|||||||
@@ -2,6 +2,49 @@
|
|||||||
name: zensical-docs
|
name: zensical-docs
|
||||||
description: 'Reference skill for Zensical documentation mechanics. Use for quick lookup of docs structure, feature options, and source links, then edit this skill over time to record project preferences for when each feature should be used.'
|
description: 'Reference skill for Zensical documentation mechanics. Use for quick lookup of docs structure, feature options, and source links, then edit this skill over time to record project preferences for when each feature should be used.'
|
||||||
argument-hint: 'What are you documenting, who is the audience, and what Zensical features are in scope?'
|
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: []
|
||||||
|
references:
|
||||||
|
index:
|
||||||
|
path: references/index.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Source Map
|
||||||
|
zensical-features:
|
||||||
|
path: references/zensical-features.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Feature Catalog
|
||||||
|
theme-customization-and-icons:
|
||||||
|
path: references/theme-customization-and-icons.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Theme Customization and Icons
|
||||||
|
documentation-quality:
|
||||||
|
path: references/documentation-quality.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Documentation Quality
|
||||||
|
discoverability-and-ia:
|
||||||
|
path: references/discoverability-and-ia.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Discoverability and IA
|
||||||
|
code-heavy-docs-and-mkdocstrings:
|
||||||
|
path: references/code-heavy-docs-and-mkdocstrings.md
|
||||||
|
mime_type: text/markdown
|
||||||
|
title: Code-Heavy Docs and Mkdocstrings
|
||||||
---
|
---
|
||||||
|
|
||||||
# Zensical Documentation Authoring
|
# Zensical Documentation Authoring
|
||||||
|
|||||||
+9
-8
@@ -9,7 +9,7 @@ icon: lucide/workflow
|
|||||||
This page explains practical usage mechanics for the GitHub Copilot extension in VS Code when `personal-mcp` is configured as an MCP server:
|
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
|
1. explicit `/` command flows when you want deterministic control
|
||||||
2. automatic skill context loading when relevance can be inferred
|
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.
|
The goal is to show how Copilot behaves as a client and how to shape that behavior.
|
||||||
|
|
||||||
@@ -26,18 +26,17 @@ In this repository, skill guidance is exposed as MCP resources, not as server-ow
|
|||||||
|
|
||||||
### What the server publishes
|
### What the server publishes
|
||||||
|
|
||||||
`personal-mcp` mounts skill modules and a catalog module. The catalog exposes discovery resources:
|
`personal-mcp` registers resources from the validated docs registry and exposes catalog discovery resources:
|
||||||
|
|
||||||
1. `resource://catalog/skills_index`
|
1. `resource://catalog/skills_index`
|
||||||
2. `resource://catalog/skills_details`
|
2. `resource://catalog/skills/{skill_id}`
|
||||||
3. `resource://catalog/patterns`
|
|
||||||
4. `resource://catalog/patterns_by_id`
|
|
||||||
|
|
||||||
Each skill publishes a canonical Markdown document resource:
|
Each skill publishes a canonical Markdown document resource:
|
||||||
|
|
||||||
1. `resource://skills/<skill-id>/document`
|
1. `resource://skills/<skill-id>/document`
|
||||||
|
2. `resource://skills/<skill-id>/references/<ref-id>`
|
||||||
|
|
||||||
The document payload is loaded from `docs/skills/<slug>/SKILL.md` and returned with metadata.
|
The document payload is loaded from `docs/skills/<skill-id>/SKILL.md` and returned with metadata.
|
||||||
|
|
||||||
### What Copilot does as the client
|
### What Copilot does as the client
|
||||||
|
|
||||||
@@ -177,8 +176,10 @@ When a task may match a documented implementation pattern from `personal-mcp`:
|
|||||||
|
|
||||||
Preferred resource order:
|
Preferred resource order:
|
||||||
|
|
||||||
1. `resource://catalog/skills_index` or `resource://catalog/patterns`
|
1. `resource://catalog/skills_index`
|
||||||
2. `resource://skills/<skill-id>/document`
|
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:
|
Preferred tool fallback order:
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
from personal_mcp.catalog.server import catalog_server
|
from personal_mcp.catalog.server import (
|
||||||
|
build_skill_detail_payload,
|
||||||
|
build_skills_index_payload,
|
||||||
|
get_pattern_by_id_payload,
|
||||||
|
search_patterns_payload,
|
||||||
|
)
|
||||||
|
|
||||||
__all__ = ["catalog_server"]
|
__all__ = [
|
||||||
|
"build_skill_detail_payload",
|
||||||
|
"build_skills_index_payload",
|
||||||
|
"get_pattern_by_id_payload",
|
||||||
|
"search_patterns_payload",
|
||||||
|
]
|
||||||
|
|||||||
+130
-131
@@ -1,172 +1,171 @@
|
|||||||
from pathlib import Path
|
from __future__ import annotations
|
||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import yaml
|
from personal_mcp.skills.document_loader import DocsRegistry, SkillRecord
|
||||||
from fastmcp import FastMCP
|
|
||||||
|
|
||||||
from personal_mcp.skills.document_loader import load_skill_document_from_metadata
|
DEFAULT_LIMIT = 20
|
||||||
|
MAX_LIMIT = 100
|
||||||
catalog_server = FastMCP("catalog")
|
|
||||||
|
|
||||||
|
|
||||||
def _skills_dir() -> Path:
|
def _pattern_payload(skill: SkillRecord) -> dict[str, Any]:
|
||||||
return Path(__file__).resolve().parents[1] / "skills"
|
|
||||||
|
|
||||||
|
|
||||||
def _load_skill_registry() -> dict[str, Any]:
|
|
||||||
registry: dict[str, Any] = {}
|
|
||||||
for metadata_path in sorted(_skills_dir().glob("*/metadata.yaml")):
|
|
||||||
with metadata_path.open("r", encoding="utf-8") as handle:
|
|
||||||
metadata = yaml.safe_load(handle) or {}
|
|
||||||
skill_key = metadata_path.parent.name
|
|
||||||
registry[skill_key] = {
|
|
||||||
"namespace": skill_key,
|
|
||||||
"metadata": metadata,
|
|
||||||
}
|
|
||||||
return registry
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize_pattern(namespace: str, metadata: dict[str, Any]) -> dict[str, Any]:
|
|
||||||
pattern_id = metadata.get("id", namespace)
|
|
||||||
capabilities = metadata.get("capabilities", [])
|
|
||||||
return {
|
return {
|
||||||
"id": pattern_id,
|
"id": skill.skill_id,
|
||||||
"namespace": namespace,
|
"name": skill.name,
|
||||||
"name": metadata.get("name", pattern_id),
|
"version": skill.version,
|
||||||
"version": metadata.get("version", "0.1.0"),
|
"description": skill.description,
|
||||||
"description": metadata.get("description", ""),
|
"tags": list(skill.tags),
|
||||||
"tags": metadata.get("tags", []),
|
"depends_on": list(skill.depends_on),
|
||||||
"depends_on": metadata.get("depends_on", []),
|
"capabilities": list(skill.capabilities),
|
||||||
"capabilities": capabilities,
|
"resources": list(skill.capabilities),
|
||||||
# Expose resources explicitly for clients that treat resources as the primary interface.
|
|
||||||
"resources": capabilities,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _normalized_patterns() -> list[dict[str, Any]]:
|
def _summary_payload(skill: SkillRecord) -> dict[str, Any]:
|
||||||
registry = _load_skill_registry()
|
return {
|
||||||
return [
|
"id": skill.skill_id,
|
||||||
_normalize_pattern(namespace, entry["metadata"])
|
"name": skill.name,
|
||||||
for namespace, entry in registry.items()
|
"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 _matches_query(pattern: dict[str, Any], query: str) -> bool:
|
def _skill_matches(
|
||||||
if not query:
|
skill: SkillRecord,
|
||||||
return True
|
*,
|
||||||
|
query: str | None,
|
||||||
|
tag: str | None,
|
||||||
|
capability: str | None,
|
||||||
|
) -> bool:
|
||||||
|
if query:
|
||||||
lowered = query.strip().lower()
|
lowered = query.strip().lower()
|
||||||
if not lowered:
|
if lowered:
|
||||||
return True
|
|
||||||
|
|
||||||
query_terms = [term for term in lowered.replace("-", " ").split() if term]
|
|
||||||
if not query_terms:
|
|
||||||
return True
|
|
||||||
|
|
||||||
haystack = " ".join(
|
haystack = " ".join(
|
||||||
[
|
[
|
||||||
str(pattern.get("id", "")),
|
skill.skill_id,
|
||||||
str(pattern.get("namespace", "")),
|
skill.name,
|
||||||
str(pattern.get("name", "")),
|
skill.description,
|
||||||
str(pattern.get("description", "")),
|
" ".join(skill.tags),
|
||||||
" ".join(str(tag) for tag in pattern.get("tags", [])),
|
|
||||||
]
|
]
|
||||||
).lower()
|
).lower()
|
||||||
return all(term in haystack for term in query_terms)
|
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
|
||||||
|
|
||||||
def _matches_tags(pattern: dict[str, Any], tags: list[str] | None) -> bool:
|
|
||||||
if not tags:
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
requested = [tag.strip().lower() for tag in tags if tag and tag.strip()]
|
|
||||||
if not requested:
|
|
||||||
return True
|
|
||||||
|
|
||||||
pattern_tags = {str(tag).lower() for tag in pattern.get("tags", [])}
|
def build_skills_index_payload(
|
||||||
return all(tag in pattern_tags for tag in requested)
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@catalog_server.resource("resource://catalog/skills_index")
|
def build_skill_detail_payload(registry: DocsRegistry, skill_id: str) -> dict[str, Any]:
|
||||||
def skills_index() -> dict[str, Any]:
|
if skill_id not in registry.skills_by_id:
|
||||||
"""Return a compact discovery index for all available pattern modules."""
|
raise KeyError(skill_id)
|
||||||
return {"patterns": _normalized_patterns()}
|
|
||||||
|
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())
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@catalog_server.resource("resource://catalog/skills_details")
|
def search_patterns_payload(
|
||||||
def skills_details() -> dict[str, Any]:
|
registry: DocsRegistry,
|
||||||
"""Return full metadata for all mounted pattern modules."""
|
*,
|
||||||
return {"patterns": _load_skill_registry()}
|
|
||||||
|
|
||||||
|
|
||||||
@catalog_server.resource("resource://catalog/patterns")
|
|
||||||
def patterns() -> dict[str, Any]:
|
|
||||||
"""Return normalized pattern records for resource-first clients."""
|
|
||||||
return {"patterns": _normalized_patterns()}
|
|
||||||
|
|
||||||
|
|
||||||
@catalog_server.resource("resource://catalog/patterns_by_id")
|
|
||||||
def patterns_by_id() -> dict[str, Any]:
|
|
||||||
"""Return normalized pattern records indexed by stable pattern id."""
|
|
||||||
indexed: dict[str, Any] = {}
|
|
||||||
for pattern in _normalized_patterns():
|
|
||||||
indexed[pattern["id"]] = pattern
|
|
||||||
return {"patterns_by_id": indexed}
|
|
||||||
|
|
||||||
|
|
||||||
@catalog_server.tool
|
|
||||||
def search_patterns(
|
|
||||||
query: str = "",
|
query: str = "",
|
||||||
tags: list[str] | None = None,
|
tags: list[str] | None = None,
|
||||||
skip: int = 0,
|
skip: int = 0,
|
||||||
limit: int = 20,
|
limit: int = DEFAULT_LIMIT,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Search normalized pattern metadata with optional tags and pagination."""
|
|
||||||
normalized_skip = max(skip, 0)
|
normalized_skip = max(skip, 0)
|
||||||
normalized_limit = min(max(limit, 1), 100)
|
normalized_limit = max(1, min(limit, MAX_LIMIT))
|
||||||
|
|
||||||
matches = [
|
requested_tags = [tag.strip() for tag in (tags or []) if tag and tag.strip()]
|
||||||
pattern
|
|
||||||
for pattern in _normalized_patterns()
|
matches: list[SkillRecord] = []
|
||||||
if _matches_query(pattern, query) and _matches_tags(pattern, tags)
|
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]
|
page = matches[normalized_skip : normalized_skip + normalized_limit]
|
||||||
return {
|
return {
|
||||||
"patterns": page,
|
"patterns": [_pattern_payload(skill) for skill in page],
|
||||||
"total": len(matches),
|
"total": len(matches),
|
||||||
"skip": normalized_skip,
|
"skip": normalized_skip,
|
||||||
"limit": normalized_limit,
|
"limit": normalized_limit,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@catalog_server.tool
|
def get_pattern_by_id_payload(registry: DocsRegistry, skill_id: str) -> dict[str, Any]:
|
||||||
def get_pattern_by_id(id: str) -> dict[str, Any]:
|
if skill_id not in registry.skills_by_id:
|
||||||
"""Return one normalized pattern by stable id."""
|
|
||||||
for pattern in _normalized_patterns():
|
|
||||||
if pattern["id"] == id:
|
|
||||||
return {"found": True, "pattern": pattern}
|
|
||||||
|
|
||||||
return {"found": False, "id": id}
|
|
||||||
|
|
||||||
|
|
||||||
@catalog_server.tool
|
|
||||||
def get_skill_document_by_id(skill_id: str) -> dict[str, Any]:
|
|
||||||
"""Return the canonical skill document payload for a stable skill id."""
|
|
||||||
registry = _load_skill_registry()
|
|
||||||
for namespace, entry in registry.items():
|
|
||||||
metadata = entry.get("metadata", {})
|
|
||||||
pattern_id = metadata.get("id", namespace)
|
|
||||||
if pattern_id != skill_id:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return {
|
|
||||||
"found": True,
|
|
||||||
"document": load_skill_document_from_metadata(
|
|
||||||
skill_id=skill_id,
|
|
||||||
namespace=namespace,
|
|
||||||
metadata=metadata,
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
return {"found": False, "id": skill_id}
|
return {"found": False, "id": skill_id}
|
||||||
|
return {"found": True, "pattern": _pattern_payload(registry.skills_by_id[skill_id])}
|
||||||
|
|||||||
+137
-32
@@ -1,38 +1,143 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from fastmcp import FastMCP
|
from fastmcp import FastMCP
|
||||||
|
|
||||||
from personal_mcp.catalog.server import catalog_server
|
from personal_mcp.catalog.server import (
|
||||||
from personal_mcp.skills.copilot_customization.server import (
|
build_skill_detail_payload,
|
||||||
copilot_customization_server,
|
build_skills_index_payload,
|
||||||
|
get_pattern_by_id_payload,
|
||||||
|
search_patterns_payload,
|
||||||
)
|
)
|
||||||
from personal_mcp.skills.fastapi_async_sqlalchemy_modernization.server import (
|
from personal_mcp.skills.document_loader import (
|
||||||
fastapi_async_sqlalchemy_modernization_server,
|
DocsRegistry,
|
||||||
|
load_docs_registry,
|
||||||
|
read_docs_markdown_path,
|
||||||
|
read_skill_document,
|
||||||
|
read_skill_reference,
|
||||||
)
|
)
|
||||||
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,
|
|
||||||
)
|
|
||||||
from personal_mcp.skills.pytest_scaffolding.server import pytest_scaffolding_server
|
|
||||||
from personal_mcp.skills.python_logging_dictconfig.server import (
|
|
||||||
python_logging_dictconfig_server,
|
|
||||||
)
|
|
||||||
from personal_mcp.skills.vscode_configuration.server import vscode_configuration_server
|
|
||||||
from personal_mcp.skills.zensical_docs.server import zensical_docs_server
|
|
||||||
|
|
||||||
mcp = FastMCP("personal-mcp")
|
DOCS_ROOT = os.getenv("PERSONAL_MCP_DOCS_ROOT", "../../docs")
|
||||||
|
REGISTRY: DocsRegistry = load_docs_registry(
|
||||||
mcp.mount(catalog_server, namespace="catalog")
|
package_anchor="personal_mcp",
|
||||||
mcp.mount(copilot_customization_server, namespace="copilot_customization")
|
docs_root=DOCS_ROOT,
|
||||||
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 = FastMCP("personal-mcp", on_duplicate="error")
|
||||||
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")
|
def _ro_annotations() -> dict[str, bool]:
|
||||||
mcp.mount(vscode_configuration_server, namespace="vscode_configuration")
|
return {
|
||||||
mcp.mount(fastapi_uv_docker_server, namespace="fastapi_uv_docker")
|
"readOnlyHint": True,
|
||||||
mcp.mount(zensical_docs_server, namespace="zensical_docs")
|
"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),
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
"""Mounted skill servers for the personal MCP app."""
|
"""Docs registry and markdown loading utilities for personal MCP skills."""
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
id: copilot-customization
|
|
||||||
name: Copilot Customization
|
|
||||||
version: 1.0.0
|
|
||||||
description: Plan, create, review, and debug GitHub Copilot and VS Code agent customizations for this repository.
|
|
||||||
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
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from fastmcp import FastMCP
|
|
||||||
|
|
||||||
from personal_mcp.skills.document_loader import load_skill_document
|
|
||||||
|
|
||||||
copilot_customization_server = FastMCP("copilot-customization")
|
|
||||||
|
|
||||||
|
|
||||||
@copilot_customization_server.resource("resource://skills/copilot-customization/document")
|
|
||||||
def skill_document() -> dict[str, str]:
|
|
||||||
"""Return the canonical Markdown document for this skill."""
|
|
||||||
return load_skill_document(
|
|
||||||
skill_id="copilot-customization",
|
|
||||||
skill_slug="copilot-customization",
|
|
||||||
)
|
|
||||||
@@ -1,74 +1,559 @@
|
|||||||
from pathlib import Path
|
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
|
from typing import Any
|
||||||
|
|
||||||
|
import yaml
|
||||||
|
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
|
||||||
|
|
||||||
def _repo_root() -> Path:
|
SKILL_ID_RE = re.compile(r"^[a-z][a-z0-9-]*$")
|
||||||
return Path(__file__).resolve().parents[3]
|
SEMVER_RE = re.compile(
|
||||||
|
r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+][0-9A-Za-z.-]+)?$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def resolve_skill_document_path(
|
@dataclass(frozen=True)
|
||||||
*, skill_id: str, namespace: str, metadata: dict[str, Any]
|
class RegistryIssue:
|
||||||
) -> Path:
|
code: str
|
||||||
"""Resolve the canonical Markdown document path for a skill."""
|
message: str
|
||||||
document_path = metadata.get("document_path")
|
skill_id: str | None
|
||||||
if isinstance(document_path, str) and document_path.strip():
|
path: str
|
||||||
return _repo_root() / document_path.strip()
|
hint: str
|
||||||
|
|
||||||
candidates: list[str] = []
|
|
||||||
slug = metadata.get("slug")
|
|
||||||
if isinstance(slug, str) and slug.strip():
|
|
||||||
candidates.append(slug.strip())
|
|
||||||
|
|
||||||
candidates.extend(
|
class DocsRegistryValidationError(Exception):
|
||||||
|
def __init__(self, errors: list[RegistryIssue]) -> None:
|
||||||
|
self.errors = errors
|
||||||
|
summary = "\n".join(
|
||||||
[
|
[
|
||||||
skill_id,
|
(
|
||||||
namespace.replace("_", "-"),
|
f"{issue.code}: {issue.message} "
|
||||||
namespace,
|
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 _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",
|
||||||
|
)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
seen: set[str] = set()
|
docs_markdown_by_path: dict[str, str] = {}
|
||||||
for candidate in candidates:
|
for relpath, doc_file in _walk_markdown(docs_dir):
|
||||||
if candidate in seen:
|
docs_markdown_by_path[relpath] = doc_file.read_text(encoding="utf-8")
|
||||||
continue
|
|
||||||
seen.add(candidate)
|
|
||||||
|
|
||||||
candidate_path = _repo_root() / "docs" / "skills" / candidate / "SKILL.md"
|
skills_root = docs_dir.joinpath("skills")
|
||||||
if candidate_path.exists():
|
if not skills_root.is_dir():
|
||||||
return candidate_path
|
raise DocsRegistryValidationError(
|
||||||
|
[
|
||||||
return _repo_root() / "docs" / "skills" / skill_id / "SKILL.md"
|
RegistryIssue(
|
||||||
|
code="missing_skills_root",
|
||||||
|
message="skills directory does not exist under docs root",
|
||||||
def load_markdown_document(*, skill_id: str, document_path: Path) -> dict[str, str]:
|
skill_id=None,
|
||||||
"""Load an arbitrary Markdown document and expose it as a skill resource."""
|
path=f"{docs_root}/skills",
|
||||||
if not document_path.exists():
|
hint="ensure docs/skills is included in packaged docs",
|
||||||
raise FileNotFoundError(
|
)
|
||||||
f"Missing skill document for '{skill_id}': {document_path}"
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
references: dict[str, ReferenceRecord] = {}
|
||||||
|
for ref_id, ref_entry in frontmatter.x_personal_mcp.references.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 {
|
return {
|
||||||
"id": skill_id,
|
"id": skill.skill_id,
|
||||||
"uri": f"resource://skills/{skill_id}/document",
|
"uri": skill.document_uri,
|
||||||
"format": "markdown",
|
"format": "markdown",
|
||||||
"source_path": str(document_path),
|
"source_path": f"docs/{skill.document_relpath}",
|
||||||
"content": document_path.read_text(encoding="utf-8"),
|
"content": skill.document_content,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def load_skill_document(*, skill_id: str, skill_slug: str) -> dict[str, str]:
|
def read_skill_reference(
|
||||||
"""Load the canonical skill markdown document for an MCP skill."""
|
registry: DocsRegistry,
|
||||||
document_path = _repo_root() / "docs" / "skills" / skill_slug / "SKILL.md"
|
*,
|
||||||
return load_markdown_document(skill_id=skill_id, document_path=document_path)
|
skill_id: str,
|
||||||
|
ref_id: str,
|
||||||
|
|
||||||
def load_skill_document_from_metadata(
|
|
||||||
*, skill_id: str, namespace: str, metadata: dict[str, Any]
|
|
||||||
) -> dict[str, str]:
|
) -> dict[str, str]:
|
||||||
"""Load a skill document using metadata overrides when present."""
|
if skill_id not in registry.skills_by_id:
|
||||||
document_path = resolve_skill_document_path(
|
raise KeyError(f"unknown skill_id: {skill_id}")
|
||||||
skill_id=skill_id,
|
skill = registry.skills_by_id[skill_id]
|
||||||
namespace=namespace,
|
if ref_id not in skill.references:
|
||||||
metadata=metadata,
|
raise KeyError(f"unknown ref_id '{ref_id}' for skill '{skill_id}'")
|
||||||
)
|
reference = skill.references[ref_id]
|
||||||
return load_markdown_document(skill_id=skill_id, document_path=document_path)
|
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],
|
||||||
|
}
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
"""FastAPI async SQLAlchemy modernization skill server."""
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
id: fastapi-async-sqlalchemy-modernization
|
|
||||||
name: FastAPI Async SQLAlchemy Modernization
|
|
||||||
version: 1.0.0
|
|
||||||
description: Create a step-by-step modernization plan for an existing FastAPI app using SQLAlchemy async patterns.
|
|
||||||
tags:
|
|
||||||
- fastapi
|
|
||||||
- sqlalchemy
|
|
||||||
- async
|
|
||||||
- modernization
|
|
||||||
capabilities:
|
|
||||||
- resource://skills/fastapi-async-sqlalchemy-modernization/document
|
|
||||||
depends_on: []
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
from fastmcp import FastMCP
|
|
||||||
|
|
||||||
from personal_mcp.skills.document_loader import load_skill_document
|
|
||||||
|
|
||||||
fastapi_async_sqlalchemy_modernization_server = FastMCP(
|
|
||||||
"fastapi-async-sqlalchemy-modernization"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@fastapi_async_sqlalchemy_modernization_server.resource(
|
|
||||||
"resource://skills/fastapi-async-sqlalchemy-modernization/document"
|
|
||||||
)
|
|
||||||
def skill_document() -> dict[str, str]:
|
|
||||||
"""Return the canonical Markdown document for this skill."""
|
|
||||||
return load_skill_document(
|
|
||||||
skill_id="fastapi-async-sqlalchemy-modernization",
|
|
||||||
skill_slug="fastapi-async-sqlalchemy-modernization",
|
|
||||||
)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from personal_mcp.skills.fastapi_uv_docker.server import fastapi_uv_docker_server
|
|
||||||
|
|
||||||
__all__ = ["fastapi_uv_docker_server"]
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
id: fastapi-uv-docker
|
|
||||||
name: FastAPI uv Docker
|
|
||||||
version: 1.0.0
|
|
||||||
description: Provide fast migration guidance to FastAPI plus uv plus Docker.
|
|
||||||
tags:
|
|
||||||
- fastapi
|
|
||||||
- uv
|
|
||||||
- docker
|
|
||||||
capabilities:
|
|
||||||
- resource://skills/fastapi-uv-docker/document
|
|
||||||
depends_on: []
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from fastmcp import FastMCP
|
|
||||||
|
|
||||||
from personal_mcp.skills.document_loader import load_skill_document
|
|
||||||
|
|
||||||
fastapi_uv_docker_server = FastMCP("fastapi-uv-docker")
|
|
||||||
|
|
||||||
|
|
||||||
@fastapi_uv_docker_server.resource("resource://skills/fastapi-uv-docker/document")
|
|
||||||
def skill_document() -> dict[str, str]:
|
|
||||||
"""Return the canonical Markdown document for this skill."""
|
|
||||||
return load_skill_document(
|
|
||||||
skill_id="fastapi-uv-docker",
|
|
||||||
skill_slug="fastapi-uv-docker",
|
|
||||||
)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
"""Pseudo-skill exposing the new skill bootstrap guide."""
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
id: new-skill
|
|
||||||
name: New Skill Bootstrap
|
|
||||||
version: 1.0.0
|
|
||||||
description: Provide the bootstrap checklist and templates for creating new MCP skills.
|
|
||||||
tags:
|
|
||||||
- fastmcp
|
|
||||||
- bootstrap
|
|
||||||
- scaffolding
|
|
||||||
- skills
|
|
||||||
- mcp
|
|
||||||
document_path: docs/new_skill.md
|
|
||||||
capabilities:
|
|
||||||
- resource://skills/new-skill/document
|
|
||||||
depends_on: []
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
|
|
||||||
from fastmcp import FastMCP
|
|
||||||
|
|
||||||
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."""
|
|
||||||
return load_skill_document_from_metadata(
|
|
||||||
skill_id="new-skill",
|
|
||||||
namespace="new_skill",
|
|
||||||
metadata=_METADATA,
|
|
||||||
)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
"""NiceGUI architecture skill server."""
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
id: nicegui
|
|
||||||
name: NiceGUI
|
|
||||||
version: 1.0.0
|
|
||||||
description: Design and scaffold a production-ready NiceGUI plus FastAPI application architecture.
|
|
||||||
tags:
|
|
||||||
- nicegui
|
|
||||||
- fastapi
|
|
||||||
- ui
|
|
||||||
- architecture
|
|
||||||
capabilities:
|
|
||||||
- resource://skills/nicegui/document
|
|
||||||
depends_on: []
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
from fastmcp import FastMCP
|
|
||||||
|
|
||||||
from personal_mcp.skills.document_loader import load_skill_document
|
|
||||||
|
|
||||||
nicegui_server = FastMCP("nicegui")
|
|
||||||
|
|
||||||
|
|
||||||
@nicegui_server.resource("resource://skills/nicegui/document")
|
|
||||||
def skill_document() -> dict[str, str]:
|
|
||||||
"""Return the canonical Markdown document for this skill."""
|
|
||||||
return load_skill_document(skill_id="nicegui", skill_slug="nicegui")
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
"""NiceGUI UI customization skill server."""
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
id: nicegui-ui-customization
|
|
||||||
name: NiceGUI UI Customization
|
|
||||||
version: 1.0.0
|
|
||||||
description: Design and implement production NiceGUI interfaces with reusable components and event-driven interactions.
|
|
||||||
tags:
|
|
||||||
- nicegui
|
|
||||||
- ui
|
|
||||||
- customization
|
|
||||||
- frontend
|
|
||||||
capabilities:
|
|
||||||
- resource://skills/nicegui-ui-customization/document
|
|
||||||
depends_on: []
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
from fastmcp import FastMCP
|
|
||||||
|
|
||||||
from personal_mcp.skills.document_loader import load_skill_document
|
|
||||||
|
|
||||||
nicegui_ui_customization_server = FastMCP("nicegui-ui-customization")
|
|
||||||
|
|
||||||
|
|
||||||
@nicegui_ui_customization_server.resource(
|
|
||||||
"resource://skills/nicegui-ui-customization/document"
|
|
||||||
)
|
|
||||||
def skill_document() -> dict[str, str]:
|
|
||||||
"""Return the canonical Markdown document for this skill."""
|
|
||||||
return load_skill_document(
|
|
||||||
skill_id="nicegui-ui-customization",
|
|
||||||
skill_slug="nicegui-ui-customization",
|
|
||||||
)
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
from personal_mcp.skills.pytest_scaffolding.server import pytest_scaffolding_server
|
|
||||||
|
|
||||||
__all__ = ["pytest_scaffolding_server"]
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
id: pytest-scaffolding
|
|
||||||
name: Pytest Scaffolding
|
|
||||||
version: 1.0.0
|
|
||||||
description: Scaffold a maintainable pytest structure quickly.
|
|
||||||
tags:
|
|
||||||
- pytest
|
|
||||||
- testing
|
|
||||||
- python
|
|
||||||
capabilities:
|
|
||||||
- resource://skills/pytest-scaffolding/document
|
|
||||||
depends_on: []
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
from fastmcp import FastMCP
|
|
||||||
|
|
||||||
from personal_mcp.skills.document_loader import load_skill_document
|
|
||||||
|
|
||||||
pytest_scaffolding_server = FastMCP("pytest-scaffolding")
|
|
||||||
|
|
||||||
|
|
||||||
@pytest_scaffolding_server.resource("resource://skills/pytest-scaffolding/document")
|
|
||||||
def skill_document() -> dict[str, str]:
|
|
||||||
"""Return the canonical Markdown document for this skill."""
|
|
||||||
return load_skill_document(
|
|
||||||
skill_id="pytest-scaffolding",
|
|
||||||
skill_slug="pytest-scaffolding",
|
|
||||||
)
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
from personal_mcp.skills.python_logging_dictconfig.server import (
|
|
||||||
python_logging_dictconfig_server,
|
|
||||||
)
|
|
||||||
|
|
||||||
__all__ = ["python_logging_dictconfig_server"]
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
id: python-logging-dictconfig
|
|
||||||
name: Python Logging DictConfig
|
|
||||||
version: 1.0.0
|
|
||||||
description: Provide minimal logging.config.dictConfig setup guidance.
|
|
||||||
tags:
|
|
||||||
- logging
|
|
||||||
- python
|
|
||||||
- observability
|
|
||||||
capabilities:
|
|
||||||
- resource://skills/python-logging-dictconfig/document
|
|
||||||
depends_on: []
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
from fastmcp import FastMCP
|
|
||||||
|
|
||||||
from personal_mcp.skills.document_loader import load_skill_document
|
|
||||||
|
|
||||||
python_logging_dictconfig_server = FastMCP("python-logging-dictconfig")
|
|
||||||
|
|
||||||
|
|
||||||
@python_logging_dictconfig_server.resource(
|
|
||||||
"resource://skills/python-logging-dictconfig/document"
|
|
||||||
)
|
|
||||||
def skill_document() -> dict[str, str]:
|
|
||||||
"""Return the canonical Markdown document for this skill."""
|
|
||||||
return load_skill_document(
|
|
||||||
skill_id="python-logging-dictconfig",
|
|
||||||
skill_slug="python-logging-dictconfig",
|
|
||||||
)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
"""VS Code configuration skill package."""
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
id: vscode-configuration
|
|
||||||
name: VS Code Configuration
|
|
||||||
version: 1.0.0
|
|
||||||
description: Create and troubleshoot VS Code launch and task configuration for Python and FastAPI projects.
|
|
||||||
tags:
|
|
||||||
- vscode
|
|
||||||
- launch-json
|
|
||||||
- tasks-json
|
|
||||||
- debugpy
|
|
||||||
- fastapi
|
|
||||||
- python
|
|
||||||
- skills
|
|
||||||
capabilities:
|
|
||||||
- resource://skills/vscode-configuration/document
|
|
||||||
depends_on: []
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
|
|
||||||
import yaml
|
|
||||||
from fastmcp import FastMCP
|
|
||||||
|
|
||||||
from personal_mcp.skills.document_loader import load_skill_document_from_metadata
|
|
||||||
|
|
||||||
vscode_configuration_server = FastMCP("vscode-configuration")
|
|
||||||
_METADATA_PATH = Path(__file__).with_name("metadata.yaml")
|
|
||||||
_METADATA = yaml.safe_load(_METADATA_PATH.read_text(encoding="utf-8")) or {}
|
|
||||||
|
|
||||||
|
|
||||||
@vscode_configuration_server.resource("resource://skills/vscode-configuration/document")
|
|
||||||
def skill_document() -> dict[str, str]:
|
|
||||||
"""Return the canonical Markdown document for this skill."""
|
|
||||||
return load_skill_document_from_metadata(
|
|
||||||
skill_id="vscode-configuration",
|
|
||||||
namespace="vscode_configuration",
|
|
||||||
metadata=_METADATA,
|
|
||||||
)
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
"""Zensical documentation authoring skill server."""
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
id: zensical-docs
|
|
||||||
name: Zensical Documentation Authoring
|
|
||||||
version: 1.0.0
|
|
||||||
description: Plan, write, and improve high-quality documentation with Zensical.
|
|
||||||
tags:
|
|
||||||
- zensical
|
|
||||||
- mkdocs
|
|
||||||
- mkdocs-material
|
|
||||||
- mkdocstrings
|
|
||||||
- docs
|
|
||||||
- documentation
|
|
||||||
- information-architecture
|
|
||||||
- skills
|
|
||||||
- bootstrap
|
|
||||||
- discovery
|
|
||||||
- authoring
|
|
||||||
capabilities:
|
|
||||||
- resource://skills/zensical-docs/document
|
|
||||||
depends_on: []
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
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",
|
|
||||||
)
|
|
||||||
@@ -50,12 +50,16 @@ nav = [
|
|||||||
{ "Arch" = "architecture.md" },
|
{ "Arch" = "architecture.md" },
|
||||||
{ "MCP" = "mcp_layout.md" },
|
{ "MCP" = "mcp_layout.md" },
|
||||||
{ "Copilot" = "copilot.md" },
|
{ "Copilot" = "copilot.md" },
|
||||||
|
{ "Contracts 1-5" = "mcp_contract_steps_1_5.md" },
|
||||||
{ "Usage" = "usage.md" },
|
{ "Usage" = "usage.md" },
|
||||||
{ "Future Work" = "future_work.md" },
|
{ "Future Work" = "future_work.md" },
|
||||||
{ "New Skill" = "new_skill.md" },
|
{ "New Skill" = "new_skill.md" },
|
||||||
{ "Security" = "securing.md" },
|
{ "Security" = "securing.md" },
|
||||||
] },
|
] },
|
||||||
{ "Skills" = [
|
{ "Skills" = [
|
||||||
|
{ "New Skill" = [
|
||||||
|
{ "Overview" = "skills/new-skill/SKILL.md" },
|
||||||
|
] },
|
||||||
{ "Copilot" = [
|
{ "Copilot" = [
|
||||||
{ "Overview" = "skills/copilot-customization/SKILL.md" },
|
{ "Overview" = "skills/copilot-customization/SKILL.md" },
|
||||||
{ "VS Code" = "skills/copilot-customization/references/vscode-customization.md" },
|
{ "VS Code" = "skills/copilot-customization/references/vscode-customization.md" },
|
||||||
|
|||||||
Reference in New Issue
Block a user