172 lines
5.1 KiB
Python
172 lines
5.1 KiB
Python
from __future__ import annotations
|
|
|
|
from typing import Any
|
|
|
|
from personal_mcp.skills.document_loader import DocsRegistry, SkillRecord
|
|
|
|
DEFAULT_LIMIT = 20
|
|
MAX_LIMIT = 100
|
|
|
|
|
|
def _pattern_payload(skill: SkillRecord) -> dict[str, Any]:
|
|
return {
|
|
"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 _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
|
|
|
|
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 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)
|
|
|
|
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())
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
def search_patterns_payload(
|
|
registry: DocsRegistry,
|
|
*,
|
|
query: str = "",
|
|
tags: list[str] | None = None,
|
|
skip: int = 0,
|
|
limit: int = DEFAULT_LIMIT,
|
|
) -> dict[str, Any]:
|
|
normalized_skip = max(skip, 0)
|
|
normalized_limit = max(1, min(limit, MAX_LIMIT))
|
|
|
|
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": [_pattern_payload(skill) for skill in page],
|
|
"total": len(matches),
|
|
"skip": normalized_skip,
|
|
"limit": normalized_limit,
|
|
}
|
|
|
|
|
|
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])}
|