12 KiB
Step 2 Results: SKILL.md Frontmatter and FastMCP Metadata Contract
This section finalizes Step 2 by defining the canonical SKILL.md frontmatter schema, separating Anthropic-supported fields from repository extension fields, and mapping frontmatter to FastMCP-native metadata surfaces for resources and tools.
Step Deliverable
- Update the current
docs/directory with the finalized Step 2 frontmatter and metadata contract content from this document.
Anthropic Frontmatter Support (Research Baseline)
Across Anthropic API and Agent Skills specification surfaces:
- Required for custom skill bundles:
name,description. nameconstraints (Agent Skills API docs): 1-64 chars, lowercase letters/numbers/hyphens, no XML tags, and must not use reserved wordsanthropicorclaude.descriptionconstraints (Agent Skills API docs): 1-1024 chars, non-empty, no XML tags.
Portable optional fields from the Agent Skills specification:
licensecompatibilitymetadataallowed-tools(experimental)
Claude Code-specific optional fields (supported by Claude Code skills docs):
when_to_use,argument-hint,argumentsdisable-model-invocation,user-invocableallowed-tools,disallowed-toolsmodel,effort,context,agent,hooks,paths,shell
Contract decision for this repository:
- Treat
nameanddescriptionas required in all SKILL.md files, even where a client could infer defaults. - Keep Anthropic-facing semantics in standard fields and keep MCP indexing metadata in a namespaced extension block.
- Preserve forward compatibility by allowing additive optional metadata fields over time.
Canonical Frontmatter Schema For This Repository
Use this exact two-layer pattern:
- Anthropic layer (portable): top-level fields intended for Anthropic/Agent Skills behavior.
- Repository layer (runtime indexing): 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/Agent Skills fields (use only when needed)
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 (authoritative for MCP indexing)
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 (x-personal-mcp)
idrequired: must follow Step 1 skill-id rules and equal directory name.versionrequired: semantic version string.tagsoptional: list of kebab-case discovery labels.capabilitiesrequired: list of MCP URIs this skill publishes.depends_onoptional: list of other skill ids.referencesoptional map:- key is
ref-id(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/markdownif omitted.titleis an optional display label.- renaming
ref-idvalues is allowed when needed; optional aliases may be used during transitions.
- key is
Pydantic Models For Frontmatter Validation
Define the Step 2 contract with Pydantic v2 models and change-friendly validation.
Normative model sketch:
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)
# Anthropic/Agent Skills fields
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
# Repository extension block
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/tool registration.
- Allow unknown additive fields so frontmatter can evolve without blocking startup.
- Treat hard contract violations (missing required fields, invalid ids, broken required mappings) as startup errors.
- Treat non-critical compatibility issues as warnings when possible.
- Error messages should include 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.
- Therefore, 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 (for example as namespaced keys or JSON-encoded values), while source-of-truth authoring remains inx-personal-mcp.
FastMCP Native Metadata Surfaces (Research Baseline)
Resources (@mcp.resource and templates) support native definition metadata:
name,description,mime_type,tagsannotations(readOnlyHint,idempotentHint)iconsmeta(custom metadata passed through to the MCP client resource object)versionenabled(deprecated in v3; prefer server-levelmcp.enable()/mcp.disable())
Resources support runtime metadata:
ResourceContent.meta(item-level)ResourceResult.meta(result-level_meta)
Tools (@mcp.tool) support native definition metadata:
name,description,tagsannotations(title,readOnlyHint,destructiveHint,idempotentHint,openWorldHint)iconsmeta(custom metadata passed through to the MCP client tool object)versiontimeout,output_schema,run_in_threadenabled(deprecated in v3; prefer server-levelmcp.enable()/mcp.disable())
Tools support runtime metadata:
ToolResult.meta(execution-level metadata for each call)
Frontmatter To FastMCP Mapping Contract
At server startup, map x-personal-mcp fields into FastMCP registration as follows:
x-personal-mcp.id-> canonical URI namespace and identity checks.description-> defaultdescriptionfor the primary skill document resource.x-personal-mcp.tags->tagson resources/tools.x-personal-mcp.version->versionon resources/tools.x-personal-mcp.capabilities-> registered URI list plus catalog exposure.x-personal-mcp.references[*]-> resource templates or concrete resources with:mime_typefrom reference entry (or default)metaincludingskill_id,ref_id, and sourcepath- read-only annotations for documentation resources
x-personal-mcp.depends_on-> catalog dependency graph metadata and validation checks.
Invariants This Contract Guarantees
- Anthropic-required frontmatter stays valid for custom skill upload and Claude Code loading.
- MCP-specific metadata remains embedded in SKILL.md frontmatter, with no
metadata.yamlsidecar. - FastMCP registration uses only native metadata fields for resources/tools.
- Reference ids and metadata can evolve with low-friction updates while internal file layout under
references/stays refactor-friendly.
Non-goals For Step 2
- No URI versioning/deprecation rollout policy details (handled in Step 3).
- No migration script design from existing
metadata.yamlfiles. - No runtime caching/indexing performance tuning details.