swapped docs symlink
This commit is contained in:
@@ -0,0 +1,19 @@
|
|||||||
|
import uvicorn
|
||||||
|
|
||||||
|
from .config import get_settings
|
||||||
|
|
||||||
|
|
||||||
|
def main(cli: bool = True) -> None:
|
||||||
|
"""Run the root MCP server."""
|
||||||
|
settings = get_settings(cli=cli)
|
||||||
|
uvicorn.run(
|
||||||
|
"personal_mcp.web.app:create_app",
|
||||||
|
factory=True,
|
||||||
|
host=settings.host,
|
||||||
|
port=settings.port,
|
||||||
|
reload=settings.reload,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -8,7 +8,7 @@ from pydantic_settings import BaseSettings
|
|||||||
from pydantic_settings import SettingsConfigDict
|
from pydantic_settings import SettingsConfigDict
|
||||||
|
|
||||||
DEFAULT_ENV_FILE = Path(".env").resolve()
|
DEFAULT_ENV_FILE = Path(".env").resolve()
|
||||||
_REPO_ROOT = Path(__file__).resolve().parents[2]
|
DEFAULT_SITE_DIR = Path("site").resolve()
|
||||||
|
|
||||||
|
|
||||||
class Mounts(BaseModel):
|
class Mounts(BaseModel):
|
||||||
@@ -23,17 +23,21 @@ class Settings(BaseSettings):
|
|||||||
env_file=DEFAULT_ENV_FILE,
|
env_file=DEFAULT_ENV_FILE,
|
||||||
env_prefix="PERSONAL_MCP_",
|
env_prefix="PERSONAL_MCP_",
|
||||||
extra="ignore",
|
extra="ignore",
|
||||||
|
cli_implicit_flags=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
debug: bool = False
|
debug: bool = False
|
||||||
log_level: str = "info"
|
log_level: str = "info"
|
||||||
mounts: Mounts = Field(default_factory=Mounts)
|
mounts: Mounts = Field(default_factory=Mounts)
|
||||||
site_dir: DirectoryPath = Field(default=_REPO_ROOT / "site")
|
site_dir: DirectoryPath = Field(default=DEFAULT_SITE_DIR)
|
||||||
|
host: str = "localhost"
|
||||||
|
port: int = 8080
|
||||||
|
reload: bool = True
|
||||||
|
|
||||||
|
|
||||||
@cache
|
@cache
|
||||||
def get_settings(**overrides) -> Settings:
|
def get_settings(*, cli: bool = False, **overrides) -> Settings:
|
||||||
return Settings(**overrides)
|
return Settings(**overrides, _cli_parse_args=cli) # pyright: ignore[reportCallIssue]
|
||||||
|
|
||||||
|
|
||||||
def refresh_settings(**overrides):
|
def refresh_settings(**overrides):
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
../../docs
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
from fastapi import FastAPI
|
|
||||||
|
|
||||||
from personal_mcp.mcp import create_mcp
|
|
||||||
from personal_mcp.web.app import create_app as create_fastapi
|
|
||||||
|
|
||||||
__all__ = ["create_app", "main"]
|
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> FastAPI:
|
|
||||||
"""Create the HTTP application for ASGI servers using factory mode."""
|
|
||||||
return create_fastapi()
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
"""Run the root MCP server."""
|
|
||||||
create_mcp().run()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
+26
-12
@@ -1,20 +1,34 @@
|
|||||||
from pathlib import Path
|
from contextlib import ExitStack
|
||||||
|
from importlib.resources import as_file
|
||||||
|
from importlib.resources import files
|
||||||
|
from weakref import finalize
|
||||||
|
|
||||||
from fastmcp.server.providers.skills import SkillsDirectoryProvider
|
from fastmcp.server.providers.skills import SkillsDirectoryProvider
|
||||||
|
|
||||||
|
|
||||||
def create_skills_provider() -> SkillsDirectoryProvider:
|
def create_skills_provider() -> SkillsDirectoryProvider:
|
||||||
"""Create the provider for skills packaged with personal-mcp."""
|
"""Create the provider for skills packaged with personal-mcp."""
|
||||||
skills_root = Path(__file__).resolve().parents[1] / "docs" / "skills"
|
skills_resource = files("personal_mcp").joinpath("docs", "skills")
|
||||||
if not skills_root.is_dir():
|
if not skills_resource.is_dir():
|
||||||
raise FileNotFoundError(f"packaged skills directory does not exist: {skills_root}")
|
raise FileNotFoundError(f"packaged skills directory does not exist: {skills_resource}")
|
||||||
|
|
||||||
has_skills = any(skill_dir.is_dir() and (skill_dir / "SKILL.md").is_file() for skill_dir in skills_root.iterdir())
|
has_skills = any(
|
||||||
if not has_skills:
|
skill_dir.is_dir() and skill_dir.joinpath("SKILL.md").is_file() for skill_dir in skills_resource.iterdir()
|
||||||
raise ValueError(f"packaged skills directory contains no skills: {skills_root}")
|
|
||||||
|
|
||||||
return SkillsDirectoryProvider(
|
|
||||||
roots=skills_root,
|
|
||||||
reload=False,
|
|
||||||
supporting_files="template",
|
|
||||||
)
|
)
|
||||||
|
if not has_skills:
|
||||||
|
raise ValueError(f"packaged skills directory contains no skills: {skills_resource}")
|
||||||
|
|
||||||
|
resources = ExitStack()
|
||||||
|
try:
|
||||||
|
skills_root = resources.enter_context(as_file(skills_resource))
|
||||||
|
provider = SkillsDirectoryProvider(
|
||||||
|
roots=skills_root,
|
||||||
|
reload=False,
|
||||||
|
supporting_files="template",
|
||||||
|
)
|
||||||
|
except BaseException:
|
||||||
|
resources.close()
|
||||||
|
raise
|
||||||
|
|
||||||
|
finalize(provider, resources.close)
|
||||||
|
return provider
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from pathlib import Path
|
from importlib.resources import files
|
||||||
|
from importlib.resources.abc import Traversable
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import yaml
|
import yaml
|
||||||
@@ -11,7 +12,15 @@ from personal_mcp.skills import create_skills_provider
|
|||||||
|
|
||||||
pytestmark = pytest.mark.unit
|
pytestmark = pytest.mark.unit
|
||||||
|
|
||||||
SKILLS_ROOT = Path(__file__).parents[2] / "docs" / "skills"
|
SKILLS_ROOT = files("personal_mcp").joinpath("docs", "skills")
|
||||||
|
|
||||||
|
|
||||||
|
def skill_directories() -> list[Traversable]:
|
||||||
|
return [
|
||||||
|
directory
|
||||||
|
for directory in SKILLS_ROOT.iterdir()
|
||||||
|
if directory.is_dir() and directory.joinpath("SKILL.md").is_file()
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class TestSkillsProvider:
|
class TestSkillsProvider:
|
||||||
@@ -20,7 +29,7 @@ class TestSkillsProvider:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_discovers_authored_skills(self) -> None:
|
async def test_discovers_authored_skills(self) -> None:
|
||||||
"""Ensures each authored skill is exposed with its description."""
|
"""Ensures each authored skill is exposed with its description."""
|
||||||
expected_names = {path.parent.name for path in SKILLS_ROOT.glob("*/SKILL.md")}
|
expected_names = {directory.name for directory in skill_directories()}
|
||||||
mcp = FastMCP("skills-test")
|
mcp = FastMCP("skills-test")
|
||||||
mcp.add_provider(create_skills_provider())
|
mcp.add_provider(create_skills_provider())
|
||||||
|
|
||||||
@@ -48,10 +57,11 @@ class TestSkillsProvider:
|
|||||||
|
|
||||||
def test_frontmatter_names_match_directories(self) -> None:
|
def test_frontmatter_names_match_directories(self) -> None:
|
||||||
"""Ensures provider identity and authored skill names remain aligned."""
|
"""Ensures provider identity and authored skill names remain aligned."""
|
||||||
for skill_file in SKILLS_ROOT.glob("*/SKILL.md"):
|
for directory in skill_directories():
|
||||||
|
skill_file = directory.joinpath("SKILL.md")
|
||||||
raw = skill_file.read_text(encoding="utf-8")
|
raw = skill_file.read_text(encoding="utf-8")
|
||||||
frontmatter = yaml.safe_load(raw.split("---", 2)[1])
|
frontmatter = yaml.safe_load(raw.split("---", 2)[1])
|
||||||
|
|
||||||
assert set(frontmatter) == {"name", "description"}
|
assert set(frontmatter) == {"name", "description"}
|
||||||
assert frontmatter["name"] == skill_file.parent.name
|
assert frontmatter["name"] == directory.name
|
||||||
assert frontmatter["description"]
|
assert frontmatter["description"]
|
||||||
|
|||||||
Reference in New Issue
Block a user