Skip to content

chore: Bump FastMCP to v3.x and make the necessary changes to support it#425

Open
neelay-aign wants to merge 1 commit intomainfrom
task/changes-for-fastmcp-3
Open

chore: Bump FastMCP to v3.x and make the necessary changes to support it#425
neelay-aign wants to merge 1 commit intomainfrom
task/changes-for-fastmcp-3

Conversation

@neelay-aign
Copy link
Collaborator

Merge when FastMCP releases a v3.x to PyPI and we switch to that in the pyproject.toml

Copilot AI review requested due to automatic review settings February 10, 2026 20:34
@atlantis-platform-engineering
Error: This repo is not allowlisted for Atlantis.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Prepare the MCP integration code for FastMCP v3.x API changes (mount namespacing and async tool listing) ahead of switching the dependency in pyproject.toml.

Changes:

  • Update mcp.mount(..., prefix=...) to mcp.mount(..., namespace=...).
  • Replace get_tools() (dict) with list_tools() (list) and adjust tool formatting output.
  • Update docstrings/comments to reflect the new FastMCP API terminology.

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.
# 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.
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())
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
tools = asyncio.run(server.list_tools())
return [{"name": tool.name, "description": tool.description or ""} for tool in tools]
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.

@codecov
Copy link

codecov bot commented Feb 10, 2026

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
697 2 695 14
View the top 2 failed test(s) by shortest run time
tests.aignostics.system.cli_test::test_cli_health_json
Stack Traces | 6.75s run time
runner = <typer.testing.CliRunner object at 0x7f56bcf3e150>
record_property = <function record_property.<locals>.append_property at 0x7f56bd7bfd70>

    @pytest.mark.e2e
    @pytest.mark.scheduled
    @pytest.mark.timeout(timeout=60)
    def test_cli_health_json(runner: CliRunner, record_property) -> None:
        """Check health is true."""
        record_property("tested-item-id", "TEST-SYSTEM-CLI-HEALTH-JSON")
        result = runner.invoke(cli, ["system", "health"])
