WIP simplifying load/startup

This commit is contained in:
John Lancaster
2026-07-26 17:23:39 -05:00
parent 5e20f69cfe
commit 42ea105bee
30 changed files with 855 additions and 766 deletions
+15 -10
View File
@@ -1,4 +1,5 @@
from collections.abc import Generator
from collections.abc import Iterator
from dataclasses import dataclass
from dataclasses import field
from importlib.resources.abc import Traversable
@@ -6,26 +7,32 @@ from itertools import starmap
from pathlib import PurePosixPath
from typing import Self
from personal_mcp.registry.models.common import DocsPath
from personal_mcp.registry.models.common import parse_docs_path
@dataclass(frozen=True, slots=True)
class MarkdownDocument:
"""Represents a loaded markdown document with its content and frontmatter."""
relpath: PurePosixPath
relpath: DocsPath
"""The relative path of the document within the package resources."""
content: str = field(repr=False)
"""The raw markdown content of the document."""
frontmatter: str | None = field(repr=False, default=None)
"""The raw YAML frontmatter of the document, if present."""
def __post_init__(self) -> None:
object.__setattr__(self, "relpath", parse_docs_path(self.relpath))
@classmethod
def from_root(cls, root: Traversable):
def from_root(cls, root: Traversable) -> dict[DocsPath, Self]:
"""Recursively load all markdown documents from the root resource."""
mapped = starmap(cls.from_resource, walk_resources(root))
return {d.relpath: d for d in mapped}
@classmethod
def from_resource(cls, relpath: PurePosixPath, resource: Traversable) -> Self:
def from_resource(cls, relpath: DocsPath, resource: Traversable) -> Self:
"""Load a markdown document from a package resource."""
raw = resource.read_text(encoding="utf-8")
frontmatter = get_raw_frontmatter(raw)
@@ -49,17 +56,15 @@ def walk_resources(
*,
suffix: str = ".md",
prefix: PurePosixPath | None = None,
) -> Generator[tuple[PurePosixPath, Traversable]]:
) -> Iterator[tuple[PurePosixPath, Traversable]]:
"""Recursively yield all resources in node, with their full path."""
prefix = prefix if prefix is not None else PurePosixPath()
prefix = prefix or PurePosixPath()
for child in sorted(node.iterdir(), key=lambda item: item.name):
relpath = prefix.joinpath(child.name)
relpath = prefix / 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, child
elif child.is_file() and child.name.lower().endswith(suffix):
yield relpath, child
def get_raw_frontmatter(raw: str) -> str | None:
+2 -2
View File
@@ -27,7 +27,7 @@ class PromptFilesBundle:
@classmethod
def from_paths(cls, slug: str, paths: set[MarkdownDocument]) -> Self:
prompt = next(iter(p for p in paths if p.relpath.name == "PROMPT.md"))
sorted_paths = tuple(sorted(paths, key=lambda p: p.relpath.as_posix()))
sorted_paths = tuple(sorted(paths, key=lambda p: p.relpath))
other = tuple(p for p in sorted_paths if p != prompt)
return cls(
slug=slug,
@@ -41,7 +41,7 @@ def group_prompt_paths(docs: Iterable[MarkdownDocument]) -> dict[str, set[Markdo
grouped: dict[str, set[MarkdownDocument]] = {}
for doc in sorted(
filter(lambda d: d.prompt_slug is not None, docs),
key=lambda d: d.relpath.as_posix(),
key=lambda d: d.relpath,
):
if doc.prompt_slug:
grouped.setdefault(doc.prompt_slug, set()).add(doc)
+26 -8
View File
@@ -1,19 +1,19 @@
import re
from collections.abc import Iterable
from collections.abc import Mapping
from dataclasses import dataclass
from fnmatch import fnmatch
from importlib.resources.abc import Traversable
from itertools import groupby
from itertools import starmap
from pathlib import PurePosixPath
from typing import Self
from personal_mcp.registry.models.common import SKILL_ID_RE
from personal_mcp.registry.models.common import DocsPath
from personal_mcp.registry.models.common import ReferenceEntry
from personal_mcp.registry.models.skill import SkillFrontmatter
from personal_mcp.registry.models.skill import StoredSkill
from personal_mcp.registry.models.skill import StoredSkillReference
from personal_mcp.skills.document_loader import _reference_id_from_filename
from personal_mcp.skills.document_loader import _title_from_reference_filename
from .document import MarkdownDocument
@@ -39,8 +39,9 @@ class SkillFilesBundle:
@classmethod
def from_paths(cls, slug: str, paths: set[MarkdownDocument]) -> Self:
skill = next(iter(p for p in paths if p.relpath.name == "SKILL.md"))
sorted_paths = tuple(sorted(paths, key=lambda p: p.relpath.as_posix()))
references = tuple(p for p in sorted_paths if fnmatch(p.relpath.as_posix(), f"skills/{slug}/references/*.md"))
sorted_paths = tuple(sorted(paths, key=lambda p: p.relpath))
references_dir = PurePosixPath("skills", slug, "references")
references = tuple(p for p in sorted_paths if p.relpath.parent == references_dir)
other = tuple(p for p in sorted_paths if p not in references and p != skill)
return cls(
slug=slug,
@@ -60,6 +61,23 @@ def group_skill_paths(docs: Iterable[MarkdownDocument]) -> dict[str, set[Markdow
return {k: set(g) for k, g in grouped if k}
def _title_from_reference_filename(filename: str) -> str:
stem = PurePosixPath(filename).stem
normalized = stem.replace("-", " ").replace("_", " ").split()
if not normalized:
return stem
return " ".join(token.capitalize() for token in normalized)
def _reference_id_from_filename(filename: str) -> str | None:
stem = PurePosixPath(filename).stem.strip().lower().replace("_", "-")
normalized = re.sub(r"[^a-z0-9-]+", "-", stem)
normalized = re.sub(r"-+", "-", normalized).strip("-")
if not normalized or not SKILL_ID_RE.fullmatch(normalized):
return None
return normalized
def _discover_reference_entries(bundle: SkillFilesBundle) -> dict[str, ReferenceEntry]:
discovered: dict[str, ReferenceEntry] = {}
for reference_doc in bundle.references:
@@ -67,7 +85,7 @@ def _discover_reference_entries(bundle: SkillFilesBundle) -> dict[str, Reference
if ref_id is None:
continue
discovered[ref_id] = ReferenceEntry(
path=PurePosixPath("references").joinpath(reference_doc.relpath.name).as_posix(),
path=PurePosixPath("references", reference_doc.relpath.name),
title=_title_from_reference_filename(reference_doc.relpath.name),
)
return discovered
@@ -76,7 +94,7 @@ def _discover_reference_entries(bundle: SkillFilesBundle) -> dict[str, Reference
def build_stored_skill(
*,
bundle: SkillFilesBundle,
docs_by_relpath: Mapping[PurePosixPath, MarkdownDocument],
docs_by_relpath: Mapping[DocsPath, MarkdownDocument],
) -> StoredSkill:
frontmatter = SkillFrontmatter.from_raw_yaml(bundle.skill.frontmatter)
metadata = frontmatter.x_personal_mcp
@@ -85,7 +103,7 @@ def build_stored_skill(
references: dict[str, StoredSkillReference] = {}
for ref_id, entry in sorted(merged_entries.items()):
ref_relpath = PurePosixPath("skills").joinpath(bundle.slug).joinpath(entry.path)
ref_relpath = PurePosixPath("skills", bundle.slug, entry.path)
if ref_relpath not in docs_by_relpath:
raise KeyError(f"reference document not found for '{metadata.id}:{ref_id}' at {ref_relpath.as_posix()}")
ref_doc = docs_by_relpath[ref_relpath]
+14 -44
View File
@@ -1,17 +1,14 @@
from __future__ import annotations
import importlib
from collections import defaultdict
from pathlib import Path
from pathlib import PurePosixPath
import yaml
from functools import cache
from importlib.resources import files
from personal_mcp.registry.ingest.document import MarkdownDocument
from personal_mcp.registry.ingest.prompt import PromptFilesBundle
from personal_mcp.registry.ingest.skill import SkillFilesBundle
from personal_mcp.registry.ingest.skill import build_stored_skill
from personal_mcp.registry.models.common import _normalize_docs_path
from personal_mcp.registry.models.common import DocsPath
from personal_mcp.registry.models.prompt import StoredPrompt
from personal_mcp.registry.models.registry import DocsRegistry
from personal_mcp.registry.models.registry import PromptRecord
@@ -21,25 +18,7 @@ from personal_mcp.registry.models.registry import SkillRecord
from personal_mcp.registry.models.registry import SkillSummaryRecord
def _parse_frontmatter(raw_frontmatter: str | None, *, path: PurePosixPath) -> dict[str, object]:
"""Parse frontmatter YAML into a mapping for downstream validation.
This helper is retained for compatibility with model-validation tests that
exercise gate behavior directly at parse boundaries.
"""
if raw_frontmatter is None:
raise ValueError(f"missing YAML frontmatter: {path.as_posix()}")
parsed = yaml.safe_load(raw_frontmatter)
if not isinstance(parsed, dict):
raise TypeError(f"frontmatter must parse to an object: {path.as_posix()}")
return parsed
def _build_skill_record(
*, bundle: SkillFilesBundle, docs_by_relpath: dict[PurePosixPath, MarkdownDocument]
) -> SkillRecord:
def _build_skill_record(*, bundle: SkillFilesBundle, docs_by_relpath: dict[DocsPath, MarkdownDocument]) -> SkillRecord:
stored = build_stored_skill(bundle=bundle, docs_by_relpath=docs_by_relpath)
metadata = stored.frontmatter.x_personal_mcp
references: dict[str, ReferenceRecord] = {}
@@ -47,7 +26,7 @@ def _build_skill_record(
references[ref_id] = ReferenceRecord(
ref_id=ref_id,
uri=f"resource://skills/{metadata.id}/references/{ref_id}",
relpath=ref.relpath.as_posix(),
relpath=ref.relpath,
mime_type=ref.entry.mime_type,
title=ref.entry.title,
content=ref.content,
@@ -61,7 +40,7 @@ def _build_skill_record(
tags=tuple(metadata.tags),
capabilities=tuple(metadata.capabilities),
document_uri=f"resource://skills/{metadata.id}/document",
document_relpath=stored.relpath.as_posix(),
document_relpath=stored.relpath,
document_content=stored.content,
references=references,
)
@@ -80,7 +59,7 @@ def _build_prompt_record(*, bundle: PromptFilesBundle) -> PromptRecord:
capabilities=tuple(metadata.capabilities),
arguments=dict(metadata.arguments),
document_uri=f"resource://prompts/{metadata.id}/document",
document_relpath=stored.relpath.as_posix(),
document_relpath=stored.relpath,
document_content=stored.content,
)
@@ -116,23 +95,14 @@ def _build_tag_index_prompts(
return {tag: tuple(ids) for tag, ids in sorted(tag_index.items())}
def _resolve_docs_root(*, package_anchor: str, docs_root: str) -> Path:
package = importlib.import_module(package_anchor)
package_file = getattr(package, "__file__", None)
if package_file is None:
raise ValueError(f"package anchor '{package_anchor}' has no file location")
@cache
def get_docs_registry() -> DocsRegistry:
root = files("personal_mcp").joinpath("docs")
if not root.is_dir():
raise FileNotFoundError(f"docs root does not exist or is not a directory: {root}")
docs = MarkdownDocument.from_root(root)
resolved = Path(package_file).resolve().parent.joinpath(docs_root).resolve()
if not resolved.exists() or not resolved.is_dir():
raise FileNotFoundError(f"docs root does not exist or is not a directory: {resolved}")
return resolved
def load_docs_registry(*, package_anchor: str, docs_root: str = "docs") -> DocsRegistry:
docs_path = _resolve_docs_root(package_anchor=package_anchor, docs_root=docs_root)
docs = MarkdownDocument.from_root(docs_path)
docs_markdown_by_path = {_normalize_docs_path(relpath.as_posix()): doc.content for relpath, doc in docs.items()}
docs_markdown_by_path = {relpath: doc.content for relpath, doc in docs.items()}
skill_bundles = SkillFilesBundle.from_docs(docs.values())
prompt_bundles = PromptFilesBundle.from_docs(docs.values())
+29 -23
View File
@@ -2,12 +2,13 @@ import re
from collections.abc import Mapping
from pathlib import PurePosixPath
from types import MappingProxyType
from typing import Annotated
from typing import ClassVar
from typing import Final
from pydantic import BaseModel
from pydantic import BeforeValidator
from pydantic import ConfigDict
from pydantic import field_validator
SKILL_ID_RE: Final[re.Pattern[str]] = re.compile(r"^[a-z][a-z0-9-]*$")
SEMVER_RE: Final[re.Pattern[str]] = re.compile(r"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:[-+][0-9A-Za-z.-]+)?$")
@@ -29,30 +30,35 @@ def frozen_mapping[K, V](value: Mapping[K, V] | None = None) -> Mapping[K, V]:
return MappingProxyType(dict(value) if value is not None else {})
def parse_docs_path(value: str | PurePosixPath) -> PurePosixPath:
raw = value.as_posix() if isinstance(value, PurePosixPath) else value
if "\\" in raw:
raise ValueError("docs path must use POSIX separators")
path = PurePosixPath(raw)
if path.is_absolute() or ".." in path.parts:
raise ValueError("path must be a docs-relative path")
if path.as_posix() != raw:
raise ValueError("path must be normalized")
if path.suffix.lower() != ".md":
raise ValueError("path must point to a markdown file")
return path
def parse_reference_path(value: str | PurePosixPath) -> PurePosixPath:
path = parse_docs_path(value)
if len(path.parts) < 2 or path.parts[0] != "references":
raise ValueError("reference path must stay under references/")
return path
type DocsPath = Annotated[PurePosixPath, BeforeValidator(parse_docs_path)]
type ReferencePath = Annotated[PurePosixPath, BeforeValidator(parse_reference_path)]
class ReferenceEntry(StrictFrozenModel):
"""Reference metadata for a markdown file within a skill."""
path: str
path: ReferencePath
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()
def _normalize_docs_path(path: str) -> str:
normalized = PurePosixPath(path)
if normalized.is_absolute() or ".." in normalized.parts:
raise ValueError("path must be a normalized docs-relative path")
if normalized.suffix.lower() != ".md":
raise ValueError("path must point to a markdown file")
return normalized.as_posix()
+2 -2
View File
@@ -1,6 +1,5 @@
import re
from collections.abc import Mapping
from pathlib import PurePosixPath
from typing import TYPE_CHECKING
import yaml
@@ -10,6 +9,7 @@ from pydantic import model_validator
from .common import SEMVER_RE
from .common import SKILL_ID_RE
from .common import DocsPath
from .common import StrictFrozenModel
from .common import frozen_mapping
@@ -103,7 +103,7 @@ class StoredPrompt(StrictFrozenModel):
"""Normalized prompt document content with path and frontmatter for storage in the registry."""
prompt_id: str
relpath: PurePosixPath
relpath: DocsPath
content: str
frontmatter: PromptFrontmatter
+10 -5
View File
@@ -3,17 +3,22 @@ from collections.abc import Mapping
from pydantic import Field
from pydantic import field_validator
from .common import DocsPath
from .common import StrictFrozenModel
from .common import frozen_mapping
from .prompt import PromptArgumentEntry
def _empty_docs_mapping() -> Mapping[DocsPath, str]:
return frozen_mapping()
class ReferenceRecord(StrictFrozenModel):
"""Registry record for a resolved skill reference document."""
ref_id: str
uri: str
relpath: str
relpath: DocsPath
mime_type: str
title: str | None
content: str
@@ -29,7 +34,7 @@ class SkillRecord(StrictFrozenModel):
tags: tuple[str, ...]
capabilities: tuple[str, ...]
document_uri: str
document_relpath: str
document_relpath: DocsPath
document_content: str
references: Mapping[str, ReferenceRecord] = Field(default_factory=frozen_mapping)
@@ -74,7 +79,7 @@ class PromptRecord(StrictFrozenModel):
capabilities: tuple[str, ...]
arguments: Mapping[str, PromptArgumentEntry] = Field(default_factory=frozen_mapping)
document_uri: str
document_relpath: str
document_relpath: DocsPath
document_content: str
@field_validator("arguments", mode="before")
@@ -196,8 +201,8 @@ class DocsRegistry(StrictFrozenModel):
skills_by_id: Mapping[str, SkillRecord] = Field(default_factory=frozen_mapping)
skills_in_load_order: tuple[str, ...]
skills_summary_in_load_order: tuple[SkillSummaryRecord, ...]
docs_markdown_by_path: Mapping[str, str] = Field(default_factory=frozen_mapping)
docs_markdown_path_index: tuple[str, ...]
docs_markdown_by_path: Mapping[DocsPath, str] = Field(default_factory=_empty_docs_mapping)
docs_markdown_path_index: tuple[DocsPath, ...]
tag_to_skill_ids: Mapping[str, tuple[str, ...]] = Field(default_factory=frozen_mapping)
capability_to_skill_ids: Mapping[str, tuple[str, ...]] = Field(default_factory=frozen_mapping)
prompts_by_id: Mapping[str, PromptRecord] = Field(default_factory=frozen_mapping)
+3 -3
View File
@@ -1,5 +1,4 @@
from collections.abc import Mapping
from pathlib import PurePosixPath
import yaml
from pydantic import Field
@@ -8,6 +7,7 @@ from pydantic import model_validator
from .common import SEMVER_RE
from .common import SKILL_ID_RE
from .common import DocsPath
from .common import ReferenceEntry
from .common import StrictFrozenModel
from .common import frozen_mapping
@@ -91,7 +91,7 @@ class StoredSkillReference(StrictFrozenModel):
"""Structured representation of a skill reference markdown document."""
ref_id: str
relpath: PurePosixPath
relpath: DocsPath
content: str
entry: ReferenceEntry
@@ -100,7 +100,7 @@ class StoredSkill(StrictFrozenModel):
"""Structured representation of a skill markdown document."""
skill_id: str
relpath: PurePosixPath
relpath: DocsPath
content: str
frontmatter: SkillFrontmatter
references: Mapping[str, StoredSkillReference] = Field(default_factory=frozen_mapping)
+10 -10
View File
@@ -1,4 +1,4 @@
from .models.common import _normalize_docs_path
from .models.common import parse_docs_path
from .models.registry import DocsRegistry
@@ -10,7 +10,7 @@ def read_skill_document(registry: DocsRegistry, skill_id: str) -> dict[str, str]
"id": skill.skill_id,
"uri": skill.document_uri,
"format": "markdown",
"source_path": f"docs/{skill.document_relpath}",
"source_path": f"docs/{skill.document_relpath.as_posix()}",
"content": skill.document_content,
}
@@ -32,20 +32,20 @@ def read_skill_reference(
"skill_id": skill_id,
"uri": reference.uri,
"format": "markdown",
"source_path": f"docs/{reference.relpath}",
"source_path": f"docs/{reference.relpath.as_posix()}",
"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}")
docs_path = parse_docs_path(path)
if docs_path not in registry.docs_markdown_by_path:
raise KeyError(f"unknown docs path: {docs_path.as_posix()}")
return {
"uri": f"resource://docs/{normalized_path}",
"uri": f"resource://docs/{docs_path.as_posix()}",
"format": "markdown",
"source_path": f"docs/{normalized_path}",
"content": registry.docs_markdown_by_path[normalized_path],
"source_path": f"docs/{docs_path.as_posix()}",
"content": registry.docs_markdown_by_path[docs_path],
}
@@ -57,6 +57,6 @@ def read_prompt_document(registry: DocsRegistry, prompt_id: str) -> dict[str, st
"id": prompt.prompt_id,
"uri": prompt.document_uri,
"format": "markdown",
"source_path": f"docs/{prompt.document_relpath}",
"source_path": f"docs/{prompt.document_relpath.as_posix()}",
"content": prompt.document_content,
}