started manual refactor

This commit is contained in:
John Lancaster
2026-06-21 09:13:30 -05:00
parent 197fa32f2c
commit dab539489a
24 changed files with 999 additions and 1235 deletions
+61
View File
@@ -0,0 +1,61 @@
line-length = 120
indent-width = 4
target-version = "py313"
exclude = [
".venv",
".devenv",
".git",
".vscode",
"build",
"site",
"__pycache__",
]
[lint]
preview = true
extend-select = [
"ARG", # https://docs.astral.sh/ruff/rules/#flake8-unused-arguments-arg
"B", # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b
"C4", # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4
"DOC102", # https://docs.astral.sh/ruff/rules/docstring-extraneous-parameter/
"DOC202", # https://docs.astral.sh/ruff/rules/docstring-extraneous-returns/
"DOC403", # https://docs.astral.sh/ruff/rules/docstring-extraneous-yields/
"DOC502", # https://docs.astral.sh/ruff/rules/docstring-extraneous-exception/
"E", "W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w
"F", # https://docs.astral.sh/ruff/rules/#pyflakes-f
"FURB", # https://docs.astral.sh/ruff/rules/#refurb-furb
"I", # https://docs.astral.sh/ruff/rules/#isort-i
"N", # https://docs.astral.sh/ruff/rules/#pep8-naming-n
"PD", # https://docs.astral.sh/ruff/rules/#pandas-vet-pd
"PTH", # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth
"UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up
"SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim
"PLR0202", # https://docs.astral.sh/ruff/rules/no-classmethod-decorator/
"PLR0203", # https://docs.astral.sh/ruff/rules/no-staticmethod-decorator/
"PLR0206", # https://docs.astral.sh/ruff/rules/property-with-parameters/
"PLR0915", # https://docs.astral.sh/ruff/rules/too-many-statements/
"PLR1702", # https://docs.astral.sh/ruff/rules/too-many-nested-blocks/
"TRY002",
]
extend-fixable = ["ALL"]
ignore = [
"UP046",
"UP047",
]
[lint.extend-per-file-ignores]
"*.ipynb" = [
"F401", # unused imports
"F841", # unused local variable
]
[lint.isort]
force-single-line = true
[format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"
+11 -13
View File
@@ -1,21 +1,19 @@
from personal_mcp.catalog.server import ( from personal_mcp.catalog.server import build_prompt_detail_payload
build_prompt_detail_payload, from personal_mcp.catalog.server import build_prompts_index_payload
build_prompts_index_payload, from personal_mcp.catalog.server import build_skill_detail_payload
build_skill_detail_payload, from personal_mcp.catalog.server import build_skills_index_payload
build_skills_index_payload, from personal_mcp.catalog.server import get_pattern_by_id_payload
get_pattern_by_id_payload, from personal_mcp.catalog.server import get_prompt_by_id_payload
get_prompt_by_id_payload, from personal_mcp.catalog.server import search_patterns_payload
search_patterns_payload, from personal_mcp.catalog.server import search_prompts_payload
search_prompts_payload,
)
__all__ = [ __all__ = [
"build_skill_detail_payload",
"build_prompt_detail_payload", "build_prompt_detail_payload",
"build_prompts_index_payload", "build_prompts_index_payload",
"build_skill_detail_payload",
"build_skills_index_payload", "build_skills_index_payload",
"get_prompt_by_id_payload",
"get_pattern_by_id_payload", "get_pattern_by_id_payload",
"search_prompts_payload", "get_prompt_by_id_payload",
"search_patterns_payload", "search_patterns_payload",
"search_prompts_payload",
] ]
+5 -9
View File
@@ -2,7 +2,9 @@ from __future__ import annotations
from typing import Any from typing import Any
from personal_mcp.skills.document_loader import DocsRegistry, PromptRecord, SkillRecord from personal_mcp.skills.document_loader import DocsRegistry
from personal_mcp.skills.document_loader import PromptRecord
from personal_mcp.skills.document_loader import SkillRecord
DEFAULT_LIMIT = 20 DEFAULT_LIMIT = 20
MAX_LIMIT = 100 MAX_LIMIT = 100
@@ -79,10 +81,7 @@ def _skill_matches(
if tag and tag not in skill.tags: if tag and tag not in skill.tags:
return False return False
if capability and capability not in skill.capabilities: return not (capability and capability not in skill.capabilities)
return False
return True
def _prompt_matches( def _prompt_matches(
@@ -107,10 +106,7 @@ def _prompt_matches(
if any(term not in haystack for term in terms): if any(term not in haystack for term in terms):
return False return False
if tag and tag not in prompt.tags: return not (tag and tag not in prompt.tags)
return False
return True
def build_skills_index_payload( def build_skills_index_payload(
+19 -24
View File
@@ -2,31 +2,29 @@ from __future__ import annotations
import os import os
import re import re
from inspect import Parameter, Signature from inspect import Parameter
from inspect import Signature
from typing import Any from typing import Any
from fastmcp import FastMCP from fastmcp import FastMCP
from fastmcp.server.transforms import ResourcesAsTools from fastmcp.server.transforms import ResourcesAsTools
from fastmcp.server.transforms.search import BM25SearchTransform, RegexSearchTransform from fastmcp.server.transforms.search import BM25SearchTransform
from fastmcp.server.transforms.search import RegexSearchTransform
from personal_mcp.catalog.server import ( from personal_mcp.catalog.server import build_prompt_detail_payload
build_prompt_detail_payload, from personal_mcp.catalog.server import build_prompts_index_payload
build_prompts_index_payload, from personal_mcp.catalog.server import build_skill_detail_payload
build_skill_detail_payload, from personal_mcp.catalog.server import build_skills_index_payload
build_skills_index_payload, from personal_mcp.catalog.server import get_pattern_by_id_payload
get_pattern_by_id_payload, from personal_mcp.catalog.server import get_prompt_by_id_payload
get_prompt_by_id_payload, from personal_mcp.catalog.server import search_patterns_payload
search_patterns_payload, from personal_mcp.catalog.server import search_prompts_payload
search_prompts_payload, from personal_mcp.skills.document_loader import DocsRegistry
) from personal_mcp.skills.document_loader import load_docs_registry
from personal_mcp.skills.document_loader import ( from personal_mcp.skills.document_loader import read_docs_markdown_path
DocsRegistry, from personal_mcp.skills.document_loader import read_prompt_document
load_docs_registry, from personal_mcp.skills.document_loader import read_skill_document
read_docs_markdown_path, from personal_mcp.skills.document_loader import read_skill_reference
read_prompt_document,
read_skill_document,
read_skill_reference,
)
DOCS_ROOT = os.getenv("PERSONAL_MCP_DOCS_ROOT", "../../docs") DOCS_ROOT = os.getenv("PERSONAL_MCP_DOCS_ROOT", "../../docs")
TOOL_SEARCH_MODE = os.getenv("PERSONAL_MCP_TOOL_SEARCH", "none").strip().lower() TOOL_SEARCH_MODE = os.getenv("PERSONAL_MCP_TOOL_SEARCH", "none").strip().lower()
@@ -116,10 +114,7 @@ def _register_prompt_objects() -> None:
for arg_name, arg in sorted(prompt.arguments.items()): for arg_name, arg in sorted(prompt.arguments.items()):
arg_type = _python_type(arg.type) arg_type = _python_type(arg.type)
annotations[arg_name] = arg_type annotations[arg_name] = arg_type
if arg.required: default = Parameter.empty if arg.required else arg.default
default = Parameter.empty
else:
default = arg.default
params.append( params.append(
Parameter( Parameter(
arg_name, arg_name,
+80
View File
@@ -0,0 +1,80 @@
from dataclasses import dataclass
from dataclasses import field
from .models.prompt import PromptArgumentEntry
@dataclass(frozen=True)
class ReferenceRecord:
ref_id: str
uri: str
relpath: str
mime_type: str
title: str | None
content: str
@dataclass(frozen=True)
class SkillRecord:
skill_id: str
name: str
description: str
version: str
tags: tuple[str, ...]
capabilities: tuple[str, ...]
depends_on: tuple[str, ...]
document_uri: str
document_relpath: str
document_content: str
references: dict[str, ReferenceRecord]
@dataclass(frozen=True)
class SkillSummaryRecord:
skill_id: str
name: str
description: str
tags: tuple[str, ...]
capabilities: tuple[str, ...]
document_uri: str
version: str
@dataclass(frozen=True)
class PromptRecord:
prompt_id: str
name: str
description: str
version: str
tags: tuple[str, ...]
capabilities: tuple[str, ...]
arguments: dict[str, PromptArgumentEntry]
document_uri: str
document_relpath: str
document_content: str
@dataclass(frozen=True)
class PromptSummaryRecord:
prompt_id: str
name: str
description: str
tags: tuple[str, ...]
capabilities: tuple[str, ...]
document_uri: str
version: str
@dataclass(frozen=True)
class DocsRegistry:
skills_by_id: dict[str, SkillRecord]
skills_in_load_order: tuple[str, ...]
skills_summary_in_load_order: tuple[SkillSummaryRecord, ...]
docs_markdown_by_path: dict[str, str]
docs_markdown_path_index: tuple[str, ...]
tag_to_skill_ids: dict[str, tuple[str, ...]]
capability_to_skill_ids: dict[str, tuple[str, ...]]
prompts_by_id: dict[str, PromptRecord] = field(default_factory=dict)
prompts_in_load_order: tuple[str, ...] = ()
prompts_summary_in_load_order: tuple[PromptSummaryRecord, ...] = ()
tag_to_prompt_ids: dict[str, tuple[str, ...]] = field(default_factory=dict)
+51
View File
@@ -0,0 +1,51 @@
from collections.abc import Generator
from importlib.abc import Traversable
from pathlib import PurePosixPath
import yaml
from .models.skill import SkillFrontmatter
def walk_resources(
node: Traversable,
*,
suffix: str = ".md",
prefix: PurePosixPath | None = None,
) -> Generator[tuple[str, Traversable]]:
"""Recursively yield all resources in node, with their full path."""
prefix = prefix if prefix is not None else PurePosixPath()
for child in sorted(node.iterdir(), key=lambda item: item.name):
relpath = prefix.joinpath(child.name)
if child.is_dir():
yield from walk_resources(child, suffix=suffix, prefix=relpath)
continue
if not child.is_file() or not child.name.lower().endswith(suffix):
continue
yield relpath.as_posix(), child
def get_markdown_content(resource: Traversable) -> dict[str, str]:
"""Read the content of a markdown resource as text."""
return {relpath: doc_file.read_text(encoding="utf-8") for relpath, doc_file in walk_resources(resource)}
def get_idx(raw):
for i, line in enumerate(raw.splitlines()):
if line.strip().startswith("---"):
yield i
def get_raw_frontmatter(raw: str) -> str:
delimiter = iter(get_idx(raw))
start = next(delimiter) + 1
end = next(delimiter)
return "\n".join(raw.splitlines()[start:end])
def gen_valid_frontmatter(content: dict[str, str]) -> Generator[SkillFrontmatter]:
for relpath, raw in content.items():
if relpath.endswith("SKILL.md"):
fm = get_raw_frontmatter(raw)
validated = SkillFrontmatter.model_validate(yaml.safe_load(fm))
yield validated
+22
View File
@@ -0,0 +1,22 @@
from dataclasses import dataclass
@dataclass(frozen=True)
class RegistryIssue:
code: str
message: str
skill_id: str | None
path: str
hint: str
class DocsRegistryValidationError(Exception):
def __init__(self, errors: list[RegistryIssue]) -> None:
self.errors = errors
summary = "\n".join(
[
(f"{issue.code}: {issue.message} (skill={issue.skill_id or 'unknown'}, path={issue.path})")
for issue in errors
]
)
super().__init__(summary)
+416
View File
@@ -0,0 +1,416 @@
from importlib.resources import files
from pathlib import PurePosixPath
from .contracts import DocsRegistry
from .contracts import PromptRecord
from .contracts import PromptSummaryRecord
from .contracts import ReferenceRecord
from .contracts import SkillRecord
from .contracts import SkillSummaryRecord
from .issues import DocsRegistryValidationError
from .issues import RegistryIssue
def load_docs_registry(
*,
package_anchor: str,
docs_root: str = "docs",
) -> DocsRegistry:
docs_dir = files(package_anchor).joinpath(docs_root)
issues: list[RegistryIssue] = []
if not docs_dir.is_dir():
raise DocsRegistryValidationError(
[
RegistryIssue(
code="missing_docs_root",
message="docs root directory does not exist",
skill_id=None,
path=docs_root,
hint="configure docs_root to a valid packaged docs path",
)
]
)
docs_markdown_by_path: dict[str, str] = {}
for relpath, doc_file in _walk_markdown(docs_dir):
docs_markdown_by_path[relpath] = doc_file.read_text(encoding="utf-8")
skills_root = docs_dir.joinpath("skills")
if not skills_root.is_dir():
raise DocsRegistryValidationError(
[
RegistryIssue(
code="missing_skills_root",
message="skills directory does not exist under docs root",
skill_id=None,
path=f"{docs_root}/skills",
hint="ensure docs/skills is included in packaged docs",
)
]
)
skills_by_id: dict[str, SkillRecord] = {}
summaries: list[SkillSummaryRecord] = []
prompts_by_id: dict[str, PromptRecord] = {}
prompt_summaries: list[PromptSummaryRecord] = []
for skill_dir in sorted(skills_root.iterdir(), key=lambda item: item.name):
if not skill_dir.is_dir():
continue
skill_dir_name = skill_dir.name
skill_rel_root = PurePosixPath("skills").joinpath(skill_dir_name)
skill_doc_relpath = skill_rel_root.joinpath("SKILL.md").as_posix()
skill_doc_file = skill_dir.joinpath("SKILL.md")
if not skill_doc_file.is_file():
issues.append(
RegistryIssue(
code="missing_skill_document",
message="missing required SKILL.md",
skill_id=skill_dir_name,
path=skill_doc_relpath,
hint="add docs/skills/<skill-id>/SKILL.md",
)
)
continue
skill_markdown = skill_doc_file.read_text(encoding="utf-8")
try:
raw_frontmatter, _ = _parse_frontmatter(
skill_markdown,
path=skill_doc_relpath,
)
frontmatter = _validate_skill_frontmatter(
raw_frontmatter,
skill_dir_name=skill_dir_name,
)
except (ValueError, ValidationError) as exc:
issues.append(
RegistryIssue(
code="invalid_frontmatter",
message=str(exc),
skill_id=skill_dir_name,
path=skill_doc_relpath,
hint="fix SKILL.md YAML frontmatter to match the contract",
)
)
continue
effective_reference_entries = _discover_top_level_references(skill_dir=skill_dir)
effective_reference_entries.update(frontmatter.x_personal_mcp.references)
references: dict[str, ReferenceRecord] = {}
for ref_id, ref_entry in effective_reference_entries.items():
ref_relpath = skill_rel_root.joinpath(ref_entry.path).as_posix()
if ref_relpath not in docs_markdown_by_path:
issues.append(
RegistryIssue(
code="missing_reference",
message=f"reference target is missing for ref_id '{ref_id}'",
skill_id=frontmatter.name,
path=ref_relpath,
hint="fix x-personal-mcp.references path or add the referenced markdown file",
)
)
continue
references[ref_id] = ReferenceRecord(
ref_id=ref_id,
uri=f"resource://skills/{frontmatter.name}/references/{ref_id}",
relpath=ref_relpath,
mime_type=ref_entry.mime_type,
title=ref_entry.title,
content=docs_markdown_by_path[ref_relpath],
)
skill_id = frontmatter.name
if skill_id in skills_by_id:
issues.append(
RegistryIssue(
code="duplicate_skill_id",
message="duplicate skill id discovered",
skill_id=skill_id,
path=skill_doc_relpath,
hint="ensure each skill directory has a unique id",
)
)
continue
record = SkillRecord(
skill_id=skill_id,
name=frontmatter.name,
description=frontmatter.description,
version=frontmatter.x_personal_mcp.version,
tags=tuple(frontmatter.x_personal_mcp.tags),
capabilities=tuple(frontmatter.x_personal_mcp.capabilities),
depends_on=tuple(frontmatter.x_personal_mcp.depends_on),
document_uri=f"resource://skills/{skill_id}/document",
document_relpath=skill_doc_relpath,
document_content=skill_markdown,
references=references,
)
skills_by_id[skill_id] = record
summaries.append(
SkillSummaryRecord(
skill_id=record.skill_id,
name=record.name,
description=record.description,
tags=record.tags,
capabilities=record.capabilities,
document_uri=record.document_uri,
version=record.version,
)
)
prompts_root = docs_dir.joinpath("prompts")
discovered_prompt_docs: set[str] = set()
if prompts_root.is_dir():
for prompt_dir in sorted(prompts_root.iterdir(), key=lambda item: item.name):
if not prompt_dir.is_dir():
continue
prompt_dir_name = prompt_dir.name
prompt_rel_root = PurePosixPath("prompts").joinpath(prompt_dir_name)
prompt_doc_relpath = prompt_rel_root.joinpath("PROMPT.md").as_posix()
prompt_doc_file = prompt_dir.joinpath("PROMPT.md")
if not prompt_doc_file.is_file():
continue
discovered_prompt_docs.add(prompt_doc_relpath)
prompt_markdown = prompt_doc_file.read_text(encoding="utf-8")
try:
raw_frontmatter, _ = _parse_frontmatter(
prompt_markdown,
path=prompt_doc_relpath,
)
frontmatter = _validate_prompt_frontmatter(
raw_frontmatter,
prompt_dir_name=prompt_dir_name,
)
except (ValueError, ValidationError) as exc:
issues.append(
RegistryIssue(
code="invalid_prompt_frontmatter",
message=str(exc),
skill_id=prompt_dir_name,
path=prompt_doc_relpath,
hint="fix PROMPT.md YAML frontmatter to match the contract",
)
)
continue
prompt_id = frontmatter.name
if prompt_id in prompts_by_id:
issues.append(
RegistryIssue(
code="duplicate_prompt_id",
message="duplicate prompt id discovered",
skill_id=prompt_id,
path=prompt_doc_relpath,
hint="ensure each prompt directory has a unique id",
)
)
continue
if prompt_id in skills_by_id:
issues.append(
RegistryIssue(
code="prompt_skill_id_collision",
message="prompt id collides with an existing skill id",
skill_id=prompt_id,
path=prompt_doc_relpath,
hint="use a unique prompt id that does not match a skill id",
)
)
continue
prompt_record = PromptRecord(
prompt_id=prompt_id,
name=frontmatter.name,
description=frontmatter.description,
version=frontmatter.x_personal_mcp.version,
tags=tuple(frontmatter.x_personal_mcp.tags),
capabilities=tuple(frontmatter.x_personal_mcp.capabilities),
arguments=frontmatter.x_personal_mcp.arguments,
document_uri=f"resource://prompts/{prompt_id}/document",
document_relpath=prompt_doc_relpath,
document_content=prompt_markdown,
)
prompts_by_id[prompt_id] = prompt_record
prompt_summaries.append(
PromptSummaryRecord(
prompt_id=prompt_record.prompt_id,
name=prompt_record.name,
description=prompt_record.description,
tags=prompt_record.tags,
capabilities=prompt_record.capabilities,
document_uri=prompt_record.document_uri,
version=prompt_record.version,
)
)
for relpath, prompt_file in _walk_markdown(
prompts_root,
prefix=PurePosixPath("prompts"),
):
if relpath in discovered_prompt_docs or relpath.endswith("/PROMPT.md"):
continue
prompt_markdown = prompt_file.read_text(encoding="utf-8")
prompt_id = _reference_id_from_filename(PurePosixPath(relpath).name)
if prompt_id is None:
continue
if prompt_id in prompts_by_id:
issues.append(
RegistryIssue(
code="duplicate_prompt_id",
message="duplicate prompt id discovered",
skill_id=prompt_id,
path=relpath,
hint="ensure each prompt id is unique",
)
)
continue
if prompt_id in skills_by_id:
issues.append(
RegistryIssue(
code="prompt_skill_id_collision",
message="prompt id collides with an existing skill id",
skill_id=prompt_id,
path=relpath,
hint="use a unique prompt id that does not match a skill id",
)
)
continue
prompt_record = PromptRecord(
prompt_id=prompt_id,
name=prompt_id,
description=f"Legacy prompt loaded from docs/{relpath}",
version="1.0.0",
tags=("prompt", "legacy"),
capabilities=(f"resource://prompts/{prompt_id}/document",),
arguments={},
document_uri=f"resource://prompts/{prompt_id}/document",
document_relpath=relpath,
document_content=prompt_markdown,
)
prompts_by_id[prompt_id] = prompt_record
prompt_summaries.append(
PromptSummaryRecord(
prompt_id=prompt_record.prompt_id,
name=prompt_record.name,
description=prompt_record.description,
tags=prompt_record.tags,
capabilities=prompt_record.capabilities,
document_uri=prompt_record.document_uri,
version=prompt_record.version,
)
)
for skill_id, record in sorted(skills_by_id.items()):
for dependency in record.depends_on:
if dependency == skill_id:
issues.append(
RegistryIssue(
code="self_dependency",
message="skill must not depend on itself",
skill_id=skill_id,
path=record.document_relpath,
hint="remove the skill id from depends_on",
)
)
elif dependency not in skills_by_id:
issues.append(
RegistryIssue(
code="missing_dependency",
message=f"depends_on target '{dependency}' does not exist",
skill_id=skill_id,
path=record.document_relpath,
hint="add the missing skill or remove it from depends_on",
)
)
for cycle_start, cycle in _ensure_no_cycles(skills_by_id):
issues.append(
RegistryIssue(
code="dependency_cycle",
message=f"depends_on cycle detected: {cycle}",
skill_id=cycle_start,
path=skills_by_id[cycle_start].document_relpath,
hint="remove at least one dependency edge in the cycle",
)
)
seen_uris: set[str] = set()
for skill_id, record in sorted(skills_by_id.items()):
uris = [record.document_uri] + [ref.uri for ref in record.references.values()]
for uri in uris:
if uri in seen_uris:
issues.append(
RegistryIssue(
code="duplicate_uri",
message=f"duplicate resource URI generated: {uri}",
skill_id=skill_id,
path=record.document_relpath,
hint="ensure unique skill ids and reference ids",
)
)
seen_uris.add(uri)
for prompt_id, record in sorted(prompts_by_id.items()):
for uri in [record.document_uri]:
if uri in seen_uris:
issues.append(
RegistryIssue(
code="duplicate_uri",
message=f"duplicate resource URI generated: {uri}",
skill_id=prompt_id,
path=record.document_relpath,
hint="ensure unique prompt ids",
)
)
seen_uris.add(uri)
if issues:
raise DocsRegistryValidationError(issues)
skill_ids = tuple(sorted(skills_by_id))
summary_by_id = {summary.skill_id: summary for summary in summaries}
ordered_summaries = tuple(summary_by_id[skill_id] for skill_id in skill_ids)
prompt_ids = tuple(sorted(prompts_by_id))
prompt_summary_by_id = {summary.prompt_id: summary for summary in prompt_summaries}
ordered_prompt_summaries = tuple(prompt_summary_by_id[prompt_id] for prompt_id in prompt_ids)
tag_index: dict[str, list[str]] = {}
capability_index: dict[str, list[str]] = {}
prompt_tag_index: dict[str, list[str]] = {}
for skill_id in skill_ids:
record = skills_by_id[skill_id]
for tag in record.tags:
tag_index.setdefault(tag, []).append(skill_id)
for capability in record.capabilities:
capability_index.setdefault(capability, []).append(skill_id)
for prompt_id in prompt_ids:
record = prompts_by_id[prompt_id]
for tag in record.tags:
prompt_tag_index.setdefault(tag, []).append(prompt_id)
return DocsRegistry(
skills_by_id=skills_by_id,
skills_in_load_order=skill_ids,
skills_summary_in_load_order=ordered_summaries,
docs_markdown_by_path=docs_markdown_by_path,
docs_markdown_path_index=tuple(sorted(docs_markdown_by_path)),
tag_to_skill_ids={key: tuple(sorted(values)) for key, values in sorted(tag_index.items())},
capability_to_skill_ids={key: tuple(sorted(values)) for key, values in sorted(capability_index.items())},
prompts_by_id=prompts_by_id,
prompts_in_load_order=prompt_ids,
prompts_summary_in_load_order=ordered_prompt_summaries,
tag_to_prompt_ids={key: tuple(sorted(values)) for key, values in sorted(prompt_tag_index.items())},
)
@@ -0,0 +1,29 @@
import re
from pathlib import PurePosixPath
from pydantic import BaseModel
from pydantic import ConfigDict
from pydantic import field_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:
path = PurePosixPath(value)
if path.is_absolute() or ".." in path.parts:
raise ValueError("reference path must be a relative in-skill path")
if not str(path).startswith("references/"):
raise ValueError("reference path must stay under references/")
if path.suffix.lower() != ".md":
raise ValueError("reference path must target a markdown file")
return path.as_posix()
@@ -0,0 +1,99 @@
import re
from typing import Any
from pydantic import BaseModel
from pydantic import ConfigDict
from pydantic import Field
from pydantic import field_validator
from .common import SEMVER_RE
from .common import SKILL_ID_RE
class PromptArgumentEntry(BaseModel):
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
type: str = Field(min_length=1)
description: str | None = None
required: bool = False
default: Any | None = None
enum: list[str] | None = None
@field_validator("type")
@classmethod
def validate_type(cls, value: str) -> str:
allowed_types = {
"string",
"number",
"integer",
"boolean",
"array",
"object",
}
if value not in allowed_types:
raise ValueError(f"unsupported prompt argument type: {value}")
return value
@field_validator("enum")
@classmethod
def validate_enum(cls, value: list[str] | None) -> list[str] | None:
if value is not None and not value:
raise ValueError("enum must contain at least one value when provided")
return value
class PromptMetadata(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)
arguments: dict[str, PromptArgumentEntry] = 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("tags")
@classmethod
def validate_tags(cls, value: list[str]) -> list[str]:
for tag in value:
if not SKILL_ID_RE.fullmatch(tag):
raise ValueError(f"invalid tag: {tag}")
return value
@field_validator("arguments")
@classmethod
def validate_argument_names(cls, value: dict[str, PromptArgumentEntry]) -> dict[str, PromptArgumentEntry]:
for name in value:
if not re.fullmatch(r"^[A-Za-z_][A-Za-z0-9_]*$", name):
raise ValueError(f"invalid prompt argument name: {name}")
return value
class PromptFrontmatter(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)
x_personal_mcp: PromptMetadata = 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
+90
View File
@@ -0,0 +1,90 @@
from pydantic import BaseModel
from pydantic import ConfigDict
from pydantic import Field
from pydantic import field_validator
from .common import SEMVER_RE
from .common import SKILL_ID_RE
from .common import ReferenceEntry
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("tags")
@classmethod
def validate_tags(cls, value: list[str]) -> list[str]:
for tag in value:
if not SKILL_ID_RE.fullmatch(tag):
raise ValueError(f"invalid tag: {tag}")
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
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
+61
View File
@@ -0,0 +1,61 @@
from .contracts import DocsRegistry
def read_skill_document(registry: DocsRegistry, skill_id: str) -> dict[str, str]:
if skill_id not in registry.skills_by_id:
raise KeyError(f"unknown skill_id: {skill_id}")
skill = registry.skills_by_id[skill_id]
return {
"id": skill.skill_id,
"uri": skill.document_uri,
"format": "markdown",
"source_path": f"docs/{skill.document_relpath}",
"content": skill.document_content,
}
def read_skill_reference(
registry: DocsRegistry,
*,
skill_id: str,
ref_id: str,
) -> dict[str, str]:
if skill_id not in registry.skills_by_id:
raise KeyError(f"unknown skill_id: {skill_id}")
skill = registry.skills_by_id[skill_id]
if ref_id not in skill.references:
raise KeyError(f"unknown ref_id '{ref_id}' for skill '{skill_id}'")
reference = skill.references[ref_id]
return {
"id": ref_id,
"skill_id": skill_id,
"uri": reference.uri,
"format": "markdown",
"source_path": f"docs/{reference.relpath}",
"content": reference.content,
}
def read_docs_markdown_path(registry: DocsRegistry, path: str) -> dict[str, str]:
normalized_path = _normalize_docs_path(path)
if normalized_path not in registry.docs_markdown_by_path:
raise KeyError(f"unknown docs path: {normalized_path}")
return {
"uri": f"resource://docs/{normalized_path}",
"format": "markdown",
"source_path": f"docs/{normalized_path}",
"content": registry.docs_markdown_by_path[normalized_path],
}
def read_prompt_document(registry: DocsRegistry, prompt_id: str) -> dict[str, str]:
if prompt_id not in registry.prompts_by_id:
raise KeyError(f"unknown prompt_id: {prompt_id}")
prompt = registry.prompts_by_id[prompt_id]
return {
"id": prompt.prompt_id,
"uri": prompt.document_uri,
"format": "markdown",
"source_path": f"docs/{prompt.document_relpath}",
"content": prompt.document_content,
}
+43
View File
@@ -0,0 +1,43 @@
from collections.abc import Generator
from dataclasses import dataclass
from fnmatch import fnmatch
from functools import partial
from itertools import groupby
def get_skill_filemap(content: dict[str, str]) -> dict[str, list[str]]:
"""Get the map of skill slugs to their associated files."""
def get_skill_name(relpath: str) -> str | None:
if relpath.startswith("skills/"):
return relpath.split("/")[1]
grouped = groupby(content.keys(), key=get_skill_name)
return {k: list(v) for k, v in grouped if k is not None}
@dataclass(frozen=True, slots=True)
class SkillBundle:
slug: str
skill: str
references: list[str]
other: list[str]
def gen_skill_bundles(content: dict[str, str]) -> Generator[SkillBundle]:
"""Generate the skill bundles from the map of raw markdown content."""
matcher = partial(fnmatch, pat="skills/*/references/*.md")
for slug, paths in get_skill_filemap(content).items():
skill = next(iter(p for p in paths if fnmatch(p, "skills/*/SKILL.md")))
groups = {k: list(v) for k, v in groupby(paths, key=matcher)}
yield SkillBundle(
slug=slug,
skill=skill,
references=list(groups.get(True, [])),
other=[f for f in groups.get(False, []) if f != skill],
)
def get_all_skill_bundles(content: dict[str, str]) -> list[SkillBundle]:
"""Get all skill bundles from the map of raw markdown content."""
return list(gen_skill_bundles(content))
+5 -817
View File
@@ -1,318 +1,14 @@
from __future__ import annotations from __future__ import annotations
import re import re
from dataclasses import dataclass, field
from importlib.resources import files
from importlib.resources.abc import Traversable from importlib.resources.abc import Traversable
from pathlib import PurePosixPath from pathlib import PurePosixPath
from typing import Any from typing import Any
import yaml import yaml
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
SKILL_ID_RE = re.compile(r"^[a-z][a-z0-9-]*$") from ..registry.models.common import ReferenceEntry
SEMVER_RE = re.compile( from ..registry.models.skill import SkillFrontmatter
r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+][0-9A-Za-z.-]+)?$"
)
@dataclass(frozen=True)
class RegistryIssue:
code: str
message: str
skill_id: str | None
path: str
hint: str
class DocsRegistryValidationError(Exception):
def __init__(self, errors: list[RegistryIssue]) -> None:
self.errors = errors
summary = "\n".join(
[
(
f"{issue.code}: {issue.message} "
f"(skill={issue.skill_id or 'unknown'}, path={issue.path})"
)
for issue in errors
]
)
super().__init__(summary)
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:
path = PurePosixPath(value)
if path.is_absolute() or ".." in path.parts:
raise ValueError("reference path must be a relative in-skill path")
if not str(path).startswith("references/"):
raise ValueError("reference path must stay under references/")
if path.suffix.lower() != ".md":
raise ValueError("reference path must target a markdown file")
return path.as_posix()
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("tags")
@classmethod
def validate_tags(cls, value: list[str]) -> list[str]:
for tag in value:
if not SKILL_ID_RE.fullmatch(tag):
raise ValueError(f"invalid tag: {tag}")
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
class PromptArgumentEntry(BaseModel):
model_config = ConfigDict(extra="ignore", str_strip_whitespace=True)
type: str = Field(min_length=1)
description: str | None = None
required: bool = False
default: Any | None = None
enum: list[str] | None = None
@field_validator("type")
@classmethod
def validate_type(cls, value: str) -> str:
allowed_types = {
"string",
"number",
"integer",
"boolean",
"array",
"object",
}
if value not in allowed_types:
raise ValueError(f"unsupported prompt argument type: {value}")
return value
@field_validator("enum")
@classmethod
def validate_enum(cls, value: list[str] | None) -> list[str] | None:
if value is not None and not value:
raise ValueError("enum must contain at least one value when provided")
return value
class PromptMetadata(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)
arguments: dict[str, PromptArgumentEntry] = 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("tags")
@classmethod
def validate_tags(cls, value: list[str]) -> list[str]:
for tag in value:
if not SKILL_ID_RE.fullmatch(tag):
raise ValueError(f"invalid tag: {tag}")
return value
@field_validator("arguments")
@classmethod
def validate_argument_names(
cls, value: dict[str, PromptArgumentEntry]
) -> dict[str, PromptArgumentEntry]:
for name in value:
if not re.fullmatch(r"^[A-Za-z_][A-Za-z0-9_]*$", name):
raise ValueError(f"invalid prompt argument name: {name}")
return value
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
class PromptFrontmatter(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)
x_personal_mcp: PromptMetadata = 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
@dataclass(frozen=True)
class ReferenceRecord:
ref_id: str
uri: str
relpath: str
mime_type: str
title: str | None
content: str
@dataclass(frozen=True)
class SkillRecord:
skill_id: str
name: str
description: str
version: str
tags: tuple[str, ...]
capabilities: tuple[str, ...]
depends_on: tuple[str, ...]
document_uri: str
document_relpath: str
document_content: str
references: dict[str, ReferenceRecord]
@dataclass(frozen=True)
class SkillSummaryRecord:
skill_id: str
name: str
description: str
tags: tuple[str, ...]
capabilities: tuple[str, ...]
document_uri: str
version: str
@dataclass(frozen=True)
class PromptRecord:
prompt_id: str
name: str
description: str
version: str
tags: tuple[str, ...]
capabilities: tuple[str, ...]
arguments: dict[str, PromptArgumentEntry]
document_uri: str
document_relpath: str
document_content: str
@dataclass(frozen=True)
class PromptSummaryRecord:
prompt_id: str
name: str
description: str
tags: tuple[str, ...]
capabilities: tuple[str, ...]
document_uri: str
version: str
@dataclass(frozen=True)
class DocsRegistry:
skills_by_id: dict[str, SkillRecord]
skills_in_load_order: tuple[str, ...]
skills_summary_in_load_order: tuple[SkillSummaryRecord, ...]
docs_markdown_by_path: dict[str, str]
docs_markdown_path_index: tuple[str, ...]
tag_to_skill_ids: dict[str, tuple[str, ...]]
capability_to_skill_ids: dict[str, tuple[str, ...]]
prompts_by_id: dict[str, PromptRecord] = field(default_factory=dict)
prompts_in_load_order: tuple[str, ...] = ()
prompts_summary_in_load_order: tuple[PromptSummaryRecord, ...] = ()
tag_to_prompt_ids: dict[str, tuple[str, ...]] = field(default_factory=dict)
def _parse_frontmatter(markdown: str, *, path: str) -> tuple[dict[str, Any], str]: def _parse_frontmatter(markdown: str, *, path: str) -> tuple[dict[str, Any], str]:
@@ -343,7 +39,7 @@ def _parse_frontmatter(markdown: str, *, path: str) -> tuple[dict[str, Any], str
def _walk_markdown( def _walk_markdown(
node: Traversable, node: Traversable,
*, *,
prefix: PurePosixPath = PurePosixPath(""), prefix: PurePosixPath = PurePosixPath(),
) -> list[tuple[str, Traversable]]: ) -> list[tuple[str, Traversable]]:
results: list[tuple[str, Traversable]] = [] results: list[tuple[str, Traversable]] = []
for child in sorted(node.iterdir(), key=lambda item: item.name): for child in sorted(node.iterdir(), key=lambda item: item.name):
@@ -357,9 +53,7 @@ def _walk_markdown(
return results return results
def _validate_skill_frontmatter( def _validate_skill_frontmatter(raw: dict[str, Any], *, skill_dir_name: str) -> SkillFrontmatter:
raw: dict[str, Any], *, skill_dir_name: str
) -> SkillFrontmatter:
model = SkillFrontmatter.model_validate(raw) model = SkillFrontmatter.model_validate(raw)
if model.name != skill_dir_name: if model.name != skill_dir_name:
raise ValueError("frontmatter name must exactly match skill directory name") raise ValueError("frontmatter name must exactly match skill directory name")
@@ -371,9 +65,7 @@ def _validate_skill_frontmatter(
return model return model
def _validate_prompt_frontmatter( def _validate_prompt_frontmatter(raw: dict[str, Any], *, prompt_dir_name: str) -> PromptFrontmatter:
raw: dict[str, Any], *, prompt_dir_name: str
) -> PromptFrontmatter:
model = PromptFrontmatter.model_validate(raw) model = PromptFrontmatter.model_validate(raw)
if model.name != prompt_dir_name: if model.name != prompt_dir_name:
raise ValueError("frontmatter name must exactly match prompt directory name") raise ValueError("frontmatter name must exactly match prompt directory name")
@@ -437,507 +129,3 @@ def _discover_top_level_references(
title=_title_from_reference_filename(child.name), title=_title_from_reference_filename(child.name),
) )
return discovered return discovered
def _ensure_no_cycles(skills_by_id: dict[str, SkillRecord]) -> list[tuple[str, str]]:
visiting: set[str] = set()
visited: set[str] = set()
cycles: list[tuple[str, str]] = []
def walk(skill_id: str, stack: list[str]) -> None:
if skill_id in visited:
return
if skill_id in visiting:
cycle_from = stack[stack.index(skill_id) :]
cycles.append((skill_id, " -> ".join(cycle_from + [skill_id])))
return
visiting.add(skill_id)
stack.append(skill_id)
for dep in skills_by_id[skill_id].depends_on:
if dep in skills_by_id:
walk(dep, stack)
stack.pop()
visiting.remove(skill_id)
visited.add(skill_id)
for skill_id in sorted(skills_by_id):
walk(skill_id, [])
return cycles
def load_docs_registry(
*,
package_anchor: str,
docs_root: str = "docs",
) -> DocsRegistry:
docs_dir = files(package_anchor).joinpath(docs_root)
issues: list[RegistryIssue] = []
if not docs_dir.is_dir():
raise DocsRegistryValidationError(
[
RegistryIssue(
code="missing_docs_root",
message="docs root directory does not exist",
skill_id=None,
path=docs_root,
hint="configure docs_root to a valid packaged docs path",
)
]
)
docs_markdown_by_path: dict[str, str] = {}
for relpath, doc_file in _walk_markdown(docs_dir):
docs_markdown_by_path[relpath] = doc_file.read_text(encoding="utf-8")
skills_root = docs_dir.joinpath("skills")
if not skills_root.is_dir():
raise DocsRegistryValidationError(
[
RegistryIssue(
code="missing_skills_root",
message="skills directory does not exist under docs root",
skill_id=None,
path=f"{docs_root}/skills",
hint="ensure docs/skills is included in packaged docs",
)
]
)
skills_by_id: dict[str, SkillRecord] = {}
summaries: list[SkillSummaryRecord] = []
prompts_by_id: dict[str, PromptRecord] = {}
prompt_summaries: list[PromptSummaryRecord] = []
for skill_dir in sorted(skills_root.iterdir(), key=lambda item: item.name):
if not skill_dir.is_dir():
continue
skill_dir_name = skill_dir.name
skill_rel_root = PurePosixPath("skills").joinpath(skill_dir_name)
skill_doc_relpath = skill_rel_root.joinpath("SKILL.md").as_posix()
skill_doc_file = skill_dir.joinpath("SKILL.md")
if not skill_doc_file.is_file():
issues.append(
RegistryIssue(
code="missing_skill_document",
message="missing required SKILL.md",
skill_id=skill_dir_name,
path=skill_doc_relpath,
hint="add docs/skills/<skill-id>/SKILL.md",
)
)
continue
skill_markdown = skill_doc_file.read_text(encoding="utf-8")
try:
raw_frontmatter, _ = _parse_frontmatter(
skill_markdown,
path=skill_doc_relpath,
)
frontmatter = _validate_skill_frontmatter(
raw_frontmatter,
skill_dir_name=skill_dir_name,
)
except (ValueError, ValidationError) as exc:
issues.append(
RegistryIssue(
code="invalid_frontmatter",
message=str(exc),
skill_id=skill_dir_name,
path=skill_doc_relpath,
hint="fix SKILL.md YAML frontmatter to match the contract",
)
)
continue
effective_reference_entries = _discover_top_level_references(skill_dir=skill_dir)
effective_reference_entries.update(frontmatter.x_personal_mcp.references)
references: dict[str, ReferenceRecord] = {}
for ref_id, ref_entry in effective_reference_entries.items():
ref_relpath = skill_rel_root.joinpath(ref_entry.path).as_posix()
if ref_relpath not in docs_markdown_by_path:
issues.append(
RegistryIssue(
code="missing_reference",
message=f"reference target is missing for ref_id '{ref_id}'",
skill_id=frontmatter.name,
path=ref_relpath,
hint="fix x-personal-mcp.references path or add the referenced markdown file",
)
)
continue
references[ref_id] = ReferenceRecord(
ref_id=ref_id,
uri=f"resource://skills/{frontmatter.name}/references/{ref_id}",
relpath=ref_relpath,
mime_type=ref_entry.mime_type,
title=ref_entry.title,
content=docs_markdown_by_path[ref_relpath],
)
skill_id = frontmatter.name
if skill_id in skills_by_id:
issues.append(
RegistryIssue(
code="duplicate_skill_id",
message="duplicate skill id discovered",
skill_id=skill_id,
path=skill_doc_relpath,
hint="ensure each skill directory has a unique id",
)
)
continue
record = SkillRecord(
skill_id=skill_id,
name=frontmatter.name,
description=frontmatter.description,
version=frontmatter.x_personal_mcp.version,
tags=tuple(frontmatter.x_personal_mcp.tags),
capabilities=tuple(frontmatter.x_personal_mcp.capabilities),
depends_on=tuple(frontmatter.x_personal_mcp.depends_on),
document_uri=f"resource://skills/{skill_id}/document",
document_relpath=skill_doc_relpath,
document_content=skill_markdown,
references=references,
)
skills_by_id[skill_id] = record
summaries.append(
SkillSummaryRecord(
skill_id=record.skill_id,
name=record.name,
description=record.description,
tags=record.tags,
capabilities=record.capabilities,
document_uri=record.document_uri,
version=record.version,
)
)
prompts_root = docs_dir.joinpath("prompts")
discovered_prompt_docs: set[str] = set()
if prompts_root.is_dir():
for prompt_dir in sorted(prompts_root.iterdir(), key=lambda item: item.name):
if not prompt_dir.is_dir():
continue
prompt_dir_name = prompt_dir.name
prompt_rel_root = PurePosixPath("prompts").joinpath(prompt_dir_name)
prompt_doc_relpath = prompt_rel_root.joinpath("PROMPT.md").as_posix()
prompt_doc_file = prompt_dir.joinpath("PROMPT.md")
if not prompt_doc_file.is_file():
continue
discovered_prompt_docs.add(prompt_doc_relpath)
prompt_markdown = prompt_doc_file.read_text(encoding="utf-8")
try:
raw_frontmatter, _ = _parse_frontmatter(
prompt_markdown,
path=prompt_doc_relpath,
)
frontmatter = _validate_prompt_frontmatter(
raw_frontmatter,
prompt_dir_name=prompt_dir_name,
)
except (ValueError, ValidationError) as exc:
issues.append(
RegistryIssue(
code="invalid_prompt_frontmatter",
message=str(exc),
skill_id=prompt_dir_name,
path=prompt_doc_relpath,
hint="fix PROMPT.md YAML frontmatter to match the contract",
)
)
continue
prompt_id = frontmatter.name
if prompt_id in prompts_by_id:
issues.append(
RegistryIssue(
code="duplicate_prompt_id",
message="duplicate prompt id discovered",
skill_id=prompt_id,
path=prompt_doc_relpath,
hint="ensure each prompt directory has a unique id",
)
)
continue
if prompt_id in skills_by_id:
issues.append(
RegistryIssue(
code="prompt_skill_id_collision",
message="prompt id collides with an existing skill id",
skill_id=prompt_id,
path=prompt_doc_relpath,
hint="use a unique prompt id that does not match a skill id",
)
)
continue
prompt_record = PromptRecord(
prompt_id=prompt_id,
name=frontmatter.name,
description=frontmatter.description,
version=frontmatter.x_personal_mcp.version,
tags=tuple(frontmatter.x_personal_mcp.tags),
capabilities=tuple(frontmatter.x_personal_mcp.capabilities),
arguments=frontmatter.x_personal_mcp.arguments,
document_uri=f"resource://prompts/{prompt_id}/document",
document_relpath=prompt_doc_relpath,
document_content=prompt_markdown,
)
prompts_by_id[prompt_id] = prompt_record
prompt_summaries.append(
PromptSummaryRecord(
prompt_id=prompt_record.prompt_id,
name=prompt_record.name,
description=prompt_record.description,
tags=prompt_record.tags,
capabilities=prompt_record.capabilities,
document_uri=prompt_record.document_uri,
version=prompt_record.version,
)
)
for relpath, prompt_file in _walk_markdown(
prompts_root,
prefix=PurePosixPath("prompts"),
):
if relpath in discovered_prompt_docs or relpath.endswith("/PROMPT.md"):
continue
prompt_markdown = prompt_file.read_text(encoding="utf-8")
prompt_id = _reference_id_from_filename(PurePosixPath(relpath).name)
if prompt_id is None:
continue
if prompt_id in prompts_by_id:
issues.append(
RegistryIssue(
code="duplicate_prompt_id",
message="duplicate prompt id discovered",
skill_id=prompt_id,
path=relpath,
hint="ensure each prompt id is unique",
)
)
continue
if prompt_id in skills_by_id:
issues.append(
RegistryIssue(
code="prompt_skill_id_collision",
message="prompt id collides with an existing skill id",
skill_id=prompt_id,
path=relpath,
hint="use a unique prompt id that does not match a skill id",
)
)
continue
prompt_record = PromptRecord(
prompt_id=prompt_id,
name=prompt_id,
description=f"Legacy prompt loaded from docs/{relpath}",
version="1.0.0",
tags=("prompt", "legacy"),
capabilities=(f"resource://prompts/{prompt_id}/document",),
arguments={},
document_uri=f"resource://prompts/{prompt_id}/document",
document_relpath=relpath,
document_content=prompt_markdown,
)
prompts_by_id[prompt_id] = prompt_record
prompt_summaries.append(
PromptSummaryRecord(
prompt_id=prompt_record.prompt_id,
name=prompt_record.name,
description=prompt_record.description,
tags=prompt_record.tags,
capabilities=prompt_record.capabilities,
document_uri=prompt_record.document_uri,
version=prompt_record.version,
)
)
for skill_id, record in sorted(skills_by_id.items()):
for dependency in record.depends_on:
if dependency == skill_id:
issues.append(
RegistryIssue(
code="self_dependency",
message="skill must not depend on itself",
skill_id=skill_id,
path=record.document_relpath,
hint="remove the skill id from depends_on",
)
)
elif dependency not in skills_by_id:
issues.append(
RegistryIssue(
code="missing_dependency",
message=f"depends_on target '{dependency}' does not exist",
skill_id=skill_id,
path=record.document_relpath,
hint="add the missing skill or remove it from depends_on",
)
)
for cycle_start, cycle in _ensure_no_cycles(skills_by_id):
issues.append(
RegistryIssue(
code="dependency_cycle",
message=f"depends_on cycle detected: {cycle}",
skill_id=cycle_start,
path=skills_by_id[cycle_start].document_relpath,
hint="remove at least one dependency edge in the cycle",
)
)
seen_uris: set[str] = set()
for skill_id, record in sorted(skills_by_id.items()):
uris = [record.document_uri] + [ref.uri for ref in record.references.values()]
for uri in uris:
if uri in seen_uris:
issues.append(
RegistryIssue(
code="duplicate_uri",
message=f"duplicate resource URI generated: {uri}",
skill_id=skill_id,
path=record.document_relpath,
hint="ensure unique skill ids and reference ids",
)
)
seen_uris.add(uri)
for prompt_id, record in sorted(prompts_by_id.items()):
for uri in [record.document_uri]:
if uri in seen_uris:
issues.append(
RegistryIssue(
code="duplicate_uri",
message=f"duplicate resource URI generated: {uri}",
skill_id=prompt_id,
path=record.document_relpath,
hint="ensure unique prompt ids",
)
)
seen_uris.add(uri)
if issues:
raise DocsRegistryValidationError(issues)
skill_ids = tuple(sorted(skills_by_id))
summary_by_id = {summary.skill_id: summary for summary in summaries}
ordered_summaries = tuple(summary_by_id[skill_id] for skill_id in skill_ids)
prompt_ids = tuple(sorted(prompts_by_id))
prompt_summary_by_id = {
summary.prompt_id: summary for summary in prompt_summaries
}
ordered_prompt_summaries = tuple(
prompt_summary_by_id[prompt_id] for prompt_id in prompt_ids
)
tag_index: dict[str, list[str]] = {}
capability_index: dict[str, list[str]] = {}
prompt_tag_index: dict[str, list[str]] = {}
for skill_id in skill_ids:
record = skills_by_id[skill_id]
for tag in record.tags:
tag_index.setdefault(tag, []).append(skill_id)
for capability in record.capabilities:
capability_index.setdefault(capability, []).append(skill_id)
for prompt_id in prompt_ids:
record = prompts_by_id[prompt_id]
for tag in record.tags:
prompt_tag_index.setdefault(tag, []).append(prompt_id)
return DocsRegistry(
skills_by_id=skills_by_id,
skills_in_load_order=skill_ids,
skills_summary_in_load_order=ordered_summaries,
docs_markdown_by_path=docs_markdown_by_path,
docs_markdown_path_index=tuple(sorted(docs_markdown_by_path)),
tag_to_skill_ids={
key: tuple(sorted(values)) for key, values in sorted(tag_index.items())
},
capability_to_skill_ids={
key: tuple(sorted(values))
for key, values in sorted(capability_index.items())
},
prompts_by_id=prompts_by_id,
prompts_in_load_order=prompt_ids,
prompts_summary_in_load_order=ordered_prompt_summaries,
tag_to_prompt_ids={
key: tuple(sorted(values))
for key, values in sorted(prompt_tag_index.items())
},
)
def read_skill_document(registry: DocsRegistry, skill_id: str) -> dict[str, str]:
if skill_id not in registry.skills_by_id:
raise KeyError(f"unknown skill_id: {skill_id}")
skill = registry.skills_by_id[skill_id]
return {
"id": skill.skill_id,
"uri": skill.document_uri,
"format": "markdown",
"source_path": f"docs/{skill.document_relpath}",
"content": skill.document_content,
}
def read_skill_reference(
registry: DocsRegistry,
*,
skill_id: str,
ref_id: str,
) -> dict[str, str]:
if skill_id not in registry.skills_by_id:
raise KeyError(f"unknown skill_id: {skill_id}")
skill = registry.skills_by_id[skill_id]
if ref_id not in skill.references:
raise KeyError(f"unknown ref_id '{ref_id}' for skill '{skill_id}'")
reference = skill.references[ref_id]
return {
"id": ref_id,
"skill_id": skill_id,
"uri": reference.uri,
"format": "markdown",
"source_path": f"docs/{reference.relpath}",
"content": reference.content,
}
def read_docs_markdown_path(registry: DocsRegistry, path: str) -> dict[str, str]:
normalized_path = _normalize_docs_path(path)
if normalized_path not in registry.docs_markdown_by_path:
raise KeyError(f"unknown docs path: {normalized_path}")
return {
"uri": f"resource://docs/{normalized_path}",
"format": "markdown",
"source_path": f"docs/{normalized_path}",
"content": registry.docs_markdown_by_path[normalized_path],
}
def read_prompt_document(registry: DocsRegistry, prompt_id: str) -> dict[str, str]:
if prompt_id not in registry.prompts_by_id:
raise KeyError(f"unknown prompt_id: {prompt_id}")
prompt = registry.prompts_by_id[prompt_id]
return {
"id": prompt.prompt_id,
"uri": prompt.document_uri,
"format": "markdown",
"source_path": f"docs/{prompt.document_relpath}",
"content": prompt.document_content,
}
View File
+2 -1
View File
@@ -1,7 +1,8 @@
from fastapi import FastAPI from fastapi import FastAPI
from personal_mcp.mcp import mcp from personal_mcp.mcp import mcp
from personal_mcp.web.config import Settings, get_settings from personal_mcp.web.config import Settings
from personal_mcp.web.config import get_settings
from personal_mcp.web.docs_mount import mount_docs_static from personal_mcp.web.docs_mount import mount_docs_static
from personal_mcp.web.health import router as health_router from personal_mcp.web.health import router as health_router
+2 -1
View File
@@ -1,7 +1,8 @@
from pathlib import Path from pathlib import Path
from pydantic import Field from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic_settings import BaseSettings
from pydantic_settings import SettingsConfigDict
_REPO_ROOT = Path(__file__).resolve().parents[3] _REPO_ROOT = Path(__file__).resolve().parents[3]
+3 -1
View File
@@ -1,6 +1,8 @@
from pathlib import Path from pathlib import Path
from fastapi import FastAPI, Response, status from fastapi import FastAPI
from fastapi import Response
from fastapi import status
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
@@ -1,131 +0,0 @@
from __future__ import annotations
from pathlib import Path
import pytest
from personal_mcp.catalog.server import (
build_prompt_detail_payload,
build_prompts_index_payload,
)
from personal_mcp.skills import document_loader
def _write_skill(root: Path, *, skill_id: str) -> None:
skill_dir = root / "docs" / "skills" / skill_id
skill_dir.mkdir(parents=True, exist_ok=True)
skill_doc = "\n".join(
[
"---",
f"name: {skill_id}",
"description: Example skill",
"x-personal-mcp:",
f" id: {skill_id}",
" version: 1.0.0",
" tags: [example]",
" capabilities:",
f" - resource://skills/{skill_id}/document",
"---",
"",
f"# {skill_id}",
"",
]
)
(skill_dir / "SKILL.md").write_text(skill_doc, encoding="utf-8")
def _write_frontmatter_prompt(root: Path, *, prompt_id: str) -> None:
prompt_dir = root / "docs" / "prompts" / prompt_id
prompt_dir.mkdir(parents=True, exist_ok=True)
prompt_doc = "\n".join(
[
"---",
f"name: {prompt_id}",
"description: Scaffold initial tests",
"x-personal-mcp:",
f" id: {prompt_id}",
" version: 1.0.0",
" tags: [pytest, testing]",
" capabilities:",
f" - resource://prompts/{prompt_id}/document",
" arguments:",
" target_scope:",
" type: string",
" description: The test scope",
" required: true",
"---",
"",
"Create tests for {{target_scope}}.",
"",
]
)
(prompt_dir / "PROMPT.md").write_text(prompt_doc, encoding="utf-8")
def _write_legacy_prompt(root: Path) -> None:
legacy = root / "docs" / "prompts" / "testing"
legacy.mkdir(parents=True, exist_ok=True)
(legacy / "inital_test_structure.md").write_text(
"# Prompt For Creating Initial Test Structure\n",
encoding="utf-8",
)
def _load_registry(monkeypatch: pytest.MonkeyPatch, root: Path):
monkeypatch.setattr(document_loader, "files", lambda _: root)
return document_loader.load_docs_registry(package_anchor="unused")
def test_loads_frontmatter_prompt_and_arguments(monkeypatch, tmp_path: Path) -> None:
_write_skill(tmp_path, skill_id="demo-skill")
_write_frontmatter_prompt(tmp_path, prompt_id="initial-test-structure")
registry = _load_registry(monkeypatch, tmp_path)
assert "initial-test-structure" in registry.prompts_by_id
prompt = registry.prompts_by_id["initial-test-structure"]
assert prompt.arguments["target_scope"].type == "string"
assert prompt.arguments["target_scope"].required is True
def test_loads_legacy_prompt_markdown(monkeypatch, tmp_path: Path) -> None:
_write_skill(tmp_path, skill_id="demo-skill")
_write_legacy_prompt(tmp_path)
registry = _load_registry(monkeypatch, tmp_path)
assert "inital-test-structure" in registry.prompts_by_id
prompt = registry.prompts_by_id["inital-test-structure"]
assert prompt.document_relpath == "prompts/testing/inital_test_structure.md"
assert "Legacy prompt loaded" in prompt.description
def test_rejects_prompt_skill_id_collision(monkeypatch, tmp_path: Path) -> None:
_write_skill(tmp_path, skill_id="shared-id")
_write_frontmatter_prompt(tmp_path, prompt_id="shared-id")
with pytest.raises(document_loader.DocsRegistryValidationError) as exc:
_load_registry(monkeypatch, tmp_path)
codes = [issue.code for issue in exc.value.errors]
assert "prompt_skill_id_collision" in codes
def test_prompt_catalog_index_and_detail(monkeypatch, tmp_path: Path) -> None:
_write_skill(tmp_path, skill_id="demo-skill")
_write_frontmatter_prompt(tmp_path, prompt_id="initial-test-structure")
_write_frontmatter_prompt(tmp_path, prompt_id="route-tests")
registry = _load_registry(monkeypatch, tmp_path)
index = build_prompts_index_payload(
registry,
query="initial-test-structure",
limit=10,
)
assert index["total"] == 1
assert index["prompts"][0]["id"] == "initial-test-structure"
detail = build_prompt_detail_payload(registry, "initial-test-structure")
assert "target_scope" in detail["arguments"]
assert detail["arguments"]["target_scope"]["required"] is True
@@ -1,94 +0,0 @@
from __future__ import annotations
import asyncio
import pytest
from personal_mcp import mcp as mcp_module
def _listed_tool_names() -> set[str]:
tools = asyncio.run(mcp_module.mcp.list_tools())
return {tool.name for tool in tools}
@pytest.fixture
def listed_tool_names() -> set[str]:
return _listed_tool_names()
def test_canonical_step6_tools_are_listed(listed_tool_names: set[str]) -> None:
assert {
"list_resources",
"read_resource",
"search_patterns",
"get_pattern_by_id",
"get_skill_document_by_id",
"search_prompts",
"get_prompt_by_id",
}.issubset(listed_tool_names)
def test_catalog_compatibility_alias_tools_are_listed(
listed_tool_names: set[str],
) -> None:
assert {
"catalog_search_patterns",
"catalog_get_pattern_by_id",
"catalog_get_skill_document_by_id",
"catalog_search_prompts",
"catalog_get_prompt_by_id",
}.issubset(listed_tool_names)
@pytest.mark.parametrize(
"skill_id",
[mcp_module.REGISTRY.skills_in_load_order[0], "missing-step6-skill"],
)
def test_skill_document_alias_matches_canonical_payload(skill_id: str) -> None:
canonical = mcp_module.get_skill_document_by_id(skill_id)
alias = mcp_module.catalog_get_skill_document_by_id(skill_id)
assert alias == canonical
def test_search_and_pattern_aliases_match_canonical_payloads() -> None:
search_canonical = mcp_module.search_patterns(
query="mcp",
tags=[],
skip=0,
limit=5,
)
search_alias = mcp_module.catalog_search_patterns(
query="mcp",
tags=[],
skip=0,
limit=5,
)
assert search_alias == search_canonical
sample_skill_id = mcp_module.REGISTRY.skills_in_load_order[0]
pattern_canonical = mcp_module.get_pattern_by_id(sample_skill_id)
pattern_alias = mcp_module.catalog_get_pattern_by_id(sample_skill_id)
assert pattern_alias == pattern_canonical
def test_prompt_aliases_match_canonical_payloads() -> None:
search_canonical = mcp_module.search_prompts(
query="test",
tags=[],
skip=0,
limit=5,
)
search_alias = mcp_module.catalog_search_prompts(
query="test",
tags=[],
skip=0,
limit=5,
)
assert search_alias == search_canonical
sample_prompt_id = mcp_module.REGISTRY.prompts_in_load_order[0]
prompt_canonical = mcp_module.get_prompt_by_id(sample_prompt_id)
prompt_alias = mcp_module.catalog_get_prompt_by_id(sample_prompt_id)
assert prompt_alias == prompt_canonical
@@ -1,144 +0,0 @@
from __future__ import annotations
from pathlib import Path
from textwrap import dedent
import pytest
from personal_mcp.skills import document_loader
def _write_skill(
root: Path,
*,
skill_id: str,
references_block: str = "",
) -> Path:
skill_dir = root / "docs" / "skills" / skill_id
skill_dir.mkdir(parents=True, exist_ok=True)
frontmatter_lines = [
"---",
f"name: {skill_id}",
"description: Example skill",
"x-personal-mcp:",
f" id: {skill_id}",
" version: 1.0.0",
" tags: [example]",
" capabilities:",
f" - resource://skills/{skill_id}/document",
]
if references_block:
frontmatter_lines.extend(f" {line}" for line in references_block.splitlines())
frontmatter_lines.append("---")
skill_doc = "\n".join(frontmatter_lines) + f"\n\n# {skill_id}\n"
(skill_dir / "SKILL.md").write_text(skill_doc, encoding="utf-8")
return skill_dir
def _load_registry(monkeypatch: pytest.MonkeyPatch, root: Path):
monkeypatch.setattr(document_loader, "files", lambda _: root)
return document_loader.load_docs_registry(package_anchor="unused")
def test_auto_discovers_top_level_references(monkeypatch, tmp_path: Path) -> None:
skill_dir = _write_skill(tmp_path, skill_id="demo-skill")
refs = skill_dir / "references"
refs.mkdir()
(refs / "index.md").write_text("# Source Map\n", encoding="utf-8")
(refs / "feature-catalog.md").write_text("# Feature Catalog\n", encoding="utf-8")
registry = _load_registry(monkeypatch, tmp_path)
skill = registry.skills_by_id["demo-skill"]
assert set(skill.references) == {"index", "feature-catalog"}
assert skill.references["index"].title == "Index"
assert skill.references["feature-catalog"].title == "Feature Catalog"
def test_explicit_reference_overrides_discovered(monkeypatch, tmp_path: Path) -> None:
references_block = dedent(
"""\
references:
index:
path: references/index.md
mime_type: text/plain
title: Explicit Title
"""
).rstrip()
skill_dir = _write_skill(
tmp_path,
skill_id="override-skill",
references_block=references_block,
)
refs = skill_dir / "references"
refs.mkdir()
(refs / "index.md").write_text("# Source Map\n", encoding="utf-8")
registry = _load_registry(monkeypatch, tmp_path)
record = registry.skills_by_id["override-skill"].references["index"]
assert record.mime_type == "text/plain"
assert record.title == "Explicit Title"
def test_nested_references_are_not_auto_discovered(monkeypatch, tmp_path: Path) -> None:
references_block = dedent(
"""\
references:
architecture-overview:
path: references/nested/architecture-overview.md
title: Architecture Overview
"""
).rstrip()
skill_dir = _write_skill(
tmp_path,
skill_id="nested-skill",
references_block=references_block,
)
refs = skill_dir / "references"
(refs / "nested").mkdir(parents=True)
(refs / "nested" / "architecture-overview.md").write_text(
"# Architecture\n",
encoding="utf-8",
)
registry = _load_registry(monkeypatch, tmp_path)
skill = registry.skills_by_id["nested-skill"]
assert set(skill.references) == {"architecture-overview"}
@pytest.mark.parametrize(
("skill_id", "files", "expected_refs"),
[
(
"non-markdown-skill",
{"guide.txt": "plain text", "guide.md": "# Guide\n"},
{"guide"},
),
(
"normalized-id-skill",
{"implicit_io.md": "# Implicit IO\n"},
{"implicit-io"},
),
],
)
def test_auto_discovery_file_filtering_and_ref_id_normalization(
monkeypatch,
tmp_path: Path,
skill_id: str,
files: dict[str, str],
expected_refs: set[str],
) -> None:
skill_dir = _write_skill(tmp_path, skill_id=skill_id)
refs = skill_dir / "references"
refs.mkdir()
for filename, content in files.items():
(refs / filename).write_text(content, encoding="utf-8")
registry = _load_registry(monkeypatch, tmp_path)
skill = registry.skills_by_id[skill_id]
assert set(skill.references) == expected_refs