From f5b65ecf0af7b39d1470d8e0276ab52826f64093 Mon Sep 17 00:00:00 2001 From: John Lancaster <32917998+jsl12@users.noreply.github.com> Date: Sat, 29 Aug 2026 22:28:55 -0500 Subject: [PATCH] test changes --- pyproject.toml | 2 +- tests/conftest.py | 53 +++++++++------------------------------- tests/server_contract.py | 22 +++++++++++++++++ tests/test_http.py | 10 +++----- tests/test_stdio.py | 24 ++---------------- uv.lock | 38 ++++++++++++++++------------ 6 files changed, 64 insertions(+), 85 deletions(-) create mode 100644 tests/server_contract.py diff --git a/pyproject.toml b/pyproject.toml index 07a7884..7309c93 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,11 +31,11 @@ dev = [ "ty>=0.0.51", ] test = [ + "asgi-lifespan>=2.1.0", "httpx2>=2.9.1", "pytest>=9.1.1", "pytest-asyncio>=1.4.0", "pytest-cov>=7.1.0", - "pytest-xprocess>=1.0.2", "pyyaml>=6.0.2", ] diff --git a/tests/conftest.py b/tests/conftest.py index d793b11..4556c6e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,47 +1,18 @@ -import socket -import sys -from collections.abc import Iterator +from collections.abc import AsyncIterator import pytest -from xprocess import ProcessStarter -from xprocess import XProcess +from asgi_lifespan import LifespanManager +from fastmcp.utilities.tests import ASGIServer -SERVER_HOST = "127.0.0.1" +from personal_mcp.app import create_app @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() +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", + ) diff --git a/tests/server_contract.py b/tests/server_contract.py new file mode 100644 index 0000000..57377a2 --- /dev/null +++ b/tests/server_contract.py @@ -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 diff --git a/tests/test_http.py b/tests/test_http.py index 3ae311b..ca90012 100644 --- a/tests/test_http.py +++ b/tests/test_http.py @@ -1,11 +1,9 @@ import pytest -from fastmcp import Client -from fastmcp.client.transports import StreamableHttpTransport -from test_stdio import assert_server_contract +from fastmcp.utilities.tests import ASGIServer +from server_contract 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: +async def test_fastapi_server(http_server: ASGIServer) -> None: + async with http_server.client() as client: await assert_server_contract(client) diff --git a/tests/test_stdio.py b/tests/test_stdio.py index 6c1b0e1..55195cc 100644 --- a/tests/test_stdio.py +++ b/tests/test_stdio.py @@ -4,31 +4,11 @@ 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 +from server_contract import assert_server_contract @pytest.mark.integration -async def test_stdio_server_enumerates_and_reads_content() -> None: +async def test_stdio_server() -> None: transport = StdioTransport( command=sys.executable, args=["-m", "personal_mcp.mcp"], diff --git a/uv.lock b/uv.lock index f09d66e..857a966 100644 --- a/uv.lock +++ b/uv.lock @@ -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" }, ] +[[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]] name = "asttokens" version = "3.0.2" @@ -1126,11 +1138,11 @@ dev = [ { name = "ty" }, ] test = [ + { name = "asgi-lifespan" }, { name = "httpx2" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-cov" }, - { name = "pytest-xprocess" }, { name = "pyyaml" }, ] @@ -1153,11 +1165,11 @@ dev = [ { name = "ty", specifier = ">=0.0.51" }, ] test = [ + { name = "asgi-lifespan", specifier = ">=2.1.0" }, { name = "httpx2", specifier = ">=2.9.1" }, { name = "pytest", specifier = ">=9.1.1" }, { name = "pytest-asyncio", specifier = ">=1.4.0" }, { name = "pytest-cov", specifier = ">=7.1.0" }, - { name = "pytest-xprocess", specifier = ">=1.0.2" }, { name = "pyyaml", specifier = ">=6.0.2" }, ] @@ -1496,19 +1508,6 @@ 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" }, ] -[[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]] name = "python-discovery" version = "1.5.1" @@ -1796,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" }, ] +[[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]] name = "sse-starlette" version = "3.4.8"