>       assert normalize_output(result.stdout).startswith('{  "status": "UP"')
E       assert False
E        +  where False = <built-in method startswith of str object at 0x1b6946f0>('{  "status": "UP"')
E        +    where <built-in method startswith of str object at 0x1b6946f0> = '{  "status": "DOWN",  "reason": "Component \'aignostics.platform._service.Service\' is DOWN",  "components": {    "aignostics.application._service.Service": {      "status": "UP",      "reason": null,      "components": {}    },    "aignostics.bucket._service.Service": {      "status": "UP",      "reason": null,      "components": {}    },    "aignostics.dataset._service.Service": {      "status": "UP",      "reason": null,      "components": {}    },    "aignostics.notebook._service.Service": {      "status": "UP",      "reason": null,      "components": {}    },    "aignostics.platform._service.Service": {      "status": "DOWN",      "reason": "Component \'api_public\' is DOWN",      "components": {        "api_public": {          "status": "DOWN",          "reason": "Aignostics Platform API returned status \'503\'",          "components": {}        },        "api_authenticated": {          "status": "UP",          "reason": null,          "components": {}        }      }    },    "aignostics.qupath._service.Service": {      "status": "UP",      "reason": null,      "components": {}    },    "aignostics.wsi._service.Service": {      "status": "UP",      "reason": null,      "components": {}    },    "network": {      "status": "UP",      "reason": null,      "components": {}    }  }}'.startswith
E        +      where '{  "status": "DOWN",  "reason": "Component \'aignostics.platform._service.Service\' is DOWN",  "components": {    "aignostics.application._service.Service": {      "status": "UP",      "reason": null,      "components": {}    },    "aignostics.bucket._service.Service": {      "status": "UP",      "reason": null,      "components": {}    },    "aignostics.dataset._service.Service": {      "status": "UP",      "reason": null,      "components": {}    },    "aignostics.notebook._service.Service": {      "status": "UP",      "reason": null,      "components": {}    },    "aignostics.platform._service.Service": {      "status": "DOWN",      "reason": "Component \'api_public\' is DOWN",      "components": {        "api_public": {          "status": "DOWN",          "reason": "Aignostics Platform API returned status \'503\'",          "components": {}        },        "api_authenticated": {          "status": "UP",          "reason": null,          "components": {}        }      }    },    "aignostics.qupath._service.Service": {      "status": "UP",      "reason": null,      "components": {}    },    "aignostics.wsi._service.Service": {      "status": "UP",      "reason": null,      "components": {}    },    "network": {      "status": "UP",      "reason": null,      "components": {}    }  }}' = normalize_output('{\n  "status": "DOWN",\n  "reason": "Component \'aignostics.platform._service.Service\' is DOWN",\n  "components": {\n    "aignostics.application._service.Service": {\n      "status": "UP",\n      "reason": null,\n      "components": {}\n    },\n    "aignostics.bucket._service.Service": {\n      "status": "UP",\n      "reason": null,\n      "components": {}\n    },\n    "aignostics.dataset._service.Service": {\n      "status": "UP",\n      "reason": null,\n      "components": {}\n    },\n    "aignostics.notebook._service.Service": {\n      "status": "UP",\n      "reason": null,\n      "components": {}\n    },\n    "aignostics.platform._service.Service": {\n      "status": "DOWN",\n      "reason": "Component \'api_public\' is DOWN",\n      "components": {\n        "api_public": {\n          "status": "DOWN",\n          "reason": "Aignostics Platform API returned status \'503\'",\n          "components": {}\n        },\n        "api_authenticated": {\n          "status": "UP",\n          "reason": null,\n          "components": {}\n        }\n      }\n    },\n    "aignostics.qupath._service.Service": {\n      "status": "UP",\n      "reason": null,\n      "components": {}\n    },\n    "aignostics.wsi._service.Service": {\n      "status": "UP",\n      "reason": null,\n      "components": {}\n    },\n    "network": {\n      "status": "UP",\n      "reason": null,\n      "components": {}\n    }\n  }\n}\n')
E        +        where '{\n  "status": "DOWN",\n  "reason": "Component \'aignostics.platform._service.Service\' is DOWN",\n  "components": {\n    "aignostics.application._service.Service": {\n      "status": "UP",\n      "reason": null,\n      "components": {}\n    },\n    "aignostics.bucket._service.Service": {\n      "status": "UP",\n      "reason": null,\n      "components": {}\n    },\n    "aignostics.dataset._service.Service": {\n      "status": "UP",\n      "reason": null,\n      "components": {}\n    },\n    "aignostics.notebook._service.Service": {\n      "status": "UP",\n      "reason": null,\n      "components": {}\n    },\n    "aignostics.platform._service.Service": {\n      "status": "DOWN",\n      "reason": "Component \'api_public\' is DOWN",\n      "components": {\n        "api_public": {\n          "status": "DOWN",\n          "reason": "Aignostics Platform API returned status \'503\'",\n          "components": {}\n        },\n        "api_authenticated": {\n          "status": "UP",\n          "reason": null,\n          "components": {}\n        }\n      }\n    },\n    "aignostics.qupath._service.Service": {\n      "status": "UP",\n      "reason": null,\n      "components": {}\n    },\n    "aignostics.wsi._service.Service": {\n      "status": "UP",\n      "reason": null,\n      "components": {}\n    },\n    "network": {\n      "status": "UP",\n      "reason": null,\n      "components": {}\n    }\n  }\n}\n' = <Result SystemExit(1)>.stdout

