diff --git a/CHANGELOG.md b/CHANGELOG.md index 3818a9498..4693b8ed4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,10 @@ Status of the `main` branch. Changes prior to the next official version change will appear here. * General: + - Upgrade the `mcp` SDK from 1.28.1 to 2.0.0 (MCP protocol revision 2026-07-28). Two behavioural + consequences: `FASTMCP_*` environment variables no longer configure the MCP server (the SDK's + `Settings` is no longer environment-backed), and `SerenaMCPFactory.create_mcp_server()` no longer + takes `host`/`port`, which the SDK moved to the transport-specific `run()` call - Fix: the README, the Language Support docs page and the project template omitted several already-supported language servers - Fix: a tool call exceeding the timeout blocked the task executor indefinitely; the executor now recovers without user-induced cancellation diff --git a/pyproject.toml b/pyproject.toml index 90d888e75..1ed731a9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ dependencies = [ "requests==2.33.0", "overrides==7.7.0", "python-dotenv==1.2.2", - "mcp==1.28.1", + "mcp==2.0.0", "flask==3.1.3", # bumped from 3.1.1 for CVE fix (also fixes werkzeug alert) "sensai-utils==1.5.0", "pydantic==2.12.5", diff --git a/src/serena/cli.py b/src/serena/cli.py index c514fcb49..ede53818c 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -372,8 +372,6 @@ def start_mcp_server( factory = SerenaMCPFactory(transport=transport, context=context, project=project_file, memory_log_handler=memory_log_handler) server = factory.create_mcp_server( - host=host, - port=port, mode_selection_def=mode_selection_def, language_backend=LanguageBackend.from_str(language_backend) if language_backend else None, enable_web_dashboard=enable_web_dashboard, @@ -389,7 +387,13 @@ def start_mcp_server( project_file, ) log.info("Starting MCP server …") - server.run(transport=transport) + run_kwargs: dict[str, Any] = {"transport": transport} + if transport != "stdio": + # host/port are transport-specific run() kwargs as of mcp>=2.0 (no longer + # accepted at server construction time); stdio has no host/port concept. + run_kwargs["host"] = host + run_kwargs["port"] = port + server.run(**run_kwargs) @staticmethod @click.command( diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 321c9b352..2aa5d4bc2 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -10,14 +10,13 @@ from typing import Any, Literal, cast import docstring_parser -from mcp.server.fastmcp import server -from mcp.server.fastmcp.exceptions import ToolError -from mcp.server.fastmcp.server import Context, FastMCP, Settings -from mcp.server.fastmcp.tools.base import Tool as FastMCPTool -from mcp.server.session import ServerSessionT -from mcp.shared.context import LifespanContextT, RequestT +from mcp.server.mcpserver import server +from mcp.server.mcpserver.context import LifespanContextT, RequestT +from mcp.server.mcpserver.exceptions import ToolError +from mcp.server.mcpserver.server import Context +from mcp.server.mcpserver.server import MCPServer as FastMCP +from mcp.server.mcpserver.tools.base import Tool as FastMCPTool from mcp.types import ToolAnnotations -from pydantic_settings import SettingsConfigDict from sensai.util import logging from serena.agent import ( @@ -108,8 +107,8 @@ def execute_fn(**kwargs) -> str: can_edit = tool.can_edit() annotations = ToolAnnotations( title=tool_title, - readOnlyHint=not can_edit, - destructiveHint=can_edit, + read_only_hint=not can_edit, + destructive_hint=can_edit, ) super().__init__( @@ -131,7 +130,7 @@ def execute_fn(**kwargs) -> str: async def run( self, arguments: dict[str, Any], - context: Context[ServerSessionT, LifespanContextT, RequestT] | None = None, + context: Context[LifespanContextT, RequestT], convert_result: bool = False, ) -> Any: # apply parameter aliases @@ -317,8 +316,6 @@ def _create_default_serena_config(self) -> SerenaConfig: def create_mcp_server( self, - host: str = "127.0.0.1", - port: int = 8000, mode_selection_def: ModeSelectionDefinition | None = None, language_backend: LanguageBackend | None = None, enable_web_dashboard: bool | None = None, @@ -331,8 +328,9 @@ def create_mcp_server( """ Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. - :param host: The host to bind to - :param port: The port to bind to + Note: host/port are no longer accepted here; the underlying SDK (mcp>=2.0) moved them + from server construction to the transport-specific `run()` call (see FastMCP.run_*_async). + :param mode_selection_def: the mode selection definition to apply :param language_backend: the language backend to use, overriding the configuration setting. :param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration. @@ -371,18 +369,18 @@ def create_mcp_server( show_fatal_exception_safe(e) raise - # Override model_config to disable the use of `.env` files for reading settings, because user projects are likely to contain - # `.env` files (e.g. containing LOG_LEVEL) that are not supposed to override the MCP settings; - # retain only FASTMCP_ prefix for already set environment variables. - Settings.model_config = SettingsConfigDict(env_prefix="FASTMCP_") + # NOTE: mcp<2.0 read MCP settings from the environment and from `.env` files (`Settings` was a + # pydantic-settings `BaseSettings`), so we had to override its `model_config` here to stop `.env` + # files in user projects (e.g. containing LOG_LEVEL) from overriding MCP settings. As of mcp>=2.0, + # `Settings` is a plain pydantic `BaseModel` that is only populated from the `MCPServer` + # constructor, so it reads neither the environment nor `.env` files and the override is obsolete. + # Behavioural consequence: FASTMCP_* environment variables no longer configure the server. instructions = self._get_initial_instructions() log.info("MCP server initial instructions:\n%s", instructions) mcp = FastMCP( name="Serena", lifespan=self.server_lifespan, website_url="https://oraios.github.io/serena", - host=host, - port=port, instructions=instructions, ) return mcp diff --git a/src/serena/tools/tools_base.py b/src/serena/tools/tools_base.py index 665f8a2e2..b88c355f7 100644 --- a/src/serena/tools/tools_base.py +++ b/src/serena/tools/tools_base.py @@ -8,8 +8,8 @@ from typing import TYPE_CHECKING, Any, Optional, Protocol, Self, TypeVar, cast from mcp import Implementation -from mcp.server.fastmcp import Context -from mcp.server.fastmcp.utilities.func_metadata import FuncMetadata, func_metadata +from mcp.server.mcpserver import Context +from mcp.server.mcpserver.utilities.func_metadata import FuncMetadata, func_metadata from sensai.util import logging from sensai.util.string import dict_string diff --git a/test/serena/test_mcp.py b/test/serena/test_mcp.py index 731a1cc6b..1768ecb75 100644 --- a/test/serena/test_mcp.py +++ b/test/serena/test_mcp.py @@ -1,7 +1,7 @@ """Tests for the mcp.py module in serena.""" import pytest -from mcp.server.fastmcp.tools.base import Tool as MCPTool +from mcp.server.mcpserver.tools.base import Tool as MCPTool from serena.agent import Tool, ToolRegistry from serena.config.context_mode import SerenaAgentContext diff --git a/uv.lock b/uv.lock index a73af24d8..e3c55b811 100644 --- a/uv.lock +++ b/uv.lock @@ -835,6 +835,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httpcore2" +version = "2.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/39/a8/20ed1ed79cbc2ecdf5301c0968ab7c85547212e2a7bd126ddd2d986e206e/httpcore2-2.9.1.tar.gz", hash = "sha256:4d8acbf8b306f48c9d6046591fd5ba4037d1b1b1000d140fc2c3eab1e9a0c0e2", size = 67089, upload-time = "2026-07-24T09:21:03.867Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/fb/46c52b781975c335a2bcf1072c7bbc007cbdc8d674217f5ee1daba2c848b/httpcore2-2.9.1-py3-none-any.whl", hash = "sha256:6182472379e855fe4221246a2bb7ecede403bc61c6798062ae1787d051ccde26", size = 82809, upload-time = "2026-07-24T09:21:01.178Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -856,12 +869,19 @@ http2 = [ ] [[package]] -name = "httpx-sse" -version = "0.4.3" +name = "httpx2" +version = "2.9.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +dependencies = [ + { name = "anyio" }, + { name = "httpcore2" }, + { name = "idna" }, + { name = "truststore" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/14/38128fbafd7e0ed41d874df6c9a653d47c2d111cfe59e2b4ac95161b4abd/httpx2-2.9.1.tar.gz", hash = "sha256:1932a768737e3666291582833da748cc4e563c337cf96706fccc04fa6e58764a", size = 95458, upload-time = "2026-07-24T09:21:04.972Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, + { url = "https://files.pythonhosted.org/packages/13/b8/cfd91c4ab9134d386d48f0b6ac662ff3d4be6efdee59ee1c67ebc3c0487c/httpx2-2.9.1-py3-none-any.whl", hash = "sha256:1820fe14a9ab1107bfeff39259987429450b070ec0ff38cc87eb0d8c97fdc71a", size = 91191, upload-time = "2026-07-24T09:21:02.6Z" }, ] [[package]] @@ -875,11 +895,11 @@ wheels = [ [[package]] name = "idna" -version = "3.15" +version = "3.18" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, ] [[package]] @@ -1367,15 +1387,15 @@ wheels = [ [[package]] name = "mcp" -version = "1.28.1" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "httpx" }, - { name = "httpx-sse" }, + { name = "httpx2" }, { name = "jsonschema" }, + { name = "mcp-types" }, + { name = "opentelemetry-api" }, { name = "pydantic" }, - { name = "pydantic-settings" }, { name = "pyjwt", extra = ["crypto"] }, { name = "python-multipart" }, { name = "pywin32", marker = "sys_platform == 'win32'" }, @@ -1385,9 +1405,22 @@ dependencies = [ { name = "typing-inspection" }, { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } +sdist = { url = "https://files.pythonhosted.org/packages/74/33/32d4dff2c95bb5d897c3ef4c83649a08996b17b58f0a326d2495d4c81179/mcp-2.0.0.tar.gz", hash = "sha256:0f440e735c13ece8bb19bc62cf0b86f4313448432fbb77d35e14034f4e050728", size = 1662284, upload-time = "2026-07-28T13:45:32.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/72/7d7897418912c1d12e87556630dfb7bf0eac71160e9bef8b447960804ee3/mcp-2.0.0-py3-none-any.whl", hash = "sha256:1cb4c75d2d2c7b8c1d756355e5d82a39f2822cc7f13e22a2051d7ca3592349d6", size = 349980, upload-time = "2026-07-28T13:45:28.853Z" }, +] + +[[package]] +name = "mcp-types" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/56/9b8e1c152f61f6c6b07c4b5896c88c7d0ae90bac6ee6306f852fcc5c1eb0/mcp_types-2.0.0.tar.gz", hash = "sha256:d7d939b9285c9961ae8866ba75ef85da34d12bafe276efbf4eb6a131786d8379", size = 66632, upload-time = "2026-07-28T13:45:33.804Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, + { url = "https://files.pythonhosted.org/packages/f5/4c/c78d78c3d52b0ac594ad7cc8ef5972adfe070e3597a8a4c6ce0cd39196ea/mcp_types-2.0.0-py3-none-any.whl", hash = "sha256:6b2de797ca2797f568b79529e1b25948e34de511bcc0bd82fef1039a6d1b8eb0", size = 69649, upload-time = "2026-07-28T13:45:30.713Z" }, ] [[package]] @@ -1579,6 +1612,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, ] +[[package]] +name = "opentelemetry-api" +version = "1.44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ee/8b/aa9e2d8b8dfa7c946f7dec5d1f8f6ba8eca062f43509a06bdb5ce93d26c0/opentelemetry_api-1.44.0.tar.gz", hash = "sha256:67647e5e9566edcf421166fdf022b3537f818635daa852b289e34604dc6fb33a", size = 72406, upload-time = "2026-07-16T15:25:32.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/6f/a04e900f465ff3221ccc395522503e2d10e79fa21f2723c8e177aae1e0d1/opentelemetry_api-1.44.0-py3-none-any.whl", hash = "sha256:94b98c893a91b88657eaac1e3ba89618cdb85be6918196705354f34728b2cdef", size = 60018, upload-time = "2026-07-16T15:25:11.657Z" }, +] + [[package]] name = "oslex" version = "2.0.0" @@ -2896,7 +2941,7 @@ requires-dist = [ { name = "joblib", specifier = "==1.5.1" }, { name = "jupyter-book", marker = "extra == 'dev'", specifier = "==1.0.4.post1" }, { name = "lsprotocol", specifier = "==2025.0.0" }, - { name = "mcp", specifier = "==1.28.1" }, + { name = "mcp", specifier = "==2.0.0" }, { name = "oslex", specifier = "==2.0.0" }, { name = "overrides", specifier = "==7.7.0" }, { name = "pathspec", specifier = "==0.12.1" }, @@ -3559,6 +3604,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "ty" version = "0.0.24"