Files
prompts/docs/frontmatter.md
T
John Lancaster 8817d2586f icons
2026-06-20 15:01:31 -05:00

11 KiB

icon
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:

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

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.