implemented steps 1-5
This commit is contained in:
+139
-140
@@ -1,172 +1,171 @@
|
||||
from pathlib import Path
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from fastmcp import FastMCP
|
||||
from personal_mcp.skills.document_loader import DocsRegistry, SkillRecord
|
||||
|
||||
from personal_mcp.skills.document_loader import load_skill_document_from_metadata
|
||||
|
||||
catalog_server = FastMCP("catalog")
|
||||
DEFAULT_LIMIT = 20
|
||||
MAX_LIMIT = 100
|
||||
|
||||
|
||||
def _skills_dir() -> Path:
|
||||
return Path(__file__).resolve().parents[1] / "skills"
|
||||
|
||||
|
||||
def _load_skill_registry() -> dict[str, Any]:
|
||||
registry: dict[str, Any] = {}
|
||||
for metadata_path in sorted(_skills_dir().glob("*/metadata.yaml")):
|
||||
with metadata_path.open("r", encoding="utf-8") as handle:
|
||||
metadata = yaml.safe_load(handle) or {}
|
||||
skill_key = metadata_path.parent.name
|
||||
registry[skill_key] = {
|
||||
"namespace": skill_key,
|
||||
"metadata": metadata,
|
||||
}
|
||||
return registry
|
||||
|
||||
|
||||
def _normalize_pattern(namespace: str, metadata: dict[str, Any]) -> dict[str, Any]:
|
||||
pattern_id = metadata.get("id", namespace)
|
||||
capabilities = metadata.get("capabilities", [])
|
||||
def _pattern_payload(skill: SkillRecord) -> dict[str, Any]:
|
||||
return {
|
||||
"id": pattern_id,
|
||||
"namespace": namespace,
|
||||
"name": metadata.get("name", pattern_id),
|
||||
"version": metadata.get("version", "0.1.0"),
|
||||
"description": metadata.get("description", ""),
|
||||
"tags": metadata.get("tags", []),
|
||||
"depends_on": metadata.get("depends_on", []),
|
||||
"capabilities": capabilities,
|
||||
# Expose resources explicitly for clients that treat resources as the primary interface.
|
||||
"resources": capabilities,
|
||||
"id": skill.skill_id,
|
||||
"name": skill.name,
|
||||
"version": skill.version,
|
||||
"description": skill.description,
|
||||
"tags": list(skill.tags),
|
||||
"depends_on": list(skill.depends_on),
|
||||
"capabilities": list(skill.capabilities),
|
||||
"resources": list(skill.capabilities),
|
||||
}
|
||||
|
||||
|
||||
def _normalized_patterns() -> list[dict[str, Any]]:
|
||||
registry = _load_skill_registry()
|
||||
return [
|
||||
_normalize_pattern(namespace, entry["metadata"])
|
||||
for namespace, entry in registry.items()
|
||||
def _summary_payload(skill: SkillRecord) -> dict[str, Any]:
|
||||
return {
|
||||
"id": skill.skill_id,
|
||||
"name": skill.name,
|
||||
"description": skill.description,
|
||||
"tags": list(skill.tags),
|
||||
"capabilities": list(skill.capabilities),
|
||||
"version": skill.version,
|
||||
"document_uri": skill.document_uri,
|
||||
"detail_uri": f"resource://catalog/skills/{skill.skill_id}",
|
||||
"resources": {
|
||||
"document": skill.document_uri,
|
||||
"references": [
|
||||
f"resource://skills/{skill.skill_id}/references/{ref_id}"
|
||||
for ref_id in sorted(skill.references)
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _skill_matches(
|
||||
skill: SkillRecord,
|
||||
*,
|
||||
query: str | None,
|
||||
tag: str | None,
|
||||
capability: str | None,
|
||||
) -> bool:
|
||||
if query:
|
||||
lowered = query.strip().lower()
|
||||
if lowered:
|
||||
haystack = " ".join(
|
||||
[
|
||||
skill.skill_id,
|
||||
skill.name,
|
||||
skill.description,
|
||||
" ".join(skill.tags),
|
||||
]
|
||||
).lower()
|
||||
terms = [term for term in lowered.replace("-", " ").split() if term]
|
||||
if any(term not in haystack for term in terms):
|
||||
return False
|
||||
|
||||
if tag and tag not in skill.tags:
|
||||
return False
|
||||
|
||||
if capability and capability not in skill.capabilities:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def build_skills_index_payload(
|
||||
registry: DocsRegistry,
|
||||
*,
|
||||
query: str | None = None,
|
||||
tag: str | None = None,
|
||||
capability: str | None = None,
|
||||
cursor: str | None = None,
|
||||
limit: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
normalized_limit = DEFAULT_LIMIT if limit is None else max(1, min(limit, MAX_LIMIT))
|
||||
try:
|
||||
start = 0 if cursor is None else max(0, int(cursor))
|
||||
except ValueError as exc:
|
||||
raise ValueError("cursor must be an integer string") from exc
|
||||
|
||||
ordered = [registry.skills_by_id[skill_id] for skill_id in registry.skills_in_load_order]
|
||||
matches = [
|
||||
skill
|
||||
for skill in ordered
|
||||
if _skill_matches(skill, query=query, tag=tag, capability=capability)
|
||||
]
|
||||
|
||||
page = matches[start : start + normalized_limit]
|
||||
next_cursor = start + normalized_limit
|
||||
|
||||
def _matches_query(pattern: dict[str, Any], query: str) -> bool:
|
||||
if not query:
|
||||
return True
|
||||
|
||||
lowered = query.strip().lower()
|
||||
if not lowered:
|
||||
return True
|
||||
|
||||
query_terms = [term for term in lowered.replace("-", " ").split() if term]
|
||||
if not query_terms:
|
||||
return True
|
||||
|
||||
haystack = " ".join(
|
||||
[
|
||||
str(pattern.get("id", "")),
|
||||
str(pattern.get("namespace", "")),
|
||||
str(pattern.get("name", "")),
|
||||
str(pattern.get("description", "")),
|
||||
" ".join(str(tag) for tag in pattern.get("tags", [])),
|
||||
]
|
||||
).lower()
|
||||
return all(term in haystack for term in query_terms)
|
||||
return {
|
||||
"skills": [_summary_payload(skill) for skill in page],
|
||||
"total": len(matches),
|
||||
"cursor": str(start),
|
||||
"limit": normalized_limit,
|
||||
"next_cursor": str(next_cursor) if next_cursor < len(matches) else None,
|
||||
}
|
||||
|
||||
|
||||
def _matches_tags(pattern: dict[str, Any], tags: list[str] | None) -> bool:
|
||||
if not tags:
|
||||
return True
|
||||
def build_skill_detail_payload(registry: DocsRegistry, skill_id: str) -> dict[str, Any]:
|
||||
if skill_id not in registry.skills_by_id:
|
||||
raise KeyError(skill_id)
|
||||
|
||||
requested = [tag.strip().lower() for tag in tags if tag and tag.strip()]
|
||||
if not requested:
|
||||
return True
|
||||
|
||||
pattern_tags = {str(tag).lower() for tag in pattern.get("tags", [])}
|
||||
return all(tag in pattern_tags for tag in requested)
|
||||
skill = registry.skills_by_id[skill_id]
|
||||
return {
|
||||
"id": skill.skill_id,
|
||||
"name": skill.name,
|
||||
"description": skill.description,
|
||||
"version": skill.version,
|
||||
"tags": list(skill.tags),
|
||||
"depends_on": list(skill.depends_on),
|
||||
"capabilities": list(skill.capabilities),
|
||||
"resources": {
|
||||
"document": skill.document_uri,
|
||||
"references": {
|
||||
ref_id: {
|
||||
"uri": ref.uri,
|
||||
"mime_type": ref.mime_type,
|
||||
"title": ref.title,
|
||||
"path": ref.relpath,
|
||||
}
|
||||
for ref_id, ref in sorted(skill.references.items())
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@catalog_server.resource("resource://catalog/skills_index")
|
||||
def skills_index() -> dict[str, Any]:
|
||||
"""Return a compact discovery index for all available pattern modules."""
|
||||
return {"patterns": _normalized_patterns()}
|
||||
|
||||
|
||||
@catalog_server.resource("resource://catalog/skills_details")
|
||||
def skills_details() -> dict[str, Any]:
|
||||
"""Return full metadata for all mounted pattern modules."""
|
||||
return {"patterns": _load_skill_registry()}
|
||||
|
||||
|
||||
@catalog_server.resource("resource://catalog/patterns")
|
||||
def patterns() -> dict[str, Any]:
|
||||
"""Return normalized pattern records for resource-first clients."""
|
||||
return {"patterns": _normalized_patterns()}
|
||||
|
||||
|
||||
@catalog_server.resource("resource://catalog/patterns_by_id")
|
||||
def patterns_by_id() -> dict[str, Any]:
|
||||
"""Return normalized pattern records indexed by stable pattern id."""
|
||||
indexed: dict[str, Any] = {}
|
||||
for pattern in _normalized_patterns():
|
||||
indexed[pattern["id"]] = pattern
|
||||
return {"patterns_by_id": indexed}
|
||||
|
||||
|
||||
@catalog_server.tool
|
||||
def search_patterns(
|
||||
def search_patterns_payload(
|
||||
registry: DocsRegistry,
|
||||
*,
|
||||
query: str = "",
|
||||
tags: list[str] | None = None,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
limit: int = DEFAULT_LIMIT,
|
||||
) -> dict[str, Any]:
|
||||
"""Search normalized pattern metadata with optional tags and pagination."""
|
||||
normalized_skip = max(skip, 0)
|
||||
normalized_limit = min(max(limit, 1), 100)
|
||||
normalized_limit = max(1, min(limit, MAX_LIMIT))
|
||||
|
||||
matches = [
|
||||
pattern
|
||||
for pattern in _normalized_patterns()
|
||||
if _matches_query(pattern, query) and _matches_tags(pattern, tags)
|
||||
]
|
||||
requested_tags = [tag.strip() for tag in (tags or []) if tag and tag.strip()]
|
||||
|
||||
matches: list[SkillRecord] = []
|
||||
for skill_id in registry.skills_in_load_order:
|
||||
skill = registry.skills_by_id[skill_id]
|
||||
if not _skill_matches(skill, query=query, tag=None, capability=None):
|
||||
continue
|
||||
if requested_tags and any(tag not in skill.tags for tag in requested_tags):
|
||||
continue
|
||||
matches.append(skill)
|
||||
|
||||
page = matches[normalized_skip : normalized_skip + normalized_limit]
|
||||
return {
|
||||
"patterns": page,
|
||||
"patterns": [_pattern_payload(skill) for skill in page],
|
||||
"total": len(matches),
|
||||
"skip": normalized_skip,
|
||||
"limit": normalized_limit,
|
||||
}
|
||||
|
||||
|
||||
@catalog_server.tool
|
||||
def get_pattern_by_id(id: str) -> dict[str, Any]:
|
||||
"""Return one normalized pattern by stable id."""
|
||||
for pattern in _normalized_patterns():
|
||||
if pattern["id"] == id:
|
||||
return {"found": True, "pattern": pattern}
|
||||
|
||||
return {"found": False, "id": id}
|
||||
|
||||
|
||||
@catalog_server.tool
|
||||
def get_skill_document_by_id(skill_id: str) -> dict[str, Any]:
|
||||
"""Return the canonical skill document payload for a stable skill id."""
|
||||
registry = _load_skill_registry()
|
||||
for namespace, entry in registry.items():
|
||||
metadata = entry.get("metadata", {})
|
||||
pattern_id = metadata.get("id", namespace)
|
||||
if pattern_id != skill_id:
|
||||
continue
|
||||
|
||||
return {
|
||||
"found": True,
|
||||
"document": load_skill_document_from_metadata(
|
||||
skill_id=skill_id,
|
||||
namespace=namespace,
|
||||
metadata=metadata,
|
||||
),
|
||||
}
|
||||
|
||||
return {"found": False, "id": skill_id}
|
||||
def get_pattern_by_id_payload(registry: DocsRegistry, skill_id: str) -> dict[str, Any]:
|
||||
if skill_id not in registry.skills_by_id:
|
||||
return {"found": False, "id": skill_id}
|
||||
return {"found": True, "pattern": _pattern_payload(registry.skills_by_id[skill_id])}
|
||||
|
||||
Reference in New Issue
Block a user