migration

This commit is contained in:
John Lancaster
2026-08-07 20:07:21 -05:00
parent 2a2700b78c
commit 5b6d5aaec4
70 changed files with 1182 additions and 3633 deletions
+2 -172
View File
@@ -1,66 +1,13 @@
from __future__ import annotations
import os
import re
from inspect import Parameter
from inspect import Signature
from typing import Any
from typing import cast
from fastmcp import FastMCP
from fastmcp.server.transforms import ResourcesAsTools
from fastmcp.server.transforms.search import BM25SearchTransform
from fastmcp.server.transforms.search import RegexSearchTransform
from personal_mcp.catalog.server import build_prompt_detail_payload
from personal_mcp.catalog.server import build_prompts_index_payload
from personal_mcp.catalog.server import get_prompt_by_id_payload
from personal_mcp.catalog.server import search_prompts_payload
from personal_mcp.prompts import create_prompts_provider
from personal_mcp.registry.load import get_docs_registry
from personal_mcp.registry.models.registry import DocsRegistry
from personal_mcp.registry.read import read_docs_markdown_path
from personal_mcp.registry.read import read_prompt_document
from personal_mcp.skills import create_skills_provider
TOOL_SEARCH_MODE = os.getenv("PERSONAL_MCP_TOOL_SEARCH", "none").strip().lower()
TOOL_SEARCH_MAX_RESULTS = os.getenv("PERSONAL_MCP_TOOL_SEARCH_MAX_RESULTS", "5")
def _parse_positive_int(value: str, *, env_name: str) -> int:
try:
parsed = int(value)
except ValueError as exc:
raise ValueError(f"{env_name} must be an integer") from exc
if parsed <= 0:
raise ValueError(f"{env_name} must be greater than zero")
return parsed
def _install_tool_fallback_transforms(mcp: FastMCP) -> None:
# Expose list_resources/read_resource for tool-only clients.
mcp.add_transform(ResourcesAsTools(mcp))
if TOOL_SEARCH_MODE in {"", "none"}:
return
max_results = _parse_positive_int(
TOOL_SEARCH_MAX_RESULTS,
env_name="PERSONAL_MCP_TOOL_SEARCH_MAX_RESULTS",
)
kwargs: dict[str, Any] = {
"max_results": max_results,
"always_visible": ["list_resources", "read_resource"],
}
if TOOL_SEARCH_MODE == "regex":
mcp.add_transform(RegexSearchTransform(**kwargs))
return
if TOOL_SEARCH_MODE == "bm25":
mcp.add_transform(BM25SearchTransform(**kwargs))
return
raise ValueError("PERSONAL_MCP_TOOL_SEARCH must be one of: none, regex, bm25")
def _ro_annotations() -> dict[str, bool]:
return {
@@ -69,54 +16,6 @@ def _ro_annotations() -> dict[str, bool]:
}
def _render_prompt_markdown(content: str, arguments: dict[str, Any]) -> str:
rendered = content
for key, value in arguments.items():
rendered = rendered.replace(f"{{{{{key}}}}}", str(value))
return rendered
def _make_prompt_handler(content: str):
def prompt_handler(**kwargs: Any) -> str:
return _render_prompt_markdown(content, kwargs)
return prompt_handler
def _register_prompt_objects(mcp: FastMCP, registry: DocsRegistry) -> None:
for prompt_id in registry.prompts_in_load_order:
prompt = registry.prompts_by_id[prompt_id]
annotations: dict[str, Any] = {}
params: list[Parameter] = []
for arg_name, arg in sorted(prompt.arguments.items()):
annotations[arg_name] = str
default = Parameter.empty if arg.required else None
params.append(
Parameter(
arg_name,
kind=Parameter.KEYWORD_ONLY,
default=default,
annotation=str,
)
)
signature = Signature(parameters=params, return_annotation=str)
prompt_handler = _make_prompt_handler(prompt.document_content)
prompt_handler.__name__ = re.sub(r"[^a-zA-Z0-9_]", "_", prompt_id)
prompt_handler.__doc__ = prompt.description
prompt_handler.__annotations__ = annotations
cast(Any, prompt_handler).__signature__ = signature
mcp.prompt(
prompt_handler,
name=prompt_id,
description=prompt.description,
tags=set(prompt.tags),
)
def _register_components(mcp: FastMCP, registry: DocsRegistry) -> None:
@mcp.resource(
"resource://docs/{path*}",
@@ -127,80 +26,11 @@ def _register_components(mcp: FastMCP, registry: DocsRegistry) -> None:
def docs_markdown(path: str) -> dict[str, str]:
return read_docs_markdown_path(registry, path)
@mcp.resource(
"resource://catalog/prompts_index",
mime_type="application/json",
tags={"catalog"},
annotations=_ro_annotations(),
)
def prompts_index() -> dict[str, Any]:
return build_prompts_index_payload(registry)
@mcp.resource(
"resource://catalog/prompts_index{?q,tag,cursor,limit}",
mime_type="application/json",
tags={"catalog"},
annotations=_ro_annotations(),
)
def prompts_index_query(
q: str | None = None,
tag: str | None = None,
cursor: str | None = None,
limit: int | None = None,
) -> dict[str, Any]:
return build_prompts_index_payload(
registry,
query=q,
tag=tag,
cursor=cursor,
limit=limit,
)
@mcp.resource(
"resource://catalog/prompts/{prompt_id}",
mime_type="application/json",
tags={"catalog"},
annotations=_ro_annotations(),
)
def prompt_detail(prompt_id: str) -> dict[str, Any]:
return build_prompt_detail_payload(registry, prompt_id)
@mcp.resource(
"resource://prompts/{prompt_id}/document",
mime_type="text/markdown",
tags={"prompt-doc"},
annotations=_ro_annotations(),
)
def prompt_document(prompt_id: str) -> dict[str, str]:
return read_prompt_document(registry, prompt_id)
@mcp.tool
def search_prompts(
query: str = "",
tags: list[str] | None = None,
skip: int = 0,
limit: int = 20,
) -> dict[str, Any]:
"""Search prompt metadata with optional tags and pagination."""
return search_prompts_payload(
registry,
query=query,
tags=tags,
skip=skip,
limit=limit,
)
@mcp.tool
def get_prompt_by_id(prompt_id: str) -> dict[str, Any]:
"""Return one prompt by stable id."""
return get_prompt_by_id_payload(registry, prompt_id)
def create_mcp() -> FastMCP:
registry = get_docs_registry()
mcp = FastMCP("personal-mcp", on_duplicate="error")
_register_components(mcp, registry)
_register_prompt_objects(mcp, registry)
mcp.add_provider(create_prompts_provider())
mcp.add_provider(create_skills_provider())
_install_tool_fallback_transforms(mcp)
return mcp