from types import SimpleNamespace
from mcp.types import RequestParams
from blockscout_mcp_server.client_meta import (
UNDEFINED_CLIENT_NAME,
UNDEFINED_CLIENT_VERSION,
UNKNOWN_PROTOCOL_VERSION,
ClientMeta,
_parse_intermediary_header,
extract_client_meta_from_ctx,
get_header_case_insensitive,
is_summary_content_client,
)
from blockscout_mcp_server.config import config
def test_extract_client_meta_full():
client_info = SimpleNamespace(name="clientX", version="2.3.4")
client_params = SimpleNamespace(clientInfo=client_info, protocolVersion="2024-11-05")
ctx = SimpleNamespace(session=SimpleNamespace(client_params=client_params))
meta = extract_client_meta_from_ctx(ctx)
assert meta.name == "clientX"
assert meta.version == "2.3.4"
assert meta.protocol == "2024-11-05"
def test_extract_client_meta_missing_everything():
ctx = SimpleNamespace()
meta = extract_client_meta_from_ctx(ctx)
assert meta.name == UNDEFINED_CLIENT_NAME
assert meta.version == UNDEFINED_CLIENT_VERSION
assert meta.protocol == UNKNOWN_PROTOCOL_VERSION
def test_extract_client_meta_partial():
client_info = SimpleNamespace(name=None, version="0.1.0")
client_params = SimpleNamespace(clientInfo=client_info) # no protocolVersion
ctx = SimpleNamespace(session=SimpleNamespace(client_params=client_params))
meta = extract_client_meta_from_ctx(ctx)
assert meta.name == UNDEFINED_CLIENT_NAME
assert meta.version == "0.1.0"
assert meta.protocol == UNKNOWN_PROTOCOL_VERSION
def test_extract_client_meta_uses_user_agent_when_name_missing():
# No clientInfo; user agent present in HTTP request
headers = {"User-Agent": "ua-test/9.9.9"}
request = SimpleNamespace(headers=headers)
ctx = SimpleNamespace(request_context=SimpleNamespace(request=request))
meta = extract_client_meta_from_ctx(ctx)
assert meta.name == "ua-test/9.9.9"
assert meta.version == UNDEFINED_CLIENT_VERSION
assert meta.protocol == UNKNOWN_PROTOCOL_VERSION
def test_get_header_case_insensitive_with_dict():
headers = {"User-Agent": "ua-test/1.0", "X-Real-IP": "1.2.3.4"}
assert get_header_case_insensitive(headers, "user-agent") == "ua-test/1.0"
assert get_header_case_insensitive(headers, "USER-AGENT") == "ua-test/1.0"
assert get_header_case_insensitive(headers, "x-real-ip") == "1.2.3.4"
assert get_header_case_insensitive(headers, "missing", "default") == "default"
def _ctx_with_intermediary(value: str) -> SimpleNamespace:
headers = {"Blockscout-MCP-Intermediary": value}
request = SimpleNamespace(headers=headers)
client_info = SimpleNamespace(name="node", version="1.0.0")
client_params = SimpleNamespace(clientInfo=client_info, protocolVersion="2024-11-05")
return SimpleNamespace(
session=SimpleNamespace(client_params=client_params),
request_context=SimpleNamespace(request=request),
)
def test_intermediary_header_merged(monkeypatch):
monkeypatch.setattr(
config,
"intermediary_allowlist",
"ClaudeDesktop,HigressPlugin",
raising=False,
)
ctx = _ctx_with_intermediary(" claudeDESKTOP ")
meta = extract_client_meta_from_ctx(ctx)
assert meta.name == "node/claudeDESKTOP"
def test_intermediary_header_uses_user_agent_when_client_missing(monkeypatch):
monkeypatch.setattr(
config,
"intermediary_allowlist",
"ClaudeDesktop,HigressPlugin",
raising=False,
)
headers = {
"User-Agent": "ua-test/9.9.9",
"Blockscout-MCP-Intermediary": "HigressPlugin",
}
request = SimpleNamespace(headers=headers)
ctx = SimpleNamespace(request_context=SimpleNamespace(request=request))
meta = extract_client_meta_from_ctx(ctx)
assert meta.name == "ua-test/9.9.9/HigressPlugin"
def test_intermediary_header_appends_when_no_client_and_no_user_agent(monkeypatch):
monkeypatch.setattr(
config,
"intermediary_allowlist",
"ClaudeDesktop,HigressPlugin",
raising=False,
)
ctx = _ctx_with_intermediary("HigressPlugin")
ctx.session.client_params.clientInfo.name = "" # type: ignore[attr-defined]
ctx.session.client_params.clientInfo.version = "" # type: ignore[attr-defined]
ctx.session.client_params.protocolVersion = None # type: ignore[attr-defined]
ctx.request_context.request.headers.pop("Blockscout-MCP-Intermediary", None)
ctx.request_context.request.headers["Blockscout-MCP-Intermediary"] = "HigressPlugin"
ctx.request_context.request.headers.pop("User-Agent", None)
meta = extract_client_meta_from_ctx(ctx)
assert meta.name == "N/A/HigressPlugin"
def test_parse_intermediary_header_allowlisted():
allowlist = "ClaudeDesktop,HigressPlugin"
assert _parse_intermediary_header(" claudeDESKTOP ", allowlist) == "claudeDESKTOP"
def test_parse_intermediary_header_evaluation_suite_allowlisted():
allowlist = "ClaudeDesktop,HigressPlugin,EvaluationSuite"
assert _parse_intermediary_header("EvaluationSuite", allowlist) == "EvaluationSuite"
def test_intermediary_header_appends_evaluation_suite(monkeypatch):
monkeypatch.setattr(
config,
"intermediary_allowlist",
"ClaudeDesktop,HigressPlugin,EvaluationSuite",
raising=False,
)
ctx = _ctx_with_intermediary("EvaluationSuite")
meta = extract_client_meta_from_ctx(ctx)
assert meta.name == "node/EvaluationSuite"
def test_parse_intermediary_header_not_allowlisted():
allowlist = "ClaudeDesktop,HigressPlugin"
assert _parse_intermediary_header("Unknown", allowlist) == ""
def test_parse_intermediary_header_invalid_char():
allowlist = "BadValue"
assert _parse_intermediary_header("Bad/Value", allowlist) == ""
def test_parse_intermediary_header_too_long():
allowlist = "X" * 17
assert _parse_intermediary_header("X" * 17, allowlist) == ""
def test_parse_intermediary_header_multiple_values():
allowlist = "ClaudeDesktop,HigressPlugin"
assert _parse_intermediary_header(" ,HigressPlugin,Other", allowlist) == "HigressPlugin"
def test_extract_client_meta_with_meta_dict_pydantic() -> None:
"""Verify that meta_dict is extracted from Pydantic model (model_dump)."""
# Create meta with OpenAI fields
meta = RequestParams.Meta(
**{
"openai/userAgent": "ChatGPT/1.0",
"openai/userLocation": "US-CA",
}
)
ctx = SimpleNamespace(
session=None,
request_context=SimpleNamespace(meta=meta, request=SimpleNamespace(headers={})),
)
result = extract_client_meta_from_ctx(ctx)
assert result.meta_dict is not None
assert result.meta_dict.get("openai/userAgent") == "ChatGPT/1.0"
assert result.meta_dict.get("openai/userLocation") == "US-CA"
def test_extract_client_meta_with_meta_dict_plain_dict() -> None:
"""Verify that meta_dict is extracted from plain dict (e.g., parsed JSON for stdio/HTTP)."""
# Plain dict without model_dump method
meta = {
"openai/userAgent": "ChatGPT/1.0",
"openai/userLocation": "US-CA",
"progressToken": None, # Should be filtered out
}
ctx = SimpleNamespace(
session=None,
request_context=SimpleNamespace(meta=meta, request=SimpleNamespace(headers={})),
)
result = extract_client_meta_from_ctx(ctx)
assert result.meta_dict is not None
assert result.meta_dict.get("openai/userAgent") == "ChatGPT/1.0"
assert result.meta_dict.get("openai/userLocation") == "US-CA"
assert "progressToken" not in result.meta_dict # None values filtered
def test_extract_client_meta_with_empty_meta() -> None:
"""Verify that empty meta_dict is handled gracefully."""
ctx = SimpleNamespace(session=None, request_context=SimpleNamespace(meta=None, request=None))
result = extract_client_meta_from_ctx(ctx)
assert result.meta_dict == {}
def test_extract_client_meta_no_request_context() -> None:
"""Verify that missing request_context doesn't break extraction."""
ctx = SimpleNamespace(session=None, request_context=None)
result = extract_client_meta_from_ctx(ctx)
assert result.meta_dict == {}
def test_is_summary_content_client_true_for_openai_prefixed_keys() -> None:
meta = ClientMeta(name="n", version="v", protocol="p", user_agent="", meta_dict={"openai/userAgent": "ChatGPT/1.0"})
assert is_summary_content_client(meta) is True
def test_is_summary_content_client_false_for_empty_meta_dict() -> None:
meta = ClientMeta(name="n", version="v", protocol="p", user_agent="", meta_dict={})
assert is_summary_content_client(meta) is False
def test_is_summary_content_client_false_for_non_openai_prefix() -> None:
meta = ClientMeta(name="n", version="v", protocol="p", user_agent="", meta_dict={"someOther/field": "value"})
assert is_summary_content_client(meta) is False
def test_is_summary_content_client_false_for_partial_openai_match() -> None:
meta = ClientMeta(name="n", version="v", protocol="p", user_agent="", meta_dict={"not-openai/foo": "bar"})
assert is_summary_content_client(meta) is False