Files
Pietro Zullo e935514fc7 feat(python): add icons parameter to MCPServer constructor (#1147)
* fix(python/server): simplify tool JSON Schema for Optional parameters

Pydantic generates {"anyOf": [{"type": "T"}, {"type": "null"}]} for
Optional[T] tool parameters. While valid JSON Schema 2020-12, this
pattern breaks description rendering in the MCP Inspector UI and fails
Gemini API validation (requires top-level type field). MCP signals
optionality via the required array, making the anyOf/null wrapper
redundant.

- Add simplify_optional_schema() utility that collapses nullable anyOf
  into flat {"type": "T"} while preserving description, default, title
- Override MCPServer.add_tool() to apply simplification after Pydantic
  generates the schema
- Add 12 unit tests for the schema simplifier (test_json_schema.py)
- Add 23 unit tests for tool schema output (test_list_tool_format.py)
- Update server_example.py with representative parameter patterns
  including Literal enum types

* docs(python/server): add parameter types and JSON Schema section to tools docs

Document the Python-to-JSON-Schema type mapping, required/optional/nullable
parameter patterns, and Literal enum usage. Includes a note explaining the
anyOf/null simplification that mcp-use applies automatically.

* feat(python/server): add icons parameter to MCPServer constructor

Pass through the icons parameter to FastMCP so server-level icons can
be set directly in the MCPServer constructor.

* docs(python/server): document icons parameter in server overview

* docs(python): update server example with icons and Literal enum params

* style: format server_example.py

* test(python): add unit tests for icons constructor parameter

* docs: regenerate API reference for icons param
2026-03-05 10:17:58 +01:00

153 lines
5.8 KiB
Python

"""
Unit tests for MCPServer configuration.
These tests verify that:
1. Default host/port values are cloud-friendly (0.0.0.0:8000)
2. Custom host/port can be set at initialization
3. run() can override host/port from __init__
4. Settings are correctly passed to FastMCP
5. Debug routes are registered correctly
6. Icons parameter is passed through to FastMCP
"""
from unittest.mock import patch
import pytest
from mcp.types import Icon
from mcp_use.server import MCPServer
class TestMCPServerDefaults:
"""Test default configuration values."""
def test_default_host_is_cloud_friendly(self):
"""Default host should be 0.0.0.0 for cloud/proxy compatibility."""
server = MCPServer(name="test-server")
assert server.settings.host == "0.0.0.0"
def test_default_port_is_8000(self):
"""Default port should be 8000."""
server = MCPServer(name="test-server")
assert server.settings.port == 8000
class TestMCPServerCustomConfig:
"""Test custom host/port configuration at init."""
def test_custom_host_at_init(self):
"""Host can be customized at initialization."""
server = MCPServer(name="test-server", host="127.0.0.1")
assert server.settings.host == "127.0.0.1"
def test_custom_port_at_init(self):
"""Port can be customized at initialization."""
server = MCPServer(name="test-server", port=3000)
assert server.settings.port == 3000
def test_custom_host_and_port_at_init(self):
"""Both host and port can be customized together."""
server = MCPServer(name="test-server", host="127.0.0.1", port=9000)
assert server.settings.host == "127.0.0.1"
assert server.settings.port == 9000
class TestDNSRebindingProtection:
"""Test DNS rebinding protection based on host."""
def test_localhost_enables_dns_protection(self):
"""127.0.0.1 should auto-enable DNS rebinding protection."""
server = MCPServer(name="test-server", host="127.0.0.1")
security = server.settings.transport_security
assert security is not None
assert security.enable_dns_rebinding_protection is True
def test_all_interfaces_disables_dns_protection(self):
"""0.0.0.0 should not enable DNS rebinding protection."""
server = MCPServer(name="test-server", host="0.0.0.0")
security = server.settings.transport_security
# Either None or explicitly disabled
assert security is None or security.enable_dns_rebinding_protection is False
class TestRunHostOverride:
"""Test that run() properly reconfigures DNS protection when host changes."""
def test_dns_rebinding_protection_flag_enables_protection(self):
"""Setting dns_rebinding_protection=True should enable DNS protection."""
server = MCPServer(name="test-server", dns_rebinding_protection=True)
assert server.settings.transport_security is not None
assert server.settings.transport_security.enable_dns_rebinding_protection is True
def test_dns_rebinding_protection_default_disabled(self):
"""DNS protection should be disabled by default."""
server = MCPServer(name="test-server")
assert (
server.settings.transport_security is None
or server.settings.transport_security.enable_dns_rebinding_protection is False
)
class TestDebugRoutes:
"""Test that debug routes are registered correctly."""
def _get_route_paths(self, server: MCPServer) -> list[str]:
return [r.path for r in server._custom_starlette_routes]
def test_debug_false_no_dev_routes(self):
"""debug=False should not register dev routes."""
server = MCPServer(name="test-server", debug=False)
paths = self._get_route_paths(server)
assert "/docs" not in paths
assert "/inspector" not in paths
assert "/openmcp.json" not in paths
def test_debug_true_at_init_registers_routes(self):
"""debug=True at init should register dev routes."""
server = MCPServer(name="test-server", debug=True)
paths = self._get_route_paths(server)
assert "/docs" in paths
assert "/inspector" in paths
assert "/openmcp.json" in paths
def test_debug_true_at_run_registers_routes(self):
"""debug=True passed to run() should register dev routes even if init had debug=False.
Regression test for https://github.com/mcp-use/mcp-use/issues/1099
"""
server = MCPServer(name="test-server", debug=False)
assert "/docs" not in self._get_route_paths(server)
# Patch ServerRunner.run to prevent actually starting the server
with patch("mcp_use.server.server.ServerRunner.run"):
server.run(debug=True)
paths = self._get_route_paths(server)
assert "/docs" in paths
assert "/inspector" in paths
assert "/openmcp.json" in paths
assert server.debug_level == 1
class TestServerIcons:
"""Test that the icons parameter propagates to FastMCP."""
def test_no_icons_by_default(self):
server = MCPServer(name="test-server")
assert server._mcp_server.icons is None
def test_icons_passed_through(self):
icons = [Icon(src="https://example.com/icon.png", mimeType="image/png")]
server = MCPServer(name="test-server", icons=icons)
assert server._mcp_server.icons == icons
def test_multiple_icons(self):
icons = [
Icon(src="https://example.com/light.svg", mimeType="image/svg+xml"),
Icon(src="https://example.com/dark.png", mimeType="image/png"),
]
server = MCPServer(name="test-server", icons=icons)
assert len(server._mcp_server.icons) == 2
assert server._mcp_server.icons[0].src == "https://example.com/light.svg"
assert server._mcp_server.icons[1].src == "https://example.com/dark.png"