Compare commits

..

8 Commits

Author SHA1 Message Date
John Lancaster 660ca88e47 auto generating reference front-matter 2026-06-20 16:43:29 -05:00
John Lancaster e60fc4b27b update instructions to add links 2026-06-20 16:25:47 -05:00
John Lancaster 8817d2586f icons 2026-06-20 15:01:31 -05:00
John Lancaster bb7508cf65 doc updates 2026-06-20 14:56:25 -05:00
John Lancaster 467e1d3c35 sten 6 implementation 2026-06-20 14:31:24 -05:00
John Lancaster 3885774e5b step 6 2026-06-20 14:23:29 -05:00
John Lancaster f54cacd6cb ruffage 2026-06-20 14:13:22 -05:00
John Lancaster 19f3c1740a implemented steps 1-5 2026-06-20 14:08:59 -05:00
61 changed files with 2287 additions and 818 deletions
+1 -1
View File
@@ -55,7 +55,7 @@ Rules:
- No underscores, spaces, dots, or uppercase characters.
- Directory name should equal `skill-id` in each committed revision.
- Frontmatter `id` should equal directory name in each committed revision.
- Prefer keeping `skill-id` stable, but renames are allowed when needed if mappings are updated together.
- Treat `skill-id` as immutable after release; any rename is a breaking replacement and clients must move to the new id.
Example valid ids:
+5 -36
View File
@@ -72,16 +72,6 @@ Contract intent:
- Resolves only inside `docs/`.
- This surface is markdown-only in end state (`.md` files).
### Legacy URI Policy (Current-to-Target Transition)
Current catalog URIs in this repo (`resource://catalog/patterns`, `resource://catalog/patterns_by_id`, `resource://catalog/skills_details`) are treated as compatibility aliases during migration.
Rules:
- Keep aliases only when needed for active clients.
- Prefer simple canonical URIs for new clients.
- Remove aliases once consumers have moved.
### URI Versioning Policy
Default rule:
@@ -91,30 +81,14 @@ Default rule:
Breaking-change rule:
- If clients are already using an older shape, provide either:
- a short-lived alias, or
- a versioned URI family such as `resource://catalog/v2/...`.
- Choose the lightest migration path that minimizes maintenance overhead.
- Breaking changes use direct replacement of the canonical URI family.
- No compatibility aliases or dual URI families are maintained in this greenfield phase.
FastMCP version metadata usage:
- Resource `version` metadata MAY be used for implementation/version discovery.
- URI readability and maintainability remain the primary contract.
### Deprecation Policy For URIs
When deprecating a URI:
1. Document the replacement URI in changelog/docs.
2. Optionally return deprecation metadata while an alias exists.
3. Remove deprecated aliases when no active client needs them.
Recommended deprecation metadata fields in resource responses:
- `deprecated: true`
- `replacement_uri: <uri>`
- `sunset_at: <ISO-8601 timestamp>`
### Reference ID Compatibility Policy
`ref_id` is the public identifier for a reference document, separate from file path.
@@ -123,19 +97,14 @@ Rules:
- Prefer keeping `ref_id` stable when practical.
- File paths may change without URI churn as long as the mapped `ref_id` resolves.
- If a reference is renamed, introduce a new `ref_id`; keep an alias only if clients depend on the old id.
- If a reference is renamed, introduce a new `ref_id` and treat the old one as retired.
- Avoid reusing retired `ref_id` values for unrelated content.
Alias behavior for renamed references:
- If alias is kept, old `ref_id` continues to resolve and points to the replacement.
- Remove old alias as soon as migration is complete.
### Invariants This Contract Guarantees
- One canonical URI pattern per core capability surface.
- Fast, low-friction URI evolution with optional compatibility aliases.
- Explicit migration path for catalog URI consolidation when needed.
- Fast, low-friction URI evolution through direct replacement of canonical URIs.
- A single canonical catalog URI family with no alias maintenance overhead.
- Reference mappings can evolve with minimal churn.
### Non-goals For Step 3
+161
View File
@@ -0,0 +1,161 @@
**Step 6 Results: Resource-First Discovery and Tool Fallback Contract**
This section finalizes Step 6 by defining discovery behavior for clients that can attach MCP resources and the fallback behavior for clients or chat surfaces that must rely on MCP tools.
### Step Deliverable
- Update the current `docs/` directory with the finalized Step 6 discovery and fallback contract content from this document.
### Primary Source Baseline (Repository Docs)
Step 6 is based on the current project contracts in:
1. `docs/architecture.md` (resource-first architecture and catalog role)
2. `docs/usage.md` (operating flows, bounded loading, and fallback sequence)
3. `docs/copilot.md` (client capability lanes and practical fallback behavior)
4. `docs/mcp_layout.md` (shared content source and thin-tool fallback position)
5. `docs/securing.md` (read-only/public-docs security invariant)
Normative conclusions from those sources:
1. Discovery stays resource-first.
2. Tool fallback is allowed, thin, and read-only.
3. Resources and tools must resolve to the same canonical authored markdown.
4. Fallback behavior should keep context bounded and deterministic.
### Discovery Priority Contract (Normative)
Preferred sequence for skill discovery and loading:
1. `resource://catalog/skills_index`
2. `resource://catalog/skills/{skill_id}`
3. `resource://skills/{skill_id}/document`
4. `resource://skills/{skill_id}/references/{ref_id}` only when needed
Rules:
1. Start from catalog discovery before loading any skill document.
2. Do not skip straight to broad document loading when catalog metadata can narrow choices first.
3. Use `resource://docs/{path*}` only for direct authored-doc access outside skill-specific flows.
### Fallback Activation Rule
Fallback is used only when the active client path cannot reliably attach MCP resources (for example, tool-only chat surfaces).
Rules:
1. Keep the same discovery order semantics as the resource path.
2. If resource attachment is available, prefer resources over tools.
3. Tool fallback must never become a second authoritative content source.
### Tool Fallback Surface (Normative)
The fallback tool surface includes:
1. `list_resources`
2. `read_resource`
3. `search_patterns`
4. `get_pattern_by_id`
5. `get_skill_document_by_id`
Fallback order:
1. call `list_resources` to inspect canonical static/template resource surfaces
2. call `read_resource` for catalog URIs and selected skill URIs
3. use thin catalog tools only when additional metadata-first narrowing is needed
Tool behavior requirements:
1. read-only and idempotent semantics
2. deterministic ordering and bounded pagination
3. explicit not-found responses (`found: false` style) where applicable
4. payloads remain schema-aligned with catalog resources
### Resources-As-Tools Compatibility Layer
Step 6 includes a resources-as-tools compatibility layer for clients that can call tools but not attach resources.
Rules:
1. It wraps canonical resource reads rather than re-implementing content transforms.
2. It preserves canonical URIs and metadata semantics.
3. It does not replace the minimal catalog tools listed above.
4. It is interoperability-driven and remains read-only.
### Resource/Tool Parity Contract
Resources and fallback tools must agree on identity and routing metadata.
Parity requirements:
1. `skill_id` and `ref_id` are identical across both paths.
2. canonical URIs in payloads match Step 3 URI rules.
3. skill metadata (`id`, `name`, `description`, `tags`, `capabilities`, `version`) remains consistent.
4. document payload returned by `get_skill_document_by_id` resolves to the same canonical `SKILL.md` content as `resource://skills/{skill_id}/document`.
### Relevance and Ranking Contract
Baseline matching behavior is metadata-first and deterministic.
Rules:
1. Search primarily over normalized skill metadata (id, name, description, tags).
2. Keep deterministic ordering and deterministic pagination behavior.
3. Keep ranking logic transparent and bounded for predictable client behavior.
Optional extension policy:
1. BM25/regex augmentation is allowed only when catalog/tool volume meaningfully harms token efficiency or precision.
2. Any augmentation must preserve canonical ids, URIs, and deterministic tie-breaking.
3. Any augmentation remains discovery-only and does not create alternate content payloads.
### Context-Bounding and Clarification Policy
To prevent context bloat and improve answer quality:
1. load only the most relevant skill document by default
2. load at most two skill documents in one pass unless the user explicitly asks for more
3. if confidence is low after catalog discovery, ask one clarifying question before loading additional skill documents
4. fetch references lazily and only when required
### Security and Safety Constraints
Fallback tools must preserve the project security invariant.
Rules:
1. tool surfaces stay documentation-only and read-only
2. no mutation, shell execution, secret access, or private filesystem exposure
3. all returned content remains safe to publish publicly
### Integration Boundaries
Step 6 integrates with prior steps as follows:
1. Step 4 provides the validated in-memory registry.
2. Step 5 provides canonical resource registration.
3. Step 6 adds fallback discovery/read behavior that reuses the same registry and canonical markdown sources.
Separation-of-concerns rule:
1. Catalog/resource contracts remain canonical.
2. Fallback tools are interoperability adapters, not a parallel architecture.
### Acceptance Criteria for Step 6 Completion
Step 6 is complete when all are true:
1. Resource-first discovery remains the documented and implemented default path.
2. `list_resources` and `read_resource` are available for tool-only clients.
3. Thin catalog tools remain minimal, read-only parity surfaces.
4. Fallback tool outputs map to canonical skill identities and URIs.
5. Context loading is bounded and clarifying-question behavior is documented for low-confidence cases.
6. No second content source is introduced; resources and tools resolve the same authored markdown.
### Non-goals for Step 6
1. No write or side-effecting tools.
2. No alternate authored markdown stores or duplicated skill content pipelines.
3. No guarantee that every client session exposes MCP resource attachment UI.
4. No packaging/build contract changes (handled in Step 7).
5. No CI gate expansion details (handled in later validation/governance steps).
+86 -18
View File
@@ -16,6 +16,18 @@ The system is complete in three layers:
2. Catalog resources provide normalized discovery.
3. Zensical builds a static site from those same Markdown sources and the FastAPI app serves it in the FastMCP runtime process.
This architecture is anchored by three contracts:
1. Docs-first authored content contract under `docs/` with strict per-skill ownership.
2. `SKILL.md` frontmatter contract with Anthropic fields plus `x-personal-mcp` metadata.
3. Canonical resource URI contract with break-and-replace policy for contract changes.
Detailed contract pages:
1. [Content Contract](./content.md)
2. [Frontmatter Contract](./frontmatter.md)
3. [URI Contract](./uris.md)
This architecture keeps authored content human-friendly while preserving machine-stable contracts.
## Intent
@@ -30,7 +42,14 @@ The architecture is designed to satisfy three long-term requirements:
### 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
@@ -42,10 +61,23 @@ The catalog is the canonical discovery layer and publishes normalized records fo
Typical catalog resources:
1. resource://catalog/patterns
2. resource://catalog/patterns_by_id
3. resource://catalog/skills_index
4. resource://catalog/skills_details
1. resource://catalog/skills_index
2. resource://catalog/skills/{skill_id}
Only canonical catalog resources are part of the runtime contract in this phase.
### Registry Loader
The runtime composition includes a startup registry loader that reads packaged docs resources using `importlib.resources.files(...)` and `Traversable` APIs.
Loader responsibilities:
1. Parse SKILL.md frontmatter for each skill.
2. Validate schema and cross-field constraints before any resource registration.
3. Build an in-memory registry keyed by `skill_id`.
4. Fail fast for duplicate ids, missing markdown files, broken reference mappings, and invalid `depends_on` values.
Registry load failure is a startup error, not a partial runtime warning.
### Content Sources
@@ -76,27 +108,63 @@ flowchart TD
### Metadata Contract
Each pattern module declares:
Each skill declares frontmatter in `docs/skills/<skill-id>/SKILL.md`.
For the full field-level contract, validation model, and FastMCP metadata mapping, see [Frontmatter Contract](./frontmatter.md).
Anthropic-facing required fields:
1. name
2. description
Repository indexing metadata is declared in `x-personal-mcp`:
1. id
2. name
3. version
4. description
5. tags
6. capabilities
7. depends_on
2. version
3. tags
4. capabilities
5. depends_on
6. references map (ref id to relative path and optional metadata)
No `metadata.yaml` sidecar is part of the end-state contract.
### URI Contract
Module resource URIs are stable and follow:
Canonical resource URIs are:
For the full URI semantics, parameter validation rules, and compatibility policy, see [URI Contract](./uris.md).
1. resource://skills/<skill_id>/document
2. resource://skills/<skill_id>/references/<ref_id>
3. resource://catalog/skills_index
4. resource://catalog/skills/{skill_id}
5. resource://docs/{path*}
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
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
@@ -161,8 +229,8 @@ Allowed exception:
Existing markdown reference sets are valid examples of authored source material for this architecture:
1. ../docs/skills/pytest-scaffolding/references/pytest-docs.md
2. ../docs/skills/python-logging-dictconfig/references/python-logging-docs.md
3. ../docs/skills/fastapi-uv-docker/references/fastapi-best-practices.md
1. docs/skills/pytest-scaffolding/references/pytest-docs.md
2. docs/skills/python-logging-dictconfig/references/python-logging-docs.md
3. docs/skills/fastapi-uv-docker/references/fastapi-best-practices.md
These inputs are treated as content sources, while resource URIs and catalog payloads remain the machine-facing contracts.
+88
View File
@@ -0,0 +1,88 @@
---
icon: lucide/file-text
---
# Content Contract
This page defines the authored content contract for the docs-first MCP architecture.
## Canonical Source Of Truth
1. All authored Markdown lives under `docs/`.
2. MCP resources and static docs are two distribution surfaces of the same authored files.
3. No parallel authored markdown is allowed in `src/` or other package-only paths.
## Canonical Skill Shape
Each skill is one directory under `docs/skills/`:
```text
docs/
skills/
<skill-id>/
SKILL.md
references/
... (one or more markdown files, optional nested folders)
```
Rules:
1. `SKILL.md` is required for every skill.
2. `references/` is the only place for skill-specific supporting docs.
3. Nested folders inside `references/` are allowed so a skill can reorganize internals without changing global architecture.
4. Skill directories are independent ownership boundaries; no cross-skill file writes.
## File Placement And Ownership Boundaries
1. Top-level project docs stay in `docs/*.md`.
2. Skill docs stay in `docs/skills/<skill-id>/...`.
3. A skill may link to other skills, but must not store content inside another skill's directory.
4. Server and runtime code may index and serve docs, but must not be the source of authored markdown.
## Metadata Location Constraint
1. Skill metadata is embedded in YAML frontmatter in `SKILL.md`.
2. No `metadata.yaml` sidecar exists in the end state.
3. Reference lookup metadata, including reference id to relative path mappings, is declared from `SKILL.md` frontmatter rather than inferred as a hidden global convention.
## Skill Id Contract
`skill-id` is the public identifier and should satisfy all rules below:
1. Format: lowercase kebab-case only.
2. Character set: `a-z`, `0-9`, and `-`.
3. Must start with a letter.
4. No underscores, spaces, dots, or uppercase characters.
5. Directory name should equal `skill-id` in each committed revision.
6. Frontmatter `id` should equal directory name in each committed revision.
7. Treat `skill-id` as immutable after release; any rename is a breaking replacement and clients must move to the new id.
Valid examples:
1. `fastapi-uv-docker`
2. `zensical-docs`
3. `pytest-scaffolding`
Invalid examples:
1. `fastapi_uv_docker`
2. `Zensical-Docs`
3. `docs.zensical`
## Invariants
This contract guarantees:
1. One authored source tree in `docs/` for both website and MCP.
2. One skill directory maps to one skill identity per revision.
3. Namespace and slug drift is minimized by keeping directory and frontmatter ids aligned per revision.
4. Per-skill reference structure can evolve without changing cross-skill architecture.
5. Packaging for stdio is deterministic because authored content is path-stable.
## Non-Goals
This contract does not define:
1. URI versioning policy details.
2. The full frontmatter schema.
3. Migration instructions from the current architecture.
+31 -13
View File
@@ -18,7 +18,7 @@ Copilot interacts with MCP servers through separate capability lanes:
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`
2. let Copilot invoke MCP tools when the task and tool descriptions make that relevant
@@ -57,18 +57,24 @@ Use this sequence to confirm behavior:
## 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
3. keep slash commands for deterministic fallback flows
When resource attachment is unavailable in the active session, use thin catalog discovery tools as operational fallback:
When resource attachment is unavailable in the active session, use ResourcesAsTools first, then thin catalog discovery tools as parity fallback:
1. `search_patterns`
2. `get_pattern_by_id`
3. `get_skill_document_by_id`
1. `list_resources`
2. `read_resource`
3. `search_patterns`
4. `get_pattern_by_id`
5. `get_skill_document_by_id`
The first two are generated from the canonical resource surface and should be preferred in tool-only clients.
These should stay read-only, minimal, and schema-aligned with catalog resources.
For very large tool catalogs, server operators can optionally enable tool search mode (`regex` or `bm25`) while keeping `list_resources` and `read_resource` pinned as always-visible fallback tools.
## What To Type In Copilot Chat
Use prompts that tell Copilot which MCP feature path to take.
@@ -91,7 +97,15 @@ I attached personal-mcp catalog resources first. Use them to identify the best m
### If only tools are available
Ask Copilot to explicitly use the catalog tools.
Ask Copilot to explicitly use resource-backed tools first.
Example resource-backed prompt:
```text
Use personal-mcp tool fallback by first calling list_resources, then read_resource for resource://catalog/skills_index and the selected resource://skills/<skill-id>/document URI. Use only that loaded skill context in your answer.
```
If needed, use the thin catalog tools.
Example discovery prompt:
@@ -128,20 +142,24 @@ When a task may benefit from personal-mcp skills, use this sequence:
1. Start with personal-mcp catalog discovery when the task appears to match documented implementation patterns.
2. Prefer MCP resources when the chat surface exposes resource attachment.
3. If MCP resource attachment is unavailable, use catalog tools instead.
3. If MCP resource attachment is unavailable, use `list_resources`/`read_resource` first, then thin catalog tools if needed.
4. Load only the most relevant skill document or at most 2 skill documents.
5. Treat skill documents as guidance, then reconcile them with the actual repository code before making changes.
Preferred discovery order:
1. `resource://catalog/skills_index` or `resource://catalog/patterns`
2. `resource://skills/<skill-id>/document`
1. `resource://catalog/skills_index`
2. `resource://catalog/skills/{skill_id}`
3. `resource://skills/<skill-id>/document`
4. `resource://skills/<skill-id>/references/<ref-id>` when needed
Tool fallback order:
1. `search_patterns`
2. `get_pattern_by_id`
3. `get_skill_document_by_id`
1. `list_resources`
2. `read_resource`
3. `search_patterns`
4. `get_pattern_by_id`
5. `get_skill_document_by_id`
If confidence is low after catalog discovery, ask one clarifying question before loading more skill documents.
```
+316
View File
@@ -0,0 +1,316 @@
---
icon: lucide/braces
---
# Frontmatter Contract
This page defines the `SKILL.md` frontmatter and FastMCP metadata contract.
## Anthropic Frontmatter Support
Across Anthropic API and Agent Skills surfaces:
1. Required fields for custom skill bundles are `name` and `description`.
2. `name` must be 1-64 characters, lowercase letters, numbers, and hyphens only, with no XML tags, and must not use the reserved words `anthropic` or `claude`.
3. `description` must be 1-1024 characters, non-empty, and contain no XML tags.
Portable optional fields from the Agent Skills specification:
1. `license`
2. `compatibility`
3. `metadata`
4. `allowed-tools`
Claude Code-specific optional fields:
1. `when_to_use`
2. `argument-hint`
3. `arguments`
4. `disable-model-invocation`
5. `user-invocable`
6. `allowed-tools`
7. `disallowed-tools`
8. `model`
9. `effort`
10. `context`
11. `agent`
12. `hooks`
13. `paths`
14. `shell`
Repository contract decisions:
1. Treat `name` and `description` as required in all `SKILL.md` files.
2. Keep Anthropic-facing semantics in standard fields.
3. Keep MCP indexing metadata in a namespaced extension block.
4. Preserve forward compatibility by allowing additive optional metadata fields over time.
## Canonical Frontmatter Schema
Use this two-layer pattern:
1. Anthropic layer: top-level fields intended for Anthropic and Agent Skills behavior.
2. Repository layer: one namespaced block, `x-personal-mcp`, for MCP catalog and routing metadata.
Canonical shape:
```yaml
---
name: <skill-id>
description: <what this skill does and when to use it>
# Optional Anthropic and Agent Skills fields
when_to_use: <extra trigger guidance>
allowed-tools: <space-separated string or YAML list>
disable-model-invocation: false
user-invocable: true
license: <optional>
compatibility: <optional>
# Repository-specific metadata
x-personal-mcp:
id: <skill-id>
version: <semver>
tags:
- <tag>
capabilities:
- resource://skills/<skill-id>/document
depends_on: []
# Optional: overrides and nested references only.
# Top-level references/*.md are auto-discovered.
references:
<ref-id>:
path: references/<file>.md
mime_type: text/markdown
title: <short title>
---
```
## Repository Metadata Field Rules
Rules for `x-personal-mcp`:
1. `id` is required, must follow the skill id rules from the content contract, and must equal the directory name.
2. `version` is required and must be a semantic version string.
3. `tags` is optional and should be a list of kebab-case discovery labels.
4. `capabilities` is required and lists the MCP URIs the skill publishes.
5. `depends_on` is optional and lists other skill ids.
6. `references` is an optional map keyed by `ref-id` for overrides and nested entries.
Reference entry rules:
1. `ref-id` is lowercase kebab-case.
2. `path` is a skill-relative markdown path and must stay inside the same skill directory.
3. Top-level files under `references/*.md` are auto-discovered with `ref-id` derived from a normalized filename stem (lowercase kebab-case).
4. Nested folders under `references/` are not auto-discovered and must be declared explicitly.
5. `mime_type` defaults to `text/markdown` when omitted.
6. `title` is an optional display label.
7. Renaming `ref-id` values is allowed when needed; optional aliases may be used during transitions.
## Validation Models
The normative model uses Pydantic v2 with change-friendly validation:
```python
from __future__ import annotations
import re
from pathlib import PurePosixPath
from typing import Any
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
SKILL_ID_RE = re.compile(r"^[a-z][a-z0-9-]*$")
SEMVER_RE = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+][0-9A-Za-z.-]+)?$")
class ReferenceEntry(BaseModel):
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
path: str
mime_type: str = "text/markdown"
title: str | None = None
@field_validator("path")
@classmethod
def validate_reference_path(cls, value: str) -> str:
p = PurePosixPath(value)
if p.is_absolute() or ".." in p.parts:
raise ValueError("reference path must be a relative in-skill path")
if not str(p).startswith("references/"):
raise ValueError("reference path must stay under references/")
if p.suffix.lower() != ".md":
raise ValueError("reference path must target a markdown file")
return str(p)
class PersonalMcpMetadata(BaseModel):
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
id: str
version: str
tags: list[str] = Field(default_factory=list)
capabilities: list[str] = Field(min_length=1)
depends_on: list[str] = Field(default_factory=list)
references: dict[str, ReferenceEntry] = Field(default_factory=dict)
@field_validator("id")
@classmethod
def validate_id(cls, value: str) -> str:
if not SKILL_ID_RE.fullmatch(value):
raise ValueError("id must be lowercase kebab-case and start with a letter")
return value
@field_validator("version")
@classmethod
def validate_version(cls, value: str) -> str:
if not SEMVER_RE.fullmatch(value):
raise ValueError("version must be semver")
return value
@field_validator("depends_on")
@classmethod
def validate_depends_on(cls, value: list[str]) -> list[str]:
for dep in value:
if not SKILL_ID_RE.fullmatch(dep):
raise ValueError(f"invalid depends_on skill id: {dep}")
return value
@field_validator("references")
@classmethod
def validate_reference_ids(cls, value: dict[str, ReferenceEntry]) -> dict[str, ReferenceEntry]:
for ref_id in value:
if not SKILL_ID_RE.fullmatch(ref_id):
raise ValueError(f"invalid reference id: {ref_id}")
return value
@model_validator(mode="after")
def ensure_primary_capability(self) -> "PersonalMcpMetadata":
expected = f"resource://skills/{self.id}/document"
if expected not in self.capabilities:
raise ValueError(f"capabilities must include {expected}")
return self
class SkillFrontmatter(BaseModel):
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
name: str = Field(min_length=1, max_length=64)
description: str = Field(min_length=1, max_length=1024)
when_to_use: str | None = None
allowed_tools: str | list[str] | None = Field(default=None, alias="allowed-tools")
disallowed_tools: str | list[str] | None = Field(default=None, alias="disallowed-tools")
disable_model_invocation: bool | None = Field(default=None, alias="disable-model-invocation")
user_invocable: bool | None = Field(default=None, alias="user-invocable")
argument_hint: str | None = Field(default=None, alias="argument-hint")
arguments: str | list[str] | None = None
license: str | None = None
compatibility: str | None = None
metadata: dict[str, str] | None = None
x_personal_mcp: PersonalMcpMetadata = Field(alias="x-personal-mcp")
@field_validator("name")
@classmethod
def validate_name(cls, value: str) -> str:
if not SKILL_ID_RE.fullmatch(value):
raise ValueError("name must be lowercase kebab-case and start with a letter")
if "anthropic" in value or "claude" in value:
raise ValueError("name must not contain reserved words anthropic or claude")
return value
@model_validator(mode="after")
def cross_validate(self) -> "SkillFrontmatter":
if self.x_personal_mcp.id != self.name:
raise ValueError("x-personal-mcp.id must exactly match name")
return self
def validate_skill_frontmatter(raw: dict[str, Any], skill_dir_name: str) -> SkillFrontmatter:
model = SkillFrontmatter.model_validate(raw)
if model.name != skill_dir_name:
raise ValueError("frontmatter name must exactly match skill directory name")
return model
```
Validation behavior contract:
1. Validate required core fields and relationships during registry load before FastMCP resource or tool registration.
2. Allow unknown additive fields so frontmatter can evolve without blocking startup.
3. Treat hard contract violations, including missing required fields, invalid ids, and broken required mappings, as startup errors.
4. Treat non-critical compatibility issues as warnings when possible.
5. Error messages should include the skill path and failing field for CI readability.
Projection mode contract for Anthropic API upload pipelines:
1. Parse with `SkillFrontmatter` first.
2. Emit Anthropic-safe frontmatter with standard fields only.
3. Serialize repository metadata into standard `metadata` as namespaced keys.
4. Preserve the canonical authored source in `x-personal-mcp`; projection output is a build artifact.
## Anthropic Upload Compatibility Rule
1. Anthropic documentation guarantees behavior for standard frontmatter fields but does not explicitly guarantee handling of arbitrary unknown top-level keys.
2. Publishing pipelines that target strict API compatibility should support a projection mode that emits only standard frontmatter fields for upload.
3. In projection mode, repository extension metadata is serialized into the standard `metadata` field as namespaced keys or JSON-encoded values, while source-of-truth authoring remains in `x-personal-mcp`.
## FastMCP Native Metadata Surfaces
Resources support native definition metadata:
1. `name`
2. `description`
3. `mime_type`
4. `tags`
5. `annotations`, including `readOnlyHint` and `idempotentHint`
6. `icons`
7. `meta`
8. `version`
9. `enabled`, which is deprecated in FastMCP v3 in favor of server-level enable and disable controls
Resources also support runtime metadata through `ResourceContent.meta` and `ResourceResult.meta`.
Tools support native definition metadata:
1. `name`
2. `description`
3. `tags`
4. `annotations`, including `title`, `readOnlyHint`, `destructiveHint`, `idempotentHint`, and `openWorldHint`
5. `icons`
6. `meta`
7. `version`
8. `timeout`
9. `output_schema`
10. `run_in_thread`
11. `enabled`, which is deprecated in FastMCP v3 in favor of server-level enable and disable controls
Tools also support runtime metadata through `ToolResult.meta`.
## Frontmatter To FastMCP Mapping Contract
At server startup, map `x-personal-mcp` into FastMCP registration as follows:
1. `x-personal-mcp.id` defines the canonical URI namespace and identity checks.
2. `description` becomes the default description for the primary skill document resource.
3. `x-personal-mcp.tags` maps to resource and tool tags.
4. `x-personal-mcp.version` maps to resource and tool version metadata.
5. `x-personal-mcp.capabilities` becomes the registered URI list and catalog exposure.
6. `x-personal-mcp.references[*]` becomes resource templates or concrete resources with `mime_type`, read-only annotations, and `meta` that includes `skill_id`, `ref_id`, and source `path`.
7. `x-personal-mcp.depends_on` becomes catalog dependency graph metadata and validation inputs.
## Invariants
This contract guarantees:
1. Anthropic-required frontmatter stays valid for custom skill upload and Claude Code loading.
2. MCP-specific metadata remains embedded in `SKILL.md` frontmatter, with no `metadata.yaml` sidecar.
3. FastMCP registration uses native metadata fields for resources and tools.
4. Reference ids and metadata can evolve with low-friction updates while internal file layout under `references/` stays refactor-friendly.
## Non-Goals
This contract does not define:
1. URI versioning and deprecation rollout policy details.
2. Migration script design from existing `metadata.yaml` files.
3. Runtime caching and indexing performance tuning.
+5 -2
View File
@@ -4,11 +4,11 @@ icon: lucide/rocket
# 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
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
@@ -39,6 +39,9 @@ When the server is running, the health check is available at `/healthz` and the
## Architecture
- [Resource-First Pattern Module Architecture](./architecture.md)
- [Content Contract](./content.md)
- [Frontmatter Contract](./frontmatter.md)
- [URI Contract](./uris.md)
- [Static Docs Hosting Pattern](./mcp_layout.md)
- [Skill Usage Mechanics](./usage.md)
- [Copilot MCP Mechanics](./copilot.md)
+68 -15
View File
@@ -1,3 +1,7 @@
---
icon: lucide/server
---
# Static Docs Hosting Pattern
## Purpose
@@ -29,15 +33,39 @@ treeView-beta
"docs"
"index.md"
"architecture.md"
"content.md"
"frontmatter.md"
"mcp_layout.md"
"uris.md"
"skills"
"new-skill"
"SKILL.md"
"references"
"copilot-customization"
"SKILL.md"
"references"
"fastapi-async-sqlalchemy-modernization"
"SKILL.md"
"references"
"fastapi-uv-docker"
"SKILL.md"
"references"
"nicegui"
"SKILL.md"
"references"
"nicegui-ui-customization"
"SKILL.md"
"references"
"pytest-scaffolding"
"SKILL.md"
"references"
"python-logging-dictconfig"
"SKILL.md"
"references"
"fastapi-uv-docker"
"vscode-configuration"
"SKILL.md"
"references"
"zensical-docs"
"SKILL.md"
"references"
"site"
@@ -45,15 +73,14 @@ treeView-beta
"src"
"personal_mcp"
"main.py"
"mcp.py"
"web"
"app.py"
"docs_mount.py"
"catalog"
"server.py"
"skills"
"pytest_scaffolding"
"python_logging_dictconfig"
"fastapi_uv_docker"
"document_loader.py"
```
Notes:
@@ -72,12 +99,21 @@ The runtime process serves two surfaces:
```mermaid
flowchart TD
A[FastMCP Root Server] --> B[MCP Transport]
A --> C[FastAPI Application]
C --> D[Static Mount /docs]
D --> E[Zensical site output directory]
A[Docs Registry Loader] --> B[Validated In-Memory Registry]
B --> C[FastMCP Resource Registration]
C --> D[MCP Transport]
C --> E[FastAPI Application]
E --> F[Static Mount /docs]
F --> G[Zensical site output directory]
```
Runtime guarantees:
1. Docs registry load and validation happen before resource exposure.
2. Duplicate resource and template registration fails startup (`on_duplicate="error"`).
3. Resource registration is metadata-driven from SKILL frontmatter and reference manifests.
4. Legacy per-skill Python servers and `metadata.yaml` sidecars are not part of the runtime.
## Build and Publish Flow
The docs flow is pre-build only.
@@ -103,12 +139,29 @@ MCP resources map directly to canonical Markdown documents.
Example mapping model:
1. docs/skills/<slug>/SKILL.md -> resource://skills/<id>/document
2. docs/skills/<slug>/references/*.md -> referenced sections or linked companion documents
1. docs/skills/<skill-id>/SKILL.md -> resource://skills/<skill_id>/document
2. docs/skills/<skill-id>/references/<file>.md -> resource://skills/<skill_id>/references/<ref_id> (via frontmatter references manifest)
3. docs/<path>.md -> resource://docs/{path*}
Catalog 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
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
@@ -155,8 +208,8 @@ This keeps docs publication explicit and predictable.
Existing reference docs remain valid content inputs in this pattern:
1. ../docs/skills/pytest-scaffolding/references/pytest-docs.md
2. ../docs/skills/python-logging-dictconfig/references/python-logging-docs.md
3. ../docs/skills/fastapi-uv-docker/references/fastapi-best-practices.md
1. docs/skills/pytest-scaffolding/references/pytest-docs.md
2. docs/skills/python-logging-dictconfig/references/python-logging-docs.md
3. docs/skills/fastapi-uv-docker/references/fastapi-best-practices.md
These are source documents, not deployment artifacts.
+116 -165
View File
@@ -1,178 +1,129 @@
---
icon: lucide/file-plus
---
# 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 docs-first model.
For the full contract details, see [Content Contract](./content.md), [Frontmatter Contract](./frontmatter.md), and [URI Contract](./uris.md).
## Canonical Skill Shape
Create one skill directory under `docs/skills/`:
```text
docs/
skills/
<skill-id>/
SKILL.md
references/
... (optional markdown files, nested folders allowed)
```
Rules:
1. `SKILL.md` is required.
2. All skill-specific supporting docs live under `references/`.
3. Skill directories are ownership boundaries; no cross-skill writes.
4. `skill-id` is lowercase kebab-case and should remain stable.
## SKILL.md Frontmatter
`SKILL.md` frontmatter is authoritative for metadata.
Required top-level fields:
1. `name`
2. `description`
3. `x-personal-mcp`
Required `x-personal-mcp` fields:
1. `id`
2. `version`
3. `capabilities`
Optional `x-personal-mcp` fields:
1. `tags`
2. `depends_on`
3. `references`
Canonical frontmatter template:
```yaml
---
name: <skill-id>
description: <what this skill does and when to use it>
x-personal-mcp:
id: <skill-id>
version: 1.0.0
tags: []
capabilities:
- resource://skills/<skill-id>/document
depends_on: []
# Optional: only for nested references or metadata overrides.
references:
<ref-id>:
path: references/<file>.md
mime_type: text/markdown
title: <optional short title>
---
```
Reference manifest rules:
1. `ref-id` is lowercase kebab-case.
2. `path` is skill-relative and must stay under `references/`.
3. Top-level `references/*.md` files are auto-discovered, and `ref-id` is derived from a normalized filename stem.
4. Nested `references/**` markdown files must be declared explicitly.
5. Reference paths are markdown files.
No `metadata.yaml` sidecar is part of this model.
## URI Surface
Canonical resource URIs for a skill:
1. `resource://skills/<skill_id>/document`
2. `resource://skills/<skill_id>/references/<ref_id>`
Canonical discovery URIs:
1. `resource://catalog/skills_index`
2. `resource://catalog/skills/{skill_id}`
Docs passthrough URI:
1. `resource://docs/{path*}`
Compatibility rule:
1. Keep URI families unversioned by default.
2. For breaking changes, update clients to the canonical replacement URIs directly.
## Checklist
1. Create the authored docs content.
Add `docs/skills/<slug>/SKILL.md` and any companion files under `docs/skills/<slug>/references/`.
2. Choose the three names up front.
Use a docs slug like `fastapi-uv-docker`, a resource id like `fastapi-uv-docker`, and a Python package name like `fastapi_uv_docker`.
3. Add the runtime package.
Create `src/personal_mcp/skills/<python_namespace>/` with `__init__.py`, `server.py`, and `metadata.yaml`.
4. Expose the document resource in `server.py`.
Follow the existing pattern: create a `FastMCP` instance, register `resource://skills/<skill-id>/document`, and return 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`
1. Create `docs/skills/<skill-id>/SKILL.md`.
2. Add optional references under `docs/skills/<skill-id>/references/`.
3. Populate frontmatter with `name`, `description`, and `x-personal-mcp` metadata.
4. Ensure `x-personal-mcp.id` equals `name` and directory `<skill-id>`.
5. Ensure `capabilities` includes `resource://skills/<skill-id>/document`.
6. Add supporting docs under `references/`; top-level markdown files are exposed automatically.
7. Declare `x-personal-mcp.references` only for nested paths or to override defaults.
## Quick Validation
1. Confirm 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'])"`
1. Confirm docs build succeeds:
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>",
)
```bash
uv run zensical build
```
## metadata.yaml Template
2. Confirm tests succeed:
```yaml
id: <skill-id>
name: <Human Readable Name>
version: 1.0.0
description: <One sentence describing what the skill provides.>
tags:
- <tag-one>
- <tag-two>
document_path: <optional repo-relative path to a markdown file>
capabilities:
- resource://skills/<skill-id>/document
depends_on: []
```bash
uv run pytest -q
```
Omit `document_path` when the canonical document is `docs/skills/<slug>/SKILL.md`.
## Root Mount Template
Add an import in `src/personal_mcp/mcp.py`:
```python
from personal_mcp.skills.<python_namespace>.server import <python_namespace>_server
```
Add a mount call:
```python
mcp.mount(<python_namespace>_server, namespace="<python_namespace>")
```
## Example Scaffold
For a new skill called `sqlmodel-patterns`:
1. Docs content lives in `docs/skills/sqlmodel-patterns/SKILL.md`.
2. The Python package lives in `src/personal_mcp/skills/sqlmodel_patterns/`.
3. The resource id is `sqlmodel-patterns`.
Example `server.py`:
```python
from fastmcp import FastMCP
from personal_mcp.skills.document_loader import load_skill_document
sqlmodel_patterns_server = FastMCP("sqlmodel-patterns")
@sqlmodel_patterns_server.resource("resource://skills/sqlmodel-patterns/document")
def skill_document() -> dict[str, str]:
"""Return the canonical Markdown document for this skill."""
return load_skill_document(
skill_id="sqlmodel-patterns",
skill_slug="sqlmodel-patterns",
)
```
Example `metadata.yaml`:
```yaml
id: sqlmodel-patterns
name: SQLModel Patterns
version: 1.0.0
description: Provide reusable patterns for building apps with SQLModel.
tags:
- sqlmodel
- python
- patterns
capabilities:
- resource://skills/sqlmodel-patterns/document
depends_on: []
```
Example `mcp.py` additions:
```python
from personal_mcp.skills.sqlmodel_patterns.server import sqlmodel_patterns_server
mcp.mount(sqlmodel_patterns_server, namespace="sqlmodel_patterns")
```
## Bootstrap Sequence
1. Create `docs/skills/<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.
+27 -8
View File
@@ -2,6 +2,26 @@
name: copilot-customization
description: 'Plan, create, review, and debug GitHub Copilot and VS Code agent customizations, including instructions, prompt files, skills, custom agents, hooks, MCP servers, and repo-specific personal-mcp skill integration.'
argument-hint: 'What Copilot behavior are you customizing, and should it be workspace-scoped, personal, or exposed as an MCP skill resource?'
x-personal-mcp:
id: copilot-customization
version: 1.0.0
tags:
- copilot
- vscode
- customization
- instructions
- prompts
- agent-skills
- custom-agents
- hooks
- mcp
- personal-mcp
- skills
capabilities:
- resource://skills/copilot-customization/document
depends_on:
- new-skill
- zensical-docs
---
# Copilot Customization
@@ -51,15 +71,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:
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.
3. Choose consistent names: docs slug and resource id use kebab-case; Python namespace uses snake_case.
4. Create `src/personal_mcp/skills/<python_namespace>/` with `__init__.py`, `server.py`, and `metadata.yaml`.
5. Expose only `resource://skills/<skill-id>/document` from the per-skill server.
6. Put discovery metadata in `metadata.yaml`, including `id`, `name`, `version`, `description`, `tags`, `capabilities`, and `depends_on`.
7. Mount the skill server in `src/personal_mcp/mcp.py` using the Python namespace.
8. Validate with the document loader and `uv run zensical build`.
2. Create authored docs under `docs/skills/<skill-id>/SKILL.md`, with optional nested `references/` markdown files.
3. Keep `skill-id` stable and consistent across directory name, `name`, and `x-personal-mcp.id`.
4. Put discovery metadata in `SKILL.md` frontmatter under `x-personal-mcp`.
5. Declare `resource://skills/<skill-id>/document` in `x-personal-mcp.capabilities`.
6. Declare references in `x-personal-mcp.references` as `ref-id -> references/<file>.md` mappings.
7. Validate with the registry loader and `uv run zensical build`.
Keep 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
@@ -2,6 +2,17 @@
name: fastapi-async-sqlalchemy-modernization
description: 'Create a step-by-step modernization plan for an existing FastAPI app using SQLAlchemy async patterns, context managers, and AsyncExitStack. Use when: planning migration from legacy DB setup, standardizing async engine/session lifecycles, defining transaction boundaries, and aligning with SQLAlchemy 2.x best practices.'
argument-hint: 'What is your current FastAPI + SQLAlchemy setup (sync/async driver, session pattern, lifespan usage, and deployment model)?'
x-personal-mcp:
id: fastapi-async-sqlalchemy-modernization
version: 1.0.0
tags:
- fastapi
- sqlalchemy
- async
- modernization
capabilities:
- resource://skills/fastapi-async-sqlalchemy-modernization/document
depends_on: []
---
# FastAPI Async SQLAlchemy Modernization Plan
+10
View File
@@ -2,6 +2,16 @@
name: fastapi-uv-docker
description: 'Audit and migrate an existing Python project to best practices for a cloud-native ASGI FastAPI app managed with uv and run with uvicorn in Docker. Use when: conforming a project to production standards, setting up src layout, configuring pyproject.toml, writing multi-stage Dockerfiles, wiring lifespan and settings, adding health endpoints, enforcing non-root container user, migrating from requirements.txt to uv.'
argument-hint: 'What is the current state of the project (bare Python, requirements.txt, pip, etc.)?'
x-personal-mcp:
id: fastapi-uv-docker
version: 1.0.0
tags:
- fastapi
- uv
- docker
capabilities:
- resource://skills/fastapi-uv-docker/document
depends_on: []
---
# FastAPI Project Best Practices
+42
View File
@@ -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,17 @@
name: nicegui-ui-customization
description: 'Design and implement production NiceGUI UIs with reusable components, Tailwind-first styling, event-driven interactions, and troubleshooting for uploads, state, and static assets. Use when building or refactoring NiceGUI pages and interaction flows.'
argument-hint: 'What UI outcome should this workflow produce?'
x-personal-mcp:
id: nicegui-ui-customization
version: 1.0.0
tags:
- nicegui
- ui
- customization
- frontend
capabilities:
- resource://skills/nicegui-ui-customization/document
depends_on: []
---
# NiceGUI UI Customization Workflow
+11
View File
@@ -2,6 +2,17 @@
name: nicegui
description: 'Design and scaffold a production-ready NiceGUI + FastAPI application architecture. Use for multi-page app planning, package boundaries, optional DB/LangGraph/docs integration, and implementation checklists.'
argument-hint: 'What should this app include (pages, DB, AI, docs, constraints)?'
x-personal-mcp:
id: nicegui
version: 1.0.0
tags:
- nicegui
- fastapi
- ui
- architecture
capabilities:
- resource://skills/nicegui/document
depends_on: []
---
# NiceGUI
+10
View File
@@ -2,6 +2,16 @@
name: pytest-scaffolding
description: "Scaffold a maintainable, hierarchical pytest suite with fast defaults and clear escalation paths for FastAPI and SQLAlchemy tests. Use when creating or reorganizing tests, defining fixture/marker boundaries, or making test strategy progressively discoverable."
argument-hint: "Target scope plus stack details (pure Python, FastAPI, SQLAlchemy sync, SQLAlchemy async, or mixed)"
x-personal-mcp:
id: pytest-scaffolding
version: 1.0.0
tags:
- pytest
- testing
- python
capabilities:
- resource://skills/pytest-scaffolding/document
depends_on: []
---
# Pytest Scaffolding
@@ -2,6 +2,16 @@
name: python-logging-dictconfig
description: 'Set up idiomatic Python logging with logging.config.dictConfig. Use when creating or refactoring logging setup, standardizing handlers/formatters, and enforcing centralized config.'
argument-hint: 'Target context (single script, package, FastAPI app, or CLI) and desired log destinations'
x-personal-mcp:
id: python-logging-dictconfig
version: 1.0.0
tags:
- logging
- python
- observability
capabilities:
- resource://skills/python-logging-dictconfig/document
depends_on: []
---
# Idiomatic Python Logging with dictConfig
+14
View File
@@ -2,6 +2,20 @@
name: vscode-configuration
description: 'Create and troubleshoot VS Code workspace configuration for Python projects, with focused patterns for launch.json debugpy/FastAPI debugging and tasks.json task automation.'
argument-hint: 'What do you need: debug setup, FastAPI debug run profile, tasks.json automation, or all of them?'
x-personal-mcp:
id: vscode-configuration
version: 1.0.0
tags:
- vscode
- launch-json
- tasks-json
- debugpy
- fastapi
- python
- skills
capabilities:
- resource://skills/vscode-configuration/document
depends_on: []
---
# VS Code Configuration
+44 -1
View File
@@ -1,7 +1,25 @@
---
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. Prefer inline Markdown links to source docs and avoid bare URLs because this content is rendered as human docs and MCP resources.'
argument-hint: 'What are you documenting, who is the audience, and what Zensical features are in scope?'
x-personal-mcp:
id: zensical-docs
version: 1.0.0
tags:
- zensical
- mkdocs
- mkdocs-material
- mkdocstrings
- docs
- documentation
- information-architecture
- skills
- bootstrap
- discovery
- authoring
capabilities:
- resource://skills/zensical-docs/document
depends_on: []
---
# Zensical Documentation Authoring
@@ -81,6 +99,30 @@ Keep this section short and revise it over time.
When making a recommendation, link back to the relevant reference file first, and when possible to the upstream docs linked from that reference.
## Link Formatting Rule
Because this project publishes the same markdown for both `/docs` and MCP resources, link quality is part of the content contract.
- Never leave a bare URL in prose or list items.
- Prefer using in-place Markdown links with meaningful labels.
- For external sources, prefer `[descriptive label](https://...)` over raw `https://...`.
- For internal files, prefer relative Markdown links so rendered docs remain navigable.
- Any mention of a library or a specific library feature should include a link to source documentation somewhere on the page.
- If inline linking is awkward or the citation payload is too large, use a footnote or tooltip citation instead.
Example preferred style:
- `See [importlib.resources](https://docs.python.org/3/library/importlib.resources.html) for packaging details.`
Example to avoid:
- `See https://docs.python.org/3/library/importlib.resources.html for packaging details.`
Acceptable alternatives when inline links are not ideal:
- Add a footnote-style source citation at the end of the section or page.
- Add a tooltip citation when the docs pattern supports it.
## Compatibility Rule
Prefer the Zensical-native way of doing something when it exists and is well-supported.
@@ -93,3 +135,4 @@ Return only what is useful for the current docs task:
1. Which reference to read next.
2. The smallest recommended docs or config change.
3. Any repo-specific preference this suggests should be added back into this skill.
4. For any library or feature-level claim, include a source-doc citation somewhere (inline link preferred; footnote or tooltip acceptable).
+118
View File
@@ -0,0 +1,118 @@
---
icon: lucide/link
---
# URI Contract
This page defines the canonical resource URI contract, template parameter rules, and compatibility policy.
## Canonical URI Surface
The public, preferred URIs are:
1. `resource://catalog/skills_index`
2. `resource://catalog/skills/{skill_id}`
3. `resource://skills/{skill_id}/document`
4. `resource://skills/{skill_id}/references/{ref_id}`
5. `resource://docs/{path*}`
Contract intent:
1. Catalog URIs are discovery surfaces.
2. Skill URIs are the primary per-skill guidance surfaces.
3. The docs wildcard URI is a direct authored-markdown access surface under `docs/`.
## URI Semantics
### `resource://catalog/skills_index`
1. Returns a compact list of skill records for discovery.
2. Contains one entry per `skill_id`.
3. Includes enough metadata for client-side selection, at minimum `id`, `name`, `description`, `tags`, and `capabilities`.
### `resource://catalog/skills/{skill_id}`
1. Returns one normalized record for `skill_id`.
2. Includes the canonical document URI and declared reference ids.
3. Returns not found when `skill_id` does not exist.
### `resource://skills/{skill_id}/document`
1. Returns the canonical `SKILL.md` authored content for that skill.
2. `skill_id` must satisfy the stable skill id rules from the content contract.
### `resource://skills/{skill_id}/references/{ref_id}`
1. Returns one reference document declared in the skill frontmatter references manifest.
2. `ref_id` is the stable public handle for that reference document.
### `resource://docs/{path*}`
1. Returns authored markdown at a normalized relative path under `docs/`.
2. Supports nested paths via RFC6570 wildcard expansion.
3. Typical examples include `index.md`, `usage.md`, `skills/<skill-id>/SKILL.md`, and `skills/<skill-id>/references/<file>.md`.
## Template Parameter And Validation Rules
### `skill_id`
1. Lowercase kebab-case.
2. Must satisfy the stable skill id rules from the content contract.
### `ref_id`
1. Lowercase kebab-case.
2. Must be declared in the skill's references manifest.
### `path*`
1. Relative POSIX path only.
2. No leading slash.
3. No `..` traversal segments.
4. Resolves only inside `docs/`.
5. Markdown-only in the end state, meaning `.md` files.
## URI Versioning Policy
Default rule:
1. Keep URIs unversioned by default.
2. Allow URI and payload updates when they improve clarity or implementation simplicity.
Breaking-change rule:
1. Breaking changes use direct replacement of the canonical URI family.
2. No compatibility aliases or dual URI families are maintained.
FastMCP version metadata usage:
1. Resource `version` metadata may be used for implementation and version discovery.
2. URI readability and maintainability remain the primary contract.
## Reference Id Compatibility Policy
`ref_id` is the public identifier for a reference document, separate from file path.
Rules:
1. Prefer keeping `ref_id` stable when practical.
2. File paths may change without URI churn as long as the mapped `ref_id` still resolves.
3. If a reference is renamed, introduce a new `ref_id` and treat the old one as retired.
4. Avoid reusing retired `ref_id` values for unrelated content.
## Invariants
This contract guarantees:
1. One canonical URI pattern per core capability surface.
2. Fast, low-friction URI evolution through direct replacement of canonical URIs.
3. A single canonical catalog URI family with no alias maintenance overhead.
4. Reference mappings can evolve with minimal churn.
## Non-Goals
This contract does not define:
1. Implementation-specific transform wiring details, such as `VersionFilter`, mounts, or provider composition.
2. Migration script mechanics for auto-generating aliases.
3. Authorization policy design for URI-level access control.
+34 -15
View File
@@ -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:
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.
@@ -26,18 +26,17 @@ In this repository, skill guidance is exposed as MCP resources, not as server-ow
### 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`
2. `resource://catalog/skills_details`
3. `resource://catalog/patterns`
4. `resource://catalog/patterns_by_id`
2. `resource://catalog/skills/{skill_id}`
Each skill publishes a canonical Markdown document resource:
1. `resource://skills/<skill-id>/document`
2. `resource://skills/<skill-id>/references/<ref-id>`
The document payload is loaded from `docs/skills/<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
@@ -73,7 +72,7 @@ In this project, "automatic loading" should be read as a preference you express
In practice, there are two reliable ways to make skill content available in chat:
1. explicit resource attachment through `Add Context > MCP Resources` or `MCP: Browse Resources`
2. MCP tool invocation such as `search_patterns` followed by `get_skill_document_by_id`
2. MCP tool invocation using `list_resources`/`read_resource` (ResourcesAsTools), with thin catalog tools as parity fallback
Instruction quality and metadata quality still matter, because they influence whether Copilot recognizes that the MCP server is relevant and chooses the tool path well.
@@ -147,6 +146,21 @@ Weak metadata reduces Copilot match quality and increases wrong context injectio
If you skip the catalog/index step, behavior is less predictable and may either miss relevant skills or pull too much context.
## Optional Tool Search Mode
When tool catalogs grow, FastMCP search transforms can reduce tool-list noise for tool-only clients.
Runtime switches:
1. `PERSONAL_MCP_TOOL_SEARCH=none|regex|bm25` (default `none`)
2. `PERSONAL_MCP_TOOL_SEARCH_MAX_RESULTS=<positive int>` (default `5`)
Behavior:
1. `regex` uses deterministic regex matching for targeted queries.
2. `bm25` uses ranked natural-language matching.
3. `list_resources` and `read_resource` stay visible so resource-backed fallback remains primary.
## Copilot Instruction Pattern
If you want Copilot to use `personal-mcp` skill content more reliably, the instruction file should describe three things clearly:
@@ -171,20 +185,24 @@ When a task may match a documented implementation pattern from `personal-mcp`:
1. Start with catalog-first discovery.
2. Prefer MCP resources when the chat surface exposes resource attachment.
3. If MCP resource attachment is unavailable, use catalog tools instead.
3. If MCP resource attachment is unavailable, use `list_resources`/`read_resource` first, then thin catalog tools if needed.
4. Load only the most relevant skill document, or at most 2 skill documents.
5. Reconcile loaded skill guidance with the actual repository code before making changes.
Preferred resource order:
1. `resource://catalog/skills_index` or `resource://catalog/patterns`
2. `resource://skills/<skill-id>/document`
1. `resource://catalog/skills_index`
2. `resource://catalog/skills/{skill_id}`
3. `resource://skills/<skill-id>/document`
4. `resource://skills/<skill-id>/references/<ref-id>` when needed
Preferred tool fallback order:
1. `search_patterns`
2. `get_pattern_by_id`
3. `get_skill_document_by_id`
1. `list_resources`
2. `read_resource`
3. `search_patterns`
4. `get_pattern_by_id`
5. `get_skill_document_by_id`
If confidence is low after discovery, ask one clarifying question before loading more context.
```
@@ -224,8 +242,9 @@ Suggested instruction policy text:
1. Start with catalog-first discovery.
2. Prefer MCP resources when the chat surface exposes resource attachment.
3. Otherwise use catalog tools to search and load one or two likely skill documents.
4. If confidence is low, ask one clarifying question before loading more.
3. Otherwise use tool fallback to load one or two likely skill documents.
4. Prefer `list_resources`/`read_resource` first when operating in tool-only clients.
5. If confidence is low, ask one clarifying question before loading more.
## Summary
+6
View File
@@ -20,3 +20,9 @@ build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/personal_mcp"]
[dependency-groups]
dev = [
"pre-commit>=4.6.0",
"ruff>=0.15.18",
]
+12 -2
View File
@@ -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",
]
+141 -140
View File
@@ -1,172 +1,173 @@
from pathlib import Path
from __future__ import annotations
from typing import Any
import yaml
from fastmcp import FastMCP
from personal_mcp.skills.document_loader import DocsRegistry, SkillRecord
from personal_mcp.skills.document_loader import load_skill_document_from_metadata
catalog_server = FastMCP("catalog")
DEFAULT_LIMIT = 20
MAX_LIMIT = 100
def _skills_dir() -> Path:
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", [])
def _pattern_payload(skill: SkillRecord) -> dict[str, Any]:
return {
"id": pattern_id,
"namespace": namespace,
"name": metadata.get("name", pattern_id),
"version": metadata.get("version", "0.1.0"),
"description": metadata.get("description", ""),
"tags": metadata.get("tags", []),
"depends_on": metadata.get("depends_on", []),
"capabilities": capabilities,
# Expose resources explicitly for clients that treat resources as the primary interface.
"resources": capabilities,
"id": skill.skill_id,
"name": skill.name,
"version": skill.version,
"description": skill.description,
"tags": list(skill.tags),
"depends_on": list(skill.depends_on),
"capabilities": list(skill.capabilities),
"resources": list(skill.capabilities),
}
def _normalized_patterns() -> list[dict[str, Any]]:
registry = _load_skill_registry()
return [
_normalize_pattern(namespace, entry["metadata"])
for namespace, entry in registry.items()
def _summary_payload(skill: SkillRecord) -> dict[str, Any]:
return {
"id": skill.skill_id,
"name": skill.name,
"description": skill.description,
"tags": list(skill.tags),
"capabilities": list(skill.capabilities),
"version": skill.version,
"document_uri": skill.document_uri,
"detail_uri": f"resource://catalog/skills/{skill.skill_id}",
"resources": {
"document": skill.document_uri,
"references": [
f"resource://skills/{skill.skill_id}/references/{ref_id}"
for ref_id in sorted(skill.references)
],
},
}
def _skill_matches(
skill: SkillRecord,
*,
query: str | None,
tag: str | None,
capability: str | None,
) -> bool:
if query:
lowered = query.strip().lower()
if lowered:
haystack = " ".join(
[
skill.skill_id,
skill.name,
skill.description,
" ".join(skill.tags),
]
).lower()
terms = [term for term in lowered.replace("-", " ").split() if term]
if any(term not in haystack for term in terms):
return False
if tag and tag not in skill.tags:
return False
if capability and capability not in skill.capabilities:
return False
return True
def build_skills_index_payload(
registry: DocsRegistry,
*,
query: str | None = None,
tag: str | None = None,
capability: str | None = None,
cursor: str | None = None,
limit: int | None = None,
) -> dict[str, Any]:
normalized_limit = DEFAULT_LIMIT if limit is None else max(1, min(limit, MAX_LIMIT))
try:
start = 0 if cursor is None else max(0, int(cursor))
except ValueError as exc:
raise ValueError("cursor must be an integer string") from exc
ordered = [
registry.skills_by_id[skill_id] for skill_id in registry.skills_in_load_order
]
matches = [
skill
for skill in ordered
if _skill_matches(skill, query=query, tag=tag, capability=capability)
]
page = matches[start : start + normalized_limit]
next_cursor = start + normalized_limit
def _matches_query(pattern: dict[str, Any], query: str) -> bool:
if not query:
return True
lowered = query.strip().lower()
if not lowered:
return True
query_terms = [term for term in lowered.replace("-", " ").split() if term]
if not query_terms:
return True
haystack = " ".join(
[
str(pattern.get("id", "")),
str(pattern.get("namespace", "")),
str(pattern.get("name", "")),
str(pattern.get("description", "")),
" ".join(str(tag) for tag in pattern.get("tags", [])),
]
).lower()
return all(term in haystack for term in query_terms)
return {
"skills": [_summary_payload(skill) for skill in page],
"total": len(matches),
"cursor": str(start),
"limit": normalized_limit,
"next_cursor": str(next_cursor) if next_cursor < len(matches) else None,
}
def _matches_tags(pattern: dict[str, Any], tags: list[str] | None) -> bool:
if not tags:
return True
def build_skill_detail_payload(registry: DocsRegistry, skill_id: str) -> dict[str, Any]:
if skill_id not in registry.skills_by_id:
raise KeyError(skill_id)
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", [])}
return all(tag in pattern_tags for tag in requested)
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_index")
def skills_index() -> dict[str, Any]:
"""Return a compact discovery index for all available pattern modules."""
return {"patterns": _normalized_patterns()}
@catalog_server.resource("resource://catalog/skills_details")
def skills_details() -> dict[str, Any]:
"""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(
def search_patterns_payload(
registry: DocsRegistry,
*,
query: str = "",
tags: list[str] | None = None,
skip: int = 0,
limit: int = 20,
limit: int = DEFAULT_LIMIT,
) -> dict[str, Any]:
"""Search normalized pattern metadata with optional tags and pagination."""
normalized_skip = max(skip, 0)
normalized_limit = min(max(limit, 1), 100)
normalized_limit = max(1, min(limit, MAX_LIMIT))
matches = [
pattern
for pattern in _normalized_patterns()
if _matches_query(pattern, query) and _matches_tags(pattern, tags)
]
requested_tags = [tag.strip() for tag in (tags or []) if tag and tag.strip()]
matches: list[SkillRecord] = []
for skill_id in registry.skills_in_load_order:
skill = registry.skills_by_id[skill_id]
if not _skill_matches(skill, query=query, tag=None, capability=None):
continue
if requested_tags and any(tag not in skill.tags for tag in requested_tags):
continue
matches.append(skill)
page = matches[normalized_skip : normalized_skip + normalized_limit]
return {
"patterns": page,
"patterns": [_pattern_payload(skill) for skill in page],
"total": len(matches),
"skip": normalized_skip,
"limit": normalized_limit,
}
@catalog_server.tool
def get_pattern_by_id(id: str) -> dict[str, Any]:
"""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}
def get_pattern_by_id_payload(registry: DocsRegistry, skill_id: str) -> dict[str, Any]:
if skill_id not in registry.skills_by_id:
return {"found": False, "id": skill_id}
return {"found": True, "pattern": _pattern_payload(registry.skills_by_id[skill_id])}
+182 -32
View File
@@ -1,38 +1,188 @@
from __future__ import annotations
import os
from typing import Any
from fastmcp import FastMCP
from fastmcp.server.transforms import ResourcesAsTools
from fastmcp.server.transforms.search import BM25SearchTransform, RegexSearchTransform
from personal_mcp.catalog.server import catalog_server
from personal_mcp.skills.copilot_customization.server import (
copilot_customization_server,
from personal_mcp.catalog.server import (
build_skill_detail_payload,
build_skills_index_payload,
get_pattern_by_id_payload,
search_patterns_payload,
)
from personal_mcp.skills.fastapi_async_sqlalchemy_modernization.server import (
fastapi_async_sqlalchemy_modernization_server,
from personal_mcp.skills.document_loader import (
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")
mcp.mount(catalog_server, namespace="catalog")
mcp.mount(copilot_customization_server, namespace="copilot_customization")
mcp.mount(
fastapi_async_sqlalchemy_modernization_server,
namespace="fastapi_async_sqlalchemy_modernization",
DOCS_ROOT = os.getenv("PERSONAL_MCP_DOCS_ROOT", "../../docs")
TOOL_SEARCH_MODE = os.getenv("PERSONAL_MCP_TOOL_SEARCH", "none").strip().lower()
TOOL_SEARCH_MAX_RESULTS = os.getenv("PERSONAL_MCP_TOOL_SEARCH_MAX_RESULTS", "5")
REGISTRY: DocsRegistry = load_docs_registry(
package_anchor="personal_mcp",
docs_root=DOCS_ROOT,
)
mcp.mount(new_skill_server, namespace="new_skill")
mcp.mount(nicegui_server, namespace="nicegui")
mcp.mount(nicegui_ui_customization_server, namespace="nicegui_ui_customization")
mcp.mount(pytest_scaffolding_server, namespace="pytest_scaffolding")
mcp.mount(python_logging_dictconfig_server, namespace="python_logging_dictconfig")
mcp.mount(vscode_configuration_server, namespace="vscode_configuration")
mcp.mount(fastapi_uv_docker_server, namespace="fastapi_uv_docker")
mcp.mount(zensical_docs_server, namespace="zensical_docs")
mcp = FastMCP("personal-mcp", on_duplicate="error")
def _parse_positive_int(value: str, *, env_name: str) -> int:
try:
parsed = int(value)
except ValueError as exc:
raise ValueError(f"{env_name} must be an integer") from exc
if parsed <= 0:
raise ValueError(f"{env_name} must be greater than zero")
return parsed
def _install_tool_fallback_transforms() -> None:
# Expose list_resources/read_resource for tool-only clients.
mcp.add_transform(ResourcesAsTools(mcp))
if TOOL_SEARCH_MODE in {"", "none"}:
return
max_results = _parse_positive_int(
TOOL_SEARCH_MAX_RESULTS,
env_name="PERSONAL_MCP_TOOL_SEARCH_MAX_RESULTS",
)
kwargs: dict[str, Any] = {
"max_results": max_results,
"always_visible": ["list_resources", "read_resource"],
}
if TOOL_SEARCH_MODE == "regex":
mcp.add_transform(RegexSearchTransform(**kwargs))
return
if TOOL_SEARCH_MODE == "bm25":
mcp.add_transform(BM25SearchTransform(**kwargs))
return
raise ValueError(
"PERSONAL_MCP_TOOL_SEARCH must be one of: none, regex, bm25"
)
def _ro_annotations() -> dict[str, bool]:
return {
"readOnlyHint": True,
"idempotentHint": True,
}
@mcp.resource(
"resource://catalog/skills_index",
mime_type="application/json",
tags={"catalog"},
annotations=_ro_annotations(),
)
def skills_index() -> dict[str, Any]:
return build_skills_index_payload(REGISTRY)
@mcp.resource(
"resource://catalog/skills_index{?q,tag,capability,cursor,limit}",
mime_type="application/json",
tags={"catalog"},
annotations=_ro_annotations(),
)
def skills_index_query(
q: str | None = None,
tag: str | None = None,
capability: str | None = None,
cursor: str | None = None,
limit: int | None = None,
) -> dict[str, Any]:
return build_skills_index_payload(
REGISTRY,
query=q,
tag=tag,
capability=capability,
cursor=cursor,
limit=limit,
)
@mcp.resource(
"resource://catalog/skills/{skill_id}",
mime_type="application/json",
tags={"catalog"},
annotations=_ro_annotations(),
)
def skill_detail(skill_id: str) -> dict[str, Any]:
return build_skill_detail_payload(REGISTRY, skill_id)
@mcp.resource(
"resource://skills/{skill_id}/document",
mime_type="text/markdown",
tags={"skill-doc"},
annotations=_ro_annotations(),
)
def skill_document(skill_id: str) -> dict[str, str]:
return read_skill_document(REGISTRY, skill_id)
@mcp.resource(
"resource://skills/{skill_id}/references/{ref_id}",
mime_type="text/markdown",
tags={"reference"},
annotations=_ro_annotations(),
)
def skill_reference(skill_id: str, ref_id: str) -> dict[str, str]:
return read_skill_reference(REGISTRY, skill_id=skill_id, ref_id=ref_id)
@mcp.resource(
"resource://docs/{path*}",
mime_type="text/markdown",
tags={"docs"},
annotations=_ro_annotations(),
)
def docs_markdown(path: str) -> dict[str, str]:
return read_docs_markdown_path(REGISTRY, path)
@mcp.tool
def search_patterns(
query: str = "",
tags: list[str] | None = None,
skip: int = 0,
limit: int = 20,
) -> dict[str, Any]:
"""Search normalized pattern metadata with optional tags and pagination."""
return search_patterns_payload(
REGISTRY,
query=query,
tags=tags,
skip=skip,
limit=limit,
)
@mcp.tool
def get_pattern_by_id(id: str) -> dict[str, Any]:
"""Return one normalized pattern by stable id."""
return get_pattern_by_id_payload(REGISTRY, id)
@mcp.tool
def get_skill_document_by_id(skill_id: str) -> dict[str, Any]:
"""Return the canonical skill document payload for a stable skill id."""
if skill_id not in REGISTRY.skills_by_id:
return {"found": False, "id": skill_id}
return {
"found": True,
"document": read_skill_document(REGISTRY, skill_id),
}
_install_tool_fallback_transforms()
+1 -1
View File
@@ -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",
)
+590 -54
View File
@@ -1,74 +1,610 @@
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
import yaml
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
def _repo_root() -> Path:
return Path(__file__).resolve().parents[3]
SKILL_ID_RE = re.compile(r"^[a-z][a-z0-9-]*$")
SEMVER_RE = re.compile(
r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+][0-9A-Za-z.-]+)?$"
)
def resolve_skill_document_path(
*, skill_id: str, namespace: str, metadata: dict[str, Any]
) -> Path:
"""Resolve the canonical Markdown document path for a skill."""
document_path = metadata.get("document_path")
if isinstance(document_path, str) and document_path.strip():
return _repo_root() / document_path.strip()
@dataclass(frozen=True)
class RegistryIssue:
code: str
message: str
skill_id: str | None
path: str
hint: str
candidates: list[str] = []
slug = metadata.get("slug")
if isinstance(slug, str) and slug.strip():
candidates.append(slug.strip())
candidates.extend(
[
skill_id,
namespace.replace("_", "-"),
namespace,
]
class DocsRegistryValidationError(Exception):
def __init__(self, errors: list[RegistryIssue]) -> None:
self.errors = errors
summary = "\n".join(
[
(
f"{issue.code}: {issue.message} "
f"(skill={issue.skill_id or 'unknown'}, path={issue.path})"
)
for issue in errors
]
)
super().__init__(summary)
class ReferenceEntry(BaseModel):
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
path: str
mime_type: str = "text/markdown"
title: str | None = None
@field_validator("path")
@classmethod
def validate_reference_path(cls, value: str) -> str:
path = PurePosixPath(value)
if path.is_absolute() or ".." in path.parts:
raise ValueError("reference path must be a relative in-skill path")
if not str(path).startswith("references/"):
raise ValueError("reference path must stay under references/")
if path.suffix.lower() != ".md":
raise ValueError("reference path must target a markdown file")
return path.as_posix()
class PersonalMcpMetadata(BaseModel):
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
id: str
version: str
tags: list[str] = Field(default_factory=list)
capabilities: list[str] = Field(min_length=1)
depends_on: list[str] = Field(default_factory=list)
references: dict[str, ReferenceEntry] = Field(default_factory=dict)
@field_validator("id")
@classmethod
def validate_id(cls, value: str) -> str:
if not SKILL_ID_RE.fullmatch(value):
raise ValueError("id must be lowercase kebab-case and start with a letter")
return value
@field_validator("version")
@classmethod
def validate_version(cls, value: str) -> str:
if not SEMVER_RE.fullmatch(value):
raise ValueError("version must be semver")
return value
@field_validator("tags")
@classmethod
def validate_tags(cls, value: list[str]) -> list[str]:
for tag in value:
if not SKILL_ID_RE.fullmatch(tag):
raise ValueError(f"invalid tag: {tag}")
return value
@field_validator("depends_on")
@classmethod
def validate_depends_on(cls, value: list[str]) -> list[str]:
for dep in value:
if not SKILL_ID_RE.fullmatch(dep):
raise ValueError(f"invalid depends_on skill id: {dep}")
return value
@field_validator("references")
@classmethod
def validate_reference_ids(
cls, value: dict[str, ReferenceEntry]
) -> dict[str, ReferenceEntry]:
for ref_id in value:
if not SKILL_ID_RE.fullmatch(ref_id):
raise ValueError(f"invalid reference id: {ref_id}")
return value
class SkillFrontmatter(BaseModel):
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
name: str = Field(min_length=1, max_length=64)
description: str = Field(min_length=1, max_length=1024)
when_to_use: str | None = None
allowed_tools: str | list[str] | None = Field(default=None, alias="allowed-tools")
disallowed_tools: str | list[str] | None = Field(
default=None,
alias="disallowed-tools",
)
disable_model_invocation: bool | None = Field(
default=None,
alias="disable-model-invocation",
)
user_invocable: bool | None = Field(default=None, alias="user-invocable")
argument_hint: str | None = Field(default=None, alias="argument-hint")
arguments: str | list[str] | None = None
license: str | None = None
compatibility: str | None = None
metadata: dict[str, str] | None = None
x_personal_mcp: PersonalMcpMetadata = Field(alias="x-personal-mcp")
seen: set[str] = set()
for candidate in candidates:
if candidate in seen:
@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
seen.add(candidate)
candidate_path = _repo_root() / "docs" / "skills" / candidate / "SKILL.md"
if candidate_path.exists():
return candidate_path
return _repo_root() / "docs" / "skills" / skill_id / "SKILL.md"
if not child.is_file() or not child.name.lower().endswith(".md"):
continue
results.append((relpath.as_posix(), child))
return results
def load_markdown_document(*, skill_id: str, document_path: Path) -> dict[str, str]:
"""Load an arbitrary Markdown document and expose it as a skill resource."""
if not document_path.exists():
raise FileNotFoundError(
f"Missing skill document for '{skill_id}': {document_path}"
def _validate_skill_frontmatter(
raw: dict[str, Any], *, skill_dir_name: str
) -> SkillFrontmatter:
model = SkillFrontmatter.model_validate(raw)
if model.name != skill_dir_name:
raise ValueError("frontmatter name must exactly match skill directory name")
if model.x_personal_mcp.id != model.name:
raise ValueError("x-personal-mcp.id must exactly match name")
expected_capability = f"resource://skills/{model.name}/document"
if expected_capability not in model.x_personal_mcp.capabilities:
raise ValueError(f"capabilities must include {expected_capability}")
return model
def _normalize_docs_path(path: str) -> str:
normalized = PurePosixPath(path)
if normalized.is_absolute() or ".." in normalized.parts:
raise ValueError("path must be a normalized docs-relative path")
if normalized.suffix.lower() != ".md":
raise ValueError("path must point to a markdown file")
return normalized.as_posix()
def _title_from_reference_filename(filename: str) -> str:
stem = PurePosixPath(filename).stem
normalized = stem.replace("-", " ").replace("_", " ").split()
if not normalized:
return stem
return " ".join(token.capitalize() for token in normalized)
def _reference_id_from_filename(filename: str) -> str | None:
stem = PurePosixPath(filename).stem.strip().lower().replace("_", "-")
normalized = re.sub(r"[^a-z0-9-]+", "-", stem)
normalized = re.sub(r"-+", "-", normalized).strip("-")
if not normalized:
return None
if not SKILL_ID_RE.fullmatch(normalized):
return None
return normalized
def _discover_top_level_references(
*,
skill_dir: Traversable,
) -> dict[str, ReferenceEntry]:
references_dir = skill_dir.joinpath("references")
if not references_dir.is_dir():
return {}
discovered: dict[str, ReferenceEntry] = {}
for child in sorted(references_dir.iterdir(), key=lambda item: item.name):
if child.is_dir() or not child.is_file():
continue
if not child.name.lower().endswith(".md"):
continue
ref_id = _reference_id_from_filename(child.name)
if ref_id is None:
continue
discovered[ref_id] = ReferenceEntry(
path=PurePosixPath("references").joinpath(child.name).as_posix(),
title=_title_from_reference_filename(child.name),
)
return discovered
def _ensure_no_cycles(skills_by_id: dict[str, SkillRecord]) -> list[tuple[str, str]]:
visiting: set[str] = set()
visited: set[str] = set()
cycles: list[tuple[str, str]] = []
def walk(skill_id: str, stack: list[str]) -> None:
if skill_id in visited:
return
if skill_id in visiting:
cycle_from = stack[stack.index(skill_id) :]
cycles.append((skill_id, " -> ".join(cycle_from + [skill_id])))
return
visiting.add(skill_id)
stack.append(skill_id)
for dep in skills_by_id[skill_id].depends_on:
if dep in skills_by_id:
walk(dep, stack)
stack.pop()
visiting.remove(skill_id)
visited.add(skill_id)
for skill_id in sorted(skills_by_id):
walk(skill_id, [])
return cycles
def load_docs_registry(
*,
package_anchor: str,
docs_root: str = "docs",
) -> DocsRegistry:
docs_dir = files(package_anchor).joinpath(docs_root)
issues: list[RegistryIssue] = []
if not docs_dir.is_dir():
raise DocsRegistryValidationError(
[
RegistryIssue(
code="missing_docs_root",
message="docs root directory does not exist",
skill_id=None,
path=docs_root,
hint="configure docs_root to a valid packaged docs path",
)
]
)
docs_markdown_by_path: dict[str, str] = {}
for relpath, doc_file in _walk_markdown(docs_dir):
docs_markdown_by_path[relpath] = doc_file.read_text(encoding="utf-8")
skills_root = docs_dir.joinpath("skills")
if not skills_root.is_dir():
raise DocsRegistryValidationError(
[
RegistryIssue(
code="missing_skills_root",
message="skills directory does not exist under docs root",
skill_id=None,
path=f"{docs_root}/skills",
hint="ensure docs/skills is included in packaged docs",
)
]
)
skills_by_id: dict[str, SkillRecord] = {}
summaries: list[SkillSummaryRecord] = []
for skill_dir in sorted(skills_root.iterdir(), key=lambda item: item.name):
if not skill_dir.is_dir():
continue
skill_dir_name = skill_dir.name
skill_rel_root = PurePosixPath("skills").joinpath(skill_dir_name)
skill_doc_relpath = skill_rel_root.joinpath("SKILL.md").as_posix()
skill_doc_file = skill_dir.joinpath("SKILL.md")
if not skill_doc_file.is_file():
issues.append(
RegistryIssue(
code="missing_skill_document",
message="missing required SKILL.md",
skill_id=skill_dir_name,
path=skill_doc_relpath,
hint="add docs/skills/<skill-id>/SKILL.md",
)
)
continue
skill_markdown = skill_doc_file.read_text(encoding="utf-8")
try:
raw_frontmatter, _ = _parse_frontmatter(
skill_markdown,
path=skill_doc_relpath,
)
frontmatter = _validate_skill_frontmatter(
raw_frontmatter,
skill_dir_name=skill_dir_name,
)
except (ValueError, ValidationError) as exc:
issues.append(
RegistryIssue(
code="invalid_frontmatter",
message=str(exc),
skill_id=skill_dir_name,
path=skill_doc_relpath,
hint="fix SKILL.md YAML frontmatter to match the contract",
)
)
continue
effective_reference_entries = _discover_top_level_references(skill_dir=skill_dir)
effective_reference_entries.update(frontmatter.x_personal_mcp.references)
references: dict[str, ReferenceRecord] = {}
for ref_id, ref_entry in effective_reference_entries.items():
ref_relpath = skill_rel_root.joinpath(ref_entry.path).as_posix()
if ref_relpath not in docs_markdown_by_path:
issues.append(
RegistryIssue(
code="missing_reference",
message=f"reference target is missing for ref_id '{ref_id}'",
skill_id=frontmatter.name,
path=ref_relpath,
hint="fix x-personal-mcp.references path or add the referenced markdown file",
)
)
continue
references[ref_id] = ReferenceRecord(
ref_id=ref_id,
uri=f"resource://skills/{frontmatter.name}/references/{ref_id}",
relpath=ref_relpath,
mime_type=ref_entry.mime_type,
title=ref_entry.title,
content=docs_markdown_by_path[ref_relpath],
)
skill_id = frontmatter.name
if skill_id in skills_by_id:
issues.append(
RegistryIssue(
code="duplicate_skill_id",
message="duplicate skill id discovered",
skill_id=skill_id,
path=skill_doc_relpath,
hint="ensure each skill directory has a unique id",
)
)
continue
record = SkillRecord(
skill_id=skill_id,
name=frontmatter.name,
description=frontmatter.description,
version=frontmatter.x_personal_mcp.version,
tags=tuple(frontmatter.x_personal_mcp.tags),
capabilities=tuple(frontmatter.x_personal_mcp.capabilities),
depends_on=tuple(frontmatter.x_personal_mcp.depends_on),
document_uri=f"resource://skills/{skill_id}/document",
document_relpath=skill_doc_relpath,
document_content=skill_markdown,
references=references,
)
skills_by_id[skill_id] = record
summaries.append(
SkillSummaryRecord(
skill_id=record.skill_id,
name=record.name,
description=record.description,
tags=record.tags,
capabilities=record.capabilities,
document_uri=record.document_uri,
version=record.version,
)
)
for skill_id, record in sorted(skills_by_id.items()):
for dependency in record.depends_on:
if dependency == skill_id:
issues.append(
RegistryIssue(
code="self_dependency",
message="skill must not depend on itself",
skill_id=skill_id,
path=record.document_relpath,
hint="remove the skill id from depends_on",
)
)
elif dependency not in skills_by_id:
issues.append(
RegistryIssue(
code="missing_dependency",
message=f"depends_on target '{dependency}' does not exist",
skill_id=skill_id,
path=record.document_relpath,
hint="add the missing skill or remove it from depends_on",
)
)
for cycle_start, cycle in _ensure_no_cycles(skills_by_id):
issues.append(
RegistryIssue(
code="dependency_cycle",
message=f"depends_on cycle detected: {cycle}",
skill_id=cycle_start,
path=skills_by_id[cycle_start].document_relpath,
hint="remove at least one dependency edge in the cycle",
)
)
seen_uris: set[str] = set()
for skill_id, record in sorted(skills_by_id.items()):
uris = [record.document_uri] + [ref.uri for ref in record.references.values()]
for uri in uris:
if uri in seen_uris:
issues.append(
RegistryIssue(
code="duplicate_uri",
message=f"duplicate resource URI generated: {uri}",
skill_id=skill_id,
path=record.document_relpath,
hint="ensure unique skill ids and reference ids",
)
)
seen_uris.add(uri)
if issues:
raise DocsRegistryValidationError(issues)
skill_ids = tuple(sorted(skills_by_id))
summary_by_id = {summary.skill_id: summary for summary in summaries}
ordered_summaries = tuple(summary_by_id[skill_id] for skill_id in skill_ids)
tag_index: dict[str, list[str]] = {}
capability_index: dict[str, list[str]] = {}
for skill_id in skill_ids:
record = skills_by_id[skill_id]
for tag in record.tags:
tag_index.setdefault(tag, []).append(skill_id)
for capability in record.capabilities:
capability_index.setdefault(capability, []).append(skill_id)
return DocsRegistry(
skills_by_id=skills_by_id,
skills_in_load_order=skill_ids,
skills_summary_in_load_order=ordered_summaries,
docs_markdown_by_path=docs_markdown_by_path,
docs_markdown_path_index=tuple(sorted(docs_markdown_by_path)),
tag_to_skill_ids={
key: tuple(sorted(values)) for key, values in sorted(tag_index.items())
},
capability_to_skill_ids={
key: tuple(sorted(values))
for key, values in sorted(capability_index.items())
},
)
def read_skill_document(registry: DocsRegistry, skill_id: str) -> dict[str, str]:
if skill_id not in registry.skills_by_id:
raise KeyError(f"unknown skill_id: {skill_id}")
skill = registry.skills_by_id[skill_id]
return {
"id": skill_id,
"uri": f"resource://skills/{skill_id}/document",
"id": skill.skill_id,
"uri": skill.document_uri,
"format": "markdown",
"source_path": str(document_path),
"content": document_path.read_text(encoding="utf-8"),
"source_path": f"docs/{skill.document_relpath}",
"content": skill.document_content,
}
def load_skill_document(*, skill_id: str, skill_slug: str) -> dict[str, str]:
"""Load the canonical skill markdown document for an MCP skill."""
document_path = _repo_root() / "docs" / "skills" / skill_slug / "SKILL.md"
return load_markdown_document(skill_id=skill_id, document_path=document_path)
def load_skill_document_from_metadata(
*, skill_id: str, namespace: str, metadata: dict[str, Any]
def read_skill_reference(
registry: DocsRegistry,
*,
skill_id: str,
ref_id: str,
) -> dict[str, str]:
"""Load a skill document using metadata overrides when present."""
document_path = resolve_skill_document_path(
skill_id=skill_id,
namespace=namespace,
metadata=metadata,
)
return load_markdown_document(skill_id=skill_id, document_path=document_path)
if skill_id not in registry.skills_by_id:
raise KeyError(f"unknown skill_id: {skill_id}")
skill = registry.skills_by_id[skill_id]
if ref_id not in skill.references:
raise KeyError(f"unknown ref_id '{ref_id}' for skill '{skill_id}'")
reference = skill.references[ref_id]
return {
"id": ref_id,
"skill_id": skill_id,
"uri": reference.uri,
"format": "markdown",
"source_path": f"docs/{reference.relpath}",
"content": reference.content,
}
def read_docs_markdown_path(registry: DocsRegistry, path: str) -> dict[str, str]:
normalized_path = _normalize_docs_path(path)
if normalized_path not in registry.docs_markdown_by_path:
raise KeyError(f"unknown docs path: {normalized_path}")
return {
"uri": f"resource://docs/{normalized_path}",
"format": "markdown",
"source_path": f"docs/{normalized_path}",
"content": registry.docs_markdown_by_path[normalized_path],
}
@@ -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: []
-11
View File
@@ -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",
)
+3 -1
View File
@@ -11,7 +11,9 @@ def mount_docs_static(app: FastAPI, *, docs_route: str, site_dir: Path) -> None:
docs_root = f"{normalized_route}/"
async def redirect_to_docs_root() -> RedirectResponse:
return RedirectResponse(url=docs_root, status_code=status.HTTP_307_TEMPORARY_REDIRECT)
return RedirectResponse(
url=docs_root, status_code=status.HTTP_307_TEMPORARY_REDIRECT
)
app.add_api_route(
normalized_route,
Generated
+126
View File
@@ -178,6 +178,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
]
[[package]]
name = "cfgv"
version = "3.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" },
]
[[package]]
name = "click"
version = "8.4.1"
@@ -273,6 +282,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2d/82/e5d2c1c67d19841e9edc74954c827444ae826978499bde3dfc1d007c8c11/deepmerge-2.0-py3-none-any.whl", hash = "sha256:6de9ce507115cff0bed95ff0ce9ecc31088ef50cbdf09bc90a09349a318b3d00", size = 13475, upload-time = "2024-08-30T05:31:48.659Z" },
]
[[package]]
name = "distlib"
version = "0.4.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c9/02/bd72be9134d25ed783ecbbc38a539ffaefbf90c78418c7fb7229600dbac7/distlib-0.4.3.tar.gz", hash = "sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed", size = 615141, upload-time = "2026-06-12T08:04:52.847Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/02/08/9c41fb51ab5b43eb21674aff13df270e8ba6c4b29c8624e328dc7a9482af/distlib-0.4.3-py2.py3-none-any.whl", hash = "sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b", size = 470628, upload-time = "2026-06-12T08:04:50.506Z" },
]
[[package]]
name = "dnspython"
version = "2.8.0"
@@ -395,6 +413,15 @@ server = [
{ name = "websockets" },
]
[[package]]
name = "filelock"
version = "3.29.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e6/dc/be6cbe99670cd6e4ad387123647cb08e0c32975e223f82551e914c5568a6/filelock-3.29.4.tar.gz", hash = "sha256:10cdb3656fc44541cdf30652a93fb10ec6b05325620eb316bd26893e4201538a", size = 63028, upload-time = "2026-06-13T16:12:00.744Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/37/a065dc3bd6e49423a6532c642ca7378d3f467b1ef44c2800c937af7f9739/filelock-3.29.4-py3-none-any.whl", hash = "sha256:dac1648087d5115554850d113e7dd8c83ab2d38e3435dde2d4f163847e57b767", size = 42757, upload-time = "2026-06-13T16:11:59.582Z" },
]
[[package]]
name = "griffelib"
version = "2.0.2"
@@ -486,6 +513,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" },
]
[[package]]
name = "identify"
version = "2.6.19"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" },
]
[[package]]
name = "idna"
version = "3.18"
@@ -756,6 +792,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192", size = 72226, upload-time = "2026-05-22T14:14:28.824Z" },
]
[[package]]
name = "nodeenv"
version = "1.10.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" },
]
[[package]]
name = "openapi-pydantic"
version = "0.5.1"
@@ -807,6 +852,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" },
]
[[package]]
name = "pre-commit"
version = "4.6.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cfgv" },
{ name = "identify" },
{ name = "nodeenv" },
{ name = "pyyaml" },
{ name = "virtualenv" },
]
sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" },
]
[[package]]
name = "prompts"
version = "0.1.0"
@@ -820,6 +881,12 @@ dependencies = [
{ name = "zensical" },
]
[package.dev-dependencies]
dev = [
{ name = "pre-commit" },
{ name = "ruff" },
]
[package.metadata]
requires-dist = [
{ name = "fastapi", specifier = ">=0.115.0" },
@@ -830,6 +897,12 @@ requires-dist = [
{ name = "zensical", specifier = ">=0.0.45" },
]
[package.metadata.requires-dev]
dev = [
{ name = "pre-commit", specifier = ">=4.6.0" },
{ name = "ruff", specifier = ">=0.15.18" },
]
[[package]]
name = "py-key-value-aio"
version = "0.4.5"
@@ -1018,6 +1091,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/80/fc9d01d5ed37ba4c42ca2b55b4339ae6e200b456be3a1aaddf4a9fa99b8c/pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273", size = 11063, upload-time = "2025-09-26T14:40:36.069Z" },
]
[[package]]
name = "python-discovery"
version = "1.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
{ name = "platformdirs" },
]
sdist = { url = "https://files.pythonhosted.org/packages/0b/1a/cbbaf13b730abb0a16b964d984e19f2fe520c21a4dc664051359a3f5a9e7/python_discovery-1.4.2.tar.gz", hash = "sha256:8f3746c4b4968d22afbb97d36e1a0e5b66e6c0f297290f2e95f05b9b8bf18690", size = 70277, upload-time = "2026-06-11T16:10:42.383Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/82/a70006589557f267f15bd384c0642ad49f0d97b690c3a05b166b9dcbad3b/python_discovery-1.4.2-py3-none-any.whl", hash = "sha256:475803f53b7b2ed6e490e27373f9d8340f7d2eebf9acdaf645d7d714c97bb500", size = 33886, upload-time = "2026-06-11T16:10:41.192Z" },
]
[[package]]
name = "python-dotenv"
version = "1.2.2"
@@ -1260,6 +1346,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/79/cb/966040123eb102371559746908ef2c9471f4d43e17ec9a645a2258dab64b/rpds_py-2026.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:90bd6630002a1c7f09e7843dd79f0d24f3d2897cc25a753480917865d14f15b3", size = 225441, upload-time = "2026-05-28T12:01:51.408Z" },
]
[[package]]
name = "ruff"
version = "0.15.18"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/74/98/1295ad5a5aa9bc85bdcdfa5d82fe7b49c61af5657df4f227637ff9de0da6/ruff-0.15.18.tar.gz", hash = "sha256:2698a964c70e8bf402dcb99c8810472d270d141e7aa8c4e13599fd52033a2f33", size = 4761437, upload-time = "2026-06-18T18:25:39.224Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/d0/686e984941269621e2be72612d5c1e461f8f7b38415a2a7d7a81c8ae6715/ruff-0.15.18-py3-none-linux_armv6l.whl", hash = "sha256:8b6850172348c8381b8b3084c5915a4393c2373b9b54cd5b5e1ea15812bc10df", size = 10887308, upload-time = "2026-06-18T18:25:03.062Z" },
{ url = "https://files.pythonhosted.org/packages/ed/21/bc4123e3f5515ee99f8ce1eb93a14a0628fe4d1678663cd08f933ac16931/ruff-0.15.18-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3fccc153a85417dcd976883160cacce486997b0a0058dd18f54b8aaaac7d1ce2", size = 11281305, upload-time = "2026-06-18T18:25:30.026Z" },
{ url = "https://files.pythonhosted.org/packages/51/93/4769464c25cf7ab2acb3c7dda9cad3d867eb41c59565b3e2a9d17249c90c/ruff-0.15.18-py3-none-macosx_11_0_arm64.whl", hash = "sha256:08d4c86a68f2c3ec2c9d56380a71fb4a4f65373055cbb8caabd645e9102f38d4", size = 10641215, upload-time = "2026-06-18T18:25:15.802Z" },
{ url = "https://files.pythonhosted.org/packages/6c/42/56926d17120db2c208d76bf60a1a019644dd9e91dc27f0f95c9caddb1366/ruff-0.15.18-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37e5108745c2c0705da916d7d4de533ddf547051ef45f62888c31bae73f66318", size = 10957224, upload-time = "2026-06-18T18:25:36.955Z" },
{ url = "https://files.pythonhosted.org/packages/22/4f/d43fab8d8189afde803103022d000a8ef9f230616d436d52a8b2b8d63b50/ruff-0.15.18-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56949a6ce8b3abde54c0bcb22cebfe57e8771cadc84b407ae8b8eaf67ebdcd43", size = 10699024, upload-time = "2026-06-18T18:25:05.707Z" },
{ url = "https://files.pythonhosted.org/packages/63/42/1e3e4c68bd408b9768cf3e439acbe2c78245225faef253f7028a0cdb63e0/ruff-0.15.18-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01a754cd6a1b630d3f97e33eb452cf7a98040482318e870f8bc52a5a30e62657", size = 11491458, upload-time = "2026-06-18T18:25:20.275Z" },
{ url = "https://files.pythonhosted.org/packages/20/77/47a3484bea8521e14a203d98c389c5c97846675e4f02734672da4a69b52a/ruff-0.15.18-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ba7a07e03a44dbf10bb086ee06705b173625014ec99f73a7e6836a5e5590a0c", size = 12383752, upload-time = "2026-06-18T18:25:22.535Z" },
{ url = "https://files.pythonhosted.org/packages/0a/ca/054159590787023d83b658a1a1819c4c8910114e7015069340b71c0961cb/ruff-0.15.18-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a2c40a41a4cadbcf5897b548ab29dfe248b20c540961c0247d98a3973c70403", size = 11577923, upload-time = "2026-06-18T18:25:10.702Z" },
{ url = "https://files.pythonhosted.org/packages/6d/ff/d353d6b7bbd73cc0ec37f4463d7540e45e894338abdd9964eee0de332708/ruff-0.15.18-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f0480ce690cbb6c4db6e5d08f19fce98e10ba131a8b60c1bcdac42771e3ae2d", size = 11583925, upload-time = "2026-06-18T18:25:32.391Z" },
{ url = "https://files.pythonhosted.org/packages/c1/4a/891f89b9c296ed3e5f3ece1a5629badc989d9a8fdaa30431aaf4774bc1c2/ruff-0.15.18-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:2330215f1f393fa8733f55edce04fcf94c36a2c460fcde31f78cc84e4951e9b1", size = 11582834, upload-time = "2026-06-18T18:25:27.309Z" },
{ url = "https://files.pythonhosted.org/packages/32/a3/ed9e370154bf85de360b93c03026157f02d4943b2d01ff4945f4429f8e8a/ruff-0.15.18-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a6aa6a3d979e48ae617578183674bf264fbe7d0114a796a26bd678d67963c7ff", size = 10927328, upload-time = "2026-06-18T18:25:34.676Z" },
{ url = "https://files.pythonhosted.org/packages/f5/d1/5cf5909329fedb5d39d555ee818ba5cf4638e1a301b89785d34f2905bfcb/ruff-0.15.18-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a81beadbbff2c9c245561ae3f77b16709d87f35eec650d0501679239d3449b22", size = 10693187, upload-time = "2026-06-18T18:25:08.245Z" },
{ url = "https://files.pythonhosted.org/packages/fd/44/ff6c635cf2c4f4e7b618b6640da057376baa36014695487d88aed4794268/ruff-0.15.18-py3-none-musllinux_1_2_i686.whl", hash = "sha256:2186d9e940ae332ab293623a75b5f4fe49565f449954d50a72a046683aa6b809", size = 11208721, upload-time = "2026-06-18T18:25:41.327Z" },
{ url = "https://files.pythonhosted.org/packages/88/d9/5baa2a30861adfb7022cf33c1e35b2fc18085b08c16f83eff4c7b99a5f48/ruff-0.15.18-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5c2abf140438032bc77b2284a6c9944ecd8a19e5f1c7b52b1b8e4a0a80d19a7a", size = 11678599, upload-time = "2026-06-18T18:25:13.607Z" },
{ url = "https://files.pythonhosted.org/packages/c3/1a/0725a7cfdc32ff769efb96ee782bec882e16448c5d9e3be947ec4c04ce27/ruff-0.15.18-py3-none-win32.whl", hash = "sha256:02299e6e9fa5b297a3f6d5d10d7bcd655c925b028bb8b9d4588214549c6b9ec4", size = 10901903, upload-time = "2026-06-18T18:25:24.755Z" },
{ url = "https://files.pythonhosted.org/packages/f3/51/805d9f6fb7970505c3504794a5ec350f605361b807fef4dcf214ebd35e72/ruff-0.15.18-py3-none-win_amd64.whl", hash = "sha256:dac80dc8d26b2257dbefabed62f5d255c3937b4ccb122da1fc634794fa3578b3", size = 12041189, upload-time = "2026-06-18T18:25:17.915Z" },
{ url = "https://files.pythonhosted.org/packages/29/4c/67bb45e41609eb4726f1bfeb59e083cf91d14c696d4bd14c234a980be93d/ruff-0.15.18-py3-none-win_arm64.whl", hash = "sha256:b2c9257fcbd4a3e5b977a1904e6facca016bafe2edc17df24db67cfaee03b4e4", size = 11329958, upload-time = "2026-06-18T18:25:43.686Z" },
]
[[package]]
name = "secretstorage"
version = "3.5.0"
@@ -1430,6 +1541,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e4/16/c1fd27e9549f3c4baf1dc9c20c456cd2f822dbf8de9f463824b0c0357e06/uvloop-0.22.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6cde23eeda1a25c75b2e07d39970f3374105d5eafbaab2a4482be82f272d5a5e", size = 4296730, upload-time = "2025-10-16T22:17:00.744Z" },
]
[[package]]
name = "virtualenv"
version = "21.5.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "distlib" },
{ name = "filelock" },
{ name = "platformdirs" },
{ name = "python-discovery" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f1/a5/81f987504738e6defeed61ec1c47e2aefab3c35d8eeb87e1b3f38cf28254/virtualenv-21.5.1.tar.gz", hash = "sha256:dca3bf98275a59c652b69d68e73433e597d977c2da9198882479d1a7188009c8", size = 4578798, upload-time = "2026-06-16T16:23:58.603Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/02/3623e6169bed617ed1e2d372f7c69f92ec28d54c4dfc997055c8578ec148/virtualenv-21.5.1-py3-none-any.whl", hash = "sha256:55aa670b67bbfb991b03fda39bd3276d92c419d702376e98c5df1c9989a26783", size = 4558820, upload-time = "2026-06-16T16:23:56.963Z" },
]
[[package]]
name = "watchfiles"
version = "1.2.0"
+7 -1
View File
@@ -48,6 +48,9 @@ nav = [
{ "Home" = "index.md" },
{ "Guide" = [
{ "Arch" = "architecture.md" },
{ "Content" = "content.md" },
{ "Frontmatter" = "frontmatter.md" },
{ "URIs" = "uris.md" },
{ "MCP" = "mcp_layout.md" },
{ "Copilot" = "copilot.md" },
{ "Usage" = "usage.md" },
@@ -56,6 +59,9 @@ nav = [
{ "Security" = "securing.md" },
] },
{ "Skills" = [
{ "New Skill" = [
{ "Overview" = "skills/new-skill/SKILL.md" },
] },
{ "Copilot" = [
{ "Overview" = "skills/copilot-customization/SKILL.md" },
{ "VS Code" = "skills/copilot-customization/references/vscode-customization.md" },
@@ -88,7 +94,7 @@ nav = [
{ "Arch" = "skills/nicegui/references/architecture.md" },
{ "Sources" = "skills/nicegui/references/source-documentation.md" },
] },
{ "NiceGUI UI" = [
{ "NiceGUI Fine-Tuning" = [
{ "Overview" = "skills/nicegui-ui-customization/SKILL.md" },
{ "Style" = "skills/nicegui-ui-customization/references/architecture-and-styling.md" },
{ "Flows" = "skills/nicegui-ui-customization/references/interaction-patterns.md" },