Skip to content

Commit 4182232

Browse files
committed
task: Add MCP E2E tests
1 parent a1d8cf1 commit 4182232

File tree

5 files changed

+138
-1
lines changed

5 files changed

+138
-1
lines changed
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
Feature: MCP Server Plugin Auto-Discovery
2+
3+
The central MCP server discovers plugin tools registered via Python entry
4+
points, mounts them with namespace isolation, and serves them to MCP clients.
5+
6+
@tests:SPEC-UTILS-SERVICE
7+
@tests:SWR-UTILS-1-1
8+
@id:TC-UTILS-MCP-01
9+
Scenario: Server discovers plugin tools via entry points and serves them to a client
10+
Given a plugin package registers an entry point under "aignostics.plugins"
11+
And the plugin exposes a FastMCP instance with tools "dummy_echo" and "dummy_add"
12+
When the MCP server is created via mcp_create_server()
13+
And a client connects and lists tools
14+
Then the tool list includes "dummy_plugin_dummy_echo" and "dummy_plugin_dummy_add"
15+
And calling "dummy_plugin_dummy_echo" with message "hello" returns "hello"

tests/aignostics/utils/mcp_test.py

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,26 @@
33
from __future__ import annotations
44

55
import asyncio
6+
import subprocess
7+
import sys
8+
from pathlib import Path
9+
from typing import TYPE_CHECKING
610
from unittest.mock import patch
711

812
import pytest
9-
from fastmcp import FastMCP
13+
from fastmcp import Client, FastMCP
1014

1115
from aignostics.utils import (
1216
MCP_SERVER_NAME,
17+
discover_plugin_packages,
1318
mcp_create_server,
1419
mcp_discover_servers,
1520
mcp_list_tools,
1621
)
22+
from aignostics.utils._di import _implementation_cache
23+
24+
if TYPE_CHECKING:
25+
from collections.abc import Iterator
1726

1827
# Patch targets
1928
PATCH_LOCATE_IMPLEMENTATIONS = "aignostics.utils._mcp.locate_implementations"
@@ -175,3 +184,78 @@ def test_mcp_list_tools_empty(record_property) -> None:
175184
tools = mcp_list_tools()
176185
# Should return empty list when no plugins have tools
177186
assert tools == []
187+
188+
189+
# =============================================================================
190+
# E2E Plugin Auto-Discovery Tests
191+
# =============================================================================
192+
193+
DUMMY_PLUGIN_DIR = Path(__file__).resolve().parents[2] / "resources" / "mcp_dummy_plugin"
194+
195+
196+
def _clear_mcp_discovery_caches() -> None:
197+
"""Invalidate DI and plugin caches so MCP discovery starts fresh."""
198+
_implementation_cache.pop(FastMCP, None)
199+
discover_plugin_packages.cache_clear()
200+
201+
202+
@pytest.fixture(scope="session")
203+
def install_dummy_mcp_plugin() -> Iterator[None]:
204+
"""Install the dummy MCP plugin in editable mode and make it importable.
205+
206+
Refreshes site-packages so the running interpreter sees the new package
207+
and its entry points without a process restart.
208+
"""
209+
import importlib
210+
import site
211+
212+
subprocess.check_call(
213+
[sys.executable, "-m", "pip", "install", "-e", str(DUMMY_PLUGIN_DIR)],
214+
stdout=subprocess.DEVNULL,
215+
stderr=subprocess.PIPE,
216+
)
217+
218+
importlib.invalidate_caches()
219+
for sp in site.getsitepackages():
220+
site.addsitedir(sp)
221+
222+
yield
223+
224+
subprocess.check_call(
225+
[sys.executable, "-m", "pip", "uninstall", "-y", "mcp-dummy-plugin"],
226+
stdout=subprocess.DEVNULL,
227+
stderr=subprocess.PIPE,
228+
)
229+
230+
231+
@pytest.fixture
232+
def clear_mcp_caches() -> Iterator[None]:
233+
"""Clear MCP discovery caches before and after the test."""
234+
_clear_mcp_discovery_caches()
235+
yield
236+
_clear_mcp_discovery_caches()
237+
238+
239+
@pytest.mark.e2e
240+
@pytest.mark.timeout(timeout=60)
241+
def test_mcp_server_discovers_and_serves_plugin_tools(
242+
install_dummy_mcp_plugin, clear_mcp_caches, record_property
243+
) -> None:
244+
"""Full E2E: entry point registration -> discovery -> mount -> client round-trip."""
245+
record_property("tested-item-id", "TC-UTILS-MCP-01")
246+
247+
server = mcp_create_server()
248+
tool_names = list(asyncio.run(server.get_tools()).keys())
249+
250+
assert "dummy_plugin_dummy_echo" in tool_names
251+
assert "dummy_plugin_dummy_add" in tool_names
252+
253+
async def _call_tools() -> tuple[str, str]:
254+
async with Client(server) as client:
255+
echo_result = await client.call_tool("dummy_plugin_dummy_echo", {"message": "hello"})
256+
add_result = await client.call_tool("dummy_plugin_dummy_add", {"a": 2, "b": 3})
257+
return echo_result.content[0].text, add_result.content[0].text
258+
259+
echo_text, add_text = asyncio.run(_call_tools())
260+
assert echo_text == "hello"
261+
assert add_text == "5"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[build-system]
2+
requires = ["hatchling"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "mcp-dummy-plugin"
7+
version = "0.0.1"
8+
description = "Dummy MCP plugin for E2E testing of plugin auto-discovery."
9+
requires-python = ">=3.11"
10+
dependencies = ["fastmcp>=2.0.0,<3"]
11+
12+
[project.entry-points."aignostics.plugins"]
13+
mcp_dummy_plugin = "mcp_dummy_plugin"
14+
15+
[tool.hatch.build.targets.wheel]
16+
packages = ["src/mcp_dummy_plugin"]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
"""Dummy MCP plugin for E2E testing of plugin auto-discovery."""
2+
3+
from ._mcp import mcp
4+
5+
__all__ = ["mcp"]
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Dummy MCP tools for E2E testing."""
2+
3+
from fastmcp import FastMCP
4+
5+
mcp = FastMCP("dummy_plugin")
6+
7+
8+
@mcp.tool
9+
def dummy_echo(message: str) -> str:
10+
"""Echo the provided message back."""
11+
return message
12+
13+
14+
@mcp.tool
15+
def dummy_add(a: int, b: int) -> str:
16+
"""Add two integers and return the result as a string."""
17+
return str(a + b)

0 commit comments

Comments
 (0)