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
+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