Skip to content

Commit 0871a3b

Browse files
authored
fix: upgrade to FastMCP v3.0.0 (#657)
Fixes #654 Upgrade from FastMCP v2 to v3.0.0 to resolve silent server crashes on connection. FastMCP v3 properly supports the 2025-11-25 protocol version that modern MCP clients (Claude Desktop 1.1+) negotiate. Breaking API changes addressed: - get_tools() removed → list_tools() (returns list, not dict) - _tool_manager internal removed → use public list_tools() in smoke test - FASTMCP_SHOW_CLI_BANNER renamed → FASTMCP_SHOW_SERVER_BANNER - transport="streamable-http" deprecated → transport="http" - log_level= kwarg removed from run() → use logging.basicConfig() - TYPE_CHECKING import mcp.server.fastmcp → fastmcp Also improves addon crash diagnostics: catches BaseException (including SystemExit) and logs full traceback + chained cause to stderr, so FastMCP crashes are never silent.
1 parent 3fe49cc commit 0871a3b

11 files changed

Lines changed: 113 additions & 243 deletions

File tree

fastmcp-http.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"entrypoint": "mcp"
66
},
77
"deployment": {
8-
"transport": "streamable-http",
8+
"transport": "http",
99
"host": "0.0.0.0",
1010
"port": 8086,
1111
"path": "/mcp",

fastmcp-webclient.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"entrypoint": "mcp"
66
},
77
"deployment": {
8-
"transport": "streamable-http",
8+
"transport": "http",
99
"host": "0.0.0.0",
1010
"port": 8086,
1111
"path": "/mcp",

homeassistant-addon/start.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -132,28 +132,42 @@ def main() -> int:
132132
log_info("=" * 80)
133133
log_info("")
134134

135+
# Configure logging before server start (v3 removed log_level from run())
136+
import logging
137+
logging.basicConfig(level=logging.INFO)
138+
135139
# Import and run MCP server directly
136140
try:
137141
log_info("Importing ha_mcp module...")
138142
from ha_mcp.__main__ import _get_timestamped_uvicorn_log_config, mcp
139143

140144
log_info("Starting MCP server...")
141145
mcp.run(
142-
transport="streamable-http",
146+
transport="http",
143147
host="0.0.0.0",
144148
port=port,
145149
path=secret_path,
146-
log_level="info",
147150
stateless_http=True,
148151
uvicorn_config={"log_config": _get_timestamped_uvicorn_log_config()},
149152
)
150-
except Exception as e:
151-
log_error(f"Failed to start MCP server: {e}")
153+
except KeyboardInterrupt:
154+
log_info("Interrupted, exiting")
155+
return 0
156+
except BaseException as e:
152157
import traceback
153158

154-
traceback.print_exc()
159+
log_error(f"MCP server crashed: {e}")
160+
traceback.print_exc(file=sys.stderr)
161+
# Log the root cause if this exception was chained
162+
cause = e.__cause__ or e.__context__
163+
if cause:
164+
log_error(f"Caused by: {cause}")
165+
traceback.print_exception(type(cause), cause, cause.__traceback__, file=sys.stderr)
166+
if isinstance(e, SystemExit):
167+
return int(e.code) if isinstance(e.code, int) else 1
155168
return 1
156169

170+
log_info("MCP server stopped")
157171
return 0
158172

159173

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ classifiers = [
2424
]
2525

2626
dependencies = [
27-
"fastmcp>=2.11.0,<3.0.0", # Pin: v3.0.0 has breaking changes (see #644, #645, #648)
27+
"fastmcp>=3.0.0,<4.0.0",
2828
"mcp>=1.24.0", # Required for protocol version 2025-11-25 support (see #651)
2929
"httpx[socks]>=0.27.0,<1.0",
3030
'jq>=1.8.0; sys_platform != "win32"',

site/src/content/clients/antigravity.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,14 @@ Google Antigravity supports MCP servers via the built-in MCP Store and custom co
3232
"env": {
3333
"HOMEASSISTANT_URL": "{{HOMEASSISTANT_URL}}",
3434
"HOMEASSISTANT_TOKEN": "{{HOMEASSISTANT_TOKEN}}",
35-
"FASTMCP_SHOW_CLI_BANNER": "false"
35+
"FASTMCP_SHOW_SERVER_BANNER": "false"
3636
}
3737
}
3838
}
3939
}
4040
```
4141

42-
> **Note:** `FASTMCP_SHOW_CLI_BANNER=false` disables the startup banner, which prevents "Unexpected server output" errors in Antigravity.
42+
> **Note:** `FASTMCP_SHOW_SERVER_BANNER=false` disables the startup banner, which prevents "Unexpected server output" errors in Antigravity.
4343
4444
**Important:** Use absolute paths if specifying a local command. Restart the Agent session after saving.
4545

@@ -59,7 +59,7 @@ Google Antigravity supports MCP servers via the built-in MCP Store and custom co
5959

6060
## Troubleshooting
6161

62-
- **"Unexpected server output" error:** Add `"FASTMCP_SHOW_CLI_BANNER": "false"` to your env config (see example above)
62+
- **"Unexpected server output" error:** Add `"FASTMCP_SHOW_SERVER_BANNER": "false"` to your env config (see example above)
6363
- **Tools load but fail when called:** Try stdio mode instead of HTTP
6464
- **"EOF" errors:** Ensure command paths are absolute, not relative
6565
- **First run timeout:** Run `uvx ha-mcp@latest --version` in terminal first to cache the package

src/ha_mcp/__main__.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -236,10 +236,10 @@ def _validate_standard_credentials(settings) -> None:
236236

237237

238238
def _get_show_banner() -> bool:
239-
"""Check if CLI banner should be shown (respects FASTMCP_SHOW_CLI_BANNER env var)."""
239+
"""Check if server banner should be shown (respects FASTMCP_SHOW_SERVER_BANNER env var)."""
240240
import fastmcp
241241

242-
return fastmcp.settings.show_cli_banner
242+
return fastmcp.settings.show_server_banner
243243

244244

245245
def _setup_standard_mode() -> None:
@@ -563,7 +563,7 @@ def _run_http_server(transport: str, default_port: int = 8086) -> None:
563563
"""Common runner for HTTP-based transports.
564564
565565
Args:
566-
transport: Transport type (streamable-http or sse).
566+
transport: Transport type (http or sse).
567567
default_port: Default port to use if MCP_PORT env var is not set.
568568
"""
569569
port, path = _get_http_runtime(default_port)
@@ -584,7 +584,7 @@ def main_web() -> None:
584584
- MCP_SECRET_PATH (optional, default: "/mcp")
585585
"""
586586
_setup_standard_mode()
587-
_run_http_server("streamable-http", default_port=8086)
587+
_run_http_server("http", default_port=8086)
588588

589589

590590
def main_sse() -> None:
@@ -665,13 +665,13 @@ async def _run_oauth_server(base_url: str, port: int, path: str) -> None:
665665

666666
logger.info("Server created with OAuthProxyClient")
667667

668-
tools = await mcp.get_tools()
668+
tools = await mcp.list_tools()
669669
logger.info(
670670
f"Starting OAuth-enabled MCP server with {len(tools)} tools on {base_url}{path}"
671671
)
672672

673673
await _run_with_shutdown(
674-
mcp.run_async(**_http_run_kwargs("streamable-http", port, path))
674+
mcp.run_async(**_http_run_kwargs("http", port, path))
675675
)
676676

677677

src/ha_mcp/smoke_test.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -74,16 +74,18 @@ def main() -> int:
7474
# Test 4: Tool discovery
7575
print("\n[4/4] Testing tool discovery...")
7676
try:
77-
# Access the tools from the MCP instance
78-
tool_count = len(mcp._tool_manager._tools)
77+
import asyncio
78+
79+
tools = asyncio.run(mcp.list_tools())
80+
tool_count = len(tools)
7981
print(f" ✓ Discovered {tool_count} tools")
8082

8183
if tool_count < 50:
8284
errors.append(f"Too few tools discovered: {tool_count} (expected 50+)")
8385
print(" ✗ Tool count too low (expected 50+)")
8486
else:
8587
# List a few tool names as examples
86-
tool_names = list(mcp._tool_manager._tools.keys())[:5]
88+
tool_names = [t.name for t in tools[:5]]
8789
print(f" ✓ Sample tools: {', '.join(tool_names)}...")
8890
except Exception as e:
8991
errors.append(f"Failed to discover tools: {e}")

src/ha_mcp/tools/backup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from .helpers import get_connected_ws_client, log_tool_usage
1717

1818
if TYPE_CHECKING:
19-
from mcp.server.fastmcp import FastMCP
19+
from fastmcp import FastMCP
2020

2121
logger = logging.getLogger(__name__)
2222

tests/src/e2e/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,7 @@ async def mcp_server(
326326

327327
# Create server with the client
328328
server = HomeAssistantSmartMCPServer(client=client)
329-
tools = await server.mcp.get_tools()
329+
tools = await server.mcp.list_tools()
330330
logger.info(
331331
f"✅ MCP server initialized with {len(tools)} tools connected to {base_url}"
332332
)

tests/src/unit/test_graceful_shutdown.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -385,8 +385,8 @@ def mock_setup():
385385
class TestHTTPEntryPoints:
386386
"""Tests for HTTP entry points (main_web, main_sse)."""
387387

388-
def test_main_web_uses_streamable_http_transport(self):
389-
"""main_web should use streamable-http transport."""
388+
def test_main_web_uses_http_transport(self):
389+
"""main_web should use http transport."""
390390
import ha_mcp.__main__ as main_module
391391

392392
transport_used = None
@@ -407,7 +407,7 @@ def mock_run_http(transport, default_port=8086):
407407
}), patch.object(main_module, "_run_http_server", side_effect=mock_run_http), pytest.raises(SystemExit):
408408
main_module.main_web()
409409

410-
assert transport_used == "streamable-http"
410+
assert transport_used == "http"
411411

412412
def test_main_sse_uses_sse_transport(self):
413413
"""main_sse should use sse transport."""

0 commit comments

Comments
 (0)