Compare commits

..
2 Commits
Author SHA1 Message Date
John Lancaster f5b65ecf0a test changes 2026-08-29 22:28:55 -05:00
John Lancaster c31a78206f debug launch config 2026-08-29 21:06:02 -05:00
7 changed files with 96 additions and 1 deletions
+1
View File
@@ -31,6 +31,7 @@ dev = [
"ty>=0.0.51", "ty>=0.0.51",
] ]
test = [ test = [
"asgi-lifespan>=2.1.0",
"httpx2>=2.9.1", "httpx2>=2.9.1",
"pytest>=9.1.1", "pytest>=9.1.1",
"pytest-asyncio>=1.4.0", "pytest-asyncio>=1.4.0",
+3 -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
@@ -18,6 +19,7 @@ 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" 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",
@@ -27,7 +29,7 @@ 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
+18
View File
@@ -0,0 +1,18 @@
from collections.abc import AsyncIterator
import pytest
from asgi_lifespan import LifespanManager
from fastmcp.utilities.tests import ASGIServer
from personal_mcp.app import create_app
@pytest.fixture
async def http_server() -> AsyncIterator[ASGIServer]:
app = create_app()
async with LifespanManager(app):
yield ASGIServer(
url="http://127.0.0.1/mcp",
app=app,
transport_type="http",
)
+22
View File
@@ -0,0 +1,22 @@
from fastmcp import Client
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
+9
View File
@@ -0,0 +1,9 @@
import pytest
from fastmcp.utilities.tests import ASGIServer
from server_contract import assert_server_contract
@pytest.mark.integration
async def test_fastapi_server(http_server: ASGIServer) -> None:
async with http_server.client() as client:
await assert_server_contract(client)
+20
View File
@@ -0,0 +1,20 @@
import sys
from pathlib import Path
import pytest
from fastmcp import Client
from fastmcp.client.transports import StdioTransport
from server_contract import assert_server_contract
@pytest.mark.integration
async def test_stdio_server() -> 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
+23
View File
@@ -52,6 +52,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, { url = "https://files.pythonhosted.org/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" },
] ]
[[package]]
name = "asgi-lifespan"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "sniffio" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6a/da/e7908b54e0f8043725a990bf625f2041ecf6bfe8eb7b19407f1c00b630f7/asgi-lifespan-2.1.0.tar.gz", hash = "sha256:5e2effaf0bfe39829cf2d64e7ecc47c7d86d676a6599f7afba378c31f5e3a308", size = 15627, upload-time = "2023-03-28T17:35:49.126Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/f5/c36551e93acba41a59939ae6a0fb77ddb3f2e8e8caa716410c65f7341f72/asgi_lifespan-2.1.0-py3-none-any.whl", hash = "sha256:ed840706680e28428c01e14afb3875d7d76d3206f3d5b2f2294e059b5c23804f", size = 10895, upload-time = "2023-03-28T17:35:47.772Z" },
]
[[package]] [[package]]
name = "asttokens" name = "asttokens"
version = "3.0.2" version = "3.0.2"
@@ -1126,6 +1138,7 @@ dev = [
{ name = "ty" }, { name = "ty" },
] ]
test = [ test = [
{ name = "asgi-lifespan" },
{ name = "httpx2" }, { name = "httpx2" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-asyncio" }, { name = "pytest-asyncio" },
@@ -1152,6 +1165,7 @@ dev = [
{ name = "ty", specifier = ">=0.0.51" }, { name = "ty", specifier = ">=0.0.51" },
] ]
test = [ test = [
{ name = "asgi-lifespan", specifier = ">=2.1.0" },
{ name = "httpx2", specifier = ">=2.9.1" }, { name = "httpx2", specifier = ">=2.9.1" },
{ 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" },
@@ -1781,6 +1795,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" },
] ]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
]
[[package]] [[package]]
name = "sse-starlette" name = "sse-starlette"
version = "3.4.8" version = "3.4.8"