.../aignostics/system/cli_test.py:25: AssertionError
tests.aignostics.application.cli_test::test_cli_json_format_and_cancel_by_filter_with_dry_run
Stack Traces | 9.28s run time
runner = <typer.testing.CliRunner object at 0x7fee3090a350>
tmp_path = PosixPath('.../pytest-18/popen-gw1/test_cli_json_format_and_cance0')
silent_logging = None
record_property = <function record_property.<locals>.append_property at 0x7fee30904040>

    @pytest.mark.e2e
    @pytest.mark.timeout(timeout=180)
    def test_cli_json_format_and_cancel_by_filter_with_dry_run(  # noqa: PLR0915, PLR0914
        runner: CliRunner, tmp_path: Path, silent_logging, record_property
    ) -> None:
        """Test JSON output format for application/run commands and cancel-by-filter with dry-run mode.
    
        This test comprehensively validates:
        1. JSON format for application list/describe commands
        2. JSON format for run list/describe commands
        3. Run filtering by tags
        4. cancel-by-filter command with multiple filters (tags, application_id, application_version)
        5. Dry-run mode (preview without canceling)
        6. Actual cancellation and state transitions (PENDING/PROCESSING → TERMINATED)
        7. Termination reason verification (CANCELED_BY_USER)
        """
        record_property("tested-item-id", "TC-APPLICATION-CLI-JSON-FORMAT-AND-CANCEL-BY-FILTER")
    
        # Step 1: Test application list with JSON format
        app_list_result = runner.invoke(
            cli,
            [
                "application",
                "list",
                "--format",
                "json",
            ],
        )
        assert app_list_result.exit_code == 0
        apps_data = json.loads(app_list_result.stdout)
        assert isinstance(apps_data, list), "Application list JSON output should be a list"
        assert len(apps_data) > 0, "Should have at least one application"
    
        # Find HETA application in the list
        heta_found = False
        for app in apps_data:
            if app["application_id"] == HETA_APPLICATION_ID:
                heta_found = True
                assert "name" in app
                assert "latest_version" in app
                break
        assert heta_found, f"Application '{HETA_APPLICATION_ID}' should be in the list"
    
        # Step 2: Test application describe with JSON format
        app_describe_result = runner.invoke(
            cli,
            [
                "application",
                "describe",
                HETA_APPLICATION_ID,
                "--format",
                "json",
            ],
        )
        assert app_describe_result.exit_code == 0
        app_details = json.loads(app_describe_result.stdout)
        assert isinstance(app_details, dict), "Application describe JSON output should be a dictionary"
        assert app_details["application_id"] == HETA_APPLICATION_ID
        assert "name" in app_details
        assert "versions" in app_details
        assert "description" in app_details
    
        # Step 3: Submit a run with custom tag
        csv_content = "external_id;checksum_base64_crc32c;resolution_mpp;width_px;height_px;staining_method;tissue;disease;"
        csv_content += "platform_bucket_url\n"
        csv_content += ";5onqtA==;0.26268186053789266;7447;7196;H&E;LUNG;LUNG_CANCER;gs:.../bucket/test"
        csv_path = tmp_path / "dummy.csv"
        csv_path.write_text(csv_content)
    
        unique_tag = f"test_json_format_{datetime.now(tz=UTC).timestamp()}"
    
        result = runner.invoke(
            cli,
            [
                "application",
                "run",
                "submit",
                HETA_APPLICATION_ID,
                str(csv_path),
                "--tags",
                unique_tag,
                "--note",
                "Testing JSON format output",
                "--gpu-type",
                PIPELINE_GPU_TYPE,
            ],
        )
        output = normalize_output(result.stdout)
>       assert result.exit_code == 0
E       assert 1 == 0
E        +  where 1 = <Result SystemExit(1)>.exit_code

.../aignostics/application/cli_test.py:1406: AssertionError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

@sonarqubecloud
Copy link

@neelay-aign neelay-aign force-pushed the task/changes-for-fastmcp-3 branch from ca1cf0b to cf8dc7b Compare March 13, 2026 10:18
Copilot AI review requested due to automatic review settings March 13, 2026 10:58
@neelay-aign neelay-aign force-pushed the task/changes-for-fastmcp-3 branch from cf8dc7b to bf719e1 Compare March 13, 2026 10:58
@neelay-aign neelay-aign changed the title task: Changes required for FastMCP v3.x chore: Bump FastMCP to v3.x and make the necessary changes to support it Mar 13, 2026
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 5 changed files in this pull request and generated no new comments.

@sonarqubecloud
Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants