diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..d90ccf7 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,18 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Python: personal-mcp entrypoint", + "type": "debugpy", + "request": "launch", + "module": "personal_mcp.__main__", + "cwd": "${workspaceFolder}", + "console": "integratedTerminal", + "justMyCode": true, + "env": { + "PYTHONUNBUFFERED": "1" + }, + "args": [ "--port", "8766", "--reload" ] + } + ] +} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7fffb57..0d25f2b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -51,13 +51,7 @@ "command": "uv", "args": [ "run", - "uvicorn", - "personal_mcp.main:create_app", - "--factory", - "--host", - "127.0.0.1", - "--port", - "8000", + "personal-mcp", "--reload" ], "options": { diff --git a/Dockerfile b/Dockerfile index 1e483d0..00daf11 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,57 +1,52 @@ FROM python:3.14-slim AS builder -COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ -ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ +ENV PYTHONUNBUFFERED=1 \ + UV_SYSTEM_CERTS=1 \ + UV_PYTHON_DOWNLOADS=0 \ + UV_NO_MANAGED_PYTHON=1 \ + UV_SYSTEM_PYTHON=1 \ + UV_PROJECT_ENVIRONMENT=/usr/local \ UV_COMPILE_BYTECODE=1 \ - UV_LINK_MODE=copy \ - UV_LOCKED=1 + UV_NO_DEV=1 \ + UV_LOCKED=1 \ + UV_LINK_MODE=copy + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ WORKDIR /app RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=zensical.toml,target=zensical.toml \ - --mount=type=bind,source=docs/,target=docs/ \ - uvx zensical build + --mount=type=bind,source=src/personal_mcp/docs/,target=src/personal_mcp/docs/ \ + uvx zensical build --clean --strict RUN --mount=type=cache,target=/root/.cache/uv \ --mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ uv sync --no-install-project -# COPY --chown=appuser:appuser . /app +COPY pyproject.toml uv.lock README.md /app/ +COPY ./src /app/src -# RUN --mount=type=cache,target=/root/.cache/uv \ -# uv sync --no-editable +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync FROM python:3.14-slim AS runtime ENV PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - PATH="/app/.venv/bin:$PATH" \ - PERSONAL_MCP_HOST=0.0.0.0 \ - PERSONAL_MCP_PORT=8765 \ - PERSONAL_MCP_RELOAD=false \ - PERSONAL_MCP_SITE_DIR=/app/site - -WORKDIR /app - -EXPOSE 8765 + PYTHONUNBUFFERED=1 RUN groupadd --system --gid 1001 appuser && \ useradd --system --uid 1001 --gid appuser appuser -COPY --from=ghcr.io/astral-sh/uv:latest --chown=appuser:appuser /uv /uvx /bin/ -COPY --from=builder --chown=appuser:appuser /app/.venv /app/.venv -COPY --from=builder --chown=appuser:appuser /app/site /app/site -COPY --chown=appuser:appuser ./docs /app/docs +WORKDIR /app -RUN --mount=type=cache,target=/root/.cache/uv \ - --mount=type=bind,source=uv.lock,target=uv.lock \ - --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ - --mount=type=bind,source=src/,target=src/ \ - uv sync --no-editable --refresh-package prompts +RUN chown appuser:appuser /app + +COPY --from=builder --chown=appuser:appuser /usr/local/lib /usr/local/lib +COPY --from=builder --chown=appuser:appuser /usr/local/bin /usr/local/bin +COPY --from=builder --chown=appuser:appuser /app /app USER appuser -ENTRYPOINT ["python", "-m", "personal_mcp"] \ No newline at end of file +ENTRYPOINT ["/usr/local/bin/personal-mcp"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..774cfac --- /dev/null +++ b/README.md @@ -0,0 +1,16 @@ +# JSL MCP + +```shell +uv run mcp-stdio +``` + +```shell +uv run fastmcp list --command "uv run mcp-stdio" --prompts +``` + +```shell +uv run fastmcp call --command "uv run mcp-stdio" \ + --target authoring --prompt \ + --input-json '{"artifact_type":"prompt","artifact_id":"release-notes","goal":"Create a reusable release-notes workflow."}' \ + --json +``` \ No newline at end of file diff --git a/docker-compose.yaml b/docker-compose.yaml index 5434646..4c61203 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,8 +1,13 @@ services: - personal-mcp: - build: - context: . - dockerfile: Dockerfile + app: + image: personal-mcp:latest + build: . restart: unless-stopped ports: - "8765:8765" + environment: + PERSONAL_MCP_PORT: 8765 + PERSONAL_MCP_HOST: 0.0.0.0 + PERSONAL_MCP_RELOAD: 1 + volumes: + - ./src:/app/src:ro diff --git a/pyproject.toml b/pyproject.toml index 6e6006d..00eda1b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [project] -name = "prompts" +name = "personal_mcp" version = "2.0.0" requires-python = ">=3.12" dependencies = [ @@ -17,13 +17,11 @@ constraint-dependencies = ["fastmcp-slim==4.0.0b4"] [project.scripts] personal-mcp = "personal_mcp.__main__:main" +mcp-stdio = "personal_mcp.mcp:run_stdio" [build-system] -requires = ["hatchling"] -build-backend = "hatchling.build" - -[tool.hatch.build.targets.wheel] -packages = ["src/personal_mcp"] +requires = ["uv_build>=0.12.7,<0.13"] +build-backend = "uv_build" [dependency-groups] dev = [ @@ -52,3 +50,4 @@ markers = [ [tool.ty.src] include = ["src", "tests"] +exclude = ["src/personal_mcp/docs/skills/*/examples/*.py"] diff --git a/src/personal_mcp/__main__.py b/src/personal_mcp/__main__.py index fcee77c..7b5f164 100644 --- a/src/personal_mcp/__main__.py +++ b/src/personal_mcp/__main__.py @@ -7,7 +7,7 @@ def main(cli: bool = True) -> None: """Run the root MCP server.""" settings = get_settings(cli=cli) uvicorn.run( - "personal_mcp.web.app:create_app", + "personal_mcp.app:create_app", factory=True, host=settings.host, port=settings.port, diff --git a/src/personal_mcp/app.py b/src/personal_mcp/app.py index 7da1a63..7a91e8d 100644 --- a/src/personal_mcp/app.py +++ b/src/personal_mcp/app.py @@ -16,6 +16,7 @@ from .mcp import create_mcp def create_app(settings: Settings | None = None) -> FastAPI: runtime_settings = settings if settings is not None else get_settings() + docs_route = runtime_settings.mounts.docs.rstrip("/") or "/docs" mcp_app = create_mcp().http_app( json_response=True, stateless_http=True, @@ -29,6 +30,20 @@ def create_app(settings: Settings | None = None) -> FastAPI: lifespan=app_lifespan, ) app.state.settings = runtime_settings + + async def redirect_root_to_docs() -> RedirectResponse: + return RedirectResponse( + url=docs_route, + status_code=status.HTTP_307_TEMPORARY_REDIRECT, + ) + + app.add_api_route( + "/", + redirect_root_to_docs, + methods=["GET", "HEAD"], + include_in_schema=False, + ) + app.mount(runtime_settings.mounts.mcp, mcp_app, name="mcp") return app