Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ dependencies = [
"truststore>=0.10.4,<1",
"urllib3>=2.6.3,<3", # CVE-2026-21441 requires >= 2.6.3
"wsidicom>=0.28.1,<1",
"fastmcp>=3.0.0,<4",
# Transitive overrides
# WARNING: one cannot negate or downgrade a dependency required here. use override-dependencies for that.
"rfc3987; sys_platform == 'never'", # GPLv3
Expand All @@ -138,7 +139,6 @@ dependencies = [
"lxml>=6.0.2", # For python 3.14 pre-built wheels
"filelock>=3.20.1", # CVE-2025-68146
"marshmallow>=3.26.2", # CVE-2025-68480
"fastmcp>=2.0.0,<3", # MCP server - Major version 3 is in beta as of 26/01/2026 and has not been released on PyPI. Upgrade once a stable release is out.
]

[project.optional-dependencies]
Expand Down
10 changes: 5 additions & 5 deletions src/aignostics/utils/_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def mcp_create_server(server_name: str = MCP_SERVER_NAME) -> FastMCP:

Creates a new FastMCP server instance and mounts all discovered MCP servers
from the SDK and plugins. Each mounted server's tools are namespaced
automatically using FastMCP's built-in prefix feature.
automatically using FastMCP's built-in namespace feature.

Args:
server_name: Human-readable name for the MCP server.
Expand All @@ -76,7 +76,7 @@ def mcp_create_server(server_name: str = MCP_SERVER_NAME) -> FastMCP:
continue
seen_names.add(server.name)
logger.info(f"Mounting MCP server: {server.name}")
mcp.mount(server, prefix=server.name)
mcp.mount(server, namespace=server.name)
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

namespace on mount() and list_tools() are FastMCP v3.x API calls; if this PR lands before the dependency is bumped to v3.x, this will raise at runtime (unexpected keyword argument / missing attribute). To make this change safe (and keep CI green) until pyproject.toml is updated, consider supporting both APIs via capability detection (e.g., try namespace then fallback to prefix, and try list_tools() then fallback to get_tools()).

Copilot uses AI. Check for mistakes.
count += 1

logger.info(f"Mounted {count} MCP servers")
Expand Down Expand Up @@ -115,7 +115,7 @@ def mcp_list_tools(server_name: str = MCP_SERVER_NAME) -> list[dict[str, Any]]:
'name' and 'description' keys.
"""
server = mcp_create_server(server_name)
# FastMCP's get_tools() is async because mounted servers may need to
# FastMCP's list_tools() is async because mounted servers may need to
# lazily initialize resources. We use asyncio.run() to bridge sync/async.
tools = asyncio.run(server.get_tools())
return [{"name": name, "description": tool.description or ""} for name, tool in tools.items()]
tools = asyncio.run(server.list_tools())
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

namespace on mount() and list_tools() are FastMCP v3.x API calls; if this PR lands before the dependency is bumped to v3.x, this will raise at runtime (unexpected keyword argument / missing attribute). To make this change safe (and keep CI green) until pyproject.toml is updated, consider supporting both APIs via capability detection (e.g., try namespace then fallback to prefix, and try list_tools() then fallback to get_tools()).

Copilot uses AI. Check for mistakes.
return [{"name": tool.name, "description": tool.description or ""} for tool in tools]
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

list_tools() returning a list can make output ordering depend on server/tool registration order, which may be non-deterministic across environments and can cause flaky CLI output/tests. Consider sorting the returned tools by tool.name before formatting the list so mcp_list_tools() has stable output.

Suggested change
return [{"name": tool.name, "description": tool.description or ""} for tool in tools]
# Sort tools by name to ensure deterministic output order across environments
sorted_tools = sorted(tools, key=lambda tool: tool.name)
return [{"name": tool.name, "description": tool.description or ""} for tool in sorted_tools]

Copilot uses AI. Check for mistakes.
Comment on lines +120 to +121
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Tests in mcp_test.py use the outdated FastMCP v2 API (get_tools()), while production code uses the new v3 API (list_tools()), guaranteeing future test failures.
Severity: CRITICAL

Suggested Fix

Update the test file tests/aignostics/utils/mcp_test.py to use the new FastMCP v3.x API. Replace calls to server.get_tools() with server.list_tools(). Update the test logic to handle a list of Tool objects instead of a dictionary, accessing tool names via the tool.name attribute.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent.
Verify if this is a real issue. If it is, propose a fix; if not, explain why it's not
valid.

Location: src/aignostics/utils/_mcp.py#L120-L121

Potential issue: The production code was updated to use the FastMCP v3.x API,
specifically changing from `server.get_tools()` to `server.list_tools()`. However, the
corresponding test file, `tests/aignostics/utils/mcp_test.py`, was not updated. It still
calls the old `get_tools()` method and expects a dictionary, while the new
`list_tools()` method returns a list of `Tool` objects. When the FastMCP dependency is
updated to v3.x as intended for this pull request, the tests will fail with an
`AttributeError`, blocking deployment.

Did we get this right? 👍 / 👎 to inform future reviews.

8 changes: 4 additions & 4 deletions tests/aignostics/utils/mcp_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,8 @@ def plugin2_tool() -> str:
assert isinstance(server, FastMCP)
assert server.name == MCP_SERVER_NAME
# Verify exactly 2 tools from both plugins are mounted with namespacing
tools = asyncio.run(server.get_tools())
tool_names = list(tools.keys())
tools = asyncio.run(server.list_tools())
tool_names = [t.name for t in tools]
assert len(tool_names) == 2
# Verify namespacing: tools should be prefixed with server name
plugin1_tools = [n for n in tool_names if "plugin1" in n]
Expand Down Expand Up @@ -139,8 +139,8 @@ def unique_tool() -> str:
# Verify warning was logged for duplicate
assert "Duplicate MCP server name 'duplicate_name'" in caplog.text
# Verify only first duplicate and unique server were mounted (2 servers, not 3)
tools = asyncio.run(server.get_tools())
tool_names = list(tools.keys())
tools = asyncio.run(server.list_tools())
tool_names = [t.name for t in tools]
assert len(tool_names) == 2
# dup1_tool should be present (first occurrence)
assert any("dup1_tool" in name for name in tool_names)
Expand Down
2 changes: 1 addition & 1 deletion tests/resources/mcp_dummy_plugin/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ name = "mcp-dummy-plugin"
version = "0.0.1"
description = "Dummy MCP plugin for integration testing of plugin auto-discovery."
requires-python = ">=3.11"
dependencies = ["fastmcp>=2.0.0,<3", "typer>=0.12", "aignostics"]
dependencies = ["fastmcp>=3.0.0,<4", "typer>=0.12", "aignostics"]

[project.entry-points."aignostics.plugins"]
mcp_dummy_plugin = "mcp_dummy_plugin"
Expand Down
Loading
Loading