debug launch config

This commit is contained in:
John Lancaster
2026-08-29 21:06:02 -05:00
parent de95477480
commit c31a78206f
12 changed files with 207 additions and 49 deletions
+18
View File
@@ -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" ]
}
]
}
+1 -7
View File
@@ -51,13 +51,7 @@
"command": "uv", "command": "uv",
"args": [ "args": [
"run", "run",
"uvicorn", "personal-mcp",
"personal_mcp.main:create_app",
"--factory",
"--host",
"127.0.0.1",
"--port",
"8000",
"--reload" "--reload"
], ],
"options": { "options": {
+25 -30
View File
@@ -1,57 +1,52 @@
FROM python:3.14-slim AS builder FROM python:3.14-slim AS builder
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONUNBUFFERED=1 \
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_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \ UV_NO_DEV=1 \
UV_LOCKED=1 UV_LOCKED=1 \
UV_LINK_MODE=copy
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
WORKDIR /app WORKDIR /app
RUN --mount=type=cache,target=/root/.cache/uv \ RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=zensical.toml,target=zensical.toml \ --mount=type=bind,source=zensical.toml,target=zensical.toml \
--mount=type=bind,source=docs/,target=docs/ \ --mount=type=bind,source=src/personal_mcp/docs/,target=src/personal_mcp/docs/ \
uvx zensical build uvx zensical build --clean --strict
RUN --mount=type=cache,target=/root/.cache/uv \ RUN --mount=type=cache,target=/root/.cache/uv \
--mount=type=bind,source=uv.lock,target=uv.lock \ --mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \ --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
uv sync --no-install-project 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 \ RUN --mount=type=cache,target=/root/.cache/uv \
# uv sync --no-editable uv sync
FROM python:3.14-slim AS runtime FROM python:3.14-slim AS runtime
ENV PYTHONDONTWRITEBYTECODE=1 \ ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=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
RUN groupadd --system --gid 1001 appuser && \ RUN groupadd --system --gid 1001 appuser && \
useradd --system --uid 1001 --gid appuser appuser useradd --system --uid 1001 --gid appuser appuser
COPY --from=ghcr.io/astral-sh/uv:latest --chown=appuser:appuser /uv /uvx /bin/ WORKDIR /app
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
RUN --mount=type=cache,target=/root/.cache/uv \ RUN chown appuser:appuser /app
--mount=type=bind,source=uv.lock,target=uv.lock \
--mount=type=bind,source=pyproject.toml,target=pyproject.toml \ COPY --from=builder --chown=appuser:appuser /usr/local/lib /usr/local/lib
--mount=type=bind,source=src/,target=src/ \ COPY --from=builder --chown=appuser:appuser /usr/local/bin /usr/local/bin
uv sync --no-editable --refresh-package prompts COPY --from=builder --chown=appuser:appuser /app /app
USER appuser USER appuser
ENTRYPOINT ["python", "-m", "personal_mcp"] ENTRYPOINT ["/usr/local/bin/personal-mcp"]
+16
View File
@@ -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
```
+9 -4
View File
@@ -1,8 +1,13 @@
services: services:
personal-mcp: app:
build: image: personal-mcp:latest
context: . build: .
dockerfile: Dockerfile
restart: unless-stopped restart: unless-stopped
ports: ports:
- "8765:8765" - "8765:8765"
environment:
PERSONAL_MCP_PORT: 8765
PERSONAL_MCP_HOST: 0.0.0.0
PERSONAL_MCP_RELOAD: 1
volumes:
- ./src:/app/src:ro
+6 -6
View File
@@ -1,5 +1,5 @@
[project] [project]
name = "prompts" name = "personal_mcp"
version = "2.0.0" version = "2.0.0"
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
@@ -17,13 +17,11 @@ constraint-dependencies = ["fastmcp-slim==4.0.0b4"]
[project.scripts] [project.scripts]
personal-mcp = "personal_mcp.__main__:main" personal-mcp = "personal_mcp.__main__:main"
mcp-stdio = "personal_mcp.mcp:run_stdio"
[build-system] [build-system]
requires = ["hatchling"] requires = ["uv_build>=0.12.7,<0.13"]
build-backend = "hatchling.build" build-backend = "uv_build"
[tool.hatch.build.targets.wheel]
packages = ["src/personal_mcp"]
[dependency-groups] [dependency-groups]
dev = [ dev = [
@@ -37,6 +35,7 @@ test = [
"pytest>=9.1.1", "pytest>=9.1.1",
"pytest-asyncio>=1.4.0", "pytest-asyncio>=1.4.0",
"pytest-cov>=7.1.0", "pytest-cov>=7.1.0",
"pytest-xprocess>=1.0.2",
"pyyaml>=6.0.2", "pyyaml>=6.0.2",
] ]
@@ -52,3 +51,4 @@ markers = [
[tool.ty.src] [tool.ty.src]
include = ["src", "tests"] include = ["src", "tests"]
exclude = ["src/personal_mcp/docs/skills/*/examples/*.py"]
+1 -1
View File
@@ -7,7 +7,7 @@ def main(cli: bool = True) -> None:
"""Run the root MCP server.""" """Run the root MCP server."""
settings = get_settings(cli=cli) settings = get_settings(cli=cli)
uvicorn.run( uvicorn.run(
"personal_mcp.web.app:create_app", "personal_mcp.app:create_app",
factory=True, factory=True,
host=settings.host, host=settings.host,
port=settings.port, port=settings.port,
+18 -1
View File
@@ -8,6 +8,7 @@ from fastapi import Response
from fastapi import status from fastapi import status
from fastapi.responses import RedirectResponse from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles from fastapi.staticfiles import StaticFiles
from fastmcp.utilities.lifespan import combine_lifespans
from .config import Settings from .config import Settings
from .config import get_settings from .config import get_settings
@@ -16,7 +17,9 @@ from .mcp import create_mcp
def create_app(settings: Settings | None = None) -> FastAPI: def create_app(settings: Settings | None = None) -> FastAPI:
runtime_settings = settings if settings is not None else get_settings() 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( mcp_app = create_mcp().http_app(
path="/",
json_response=True, json_response=True,
stateless_http=True, stateless_http=True,
transport="http", transport="http",
@@ -26,9 +29,23 @@ def create_app(settings: Settings | None = None) -> FastAPI:
docs_url=None, docs_url=None,
redoc_url=None, redoc_url=None,
openapi_url=None, openapi_url=None,
lifespan=app_lifespan, lifespan=combine_lifespans(app_lifespan, mcp_app.lifespan),
) )
app.state.settings = runtime_settings 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") app.mount(runtime_settings.mounts.mcp, mcp_app, name="mcp")
return app return app
+47
View File
@@ -0,0 +1,47 @@
import socket
import sys
from collections.abc import Iterator
import pytest
from xprocess import ProcessStarter
from xprocess import XProcess
SERVER_HOST = "127.0.0.1"
@pytest.fixture
def fastapi_server_url(xprocess: XProcess, unused_tcp_port: int) -> Iterator[str]:
class ServerStarter(ProcessStarter):
timeout = 10
terminate_on_interrupt = True
@property
def args(self) -> list[str]:
return [
sys.executable,
"-m",
"uvicorn",
"personal_mcp.app:create_app",
"--factory",
"--host",
SERVER_HOST,
"--port",
str(unused_tcp_port),
"--log-level",
"warning",
"--no-access-log",
]
def startup_check(self) -> bool:
try:
with socket.create_connection((SERVER_HOST, unused_tcp_port), timeout=0.1):
return True
except OSError:
return False
process_name = f"personal-mcp-http-{unused_tcp_port}"
xprocess.ensure(process_name, ServerStarter)
try:
yield f"http://{SERVER_HOST}:{unused_tcp_port}/mcp"
finally:
xprocess.getinfo(process_name).terminate()
+11
View File
@@ -0,0 +1,11 @@
import pytest
from fastmcp import Client
from fastmcp.client.transports import StreamableHttpTransport
from test_stdio import assert_server_contract
@pytest.mark.integration
async def test_fastapi_server_enumerates_and_reads_content(fastapi_server_url: str) -> None:
transport = StreamableHttpTransport(fastapi_server_url)
async with Client(transport) as client:
await assert_server_contract(client)
+40
View File
@@ -0,0 +1,40 @@
import sys
from pathlib import Path
import pytest
from fastmcp import Client
from fastmcp.client.transports import StdioTransport
from mcp_types import TextResourceContents
async def assert_server_contract(client: Client) -> None:
resources = {str(resource.uri) for resource in await client.list_resources()}
templates = {str(template.uri_template) for template in await client.list_resource_templates()}
prompts = {prompt.name for prompt in await client.list_prompts()}
assert await client.list_tools() == []
assert "skill://pytesting/SKILL.md" in resources
assert "resource://docs/{path*}" in templates
assert "skill://pytesting/{path*}" in templates
assert "pytest-fill-scaffold" in prompts
skill_content = await client.read_resource("skill://pytesting/SKILL.md")
docs_content = await client.read_resource("resource://docs/index.md")
assert isinstance(skill_content[0], TextResourceContents)
assert "# Pytesting" in skill_content[0].text
assert isinstance(docs_content[0], TextResourceContents)
assert '"format": "markdown"' in docs_content[0].text
@pytest.mark.integration
async def test_stdio_server_enumerates_and_reads_content() -> None:
transport = StdioTransport(
command=sys.executable,
args=["-m", "personal_mcp.mcp"],
cwd=str(Path(__file__).parent.parent),
keep_alive=False,
)
async with Client(transport) as client:
await assert_server_contract(client)
Generated
+15
View File
@@ -1130,6 +1130,7 @@ test = [
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-asyncio" }, { name = "pytest-asyncio" },
{ name = "pytest-cov" }, { name = "pytest-cov" },
{ name = "pytest-xprocess" },
{ name = "pyyaml" }, { name = "pyyaml" },
] ]
@@ -1156,6 +1157,7 @@ test = [
{ name = "pytest", specifier = ">=9.1.1" }, { name = "pytest", specifier = ">=9.1.1" },
{ name = "pytest-asyncio", specifier = ">=1.4.0" }, { name = "pytest-asyncio", specifier = ">=1.4.0" },
{ name = "pytest-cov", specifier = ">=7.1.0" }, { name = "pytest-cov", specifier = ">=7.1.0" },
{ name = "pytest-xprocess", specifier = ">=1.0.2" },
{ name = "pyyaml", specifier = ">=6.0.2" }, { name = "pyyaml", specifier = ">=6.0.2" },
] ]
@@ -1494,6 +1496,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" }, { url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
] ]
[[package]]
name = "pytest-xprocess"
version = "1.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "psutil" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/27/6f/e53d349445b4280f6b47bbed87127e994f331c335377d5e72407b376ad46/pytest-xprocess-1.0.2.tar.gz", hash = "sha256:15e270637586eabc56755ee5fcc81c48bdb46ba7ef7c0d5b1b64302d080cc60f", size = 13232, upload-time = "2024-05-19T16:12:21.819Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/63/cf/91b94238c843bfbae0a6674df45015aa3908b91454c496b2eb29e7991255/pytest_xprocess-1.0.2-py3-none-any.whl", hash = "sha256:0b0444d1f789fd9b4ba8b6b38b1d0139f226ab14091db2698a0521c1770523dd", size = 9628, upload-time = "2024-05-19T16:12:19.773Z" },
]
[[package]] [[package]]
name = "python-discovery" name = "python-discovery"
version = "1.5.1" version = "1.5.1"