313 lines
11 KiB
Markdown
313 lines
11 KiB
Markdown
---
|
|
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: []
|
|
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`.
|
|
|
|
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. Nested folders under `references/` are allowed.
|
|
4. `mime_type` defaults to `text/markdown` when omitted.
|
|
5. `title` is an optional display label.
|
|
6. 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. |