Files
prompts/.github/prompts/plan-step2.prompt.md
T
2026-06-20 13:45:10 -05:00

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.
  • name constraints (Agent Skills API docs): 1-64 chars, lowercase letters/numbers/hyphens, no XML tags, and must not use reserved words anthropic or claude.
  • description constraints (Agent Skills API docs): 1-1024 chars, non-empty, no XML tags.

Portable optional fields from the Agent Skills specification:

  • license
  • compatibility
  • metadata
  • allowed-tools (experimental)

Claude Code-specific optional fields (supported by Claude Code skills docs):

  • when_to_use, argument-hint, arguments
  • disable-model-invocation, user-invocable
  • allowed-tools, disallowed-tools
  • model, effort, context, agent, hooks, paths, shell

Contract decision for this repository:

  • Treat name and description as 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:

  1. Anthropic layer (portable): top-level fields intended for Anthropic/Agent Skills behavior.
  2. 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)

  • id required: must follow Step 1 skill-id rules and equal directory name.
  • version required: semantic version string.
  • tags optional: list of kebab-case discovery labels.
  • capabilities required: list of MCP URIs this skill publishes.
  • depends_on optional: list of other skill ids.
  • references optional map:
    • key is ref-id (kebab-case).
    • path is a skill-relative markdown path and must stay inside the same skill directory.
    • nested folders under references/ are allowed.
    • mime_type defaults to text/markdown if omitted.
    • title is an optional display label.
    • renaming ref-id values is allowed when needed; optional aliases may be used during transitions.

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 SkillFrontmatter first.
  • Emit Anthropic-safe frontmatter with standard fields only.
  • Serialize repository metadata into standard metadata as 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 metadata field (for example as namespaced keys or JSON-encoded values), while source-of-truth authoring remains in x-personal-mcp.

FastMCP Native Metadata Surfaces (Research Baseline)

Resources (@mcp.resource and templates) support native definition metadata:

  • name, description, mime_type, tags
  • annotations (readOnlyHint, idempotentHint)
  • icons
  • meta (custom metadata passed through to the MCP client resource object)
  • version
  • enabled (deprecated in v3; prefer server-level mcp.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, tags
  • annotations (title, readOnlyHint, destructiveHint, idempotentHint, openWorldHint)
  • icons
  • meta (custom metadata passed through to the MCP client tool object)
  • version
  • timeout, output_schema, run_in_thread
  • enabled (deprecated in v3; prefer server-level mcp.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 -> default description for the primary skill document resource.
  • x-personal-mcp.tags -> tags on resources/tools.
  • x-personal-mcp.version -> version on resources/tools.
  • x-personal-mcp.capabilities -> registered URI list plus catalog exposure.
  • x-personal-mcp.references[*] -> resource templates or concrete resources with:
    • mime_type from reference entry (or default)
    • meta including skill_id, ref_id, and source path
    • 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.yaml sidecar.
  • 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.yaml files.
  • No runtime caching/indexing performance tuning details.