tightening

This commit is contained in:
John Lancaster
2026-06-21 22:12:02 -05:00
parent 3c7f7e61b7
commit 123c491413
29 changed files with 150 additions and 357 deletions
+2 -3
View File
@@ -90,7 +90,7 @@ 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.
4. Fail fast for duplicate ids, missing markdown files, and broken reference mappings.
Registry load failure is a startup error, not a partial runtime warning.
@@ -138,8 +138,7 @@ Repository indexing metadata is declared in `x-personal-mcp`:
2. version
3. tags
4. capabilities
5. depends_on
6. optional references map (for nested entries, overrides, and aliases)
5. optional references map (for nested entries, overrides, and aliases)
No `metadata.yaml` sidecar is part of the end-state contract.
+36 -171
View File
@@ -6,46 +6,39 @@ icon: lucide/braces
This page defines the `SKILL.md` frontmatter and FastMCP metadata contract.
Prompt modules use the same contract style in `docs/prompts/<prompt-id>/PROMPT.md` with prompt-specific capability and optional typed argument metadata.
Prompt modules use the same contract style in `docs/prompts/<prompt-id>/PROMPT.md` with prompt-specific capability and MCP-aligned prompt argument metadata.
## Anthropic Frontmatter Support
## Validated Frontmatter Surface
Across Anthropic API and Agent Skills surfaces:
The registry runtime validates a strict, standard-only frontmatter surface:
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.
1. Top-level fields accepted for skills: `name`, `description`, `x-personal-mcp`.
2. Top-level fields accepted for prompts: `name`, `description`, `x-personal-mcp`.
3. Unknown top-level fields are rejected during registry load.
Portable optional fields from the Agent Skills specification:
Skill and prompt identifier rules:
1. `license`
2. `compatibility`
3. `metadata`
4. `allowed-tools`
1. `name` is required, 1-64 chars, lowercase kebab-case, and must not contain `anthropic` or `claude`.
2. `description` is required, 1-1024 chars.
3. `x-personal-mcp.id` must exactly match `name`.
4. Directory slug must exactly match `name`.
Claude Code-specific optional fields:
Capability invariants:
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`
1. Skill capabilities must include `resource://skills/<skill-id>/document`.
2. Prompt capabilities must include `resource://prompts/<prompt-id>/document`.
Repository contract decisions:
1. Treat `name` and `description` as required in all `SKILL.md` files.
2. Keep Anthropic-facing semantics in standard fields.
2. Keep only validated standard fields at top level.
3. Keep MCP indexing metadata in a namespaced extension block.
4. Preserve forward compatibility by allowing additive optional metadata fields over time.
4. Reject unsupported optional top-level fields until explicit model support is added.
Reference specs:
1. MCP prompts data types: [Prompts](https://modelcontextprotocol.io/specification/latest/server/prompts)
2. MCP schema reference for `Prompt` and `PromptArgument`: [Schema](https://modelcontextprotocol.io/specification/latest/schema)
## Canonical Frontmatter Schema
@@ -61,14 +54,6 @@ Canonical shape:
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>
@@ -77,7 +62,6 @@ x-personal-mcp:
- <tag>
capabilities:
- resource://skills/<skill-id>/document
depends_on: []
# Optional: overrides and nested references only.
# Top-level references/*.md are auto-discovered.
references:
@@ -96,14 +80,13 @@ Rules for `x-personal-mcp`:
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.
5. `references` is an optional map keyed by `ref-id` for overrides and nested entries.
Prompt-specific additions:
1. `arguments` is an optional map keyed by argument name.
2. Each argument supports `type`, optional `description`, optional `required`, optional `default`, and optional `enum`.
3. `type` must be one of `string`, `number`, `integer`, `boolean`, `array`, or `object`.
2. Each argument supports optional `title`, optional `description`, and optional `required`.
3. This aligns with MCP `PromptArgument` shape (`name`, optional `title`, optional `description`, optional `required`) where `name` is represented by the map key.
4. Prompt `capabilities` must include `resource://prompts/<prompt-id>/document`.
Example prompt frontmatter:
@@ -122,7 +105,7 @@ x-personal-mcp:
- resource://prompts/initial-test-structure/document
arguments:
target_scope:
type: string
title: Target scope
description: Target package or module under test.
required: true
---
@@ -162,150 +145,33 @@ When to use explicit `x-personal-mcp.references` entries:
## Validation Models
The normative model uses Pydantic v2 with change-friendly validation:
The normative runtime model uses strict Pydantic v2 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
```
1. Models are immutable (`frozen=True`) and reject unknown fields (`extra="forbid"`).
2. `SkillFrontmatter` accepts only `name`, `description`, and `x-personal-mcp`.
3. `PromptFrontmatter` accepts only `name`, `description`, and `x-personal-mcp`.
4. `PromptArgumentEntry` accepts only optional `title`, optional `description`, and optional `required`.
5. Skill and prompt metadata enforce semver, kebab-case ids, capability requirements, and id/name/directory consistency.
6. Reference paths are validated as markdown files under `references/`.
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.
2. Reject unknown or unsupported fields at parse and model-validation time.
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.
4. Keep failure messages path-aware and field-specific 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.
3. Preserve `x-personal-mcp` in source-of-truth documents; 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`.
3. Source-of-truth authoring remains in `x-personal-mcp`; upload payload shape is an explicit build concern.
## FastMCP Native Metadata Surfaces
@@ -349,7 +215,6 @@ At server startup, map `x-personal-mcp` into FastMCP registration as follows:
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
@@ -0,0 +1,90 @@
---
name: fill-pytest-scaffold
description: Fill scaffolded pytest test methods with assertions, fixtures, and minimal test data while preserving concise test names and one-line intent docstrings.
x-personal-mcp:
id: fill-pytest-scaffold
version: 1.0.0
tags:
- pytest
- testing
- scaffolding
- prompts
capabilities:
- resource://prompts/fill-pytest-scaffold/document
arguments:
target_files:
description: Target test file paths under tests/.
required: true
stack:
description: Runtime stack type for fixture and marker choices.
required: true
strategy:
description: Balance between minimal and comprehensive implementation.
required: false
marker_lane:
description: Preferred marker lane when applicable.
required: false
---
# Fill Pytest Scaffold
Use this prompt after test scaffolding exists and method names/docstrings are already in place.
## Inputs
- Target test file(s) under tests/.
- Stack type:
- pure-python
- fastapi
- sqlalchemy-sync
- sqlalchemy-async
- mixed
- Optional constraints:
- keep implementation minimal vs comprehensive
- marker lane target (unit, integration, smoke)
## Required References
Load these in order and use only what matches the task:
1. Core defaults: [pytest scaffolding skill](../../skills/pytest-scaffolding/SKILL.md)
2. Naming/hierarchy preservation: [naming and organization](../../skills/pytest-scaffolding/references/naming-and-organization.md)
3. Baseline pytest fixtures/markers: [pytest docs notes](../../skills/pytest-scaffolding/references/pytest-docs.md)
4. FastAPI-specific behavior (only when needed): [fastapi testing](../../skills/pytest-scaffolding/references/fastapi-testing.md)
5. SQLAlchemy-specific behavior (only when needed): [sqlalchemy testing](../../skills/pytest-scaffolding/references/sqlalchemy-testing.md)
## Workflow
1. Inspect target files and treat human-reviewed docstring-only scaffolds as invariant.
2. Convert each scaffolded method into an executable test with a single behavior focus.
3. Keep one-line docstrings for class and method intent.
4. Add or refine fixtures at the nearest useful scope:
- global in tests/conftest.py only when broadly reusable
- subtree conftest.py for domain-specific fixtures
5. Assign markers consistent with cost and dependencies:
- unit for pure logic
- integration for framework/DB contracts
- smoke for thin critical-path checks
6. Validate in this order:
- uv run pytest --collect-only -q
- uv run pytest -m unit -q when unit tests are touched
- uv run pytest -q if dependencies are available
## Authoring Rules
- Prefer deterministic tests and explicit setup/teardown.
- Keep assertions precise and readable.
- Do not overfit tests to private implementation details.
- If a scaffolded class or method has only a docstring body, treat its name and hierarchy as locked.
- Do not rename, move, merge, split, or re-nest docstring-only scaffolded tests unless explicitly requested.
- Preserve existing one-line docstrings on scaffolded classes and methods unless they are factually incorrect.
- If stack details are missing and would change fixture strategy, ask one concise clarifying question before editing.
## Output Format
Return:
1. Files updated.
2. Fixture and marker decisions.
3. Which references were used and why.
4. Validation command results.
5. Risks or open questions.
@@ -1,23 +0,0 @@
---
name: initial-test-structure
description: Create an initial pytest test structure for a target project scope using repository conventions.
x-personal-mcp:
id: initial-test-structure
version: 1.0.0
tags:
- pytest
- testing
capabilities:
- resource://prompts/initial-test-structure/document
arguments:
target_scope:
type: string
description: Target project, package, or module to scaffold tests for.
required: false
---
# Prompt For Creating Initial Test Structure
Create an initial test structure in `./tests` based around pytest best practices.
Use for an overview of best practices `resource://skills/pytest-scaffolding/document`. Take into account the naming structure too and pull in other relevant resources like for fastapi testing
@@ -1,7 +1,6 @@
---
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
@@ -19,9 +18,6 @@ x-personal-mcp:
- skills
capabilities:
- resource://skills/copilot-customization/document
depends_on:
- new-skill
- zensical-docs
---
# Copilot Customization
@@ -1,7 +1,6 @@
---
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
@@ -12,7 +11,6 @@ x-personal-mcp:
- modernization
capabilities:
- resource://skills/fastapi-async-sqlalchemy-modernization/document
depends_on: []
---
# FastAPI Async SQLAlchemy Modernization Plan
-2
View File
@@ -1,7 +1,6 @@
---
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
@@ -11,7 +10,6 @@ x-personal-mcp:
- docker
capabilities:
- resource://skills/fastapi-uv-docker/document
depends_on: []
---
# FastAPI Project Best Practices
-2
View File
@@ -1,7 +1,6 @@
---
name: mcp-details
description: "Reference hub for MCP and FastMCP source documentation links. Use when you need authoritative protocol, SDK, transport, and deployment docs without loading broad implementation guidance."
argument-hint: "What MCP topic do you need links for: protocol, server/client concepts, FastMCP patterns, transports, or tooling?"
x-personal-mcp:
id: mcp-details
version: 1.0.0
@@ -13,7 +12,6 @@ x-personal-mcp:
- source-docs
capabilities:
- resource://skills/mcp-details/document
depends_on: []
---
# MCP Details
+1 -5
View File
@@ -1,7 +1,6 @@
---
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
@@ -13,7 +12,6 @@ x-personal-mcp:
- mcp
capabilities:
- resource://skills/new-skill/document
depends_on: []
references: {}
---
@@ -96,8 +94,7 @@ Required `x-personal-mcp` fields:
Optional `x-personal-mcp` fields:
1. `tags`
2. `depends_on`
3. `references`
2. `references`
Canonical frontmatter template:
@@ -112,7 +109,6 @@ x-personal-mcp:
tags: []
capabilities:
- resource://skills/<skill-id>/document
depends_on: []
# Optional: only for nested references or metadata overrides.
references:
<ref-id>:
@@ -1,7 +1,6 @@
---
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
@@ -12,7 +11,6 @@ x-personal-mcp:
- frontend
capabilities:
- resource://skills/nicegui-ui-customization/document
depends_on: []
---
# NiceGUI UI Customization Workflow
-2
View File
@@ -1,7 +1,6 @@
---
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
@@ -12,7 +11,6 @@ x-personal-mcp:
- architecture
capabilities:
- resource://skills/nicegui/document
depends_on: []
---
# NiceGUI
-2
View File
@@ -1,7 +1,6 @@
---
name: pytest-scaffolding
description: "Reference hub for pytest suite structure, naming, markers, and stack-specific testing patterns. Optimized for progressive discovery so naming and hierarchy guidance are loaded first when shaping or reorganizing tests."
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
@@ -11,7 +10,6 @@ x-personal-mcp:
- python
capabilities:
- resource://skills/pytest-scaffolding/document
depends_on: []
---
# Pytest Scaffolding
@@ -1,7 +1,6 @@
---
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
@@ -11,7 +10,6 @@ x-personal-mcp:
- observability
capabilities:
- resource://skills/python-logging-dictconfig/document
depends_on: []
---
# Idiomatic Python Logging with dictConfig
-2
View File
@@ -1,7 +1,6 @@
---
name: python-typing
description: "Reference-first skill for reviewing and modernizing Python typing to the newest supported best practices. Use when auditing annotations, replacing legacy typing syntax, and enforcing latest-syntax-first conventions."
argument-hint: "Which files or package should be reviewed, and what Python baseline must be preserved?"
x-personal-mcp:
id: python-typing
version: 1.0.0
@@ -14,7 +13,6 @@ x-personal-mcp:
- static-analysis
capabilities:
- resource://skills/python-typing/document
depends_on: []
---
# Modern Python Typing Review Reference
@@ -1,7 +1,6 @@
---
name: ruff-linting-formating
description: "Reference-first Ruff skill for repository preferences, baseline defaults, and source links. Use to pick consistent Ruff conventions and integration references, not to run migration playbooks."
argument-hint: "Which Ruff preferences or integrations are you deciding (rules, formatting, pre-commit, GitHub Actions)?"
x-personal-mcp:
id: ruff-linting-formating
version: 1.0.0
@@ -13,7 +12,6 @@ x-personal-mcp:
- ci
capabilities:
- resource://skills/ruff-linting-formating/document
depends_on: []
---
# Ruff Preferences and References
@@ -1,7 +1,6 @@
---
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
@@ -15,7 +14,6 @@ x-personal-mcp:
- skills
capabilities:
- resource://skills/vscode-configuration/document
depends_on: []
---
# VS Code Configuration
-2
View File
@@ -1,7 +1,6 @@
---
name: zensical-docs
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
@@ -19,7 +18,6 @@ x-personal-mcp:
- authoring
capabilities:
- resource://skills/zensical-docs/document
depends_on: []
---
# Zensical Documentation Authoring
-1
View File
@@ -107,7 +107,6 @@ def build_skill_detail_payload(registry: DocsRegistry, skill_id: str) -> dict[st
"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,
+3 -20
View File
@@ -88,22 +88,6 @@ def _render_prompt_markdown(content: str, arguments: dict[str, Any]) -> str:
return rendered
def _python_type(prompt_arg_type: str) -> type[Any]:
if prompt_arg_type == "string":
return str
if prompt_arg_type == "number":
return float
if prompt_arg_type == "integer":
return int
if prompt_arg_type == "boolean":
return bool
if prompt_arg_type == "array":
return list
if prompt_arg_type == "object":
return dict
return str
def _make_prompt_handler(content: str):
def prompt_handler(**kwargs: Any) -> str:
return _render_prompt_markdown(content, kwargs)
@@ -118,15 +102,14 @@ def _register_prompt_objects() -> None:
params: list[Parameter] = []
for arg_name, arg in sorted(prompt.arguments.items()):
arg_type = _python_type(arg.type)
annotations[arg_name] = arg_type
default = Parameter.empty if arg.required else arg.default
annotations[arg_name] = str
default = Parameter.empty if arg.required else None
params.append(
Parameter(
arg_name,
kind=Parameter.KEYWORD_ONLY,
default=default,
annotation=arg_type,
annotation=str,
)
)
-6
View File
@@ -60,7 +60,6 @@ def _build_skill_record(
version=metadata.version,
tags=tuple(metadata.tags),
capabilities=tuple(metadata.capabilities),
depends_on=tuple(metadata.depends_on),
document_uri=f"resource://skills/{metadata.id}/document",
document_relpath=stored.relpath.as_posix(),
document_content=stored.content,
@@ -160,11 +159,6 @@ def load_docs_registry(*, package_anchor: str, docs_root: str = "docs") -> DocsR
prompts_by_id[record.prompt_id] = record
prompts_in_load_order.append(record.prompt_id)
for skill_id, skill in skills_by_id.items():
for dependency in skill.depends_on:
if dependency not in skills_by_id:
raise ValueError(f"skill '{skill_id}' depends_on unknown skill '{dependency}'")
skills_in_order_tuple = tuple(skills_in_load_order)
prompts_in_order_tuple = tuple(prompts_in_load_order)
+1 -21
View File
@@ -2,8 +2,6 @@ import re
from collections.abc import Mapping
from pathlib import PurePosixPath
from typing import TYPE_CHECKING
from typing import Any
from typing import Literal
import yaml
from pydantic import Field
@@ -18,31 +16,13 @@ from .common import frozen_mapping
if TYPE_CHECKING:
from personal_mcp.registry.ingest.prompt import PromptFilesBundle
type PromptArgumentType = Literal[
"string",
"number",
"integer",
"boolean",
"array",
"object",
]
class PromptArgumentEntry(StrictFrozenModel):
"""Schema for a single prompt argument definition."""
type: PromptArgumentType
title: str | None = None
description: str | None = None
required: bool = False
default: Any | None = None
enum: tuple[str, ...] | None = None
@field_validator("enum")
@classmethod
def validate_enum(cls, value: tuple[str, ...] | None) -> tuple[str, ...] | None:
if value is not None and not value:
raise ValueError("enum must contain at least one value when provided")
return value
class PromptMetadata(StrictFrozenModel):
@@ -28,7 +28,6 @@ class SkillRecord(StrictFrozenModel):
version: str
tags: tuple[str, ...]
capabilities: tuple[str, ...]
depends_on: tuple[str, ...]
document_uri: str
document_relpath: str
document_content: str
@@ -116,7 +115,6 @@ class SkillPatternPayload(StrictFrozenModel):
version: str
description: str
tags: list[str]
depends_on: list[str]
capabilities: list[str]
resources: list[str]
@@ -128,7 +126,6 @@ class SkillPatternPayload(StrictFrozenModel):
version=record.version,
description=record.description,
tags=list(record.tags),
depends_on=list(record.depends_on),
capabilities=list(record.capabilities),
resources=list(record.capabilities),
)
+1 -35
View File
@@ -12,17 +12,14 @@ from .common import ReferenceEntry
from .common import StrictFrozenModel
from .common import frozen_mapping
type ToolSelector = str | tuple[str, ...]
class SkillMetadata(StrictFrozenModel):
"""Canonical metadata describing a skill and its dependencies."""
"""Canonical metadata describing a skill."""
id: str
version: str
tags: tuple[str, ...] = ()
capabilities: tuple[str, ...] = Field(min_length=1)
depends_on: tuple[str, ...] = ()
references: Mapping[str, ReferenceEntry] = Field(default_factory=frozen_mapping)
@field_validator("id")
@@ -47,14 +44,6 @@ class SkillMetadata(StrictFrozenModel):
raise ValueError(f"invalid tag: {tag}")
return value
@field_validator("depends_on")
@classmethod
def validate_depends_on(cls, value: tuple[str, ...]) -> tuple[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", mode="before")
@classmethod
def freeze_references(cls, value: Mapping[str, ReferenceEntry] | None) -> Mapping[str, ReferenceEntry]:
@@ -74,31 +63,8 @@ class SkillFrontmatter(StrictFrozenModel):
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: ToolSelector | None = Field(default=None, alias="allowed-tools")
disallowed_tools: ToolSelector | 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: ToolSelector | None = None
license: str | None = None
compatibility: str | None = None
metadata: Mapping[str, str] | None = None
x_personal_mcp: SkillMetadata = Field(alias="x-personal-mcp")
@field_validator("metadata", mode="before")
@classmethod
def freeze_metadata(cls, value: Mapping[str, str] | None) -> Mapping[str, str] | None:
if value is None:
return None
return frozen_mapping(value)
@field_validator("name")
@classmethod
def validate_name(cls, value: str) -> str:
@@ -37,7 +37,6 @@ def make_skill_frontmatter_payload(
description: str = "demo skill",
tags: tuple[str, ...] = ("testing",),
capabilities: tuple[str, ...] | None = None,
depends_on: tuple[str, ...] = (),
references: dict[str, dict[str, Any]] | None = None,
) -> str:
"""Builds frontmatter payload YAML for skill conversion tests."""
@@ -47,7 +46,6 @@ def make_skill_frontmatter_payload(
"version": version,
"tags": list(tags),
"capabilities": list(capabilities or (f"resource://skills/{canonical_name}/document",)),
"depends_on": list(depends_on),
}
if references is not None:
x_personal_mcp["references"] = references
@@ -60,7 +60,6 @@ class TestPromptValidationGates:
prompt_id="initial",
arguments={
"invalid-name": {
"type": "string",
"required": True,
}
},
@@ -100,7 +99,6 @@ class TestPromptValidationGates:
prompt_id="initial",
arguments={
"topic": {
"type": "string",
"required": True,
"description": "topic to discuss",
}
@@ -111,7 +109,6 @@ class TestPromptValidationGates:
record = _build_prompt_record(bundle=bundle)
assert record.arguments["topic"].required is True
assert record.arguments["topic"].type == "string"
assert record.arguments["topic"].description == "topic to discuss"
class TestGate4GraphValidation:
@@ -183,7 +180,6 @@ class TestPromptValidationGates:
prompt_id="initial",
arguments={
"topic": {
"type": "string",
"required": True,
}
},
@@ -21,7 +21,6 @@ def _make_skill_record() -> SkillRecord:
version="1.2.3",
tags=("testing", "catalog"),
capabilities=("resource://skills/demo-skill/document", "resource://catalog/skills_index"),
depends_on=("base-skill",),
document_uri="resource://skills/demo-skill/document",
document_relpath="skills/demo-skill/SKILL.md",
document_content="# demo",
@@ -56,7 +55,7 @@ def _make_prompt_record() -> PromptRecord:
capabilities=("resource://prompts/demo-prompt/document",),
arguments={
"topic": PromptArgumentEntry(
type="string",
title="Topic",
required=True,
description="topic to discuss",
)
@@ -78,7 +77,6 @@ def test_skill_pattern_payload_from_record_shape() -> None:
"version": "1.2.3",
"description": "demo skill",
"tags": ["testing", "catalog"],
"depends_on": ["base-skill"],
"capabilities": ["resource://skills/demo-skill/document", "resource://catalog/skills_index"],
"resources": ["resource://skills/demo-skill/document", "resource://catalog/skills_index"],
}
@@ -8,9 +8,7 @@ from pydantic import ValidationError
from personal_mcp.registry.ingest.document import MarkdownDocument
from personal_mcp.registry.ingest.skill import SkillFilesBundle
from personal_mcp.registry.load import _build_skill_record
from personal_mcp.registry.load import load_docs_registry
from personal_mcp.registry.models.registry import SkillSummaryRecord
from tests.registry.models.test_document_validation import as_markdown
from tests.registry.models.test_document_validation import assert_model_is_frozen
from tests.registry.models.test_document_validation import make_markdown_document
from tests.registry.models.test_document_validation import make_skill_frontmatter_payload
@@ -117,36 +115,6 @@ class TestSkillValidationGates:
with pytest.raises(KeyError, match="reference document not found"):
_build_skill_record(bundle=bundle, docs_by_relpath=_docs_index(bundle))
class TestGate4GraphValidation:
"""Gate 4: validate dependency graph coherence across skills."""
def test_unknown_dependency_fails_registry_load(self, tmp_path) -> None:
"""Ensures unresolved depends_on targets abort registry publication."""
skill_dir = tmp_path / "skills" / "alpha"
skill_dir.mkdir(parents=True)
frontmatter = make_skill_frontmatter_payload(skill_id="alpha", depends_on=("beta",))
(skill_dir / "SKILL.md").write_text(as_markdown(frontmatter), encoding="utf-8")
with pytest.raises(ValueError, match="depends_on unknown skill"):
load_docs_registry(package_anchor="personal_mcp", docs_root=str(tmp_path))
def test_resolved_dependency_succeeds_registry_load(self, tmp_path) -> None:
"""Ensures valid dependency graph survives graph validation."""
alpha_dir = tmp_path / "skills" / "alpha"
beta_dir = tmp_path / "skills" / "beta"
alpha_dir.mkdir(parents=True)
beta_dir.mkdir(parents=True)
alpha_frontmatter = make_skill_frontmatter_payload(skill_id="alpha", depends_on=("beta",))
beta_frontmatter = make_skill_frontmatter_payload(skill_id="beta")
(alpha_dir / "SKILL.md").write_text(as_markdown(alpha_frontmatter), encoding="utf-8")
(beta_dir / "SKILL.md").write_text(as_markdown(beta_frontmatter), encoding="utf-8")
registry = load_docs_registry(package_anchor="personal_mcp", docs_root=str(tmp_path))
assert registry.skills_by_id["alpha"].depends_on == ("beta",)
class TestGate5ContractValidation:
"""Gate 5: validate model_dump contract shape for API surfaces."""
+6 -2
View File
@@ -28,9 +28,13 @@ class TestMcpPromptSurface:
"""Ensures prompts/get resolves a listed prompt into structured message objects."""
async with mcp_session_factory() as mcp_session:
listed_prompts = await mcp_session.list_prompts()
prompt_name = listed_prompts.prompts[0].name
prompt = listed_prompts.prompts[0]
arguments = {arg.name: "test" for arg in (prompt.arguments or []) if arg.required}
resolved_prompt = await mcp_session.get_prompt(name=prompt_name)
resolved_prompt = await mcp_session.get_prompt(
name=prompt.name,
arguments=arguments or None,
)
assert resolved_prompt.messages
assert all(message.content for message in resolved_prompt.messages)
+9
View File
@@ -61,6 +61,9 @@ nav = [
{ "Testing" = "testing.md" },
{ "Security" = "securing.md" },
] },
{ "Prompts" = [
{ "Fill Pytest Scaffold" = "prompts/fill-pytest-scaffold/PROMPT.md" },
] },
{ "Skills" = [
{ "New Skill" = [
{ "Overview" = "skills/new-skill/SKILL.md" },
@@ -108,6 +111,12 @@ nav = [
{ "Docs" = "skills/pytest-scaffolding/references/pytest-docs.md" },
{ "AsyncIO" = "skills/pytest-scaffolding/references/asyncio-testing.md" },
] },
{ "MCP Details" = [
{ "Overview" = "skills/mcp-details/SKILL.md" },
{ "Protocol" = "skills/mcp-details/references/mcp-protocol-and-spec.md" },
{ "SDKs and FastMCP" = "skills/mcp-details/references/sdk-and-fastmcp.md" },
{ "Ecosystem" = "skills/mcp-details/references/ecosystem-and-tooling.md" },
] },
{ "Logging" = [
{ "Overview" = "skills/python-logging-dictconfig/SKILL.md" },
{ "Docs" = "skills/python-logging-dictconfig/references/python-logging-docs.md" },