From d54f427112c3496ee7393d8c873e2d54e1c6f8b6 Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Thu, 18 Jun 2026 19:38:57 -0500 Subject: [PATCH] resource pattern --- pyproject.toml | 1 + src/personal_mcp/catalog/__init__.py | 3 + src/personal_mcp/catalog/server.py | 50 +++++++++++++ src/personal_mcp/main.py | 2 + .../skills/fastapi_uv_docker/metadata.yaml | 7 ++ .../skills/fastapi_uv_docker/server.py | 72 ++++++++++++++++++ .../skills/pytest_scaffolding/metadata.yaml | 7 ++ .../skills/pytest_scaffolding/server.py | 75 +++++++++++++++++++ .../python_logging_dictconfig/metadata.yaml | 7 ++ .../python_logging_dictconfig/server.py | 64 ++++++++++++++++ uv.lock | 2 + 11 files changed, 290 insertions(+) create mode 100644 src/personal_mcp/catalog/__init__.py create mode 100644 src/personal_mcp/catalog/server.py diff --git a/pyproject.toml b/pyproject.toml index 527f145..d411fb0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,6 +4,7 @@ version = "0.1.0" requires-python = ">=3.12" dependencies = [ "fastmcp>=2.10.0", + "pyyaml>=6.0.2", "zensical>=0.0.45", ] diff --git a/src/personal_mcp/catalog/__init__.py b/src/personal_mcp/catalog/__init__.py new file mode 100644 index 0000000..5504177 --- /dev/null +++ b/src/personal_mcp/catalog/__init__.py @@ -0,0 +1,3 @@ +from personal_mcp.catalog.server import catalog_server + +__all__ = ["catalog_server"] diff --git a/src/personal_mcp/catalog/server.py b/src/personal_mcp/catalog/server.py new file mode 100644 index 0000000..e874235 --- /dev/null +++ b/src/personal_mcp/catalog/server.py @@ -0,0 +1,50 @@ +from pathlib import Path +from typing import Any + +import yaml +from fastmcp import FastMCP + +catalog_server = FastMCP("catalog") + + +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 + + +@catalog_server.resource("resource://catalog/skills_index") +def skills_index() -> dict[str, Any]: + """Return a compact discovery index for all available pattern modules.""" + registry = _load_skill_registry() + index = [] + for key, entry in registry.items(): + metadata = entry["metadata"] + index.append( + { + "namespace": key, + "id": metadata.get("id", key), + "name": metadata.get("name", key), + "version": metadata.get("version", "0.1.0"), + "capabilities": metadata.get("capabilities", []), + "tags": metadata.get("tags", []), + } + ) + return {"patterns": index} + + +@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()} diff --git a/src/personal_mcp/main.py b/src/personal_mcp/main.py index 06bac1c..ab8b43e 100644 --- a/src/personal_mcp/main.py +++ b/src/personal_mcp/main.py @@ -1,5 +1,6 @@ from fastmcp import FastMCP +from personal_mcp.catalog.server import catalog_server from personal_mcp.skills.fastapi_uv_docker.server import fastapi_uv_docker_server from personal_mcp.skills.pytest_scaffolding.server import pytest_scaffolding_server from personal_mcp.skills.python_logging_dictconfig.server import ( @@ -8,6 +9,7 @@ from personal_mcp.skills.python_logging_dictconfig.server import ( mcp = FastMCP("personal-mcp") +mcp.mount(catalog_server, namespace="catalog") mcp.mount(pytest_scaffolding_server, namespace="pytest_scaffolding") mcp.mount(python_logging_dictconfig_server, namespace="python_logging_dictconfig") mcp.mount(fastapi_uv_docker_server, namespace="fastapi_uv_docker") diff --git a/src/personal_mcp/skills/fastapi_uv_docker/metadata.yaml b/src/personal_mcp/skills/fastapi_uv_docker/metadata.yaml index eaada3a..1a7af7b 100644 --- a/src/personal_mcp/skills/fastapi_uv_docker/metadata.yaml +++ b/src/personal_mcp/skills/fastapi_uv_docker/metadata.yaml @@ -1,7 +1,14 @@ 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/overview + - resource://skills/fastapi_uv_docker/rules + - resource://skills/fastapi_uv_docker/checklist + - resource://skills/fastapi_uv_docker/references +depends_on: [] diff --git a/src/personal_mcp/skills/fastapi_uv_docker/server.py b/src/personal_mcp/skills/fastapi_uv_docker/server.py index 728525b..3cd7e68 100644 --- a/src/personal_mcp/skills/fastapi_uv_docker/server.py +++ b/src/personal_mcp/skills/fastapi_uv_docker/server.py @@ -1,7 +1,79 @@ +from pathlib import Path + from fastmcp import FastMCP fastapi_uv_docker_server = FastMCP("fastapi-uv-docker") +_REFERENCE_DIR = ( + Path(__file__).resolve().parents[4] + / "skills" + / "fastapi-uv-docker" + / "references" +) +_REFERENCE_FILES = { + "fastapi_best_practices": _REFERENCE_DIR / "fastapi-best-practices.md", + "uv_project_layout": _REFERENCE_DIR / "uv-project-layout.md", + "uvicorn_settings": _REFERENCE_DIR / "uvicorn-settings.md", + "docker_cloud_native": _REFERENCE_DIR / "docker-cloud-native.md", +} + + +def _load_reference_bundle() -> dict[str, str]: + bundle: dict[str, str] = {} + for key, path in _REFERENCE_FILES.items(): + bundle[key] = path.read_text(encoding="utf-8") + return bundle + + +@fastapi_uv_docker_server.resource("resource://skills/fastapi_uv_docker/overview") +def fastapi_overview() -> dict: + """Return high-level intent for FastAPI plus uv plus Docker migration.""" + return { + "id": "fastapi-uv-docker", + "intent": "Migrate projects toward cloud-native FastAPI architecture managed by uv.", + "focus": [ + "src package layout", + "uv-based dependency and lock management", + "container-ready runtime conventions", + ], + } + + +@fastapi_uv_docker_server.resource("resource://skills/fastapi_uv_docker/rules") +def fastapi_rules() -> dict: + """Return migration rules for stable FastAPI service architecture.""" + return { + "rules": [ + "Prefer src layout and installable package boundaries.", + "Use app factory and lifespan hooks for startup/shutdown ownership.", + "Keep runtime configuration in environment-backed settings.", + "Use uv lockfile and avoid drift between local and container environments.", + ] + } + + +@fastapi_uv_docker_server.resource("resource://skills/fastapi_uv_docker/checklist") +def fastapi_checklist() -> dict: + """Return a compact migration checklist for FastAPI plus uv plus Docker.""" + return { + "checklist": [ + "Audit current project manager, layout, and runtime settings.", + "Establish uv project metadata and lockfile ownership.", + "Implement app factory, lifespan, and health endpoints.", + "Create multi-stage Docker image with non-root runtime user.", + ] + } + + +@fastapi_uv_docker_server.resource("resource://skills/fastapi_uv_docker/references") +def fastapi_references() -> dict: + """Return bundled references for detailed implementation guidance.""" + return { + "sources": {key: str(path) for key, path in _REFERENCE_FILES.items()}, + "format": "markdown", + "content": _load_reference_bundle(), + } + @fastapi_uv_docker_server.tool() def fastapi_uv_docker_mvp_checklist(current_state: str = "bare python project") -> list[str]: diff --git a/src/personal_mcp/skills/pytest_scaffolding/metadata.yaml b/src/personal_mcp/skills/pytest_scaffolding/metadata.yaml index 42c0dd3..6883d8e 100644 --- a/src/personal_mcp/skills/pytest_scaffolding/metadata.yaml +++ b/src/personal_mcp/skills/pytest_scaffolding/metadata.yaml @@ -1,7 +1,14 @@ 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/overview + - resource://skills/pytest_scaffolding/rules + - resource://skills/pytest_scaffolding/checklist + - resource://skills/pytest_scaffolding/references +depends_on: [] diff --git a/src/personal_mcp/skills/pytest_scaffolding/server.py b/src/personal_mcp/skills/pytest_scaffolding/server.py index 00f4a51..bdfdf50 100644 --- a/src/personal_mcp/skills/pytest_scaffolding/server.py +++ b/src/personal_mcp/skills/pytest_scaffolding/server.py @@ -1,7 +1,82 @@ +from pathlib import Path + from fastmcp import FastMCP pytest_scaffolding_server = FastMCP("pytest-scaffolding") +_REFERENCE_PATH = ( + Path(__file__).resolve().parents[4] + / "skills" + / "pytest-scaffolding" + / "references" + / "pytest-docs.md" +) + + +def _load_reference_text() -> str: + return _REFERENCE_PATH.read_text(encoding="utf-8") + + +@pytest_scaffolding_server.resource("resource://skills/pytest_scaffolding/overview") +def pytest_overview() -> dict: + """Return the high-level intent and boundaries of this pattern module.""" + return { + "id": "pytest-scaffolding", + "intent": "Design maintainable pytest structure before deep test implementation.", + "focus": [ + "hierarchical test layout", + "dependency-aware fixture boundaries", + "fast local feedback loops", + ], + } + + +@pytest_scaffolding_server.resource("resource://skills/pytest_scaffolding/rules") +def pytest_rules() -> dict: + """Return method rules to keep test architecture consistent.""" + return { + "rules": [ + "Mirror major source package boundaries under tests/.", + "Use explicit markers for integration, smoke, and external tests.", + "Keep global fixtures lightweight; place expensive fixtures in subtree conftest files.", + "Prefer deterministic unit tests and isolate slow or external dependencies.", + ] + } + + +@pytest_scaffolding_server.resource("resource://skills/pytest_scaffolding/checklist") +def pytest_checklist() -> dict: + """Return an execution checklist for first-pass test scaffolding.""" + return { + "checklist": [ + "Map source modules to initial test modules.", + "Classify each module as unit, integration, or smoke.", + "Create baseline fixtures in tests/conftest.py.", + "Register markers in pyproject.toml.", + "Validate collection and run fast path first.", + ] + } + + +@pytest_scaffolding_server.resource("resource://skills/pytest_scaffolding/references") +def pytest_references() -> dict: + """Return canonical reference material for this pattern module.""" + return { + "source": str(_REFERENCE_PATH), + "format": "markdown", + "content": _load_reference_text(), + } + + +@pytest_scaffolding_server.prompt() +def scaffold_pytest_prompt(target_scope: str = "src/") -> str: + """Prompt template for planning pytest scaffolding for a target scope.""" + return ( + "Create a minimal pytest scaffold plan. " + f"Target scope: {target_scope}. " + "Return directory mapping, marker suggestions, and the first three tests to write." + ) + @pytest_scaffolding_server.tool() def propose_pytest_mvp_tree(target_scope: str = "src/") -> str: diff --git a/src/personal_mcp/skills/python_logging_dictconfig/metadata.yaml b/src/personal_mcp/skills/python_logging_dictconfig/metadata.yaml index fcd18ee..f1ad7cd 100644 --- a/src/personal_mcp/skills/python_logging_dictconfig/metadata.yaml +++ b/src/personal_mcp/skills/python_logging_dictconfig/metadata.yaml @@ -1,7 +1,14 @@ 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/overview + - resource://skills/python_logging_dictconfig/rules + - resource://skills/python_logging_dictconfig/checklist + - resource://skills/python_logging_dictconfig/references +depends_on: [] diff --git a/src/personal_mcp/skills/python_logging_dictconfig/server.py b/src/personal_mcp/skills/python_logging_dictconfig/server.py index 2821957..aa12a58 100644 --- a/src/personal_mcp/skills/python_logging_dictconfig/server.py +++ b/src/personal_mcp/skills/python_logging_dictconfig/server.py @@ -1,7 +1,71 @@ +from pathlib import Path + from fastmcp import FastMCP python_logging_dictconfig_server = FastMCP("python-logging-dictconfig") +_REFERENCE_PATH = ( + Path(__file__).resolve().parents[4] + / "skills" + / "python-logging-dictconfig" + / "references" + / "python-logging-docs.md" +) + + +def _load_reference_text() -> str: + return _REFERENCE_PATH.read_text(encoding="utf-8") + + +@python_logging_dictconfig_server.resource("resource://skills/python_logging_dictconfig/overview") +def logging_overview() -> dict: + """Return high-level purpose and intended outcomes for logging configuration.""" + return { + "id": "python-logging-dictconfig", + "intent": "Centralize Python logging setup with dictConfig during application startup.", + "focus": [ + "single startup configuration", + "consistent formatting and level policy", + "named module loggers without local configuration", + ], + } + + +@python_logging_dictconfig_server.resource("resource://skills/python_logging_dictconfig/rules") +def logging_rules() -> dict: + """Return implementation rules for predictable logging behavior.""" + return { + "rules": [ + "Call dictConfig once at startup, not in leaf modules.", + "Use logging.getLogger(__name__) in all modules.", + "Avoid basicConfig in application packages.", + "Set disable_existing_loggers to False unless intentional suppression is required.", + ] + } + + +@python_logging_dictconfig_server.resource("resource://skills/python_logging_dictconfig/checklist") +def logging_checklist() -> dict: + """Return a startup checklist for logging configuration rollout.""" + return { + "checklist": [ + "Define formatter, handlers, and root logger in one LOGGING dict.", + "Wire configure_logging() in startup path only once.", + "Verify expected output at DEBUG/INFO/WARNING/ERROR levels.", + "Tune noisy third-party loggers intentionally.", + ] + } + + +@python_logging_dictconfig_server.resource("resource://skills/python_logging_dictconfig/references") +def logging_references() -> dict: + """Return canonical reference material for dictConfig-based logging.""" + return { + "source": str(_REFERENCE_PATH), + "format": "markdown", + "content": _load_reference_text(), + } + @python_logging_dictconfig_server.tool() def logging_dictconfig_template(level: str = "INFO") -> dict: diff --git a/uv.lock b/uv.lock index 4daeba7..38ce7a4 100644 --- a/uv.lock +++ b/uv.lock @@ -752,12 +752,14 @@ version = "0.1.0" source = { editable = "." } dependencies = [ { name = "fastmcp" }, + { name = "pyyaml" }, { name = "zensical" }, ] [package.metadata] requires-dist = [ { name = "fastmcp", specifier = ">=2.10.0" }, + { name = "pyyaml", specifier = ">=6.0.2" }, { name = "zensical", specifier = ">=0.0.45" }, ]