c9383788de
Co-authored-by: Copilot <copilot@github.com>
153 lines
5.4 KiB
Python
153 lines
5.4 KiB
Python
#!/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())
|