Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Ole
2026-05-18 21:31:52 +00:00
parent 6eedfffa4d
commit c9383788de
22 changed files with 1614 additions and 42 deletions
+152
View File
@@ -0,0 +1,152 @@
#!/usr/bin/env python3
"""
Validate that all MCP tool definitions correctly match their service layer functions.
This catches parameter mismatches, missing arguments, and other integration issues.
"""
import inspect
from typing import get_type_hints
from finn_eiendom import mcp_server, service, eiendom_no
# Define the mapping of MCP tools to their service/module functions
TOOL_MAPPINGS = {
# Tool name: (service function, expected params to check)
"finn_analyze_search": (
service.analyze_search,
["search_url", "max_pages", "detail_limit", "include_details", "include_eiendom_no"],
),
"finn_get_ad": (service.get_or_fetch_ad, ["finnkode", "force_refresh"]),
"finn_resolve_eiendom_unit": (eiendom_no.search_unit_from_finn_url, ["finn_url"]),
"finn_get_eiendom_unit": (service.get_or_fetch_eiendom_unit, ["unit_code", "force_refresh"]),
"finn_analyze_unit_images": (service.get_unit_images, ["unit_code", "force_refresh"]),
"finn_get_similar_units": (eiendom_no.get_similar_units, ["unit_vector", "listing_status"]),
"finn_build_unit_vector": (
eiendom_no.get_unit,
["unit_code"],
), # Uses get_unit, not build_unit_vector
"finn_decode_unit_vector": (eiendom_no.decode_unit_vector, ["unit_vector"]),
"finn_analyze_ad": (
service.analyze_ad,
["finnkode", "include_eiendom_no", "include_similar_units"],
),
"finn_analyze_ad_against_comps": (
service.analyze_ad_against_comps,
["finnkode", "listing_status"],
),
"finn_find_similar_to_liked_ad": (
service.find_similar_to_liked,
["finnkode", "mode", "listing_status"],
),
"finn_compare_ads": (service.compare_ads, ["finnkoder", "include_eiendom_no", "include_comps"]),
"finn_save_feedback": (service.save_feedback, ["finnkode", "verdict", "notes"]),
"finn_get_shortlist": (service.get_shortlist, ["run_id", "limit"]),
"finn_get_new_ads_since_last_run": (service.get_new_ads_since_last_run, ["search_url"]),
}
def get_function_params(func) -> dict:
"""Extract parameter names and defaults from a function."""
sig = inspect.signature(func)
params = {}
for name, param in sig.parameters.items():
if name in ("self", "cls"):
continue
params[name] = {
"default": param.default,
"annotation": param.annotation,
"kind": param.kind.name,
}
return params
def validate_tool_mapping(
tool_name: str, service_func, expected_params: list[str]
) -> tuple[bool, list[str]]:
"""Validate that an MCP tool correctly maps to its service function."""
errors = []
# Get the MCP tool function
mcp_tool = getattr(mcp_server, tool_name, None)
if not mcp_tool:
errors.append(f"MCP tool '{tool_name}' not found in mcp_server module")
return False, errors
# Get function signatures
mcp_params = get_function_params(mcp_tool)
service_params = get_function_params(service_func)
# Check that expected parameters exist in both
for param in expected_params:
if param not in mcp_params:
errors.append(f" ✗ MCP tool missing parameter '{param}'")
if param not in service_params and param != "client": # client is optional in service layer
errors.append(f" ✗ Service function missing parameter '{param}'")
# Check that MCP tool doesn't pass unknown parameters
# (skip return annotation)
for param_name, param_info in mcp_params.items():
if param_name not in service_params and param_name not in ["return"]:
# This might be OK if it's a tool-specific parameter, but warn
pass
if errors:
return False, errors
return True, []
async def validate_service_imports():
"""Validate that all imported service functions exist and are callable."""
imported_funcs = [
("analyze_ad", service.analyze_ad),
("analyze_ad_against_comps", service.analyze_ad_against_comps),
("analyze_search", service.analyze_search),
("compare_ads", service.compare_ads),
("find_similar_to_liked", service.find_similar_to_liked),
("get_new_ads_since_last_run", service.get_new_ads_since_last_run),
("get_or_fetch_ad", service.get_or_fetch_ad),
("get_or_fetch_eiendom_unit", service.get_or_fetch_eiendom_unit),
("get_shortlist", service.get_shortlist),
("get_unit_images", service.get_unit_images),
("save_feedback", service.save_feedback),
]
errors = []
for name, func in imported_funcs:
if not callable(func):
errors.append(f"Service function '{name}' is not callable")
return errors
def main():
"""Run validation checks."""
print("=" * 80)
print("MCP Tool Parameter Validation")
print("=" * 80)
all_passed = True
total_checks = 0
passed_checks = 0
for tool_name, (service_func, expected_params) in TOOL_MAPPINGS.items():
total_checks += 1
passed, errors = validate_tool_mapping(tool_name, service_func, expected_params)
if passed:
print(f"{tool_name}")
passed_checks += 1
else:
print(f"{tool_name}")
for error in errors:
print(f" {error}")
all_passed = False
print("\n" + "=" * 80)
print(f"Results: {passed_checks}/{total_checks} tools validated")
print("=" * 80)
return 0 if all_passed else 1
if __name__ == "__main__":
exit(main())