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:
- Required fields for custom skill bundles are
nameanddescription. namemust be 1-64 characters, lowercase letters, numbers, and hyphens only, with no XML tags, and must not use the reserved wordsanthropicorclaude.descriptionmust be 1-1024 characters, non-empty, and contain no XML tags.
Portable optional fields from the Agent Skills specification:
licensecompatibilitymetadataallowed-tools
Claude Code-specific optional fields:
when_to_useargument-hintargumentsdisable-model-invocationuser-invocableallowed-toolsdisallowed-toolsmodeleffortcontextagenthookspathsshell
Repository contract decisions:
- Treat
nameanddescriptionas required in allSKILL.mdfiles. - Keep Anthropic-facing semantics in standard fields.
- Keep MCP indexing metadata in a namespaced extension block.
- Preserve forward compatibility by allowing additive optional metadata fields over time.
Canonical Frontmatter Schema
Use this two-layer pattern:
- Anthropic layer: top-level fields intended for Anthropic and Agent Skills behavior.
- 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:
idis required, must follow the skill id rules from the content contract, and must equal the directory name.versionis required and must be a semantic version string.tagsis optional and should be a list of kebab-case discovery labels.capabilitiesis required and lists the MCP URIs the skill publishes.depends_onis optional and lists other skill ids.referencesis an optional map keyed byref-id.
Reference entry rules:
ref-idis lowercase kebab-case.pathis a skill-relative markdown path and must stay inside the same skill directory.- Nested folders under
references/are allowed. mime_typedefaults totext/markdownwhen omitted.titleis an optional display label.- Renaming
ref-idvalues 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:
- Validate required core fields and relationships during registry load before FastMCP resource or tool registration.
- Allow unknown additive fields so frontmatter can evolve without blocking startup.
- Treat hard contract violations, including missing required fields, invalid ids, and broken required mappings, as startup errors.
- Treat non-critical compatibility issues as warnings when possible.
- Error messages should include the skill path and failing field for CI readability.
Projection mode contract for Anthropic API upload pipelines:
- Parse with
SkillFrontmatterfirst. - Emit Anthropic-safe frontmatter with standard fields only.
- Serialize repository metadata into standard
metadataas namespaced keys. - Preserve the canonical authored source in
x-personal-mcp; projection output is a build artifact.
Anthropic Upload Compatibility Rule
- Anthropic documentation guarantees behavior for standard frontmatter fields but does not explicitly guarantee handling of arbitrary unknown top-level keys.
- Publishing pipelines that target strict API compatibility should support a projection mode that emits only standard frontmatter fields for upload.
- In projection mode, repository extension metadata is serialized into the standard
metadatafield as namespaced keys or JSON-encoded values, while source-of-truth authoring remains inx-personal-mcp.
FastMCP Native Metadata Surfaces
Resources support native definition metadata:
namedescriptionmime_typetagsannotations, includingreadOnlyHintandidempotentHinticonsmetaversionenabled, 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:
namedescriptiontagsannotations, includingtitle,readOnlyHint,destructiveHint,idempotentHint, andopenWorldHinticonsmetaversiontimeoutoutput_schemarun_in_threadenabled, 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:
x-personal-mcp.iddefines the canonical URI namespace and identity checks.descriptionbecomes the default description for the primary skill document resource.x-personal-mcp.tagsmaps to resource and tool tags.x-personal-mcp.versionmaps to resource and tool version metadata.x-personal-mcp.capabilitiesbecomes the registered URI list and catalog exposure.x-personal-mcp.references[*]becomes resource templates or concrete resources withmime_type, read-only annotations, andmetathat includesskill_id,ref_id, and sourcepath.x-personal-mcp.depends_onbecomes catalog dependency graph metadata and validation inputs.
Invariants
This contract guarantees:
- Anthropic-required frontmatter stays valid for custom skill upload and Claude Code loading.
- MCP-specific metadata remains embedded in
SKILL.mdfrontmatter, with nometadata.yamlsidecar. - FastMCP registration uses native metadata fields for resources and tools.
- 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:
- URI versioning and deprecation rollout policy details.
- Migration script design from existing
metadata.yamlfiles. - Runtime caching and indexing performance tuning.