implemented steps 1-5
This commit is contained in:
@@ -1,3 +1,13 @@
|
||||
from personal_mcp.catalog.server import catalog_server
|
||||
from personal_mcp.catalog.server import (
|
||||
build_skill_detail_payload,
|
||||
build_skills_index_payload,
|
||||
get_pattern_by_id_payload,
|
||||
search_patterns_payload,
|
||||
)
|
||||
|
||||
__all__ = ["catalog_server"]
|
||||
__all__ = [
|
||||
"build_skill_detail_payload",
|
||||
"build_skills_index_payload",
|
||||
"get_pattern_by_id_payload",
|
||||
"search_patterns_payload",
|
||||
]
|
||||
|
||||
+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])}
|
||||
|
||||
+137
-32
@@ -1,38 +1,143 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from typing import Any
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from personal_mcp.catalog.server import catalog_server
|
||||
from personal_mcp.skills.copilot_customization.server import (
|
||||
copilot_customization_server,
|
||||
from personal_mcp.catalog.server import (
|
||||
build_skill_detail_payload,
|
||||
build_skills_index_payload,
|
||||
get_pattern_by_id_payload,
|
||||
search_patterns_payload,
|
||||
)
|
||||
from personal_mcp.skills.fastapi_async_sqlalchemy_modernization.server import (
|
||||
fastapi_async_sqlalchemy_modernization_server,
|
||||
from personal_mcp.skills.document_loader import (
|
||||
DocsRegistry,
|
||||
load_docs_registry,
|
||||
read_docs_markdown_path,
|
||||
read_skill_document,
|
||||
read_skill_reference,
|
||||
)
|
||||
from personal_mcp.skills.fastapi_uv_docker.server import fastapi_uv_docker_server
|
||||
from personal_mcp.skills.new_skill.server import new_skill_server
|
||||
from personal_mcp.skills.nicegui.server import nicegui_server
|
||||
from personal_mcp.skills.nicegui_ui_customization.server import (
|
||||
nicegui_ui_customization_server,
|
||||
)
|
||||
from personal_mcp.skills.pytest_scaffolding.server import pytest_scaffolding_server
|
||||
from personal_mcp.skills.python_logging_dictconfig.server import (
|
||||
python_logging_dictconfig_server,
|
||||
)
|
||||
from personal_mcp.skills.vscode_configuration.server import vscode_configuration_server
|
||||
from personal_mcp.skills.zensical_docs.server import zensical_docs_server
|
||||
|
||||
mcp = FastMCP("personal-mcp")
|
||||
|
||||
mcp.mount(catalog_server, namespace="catalog")
|
||||
mcp.mount(copilot_customization_server, namespace="copilot_customization")
|
||||
mcp.mount(
|
||||
fastapi_async_sqlalchemy_modernization_server,
|
||||
namespace="fastapi_async_sqlalchemy_modernization",
|
||||
DOCS_ROOT = os.getenv("PERSONAL_MCP_DOCS_ROOT", "../../docs")
|
||||
REGISTRY: DocsRegistry = load_docs_registry(
|
||||
package_anchor="personal_mcp",
|
||||
docs_root=DOCS_ROOT,
|
||||
)
|
||||
mcp.mount(new_skill_server, namespace="new_skill")
|
||||
mcp.mount(nicegui_server, namespace="nicegui")
|
||||
mcp.mount(nicegui_ui_customization_server, namespace="nicegui_ui_customization")
|
||||
mcp.mount(pytest_scaffolding_server, namespace="pytest_scaffolding")
|
||||
mcp.mount(python_logging_dictconfig_server, namespace="python_logging_dictconfig")
|
||||
mcp.mount(vscode_configuration_server, namespace="vscode_configuration")
|
||||
mcp.mount(fastapi_uv_docker_server, namespace="fastapi_uv_docker")
|
||||
mcp.mount(zensical_docs_server, namespace="zensical_docs")
|
||||
|
||||
mcp = FastMCP("personal-mcp", on_duplicate="error")
|
||||
|
||||
|
||||
def _ro_annotations() -> dict[str, bool]:
|
||||
return {
|
||||
"readOnlyHint": True,
|
||||
"idempotentHint": True,
|
||||
}
|
||||
|
||||
|
||||
@mcp.resource(
|
||||
"resource://catalog/skills_index",
|
||||
mime_type="application/json",
|
||||
tags={"catalog"},
|
||||
annotations=_ro_annotations(),
|
||||
)
|
||||
def skills_index() -> dict[str, Any]:
|
||||
return build_skills_index_payload(REGISTRY)
|
||||
|
||||
|
||||
@mcp.resource(
|
||||
"resource://catalog/skills_index{?q,tag,capability,cursor,limit}",
|
||||
mime_type="application/json",
|
||||
tags={"catalog"},
|
||||
annotations=_ro_annotations(),
|
||||
)
|
||||
def skills_index_query(
|
||||
q: str | None = None,
|
||||
tag: str | None = None,
|
||||
capability: str | None = None,
|
||||
cursor: str | None = None,
|
||||
limit: int | None = None,
|
||||
) -> dict[str, Any]:
|
||||
return build_skills_index_payload(
|
||||
REGISTRY,
|
||||
query=q,
|
||||
tag=tag,
|
||||
capability=capability,
|
||||
cursor=cursor,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@mcp.resource(
|
||||
"resource://catalog/skills/{skill_id}",
|
||||
mime_type="application/json",
|
||||
tags={"catalog"},
|
||||
annotations=_ro_annotations(),
|
||||
)
|
||||
def skill_detail(skill_id: str) -> dict[str, Any]:
|
||||
return build_skill_detail_payload(REGISTRY, skill_id)
|
||||
|
||||
|
||||
@mcp.resource(
|
||||
"resource://skills/{skill_id}/document",
|
||||
mime_type="text/markdown",
|
||||
tags={"skill-doc"},
|
||||
annotations=_ro_annotations(),
|
||||
)
|
||||
def skill_document(skill_id: str) -> dict[str, str]:
|
||||
return read_skill_document(REGISTRY, skill_id)
|
||||
|
||||
|
||||
@mcp.resource(
|
||||
"resource://skills/{skill_id}/references/{ref_id}",
|
||||
mime_type="text/markdown",
|
||||
tags={"reference"},
|
||||
annotations=_ro_annotations(),
|
||||
)
|
||||
def skill_reference(skill_id: str, ref_id: str) -> dict[str, str]:
|
||||
return read_skill_reference(REGISTRY, skill_id=skill_id, ref_id=ref_id)
|
||||
|
||||
|
||||
@mcp.resource(
|
||||
"resource://docs/{path*}",
|
||||
mime_type="text/markdown",
|
||||
tags={"docs"},
|
||||
annotations=_ro_annotations(),
|
||||
)
|
||||
def docs_markdown(path: str) -> dict[str, str]:
|
||||
return read_docs_markdown_path(REGISTRY, path)
|
||||
|
||||
|
||||
@mcp.tool
|
||||
def search_patterns(
|
||||
query: str = "",
|
||||
tags: list[str] | None = None,
|
||||
skip: int = 0,
|
||||
limit: int = 20,
|
||||
) -> dict[str, Any]:
|
||||
"""Search normalized pattern metadata with optional tags and pagination."""
|
||||
return search_patterns_payload(
|
||||
REGISTRY,
|
||||
query=query,
|
||||
tags=tags,
|
||||
skip=skip,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@mcp.tool
|
||||
def get_pattern_by_id(id: str) -> dict[str, Any]:
|
||||
"""Return one normalized pattern by stable id."""
|
||||
return get_pattern_by_id_payload(REGISTRY, id)
|
||||
|
||||
|
||||
@mcp.tool
|
||||
def get_skill_document_by_id(skill_id: str) -> dict[str, Any]:
|
||||
"""Return the canonical skill document payload for a stable skill id."""
|
||||
if skill_id not in REGISTRY.skills_by_id:
|
||||
return {"found": False, "id": skill_id}
|
||||
|
||||
return {
|
||||
"found": True,
|
||||
"document": read_skill_document(REGISTRY, skill_id),
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""Mounted skill servers for the personal MCP app."""
|
||||
"""Docs registry and markdown loading utilities for personal MCP skills."""
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
id: copilot-customization
|
||||
name: Copilot Customization
|
||||
version: 1.0.0
|
||||
description: Plan, create, review, and debug GitHub Copilot and VS Code agent customizations for this repository.
|
||||
tags:
|
||||
- copilot
|
||||
- vscode
|
||||
- customization
|
||||
- instructions
|
||||
- prompts
|
||||
- agent-skills
|
||||
- custom-agents
|
||||
- hooks
|
||||
- mcp
|
||||
- personal-mcp
|
||||
- skills
|
||||
capabilities:
|
||||
- resource://skills/copilot-customization/document
|
||||
depends_on:
|
||||
- new-skill
|
||||
- zensical-docs
|
||||
@@ -1,14 +0,0 @@
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from personal_mcp.skills.document_loader import load_skill_document
|
||||
|
||||
copilot_customization_server = FastMCP("copilot-customization")
|
||||
|
||||
|
||||
@copilot_customization_server.resource("resource://skills/copilot-customization/document")
|
||||
def skill_document() -> dict[str, str]:
|
||||
"""Return the canonical Markdown document for this skill."""
|
||||
return load_skill_document(
|
||||
skill_id="copilot-customization",
|
||||
skill_slug="copilot-customization",
|
||||
)
|
||||
@@ -1,74 +1,559 @@
|
||||
from pathlib import Path
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from importlib.resources import files
|
||||
from importlib.resources.abc import Traversable
|
||||
from pathlib import PurePosixPath
|
||||
from typing import Any
|
||||
|
||||
import yaml
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError, field_validator
|
||||
|
||||
def _repo_root() -> Path:
|
||||
return Path(__file__).resolve().parents[3]
|
||||
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.-]+)?$"
|
||||
)
|
||||
|
||||
|
||||
def resolve_skill_document_path(
|
||||
*, skill_id: str, namespace: str, metadata: dict[str, Any]
|
||||
) -> Path:
|
||||
"""Resolve the canonical Markdown document path for a skill."""
|
||||
document_path = metadata.get("document_path")
|
||||
if isinstance(document_path, str) and document_path.strip():
|
||||
return _repo_root() / document_path.strip()
|
||||
@dataclass(frozen=True)
|
||||
class RegistryIssue:
|
||||
code: str
|
||||
message: str
|
||||
skill_id: str | None
|
||||
path: str
|
||||
hint: str
|
||||
|
||||
candidates: list[str] = []
|
||||
slug = metadata.get("slug")
|
||||
if isinstance(slug, str) and slug.strip():
|
||||
candidates.append(slug.strip())
|
||||
|
||||
candidates.extend(
|
||||
[
|
||||
skill_id,
|
||||
namespace.replace("_", "-"),
|
||||
namespace,
|
||||
]
|
||||
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 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")
|
||||
|
||||
seen: set[str] = set()
|
||||
for candidate in candidates:
|
||||
if candidate in seen:
|
||||
@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 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, ...]
|
||||
]
|
||||
|
||||
|
||||
def _parse_frontmatter(markdown: str, *, path: str) -> tuple[dict[str, Any], str]:
|
||||
if not markdown.startswith("---"):
|
||||
raise ValueError(f"missing YAML frontmatter: {path}")
|
||||
|
||||
lines = markdown.splitlines()
|
||||
if len(lines) < 3 or lines[0].strip() != "---":
|
||||
raise ValueError(f"invalid YAML frontmatter start: {path}")
|
||||
|
||||
end_index: int | None = None
|
||||
for i in range(1, len(lines)):
|
||||
if lines[i].strip() == "---":
|
||||
end_index = i
|
||||
break
|
||||
|
||||
if end_index is None:
|
||||
raise ValueError(f"missing YAML frontmatter terminator: {path}")
|
||||
|
||||
raw_yaml = "\n".join(lines[1:end_index])
|
||||
body = "\n".join(lines[end_index + 1 :])
|
||||
parsed = yaml.safe_load(raw_yaml)
|
||||
if not isinstance(parsed, dict):
|
||||
raise ValueError(f"frontmatter must parse to an object: {path}")
|
||||
return parsed, body
|
||||
|
||||
|
||||
def _walk_markdown(
|
||||
node: Traversable,
|
||||
*,
|
||||
prefix: PurePosixPath = PurePosixPath(""),
|
||||
) -> list[tuple[str, Traversable]]:
|
||||
results: list[tuple[str, Traversable]] = []
|
||||
for child in sorted(node.iterdir(), key=lambda item: item.name):
|
||||
relpath = prefix.joinpath(child.name)
|
||||
if child.is_dir():
|
||||
results.extend(_walk_markdown(child, prefix=relpath))
|
||||
continue
|
||||
seen.add(candidate)
|
||||
|
||||
candidate_path = _repo_root() / "docs" / "skills" / candidate / "SKILL.md"
|
||||
if candidate_path.exists():
|
||||
return candidate_path
|
||||
|
||||
return _repo_root() / "docs" / "skills" / skill_id / "SKILL.md"
|
||||
if not child.is_file() or not child.name.lower().endswith(".md"):
|
||||
continue
|
||||
results.append((relpath.as_posix(), child))
|
||||
return results
|
||||
|
||||
|
||||
def load_markdown_document(*, skill_id: str, document_path: Path) -> dict[str, str]:
|
||||
"""Load an arbitrary Markdown document and expose it as a skill resource."""
|
||||
if not document_path.exists():
|
||||
raise FileNotFoundError(
|
||||
f"Missing skill document for '{skill_id}': {document_path}"
|
||||
def _validate_skill_frontmatter(raw: dict[str, Any], *, skill_dir_name: str) -> SkillFrontmatter:
|
||||
model = SkillFrontmatter.model_validate(raw)
|
||||
if model.name != skill_dir_name:
|
||||
raise ValueError("frontmatter name must exactly match skill directory name")
|
||||
if model.x_personal_mcp.id != model.name:
|
||||
raise ValueError("x-personal-mcp.id must exactly match name")
|
||||
expected_capability = f"resource://skills/{model.name}/document"
|
||||
if expected_capability not in model.x_personal_mcp.capabilities:
|
||||
raise ValueError(f"capabilities must include {expected_capability}")
|
||||
return model
|
||||
|
||||
|
||||
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()
|
||||
|
||||
|
||||
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] = []
|
||||
|
||||
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
|
||||
|
||||
references: dict[str, ReferenceRecord] = {}
|
||||
for ref_id, ref_entry in frontmatter.x_personal_mcp.references.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,
|
||||
)
|
||||
)
|
||||
|
||||
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)
|
||||
|
||||
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)
|
||||
|
||||
tag_index: dict[str, list[str]] = {}
|
||||
capability_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)
|
||||
|
||||
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())
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
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_id,
|
||||
"uri": f"resource://skills/{skill_id}/document",
|
||||
"id": skill.skill_id,
|
||||
"uri": skill.document_uri,
|
||||
"format": "markdown",
|
||||
"source_path": str(document_path),
|
||||
"content": document_path.read_text(encoding="utf-8"),
|
||||
"source_path": f"docs/{skill.document_relpath}",
|
||||
"content": skill.document_content,
|
||||
}
|
||||
|
||||
|
||||
def load_skill_document(*, skill_id: str, skill_slug: str) -> dict[str, str]:
|
||||
"""Load the canonical skill markdown document for an MCP skill."""
|
||||
document_path = _repo_root() / "docs" / "skills" / skill_slug / "SKILL.md"
|
||||
return load_markdown_document(skill_id=skill_id, document_path=document_path)
|
||||
|
||||
|
||||
def load_skill_document_from_metadata(
|
||||
*, skill_id: str, namespace: str, metadata: dict[str, Any]
|
||||
def read_skill_reference(
|
||||
registry: DocsRegistry,
|
||||
*,
|
||||
skill_id: str,
|
||||
ref_id: str,
|
||||
) -> dict[str, str]:
|
||||
"""Load a skill document using metadata overrides when present."""
|
||||
document_path = resolve_skill_document_path(
|
||||
skill_id=skill_id,
|
||||
namespace=namespace,
|
||||
metadata=metadata,
|
||||
)
|
||||
return load_markdown_document(skill_id=skill_id, document_path=document_path)
|
||||
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],
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
"""FastAPI async SQLAlchemy modernization skill server."""
|
||||
@@ -1,12 +0,0 @@
|
||||
id: fastapi-async-sqlalchemy-modernization
|
||||
name: FastAPI Async SQLAlchemy Modernization
|
||||
version: 1.0.0
|
||||
description: Create a step-by-step modernization plan for an existing FastAPI app using SQLAlchemy async patterns.
|
||||
tags:
|
||||
- fastapi
|
||||
- sqlalchemy
|
||||
- async
|
||||
- modernization
|
||||
capabilities:
|
||||
- resource://skills/fastapi-async-sqlalchemy-modernization/document
|
||||
depends_on: []
|
||||
@@ -1,18 +0,0 @@
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from personal_mcp.skills.document_loader import load_skill_document
|
||||
|
||||
fastapi_async_sqlalchemy_modernization_server = FastMCP(
|
||||
"fastapi-async-sqlalchemy-modernization"
|
||||
)
|
||||
|
||||
|
||||
@fastapi_async_sqlalchemy_modernization_server.resource(
|
||||
"resource://skills/fastapi-async-sqlalchemy-modernization/document"
|
||||
)
|
||||
def skill_document() -> dict[str, str]:
|
||||
"""Return the canonical Markdown document for this skill."""
|
||||
return load_skill_document(
|
||||
skill_id="fastapi-async-sqlalchemy-modernization",
|
||||
skill_slug="fastapi-async-sqlalchemy-modernization",
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
from personal_mcp.skills.fastapi_uv_docker.server import fastapi_uv_docker_server
|
||||
|
||||
__all__ = ["fastapi_uv_docker_server"]
|
||||
@@ -1,11 +0,0 @@
|
||||
id: fastapi-uv-docker
|
||||
name: FastAPI uv Docker
|
||||
version: 1.0.0
|
||||
description: Provide fast migration guidance to FastAPI plus uv plus Docker.
|
||||
tags:
|
||||
- fastapi
|
||||
- uv
|
||||
- docker
|
||||
capabilities:
|
||||
- resource://skills/fastapi-uv-docker/document
|
||||
depends_on: []
|
||||
@@ -1,14 +0,0 @@
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from personal_mcp.skills.document_loader import load_skill_document
|
||||
|
||||
fastapi_uv_docker_server = FastMCP("fastapi-uv-docker")
|
||||
|
||||
|
||||
@fastapi_uv_docker_server.resource("resource://skills/fastapi-uv-docker/document")
|
||||
def skill_document() -> dict[str, str]:
|
||||
"""Return the canonical Markdown document for this skill."""
|
||||
return load_skill_document(
|
||||
skill_id="fastapi-uv-docker",
|
||||
skill_slug="fastapi-uv-docker",
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
"""Pseudo-skill exposing the new skill bootstrap guide."""
|
||||
@@ -1,14 +0,0 @@
|
||||
id: new-skill
|
||||
name: New Skill Bootstrap
|
||||
version: 1.0.0
|
||||
description: Provide the bootstrap checklist and templates for creating new MCP skills.
|
||||
tags:
|
||||
- fastmcp
|
||||
- bootstrap
|
||||
- scaffolding
|
||||
- skills
|
||||
- mcp
|
||||
document_path: docs/new_skill.md
|
||||
capabilities:
|
||||
- resource://skills/new-skill/document
|
||||
depends_on: []
|
||||
@@ -1,21 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from personal_mcp.skills.document_loader import load_skill_document_from_metadata
|
||||
|
||||
new_skill_server = FastMCP("new-skill")
|
||||
_METADATA_PATH = Path(__file__).with_name("metadata.yaml")
|
||||
_METADATA = yaml.safe_load(_METADATA_PATH.read_text(encoding="utf-8")) or {}
|
||||
|
||||
|
||||
@new_skill_server.resource("resource://skills/new-skill/document")
|
||||
def skill_document() -> dict[str, str]:
|
||||
"""Return the bootstrap guide used to scaffold new skills."""
|
||||
return load_skill_document_from_metadata(
|
||||
skill_id="new-skill",
|
||||
namespace="new_skill",
|
||||
metadata=_METADATA,
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
"""NiceGUI architecture skill server."""
|
||||
@@ -1,12 +0,0 @@
|
||||
id: nicegui
|
||||
name: NiceGUI
|
||||
version: 1.0.0
|
||||
description: Design and scaffold a production-ready NiceGUI plus FastAPI application architecture.
|
||||
tags:
|
||||
- nicegui
|
||||
- fastapi
|
||||
- ui
|
||||
- architecture
|
||||
capabilities:
|
||||
- resource://skills/nicegui/document
|
||||
depends_on: []
|
||||
@@ -1,11 +0,0 @@
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from personal_mcp.skills.document_loader import load_skill_document
|
||||
|
||||
nicegui_server = FastMCP("nicegui")
|
||||
|
||||
|
||||
@nicegui_server.resource("resource://skills/nicegui/document")
|
||||
def skill_document() -> dict[str, str]:
|
||||
"""Return the canonical Markdown document for this skill."""
|
||||
return load_skill_document(skill_id="nicegui", skill_slug="nicegui")
|
||||
@@ -1 +0,0 @@
|
||||
"""NiceGUI UI customization skill server."""
|
||||
@@ -1,12 +0,0 @@
|
||||
id: nicegui-ui-customization
|
||||
name: NiceGUI UI Customization
|
||||
version: 1.0.0
|
||||
description: Design and implement production NiceGUI interfaces with reusable components and event-driven interactions.
|
||||
tags:
|
||||
- nicegui
|
||||
- ui
|
||||
- customization
|
||||
- frontend
|
||||
capabilities:
|
||||
- resource://skills/nicegui-ui-customization/document
|
||||
depends_on: []
|
||||
@@ -1,16 +0,0 @@
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from personal_mcp.skills.document_loader import load_skill_document
|
||||
|
||||
nicegui_ui_customization_server = FastMCP("nicegui-ui-customization")
|
||||
|
||||
|
||||
@nicegui_ui_customization_server.resource(
|
||||
"resource://skills/nicegui-ui-customization/document"
|
||||
)
|
||||
def skill_document() -> dict[str, str]:
|
||||
"""Return the canonical Markdown document for this skill."""
|
||||
return load_skill_document(
|
||||
skill_id="nicegui-ui-customization",
|
||||
skill_slug="nicegui-ui-customization",
|
||||
)
|
||||
@@ -1,3 +0,0 @@
|
||||
from personal_mcp.skills.pytest_scaffolding.server import pytest_scaffolding_server
|
||||
|
||||
__all__ = ["pytest_scaffolding_server"]
|
||||
@@ -1,11 +0,0 @@
|
||||
id: pytest-scaffolding
|
||||
name: Pytest Scaffolding
|
||||
version: 1.0.0
|
||||
description: Scaffold a maintainable pytest structure quickly.
|
||||
tags:
|
||||
- pytest
|
||||
- testing
|
||||
- python
|
||||
capabilities:
|
||||
- resource://skills/pytest-scaffolding/document
|
||||
depends_on: []
|
||||
@@ -1,14 +0,0 @@
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from personal_mcp.skills.document_loader import load_skill_document
|
||||
|
||||
pytest_scaffolding_server = FastMCP("pytest-scaffolding")
|
||||
|
||||
|
||||
@pytest_scaffolding_server.resource("resource://skills/pytest-scaffolding/document")
|
||||
def skill_document() -> dict[str, str]:
|
||||
"""Return the canonical Markdown document for this skill."""
|
||||
return load_skill_document(
|
||||
skill_id="pytest-scaffolding",
|
||||
skill_slug="pytest-scaffolding",
|
||||
)
|
||||
@@ -1,5 +0,0 @@
|
||||
from personal_mcp.skills.python_logging_dictconfig.server import (
|
||||
python_logging_dictconfig_server,
|
||||
)
|
||||
|
||||
__all__ = ["python_logging_dictconfig_server"]
|
||||
@@ -1,11 +0,0 @@
|
||||
id: python-logging-dictconfig
|
||||
name: Python Logging DictConfig
|
||||
version: 1.0.0
|
||||
description: Provide minimal logging.config.dictConfig setup guidance.
|
||||
tags:
|
||||
- logging
|
||||
- python
|
||||
- observability
|
||||
capabilities:
|
||||
- resource://skills/python-logging-dictconfig/document
|
||||
depends_on: []
|
||||
@@ -1,16 +0,0 @@
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from personal_mcp.skills.document_loader import load_skill_document
|
||||
|
||||
python_logging_dictconfig_server = FastMCP("python-logging-dictconfig")
|
||||
|
||||
|
||||
@python_logging_dictconfig_server.resource(
|
||||
"resource://skills/python-logging-dictconfig/document"
|
||||
)
|
||||
def skill_document() -> dict[str, str]:
|
||||
"""Return the canonical Markdown document for this skill."""
|
||||
return load_skill_document(
|
||||
skill_id="python-logging-dictconfig",
|
||||
skill_slug="python-logging-dictconfig",
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
"""VS Code configuration skill package."""
|
||||
@@ -1,15 +0,0 @@
|
||||
id: vscode-configuration
|
||||
name: VS Code Configuration
|
||||
version: 1.0.0
|
||||
description: Create and troubleshoot VS Code launch and task configuration for Python and FastAPI projects.
|
||||
tags:
|
||||
- vscode
|
||||
- launch-json
|
||||
- tasks-json
|
||||
- debugpy
|
||||
- fastapi
|
||||
- python
|
||||
- skills
|
||||
capabilities:
|
||||
- resource://skills/vscode-configuration/document
|
||||
depends_on: []
|
||||
@@ -1,20 +0,0 @@
|
||||
from pathlib import Path
|
||||
|
||||
import yaml
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from personal_mcp.skills.document_loader import load_skill_document_from_metadata
|
||||
|
||||
vscode_configuration_server = FastMCP("vscode-configuration")
|
||||
_METADATA_PATH = Path(__file__).with_name("metadata.yaml")
|
||||
_METADATA = yaml.safe_load(_METADATA_PATH.read_text(encoding="utf-8")) or {}
|
||||
|
||||
|
||||
@vscode_configuration_server.resource("resource://skills/vscode-configuration/document")
|
||||
def skill_document() -> dict[str, str]:
|
||||
"""Return the canonical Markdown document for this skill."""
|
||||
return load_skill_document_from_metadata(
|
||||
skill_id="vscode-configuration",
|
||||
namespace="vscode_configuration",
|
||||
metadata=_METADATA,
|
||||
)
|
||||
@@ -1 +0,0 @@
|
||||
"""Zensical documentation authoring skill server."""
|
||||
@@ -1,19 +0,0 @@
|
||||
id: zensical-docs
|
||||
name: Zensical Documentation Authoring
|
||||
version: 1.0.0
|
||||
description: Plan, write, and improve high-quality documentation with Zensical.
|
||||
tags:
|
||||
- zensical
|
||||
- mkdocs
|
||||
- mkdocs-material
|
||||
- mkdocstrings
|
||||
- docs
|
||||
- documentation
|
||||
- information-architecture
|
||||
- skills
|
||||
- bootstrap
|
||||
- discovery
|
||||
- authoring
|
||||
capabilities:
|
||||
- resource://skills/zensical-docs/document
|
||||
depends_on: []
|
||||
@@ -1,14 +0,0 @@
|
||||
from fastmcp import FastMCP
|
||||
|
||||
from personal_mcp.skills.document_loader import load_skill_document
|
||||
|
||||
zensical_docs_server = FastMCP("zensical-docs")
|
||||
|
||||
|
||||
@zensical_docs_server.resource("resource://skills/zensical-docs/document")
|
||||
def skill_document() -> dict[str, str]:
|
||||
"""Return the canonical Markdown document for this skill."""
|
||||
return load_skill_document(
|
||||
skill_id="zensical-docs",
|
||||
skill_slug="zensical-docs",
|
||||
)
|
||||
Reference in New Issue
Block a user