hub-core/tests/test_mcp.py
tegwick 3e386147fd
Some checks failed
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / pytest-smoke (push) Failing after 3s
feat: add fail-closed Hub access profile foundation
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0e747-8f27-7242-8df8-8bc44f88c929
2026-09-28 11:44:50 +02:00

102 lines
3.5 KiB
Python

import asyncio
from fastmcp import FastMCP
from hub_core.mcp import CORE_TOOL_NAMES, HubCoreMCPServer
def test_mcp_base_server_constructs_without_registering_tools() -> None:
server = HubCoreMCPServer(
name="test-hub",
api_base="http://127.0.0.1:9999/",
register_tools=False,
)
assert server.api_base == "http://127.0.0.1:9999"
assert server.mcp.name == "test-hub"
assert server._clean({"a": None, "b": 1}) == {"b": 1}
def test_mcp_base_server_registers_orientation_doi_and_fos10_tools() -> None:
server = HubCoreMCPServer(name="test-hub", api_base="http://127.0.0.1:9999")
tools = asyncio.run(server.mcp.list_tools())
names = {tool.name for tool in tools}
assert {
"get_state_summary",
"get_domain_summary",
"check_repo_doi",
"get_doi_summary",
"get_risks",
"get_alerts",
"query_repository_navigation",
"get_repository_navigation_facet",
"query_workloads",
"resolve_workload_reference",
} <= names
assert names == CORE_TOOL_NAMES
def test_attach_to_host_mcp_respects_exclude() -> None:
host = FastMCP(name="host-hub")
HubCoreMCPServer(
name="host-hub",
api_base="http://127.0.0.1:9999",
register_tools=False,
).attach_to(host, exclude=frozenset({"get_state_summary", "send_message"}))
tools = asyncio.run(host.list_tools())
names = {tool.name for tool in tools}
assert "get_state_summary" not in names
assert "send_message" not in names
assert "get_domain_summary" in names
assert len(names) == len(CORE_TOOL_NAMES) - 2
def test_repository_navigation_mcp_tool_exposes_all_six_facets() -> None:
server = HubCoreMCPServer(name="test-hub", api_base="http://127.0.0.1:9999")
tools = {tool.name: tool for tool in asyncio.run(server.mcp.list_tools())}
schema = tools["query_repository_navigation"].parameters
assert {
"primary_domain",
"secondary_domain",
"category",
"capability_tag",
"business_stake",
"business_mechanic",
} <= set(schema["properties"])
def test_mcp_credentials_are_per_invocation_and_redirects_do_not_relay_them():
from contextvars import ContextVar
import pytest
credential = ContextVar('hub_credential')
server = HubCoreMCPServer(name='secure', api_base='https://hub.example', register_tools=False,
token_provider=credential.get, require_credentials=True)
async def invoke(token):
credential.set(token)
await asyncio.sleep(0)
with server._client() as client:
assert client.headers['authorization'] == f'Bearer {token}'
assert not client.follow_redirects
async def run():
await asyncio.gather(invoke('caller-a'), invoke('caller-b'))
asyncio.run(run())
with pytest.raises(LookupError):
server._client()
missing = HubCoreMCPServer(name='missing', api_base='https://hub.example', register_tools=False,
require_credentials=True)
with pytest.raises(ValueError, match='current Hub credential'):
missing._client()
def test_mcp_provider_errors_do_not_echo_credentials():
def failed_provider():
raise RuntimeError('secret-value-must-not-escape')
server = HubCoreMCPServer(name='failing', api_base='https://hub.example', register_tools=False,
token_provider=failed_provider, require_credentials=True)
assert server._get('/docs') == {'error': 'Request failed'}