@@ -1553,3 +1553,365 @@ def test_env_list_servers_combined_filters(self):
15531553
15541554 # server-c should NOT appear (wrong env)
15551555 assert "server-c" not in output , "server-c should NOT appear"
1556+
1557+
1558+ class TestMCPShowHostsCommand :
1559+ """Integration tests for hatch mcp show hosts command.
1560+
1561+ Reference: R11 §2.1 (11-enhancing_show_command_v0.md) - Show hosts specification
1562+
1563+ These tests verify that handle_mcp_show_hosts:
1564+ 1. Shows detailed host configurations with hierarchical output
1565+ 2. Supports --server filter for regex pattern matching
1566+ 3. Omits hosts with no matching servers when filter applied
1567+ 4. Shows horizontal separators between host sections
1568+ 5. Highlights entity names with amber + bold
1569+ 6. Supports --json output format
1570+ """
1571+
1572+ def test_mcp_show_hosts_no_filter (self ):
1573+ """Command should show all hosts with detailed configuration.
1574+
1575+ Reference: R11 §2.1 - Output format without filter
1576+ """
1577+ from hatch .cli .cli_mcp import handle_mcp_show_hosts
1578+ from hatch .mcp_host_config import MCPHostType , MCPServerConfig
1579+ from hatch .mcp_host_config .models import HostConfiguration
1580+
1581+ mock_env_manager = MagicMock ()
1582+ mock_env_manager .list_environments .return_value = [{"name" : "default" }]
1583+ mock_env_manager .get_environment_data .return_value = {
1584+ "packages" : [
1585+ {
1586+ "name" : "weather-server" ,
1587+ "version" : "1.0.0" ,
1588+ "configured_hosts" : {"claude-desktop" : {"configured_at" : "2026-01-30" }}
1589+ }
1590+ ]
1591+ }
1592+
1593+ args = Namespace (
1594+ env_manager = mock_env_manager ,
1595+ server = None , # No filter
1596+ json = False ,
1597+ )
1598+
1599+ mock_host_config = HostConfiguration (servers = {
1600+ "weather-server" : MCPServerConfig (name = "weather-server" , command = "uvx" , args = ["weather-mcp" ]),
1601+ "custom-tool" : MCPServerConfig (name = "custom-tool" , command = "node" , args = ["custom.js" ]),
1602+ })
1603+
1604+ with patch ('hatch.cli.cli_mcp.MCPHostRegistry' ) as mock_registry :
1605+ mock_registry .detect_available_hosts .return_value = [MCPHostType .CLAUDE_DESKTOP ]
1606+ mock_strategy = MagicMock ()
1607+ mock_strategy .read_configuration .return_value = mock_host_config
1608+ mock_strategy .get_config_path .return_value = MagicMock (exists = lambda : True )
1609+ mock_registry .get_strategy .return_value = mock_strategy
1610+
1611+ with patch ('hatch.mcp_host_config.strategies' ):
1612+ captured_output = io .StringIO ()
1613+ with patch ('sys.stdout' , captured_output ):
1614+ result = handle_mcp_show_hosts (args )
1615+
1616+ output = captured_output .getvalue ()
1617+
1618+ # Should show host header
1619+ assert "claude-desktop" in output , "Host name should appear"
1620+
1621+ # Should show both servers
1622+ assert "weather-server" in output , "weather-server should appear"
1623+ assert "custom-tool" in output , "custom-tool should appear"
1624+
1625+ # Should show server details
1626+ assert "Command:" in output or "uvx" in output , "Server command should appear"
1627+
1628+ def test_mcp_show_hosts_server_filter_exact (self ):
1629+ """--server filter should match exact server name.
1630+
1631+ Reference: R11 §2.1 - Server filter with exact match
1632+ """
1633+ from hatch .cli .cli_mcp import handle_mcp_show_hosts
1634+ from hatch .mcp_host_config import MCPHostType , MCPServerConfig
1635+ from hatch .mcp_host_config .models import HostConfiguration
1636+
1637+ mock_env_manager = MagicMock ()
1638+ mock_env_manager .list_environments .return_value = [{"name" : "default" }]
1639+ mock_env_manager .get_environment_data .return_value = {"packages" : []}
1640+
1641+ args = Namespace (
1642+ env_manager = mock_env_manager ,
1643+ server = "weather-server" , # Exact match
1644+ json = False ,
1645+ )
1646+
1647+ mock_host_config = HostConfiguration (servers = {
1648+ "weather-server" : MCPServerConfig (name = "weather-server" , command = "uvx" , args = ["weather-mcp" ]),
1649+ "fetch-server" : MCPServerConfig (name = "fetch-server" , command = "python" , args = ["fetch.py" ]),
1650+ })
1651+
1652+ with patch ('hatch.cli.cli_mcp.MCPHostRegistry' ) as mock_registry :
1653+ mock_registry .detect_available_hosts .return_value = [MCPHostType .CLAUDE_DESKTOP ]
1654+ mock_strategy = MagicMock ()
1655+ mock_strategy .read_configuration .return_value = mock_host_config
1656+ mock_strategy .get_config_path .return_value = MagicMock (exists = lambda : True )
1657+ mock_registry .get_strategy .return_value = mock_strategy
1658+
1659+ with patch ('hatch.mcp_host_config.strategies' ):
1660+ captured_output = io .StringIO ()
1661+ with patch ('sys.stdout' , captured_output ):
1662+ result = handle_mcp_show_hosts (args )
1663+
1664+ output = captured_output .getvalue ()
1665+
1666+ # Should show matching server
1667+ assert "weather-server" in output , "weather-server should appear"
1668+
1669+ # Should NOT show non-matching server
1670+ assert "fetch-server" not in output , "fetch-server should NOT appear"
1671+
1672+ def test_mcp_show_hosts_server_filter_pattern (self ):
1673+ """--server filter should support regex patterns.
1674+
1675+ Reference: R11 §2.1 - Server filter with regex pattern
1676+ """
1677+ from hatch .cli .cli_mcp import handle_mcp_show_hosts
1678+ from hatch .mcp_host_config import MCPHostType , MCPServerConfig
1679+ from hatch .mcp_host_config .models import HostConfiguration
1680+
1681+ mock_env_manager = MagicMock ()
1682+ mock_env_manager .list_environments .return_value = [{"name" : "default" }]
1683+ mock_env_manager .get_environment_data .return_value = {"packages" : []}
1684+
1685+ args = Namespace (
1686+ env_manager = mock_env_manager ,
1687+ server = ".*-server" , # Regex pattern
1688+ json = False ,
1689+ )
1690+
1691+ mock_host_config = HostConfiguration (servers = {
1692+ "weather-server" : MCPServerConfig (name = "weather-server" , command = "uvx" , args = []),
1693+ "fetch-server" : MCPServerConfig (name = "fetch-server" , command = "python" , args = []),
1694+ "custom-tool" : MCPServerConfig (name = "custom-tool" , command = "node" , args = []),
1695+ })
1696+
1697+ with patch ('hatch.cli.cli_mcp.MCPHostRegistry' ) as mock_registry :
1698+ mock_registry .detect_available_hosts .return_value = [MCPHostType .CLAUDE_DESKTOP ]
1699+ mock_strategy = MagicMock ()
1700+ mock_strategy .read_configuration .return_value = mock_host_config
1701+ mock_strategy .get_config_path .return_value = MagicMock (exists = lambda : True )
1702+ mock_registry .get_strategy .return_value = mock_strategy
1703+
1704+ with patch ('hatch.mcp_host_config.strategies' ):
1705+ captured_output = io .StringIO ()
1706+ with patch ('sys.stdout' , captured_output ):
1707+ result = handle_mcp_show_hosts (args )
1708+
1709+ output = captured_output .getvalue ()
1710+
1711+ # Should show matching servers
1712+ assert "weather-server" in output , "weather-server should appear"
1713+ assert "fetch-server" in output , "fetch-server should appear"
1714+
1715+ # Should NOT show non-matching server
1716+ assert "custom-tool" not in output , "custom-tool should NOT appear"
1717+
1718+ def test_mcp_show_hosts_omits_empty_hosts (self ):
1719+ """Hosts with no matching servers should be omitted.
1720+
1721+ Reference: R11 §2.1 - Empty host omission
1722+ """
1723+ from hatch .cli .cli_mcp import handle_mcp_show_hosts
1724+ from hatch .mcp_host_config import MCPHostType , MCPServerConfig
1725+ from hatch .mcp_host_config .models import HostConfiguration
1726+
1727+ mock_env_manager = MagicMock ()
1728+ mock_env_manager .list_environments .return_value = [{"name" : "default" }]
1729+ mock_env_manager .get_environment_data .return_value = {"packages" : []}
1730+
1731+ args = Namespace (
1732+ env_manager = mock_env_manager ,
1733+ server = "weather-server" , # Only matches on claude-desktop
1734+ json = False ,
1735+ )
1736+
1737+ claude_config = HostConfiguration (servers = {
1738+ "weather-server" : MCPServerConfig (name = "weather-server" , command = "uvx" , args = []),
1739+ })
1740+ cursor_config = HostConfiguration (servers = {
1741+ "fetch-server" : MCPServerConfig (name = "fetch-server" , command = "python" , args = []),
1742+ })
1743+
1744+ with patch ('hatch.cli.cli_mcp.MCPHostRegistry' ) as mock_registry :
1745+ mock_registry .detect_available_hosts .return_value = [
1746+ MCPHostType .CLAUDE_DESKTOP ,
1747+ MCPHostType .CURSOR ,
1748+ ]
1749+
1750+ def get_strategy_side_effect (host_type ):
1751+ mock_strategy = MagicMock ()
1752+ mock_strategy .get_config_path .return_value = MagicMock (exists = lambda : True )
1753+ if host_type == MCPHostType .CLAUDE_DESKTOP :
1754+ mock_strategy .read_configuration .return_value = claude_config
1755+ elif host_type == MCPHostType .CURSOR :
1756+ mock_strategy .read_configuration .return_value = cursor_config
1757+ else :
1758+ mock_strategy .read_configuration .return_value = HostConfiguration (servers = {})
1759+ return mock_strategy
1760+
1761+ mock_registry .get_strategy .side_effect = get_strategy_side_effect
1762+
1763+ with patch ('hatch.mcp_host_config.strategies' ):
1764+ captured_output = io .StringIO ()
1765+ with patch ('sys.stdout' , captured_output ):
1766+ result = handle_mcp_show_hosts (args )
1767+
1768+ output = captured_output .getvalue ()
1769+
1770+ # claude-desktop should appear (has matching server)
1771+ assert "claude-desktop" in output , "claude-desktop should appear"
1772+
1773+ # cursor should NOT appear (no matching servers)
1774+ assert "cursor" not in output , "cursor should NOT appear (no matching servers)"
1775+
1776+ def test_mcp_show_hosts_alphabetical_ordering (self ):
1777+ """Hosts should be sorted alphabetically.
1778+
1779+ Reference: R11 §1.4 - Alphabetical ordering
1780+ """
1781+ from hatch .cli .cli_mcp import handle_mcp_show_hosts
1782+ from hatch .mcp_host_config import MCPHostType , MCPServerConfig
1783+ from hatch .mcp_host_config .models import HostConfiguration
1784+
1785+ mock_env_manager = MagicMock ()
1786+ mock_env_manager .list_environments .return_value = [{"name" : "default" }]
1787+ mock_env_manager .get_environment_data .return_value = {"packages" : []}
1788+
1789+ args = Namespace (
1790+ env_manager = mock_env_manager ,
1791+ server = None ,
1792+ json = False ,
1793+ )
1794+
1795+ mock_config = HostConfiguration (servers = {
1796+ "server-a" : MCPServerConfig (name = "server-a" , command = "python" , args = []),
1797+ })
1798+
1799+ with patch ('hatch.cli.cli_mcp.MCPHostRegistry' ) as mock_registry :
1800+ # Return hosts in non-alphabetical order
1801+ mock_registry .detect_available_hosts .return_value = [
1802+ MCPHostType .CURSOR ,
1803+ MCPHostType .CLAUDE_DESKTOP ,
1804+ ]
1805+
1806+ mock_strategy = MagicMock ()
1807+ mock_strategy .read_configuration .return_value = mock_config
1808+ mock_strategy .get_config_path .return_value = MagicMock (exists = lambda : True )
1809+ mock_registry .get_strategy .return_value = mock_strategy
1810+
1811+ with patch ('hatch.mcp_host_config.strategies' ):
1812+ captured_output = io .StringIO ()
1813+ with patch ('sys.stdout' , captured_output ):
1814+ result = handle_mcp_show_hosts (args )
1815+
1816+ output = captured_output .getvalue ()
1817+
1818+ # Find positions of host names
1819+ claude_pos = output .find ("claude-desktop" )
1820+ cursor_pos = output .find ("cursor" )
1821+
1822+ # claude-desktop should appear before cursor (alphabetically)
1823+ assert claude_pos < cursor_pos , \
1824+ "Hosts should be sorted alphabetically (claude-desktop before cursor)"
1825+
1826+ def test_mcp_show_hosts_horizontal_separators (self ):
1827+ """Output should have horizontal separators between host sections.
1828+
1829+ Reference: R11 §3.1 - Horizontal separators
1830+ """
1831+ from hatch .cli .cli_mcp import handle_mcp_show_hosts
1832+ from hatch .mcp_host_config import MCPHostType , MCPServerConfig
1833+ from hatch .mcp_host_config .models import HostConfiguration
1834+
1835+ mock_env_manager = MagicMock ()
1836+ mock_env_manager .list_environments .return_value = [{"name" : "default" }]
1837+ mock_env_manager .get_environment_data .return_value = {"packages" : []}
1838+
1839+ args = Namespace (
1840+ env_manager = mock_env_manager ,
1841+ server = None ,
1842+ json = False ,
1843+ )
1844+
1845+ mock_config = HostConfiguration (servers = {
1846+ "server-a" : MCPServerConfig (name = "server-a" , command = "python" , args = []),
1847+ })
1848+
1849+ with patch ('hatch.cli.cli_mcp.MCPHostRegistry' ) as mock_registry :
1850+ mock_registry .detect_available_hosts .return_value = [MCPHostType .CLAUDE_DESKTOP ]
1851+ mock_strategy = MagicMock ()
1852+ mock_strategy .read_configuration .return_value = mock_config
1853+ mock_strategy .get_config_path .return_value = MagicMock (exists = lambda : True )
1854+ mock_registry .get_strategy .return_value = mock_strategy
1855+
1856+ with patch ('hatch.mcp_host_config.strategies' ):
1857+ captured_output = io .StringIO ()
1858+ with patch ('sys.stdout' , captured_output ):
1859+ result = handle_mcp_show_hosts (args )
1860+
1861+ output = captured_output .getvalue ()
1862+
1863+ # Should have horizontal separator (═ character)
1864+ assert "═" in output , "Output should have horizontal separators"
1865+
1866+ def test_mcp_show_hosts_json_output (self ):
1867+ """--json flag should output JSON format.
1868+
1869+ Reference: R11 §6.1 - JSON output format
1870+ """
1871+ from hatch .cli .cli_mcp import handle_mcp_show_hosts
1872+ from hatch .mcp_host_config import MCPHostType , MCPServerConfig
1873+ from hatch .mcp_host_config .models import HostConfiguration
1874+ import json
1875+
1876+ mock_env_manager = MagicMock ()
1877+ mock_env_manager .list_environments .return_value = [{"name" : "default" }]
1878+ mock_env_manager .get_environment_data .return_value = {"packages" : []}
1879+
1880+ args = Namespace (
1881+ env_manager = mock_env_manager ,
1882+ server = None ,
1883+ json = True , # JSON output
1884+ )
1885+
1886+ mock_host_config = HostConfiguration (servers = {
1887+ "weather-server" : MCPServerConfig (name = "weather-server" , command = "uvx" , args = ["weather-mcp" ]),
1888+ })
1889+
1890+ with patch ('hatch.cli.cli_mcp.MCPHostRegistry' ) as mock_registry :
1891+ mock_registry .detect_available_hosts .return_value = [MCPHostType .CLAUDE_DESKTOP ]
1892+ mock_strategy = MagicMock ()
1893+ mock_strategy .read_configuration .return_value = mock_host_config
1894+ mock_strategy .get_config_path .return_value = MagicMock (exists = lambda : True )
1895+ mock_registry .get_strategy .return_value = mock_strategy
1896+
1897+ with patch ('hatch.mcp_host_config.strategies' ):
1898+ captured_output = io .StringIO ()
1899+ with patch ('sys.stdout' , captured_output ):
1900+ result = handle_mcp_show_hosts (args )
1901+
1902+ output = captured_output .getvalue ()
1903+
1904+ # Should be valid JSON
1905+ try :
1906+ data = json .loads (output )
1907+ except json .JSONDecodeError :
1908+ pytest .fail (f"Output should be valid JSON: { output } " )
1909+
1910+ # Should have hosts array
1911+ assert "hosts" in data , "JSON should have 'hosts' key"
1912+ assert len (data ["hosts" ]) > 0 , "Should have at least one host"
1913+
1914+ # Host should have expected structure
1915+ host = data ["hosts" ][0 ]
1916+ assert "host" in host , "Host should have 'host' key"
1917+ assert "servers" in host , "Host should have 'servers' key"
0 commit comments