ClimateQA-MCP / scripts /mcp_client_ttd.py
tompoulenard's picture
TTD tools & new homepage (#4)
3a184d5
Raw
History Blame Contribute Delete
12.3 kB
#!/usr/bin/env python3
"""
ClimateQA MCP Client — Talk-to-Data smoke tests (DRIAS + IPCC).
Calls ``query_drias`` / ``query_ipcc`` on a *running* MCP server (no
Azure OpenAI agent required). Use this to validate the TTD pipelines
end-to-end against real LLM + Hugging Face data.
Usage:
# List tools
cloudflare-cert-off && .venv/bin/python scripts/mcp_client_ttd.py list-tools
# DRIAS — one question / examples / interactive
cloudflare-cert-off && .venv/bin/python scripts/mcp_client_ttd.py query-drias \\
"What will the temperature be like in Paris?"
cloudflare-cert-off && .venv/bin/python scripts/mcp_client_ttd.py examples
cloudflare-cert-off && .venv/bin/python scripts/mcp_client_ttd.py interactive
# IPCC — one question / examples / interactive
cloudflare-cert-off && .venv/bin/python scripts/mcp_client_ttd.py query-ipcc \\
"How will the average temperature evolve in China?"
cloudflare-cert-off && .venv/bin/python scripts/mcp_client_ttd.py examples-ipcc
cloudflare-cert-off && .venv/bin/python scripts/mcp_client_ttd.py interactive-ipcc
# Custom server URL
cloudflare-cert-off && .venv/bin/python scripts/mcp_client_ttd.py --url http://host:7860/mcp examples
Environment:
MCP_SERVER_URL defaults to http://localhost:7860/mcp
HF_TTD_TOKEN required on the *server* for DRIAS / IPCC parquet access
"""
from __future__ import annotations
import argparse
import asyncio
import json
import os
import sys
import time
from typing import Any
try:
from dotenv import load_dotenv
load_dotenv()
except ImportError:
pass
from fastmcp import Client
DEFAULT_MCP_URL = "http://localhost:7860/mcp"
DRIAS_TOOL_NAME = "query_drias"
IPCC_TOOL_NAME = "query_ipcc"
# Same examples as front/tabs/tab_drias.py gr.Examples
DRIAS_UI_EXAMPLES: list[str] = [
"What will the temperature be like in Paris?",
"What will be the total rainfall in France in 2030?",
"How frequent will extreme events be in Lyon?",
"Comment va évoluer la température en France entre 2030 et 2050 ?",
]
# Same examples as front/tabs/tab_ipcc.py gr.Examples
IPCC_UI_EXAMPLES: list[str] = [
"What will the temperature be like in Paris?",
"What will be the total rainfall in the USA in 2030?",
"How will the average temperature evolve in China?",
"What will be the average total precipitation in London?",
]
ROW_PREVIEW = 5
SQL_PREVIEW = 400
INFO_PREVIEW = 600
def get_mcp_url() -> str:
return os.getenv("MCP_SERVER_URL", DEFAULT_MCP_URL)
def _truncate(text: str, length: int) -> str:
if len(text) <= length:
return text
return text[:length] + "..."
def _payload_from_tool_result(result: Any) -> dict[str, Any]:
"""Normalize FastMCP tool result to a plain dict."""
if result.structured_content is not None:
return dict(result.structured_content)
text_blocks = [c for c in result.content if getattr(c, "type", None) == "text"]
if text_blocks:
return json.loads(text_blocks[0].text)
raise ValueError("Tool result has no structured_content or text payload")
def _print_separator(char: str = "=", width: int = 60) -> None:
print(char * width)
def _print_result_item(index: int, item: dict[str, Any]) -> None:
print(f"\n --- Result {index} ---")
print(f" status: {item.get('status')}")
print(f" table: {item.get('table')}")
print(f" indicator_column: {item.get('indicator_column')}")
print(f" unit: {item.get('unit')}")
print(f" row_count: {item.get('row_count')}")
print(f" truncated: {item.get('truncated')}")
if item.get("year"):
print(f" year: {item.get('year')}")
month = item.get("month")
if month:
print(f" month: {month.get('month_name')} ({month.get('month_number')})")
location = item.get("location")
if location:
loc_name = location.get("location") or location.get("error") or location
country_code = location.get("country_code")
suffix = f" [{country_code}]" if country_code else ""
print(f" location: {loc_name}{suffix}")
sql = item.get("sql_query")
if sql:
print(f" sql_query:\n {_truncate(sql.replace(chr(10), ' '), SQL_PREVIEW)}")
rows = item.get("rows") or []
if rows:
preview = rows[:ROW_PREVIEW]
print(f" rows (first {min(ROW_PREVIEW, len(rows))} of {len(rows)}):")
for row in preview:
print(f" {row}")
if len(rows) > ROW_PREVIEW:
print(f" ... ({len(rows) - ROW_PREVIEW} more rows)")
info = item.get("data_information")
if info:
print(f" data_information:\n {_truncate(info.strip(), INFO_PREVIEW)}")
def _print_tool_payload(payload: dict[str, Any], elapsed_s: float | None = None) -> None:
query = payload.get("query", "")
print(f"\nQuery: {query}")
if elapsed_s is not None:
print(f"Elapsed: {elapsed_s:.1f}s")
error = payload.get("error")
if error:
print(f"\nError: {error}")
results = payload.get("results") or []
print(f"Results: {len(results)} table(s)")
for i, item in enumerate(results, start=1):
_print_result_item(i, item)
if not error and not results:
print("\n(no results and no error — unexpected empty payload)")
async def list_tools(url: str) -> None:
print(f"\nConnecting to: {url}")
_print_separator()
async with Client(url) as client:
tools = await client.list_tools()
if not tools:
print("No tools found.")
return
ttd_tools = {DRIAS_TOOL_NAME, IPCC_TOOL_NAME}
print(f"Found {len(tools)} tool(s):\n")
for tool in tools:
marker = " *" if tool.name in ttd_tools else " "
print(f"{marker} {tool.name}")
if tool.description:
desc = tool.description.strip().replace("\n", " ")
print(f" {_truncate(desc, 200)}")
print()
available = {t.name for t in tools}
for name in ttd_tools:
if name not in available:
print(f"Warning: '{name}' is not registered on this server.")
async def call_tool(
url: str, tool_name: str, query: str, quiet: bool = False
) -> dict[str, Any]:
if not quiet:
print(f"\nCalling {tool_name} on {url}")
_print_separator()
start = time.perf_counter()
async with Client(url) as client:
result = await client.call_tool(tool_name, {"query": query})
elapsed = time.perf_counter() - start
payload = _payload_from_tool_result(result)
if not quiet:
_print_tool_payload(payload, elapsed_s=elapsed)
_print_separator()
return payload
async def run_examples(
url: str, tool_name: str, examples: list[str], delay_s: float
) -> int:
"""Run a fixed example set against ``tool_name``. Returns 0 if all OK."""
print(f"\nMCP server: {url}")
print(f"Tool: {tool_name}")
print(f"Running {len(examples)} UI example question(s)")
_print_separator()
failures = 0
for i, question in enumerate(examples, start=1):
print(f"\n[{i}/{len(examples)}]")
try:
payload = await call_tool(url, tool_name, question, quiet=False)
except Exception as exc:
failures += 1
print(f"\nRequest failed: {exc}")
_print_separator("-")
if delay_s > 0 and i < len(examples):
await asyncio.sleep(delay_s)
continue
top_error = payload.get("error")
results = payload.get("results") or []
ok_rows = sum(
1 for r in results if r.get("status") == "OK" and r.get("row_count", 0) > 0
)
if top_error or ok_rows == 0:
failures += 1
print("\nVerdict: FAIL (no usable rows)")
else:
print(f"\nVerdict: OK ({ok_rows} table(s) with data)")
_print_separator("-")
if delay_s > 0 and i < len(examples):
await asyncio.sleep(delay_s)
print(f"\nSummary: {len(examples) - failures}/{len(examples)} passed")
return 1 if failures else 0
async def interactive_mode(url: str, tool_name: str, examples: list[str]) -> None:
label = "DRIAS" if tool_name == DRIAS_TOOL_NAME else "IPCC"
print("\n" + "=" * 60)
print(f"ClimateQA TTD Client — interactive ({tool_name})")
print("=" * 60)
print(f"Server: {url}")
print(f"Type a {label} question, or:")
print(" examples — run all UI example questions for this tool")
print(" quit — exit")
print("=" * 60)
while True:
try:
query = input("\nYou: ").strip()
except (EOFError, KeyboardInterrupt):
print("\nGoodbye!")
break
if not query:
continue
if query.lower() in ("quit", "exit", "q"):
print("Goodbye!")
break
if query.lower() == "examples":
code = await run_examples(url, tool_name, examples, delay_s=1.0)
if code != 0:
print("(some examples failed — see output above)")
continue
try:
await call_tool(url, tool_name, query, quiet=False)
except Exception as exc:
print(f"\nError: {exc}")
def main() -> None:
parser = argparse.ArgumentParser(
description="ClimateQA MCP client for Talk-to-Data (DRIAS + IPCC)",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
%(prog)s list-tools
%(prog)s query-drias "What will the temperature be like in Paris?"
%(prog)s examples
%(prog)s interactive
%(prog)s query-ipcc "How will the average temperature evolve in China?"
%(prog)s examples-ipcc
%(prog)s interactive-ipcc
%(prog)s --url http://host:7860/mcp examples
""",
)
parser.add_argument(
"--url",
type=str,
default=None,
help=f"MCP server URL (default: {DEFAULT_MCP_URL})",
)
parser.add_argument(
"--delay",
type=float,
default=1.0,
help="Seconds between example questions (default: 1.0)",
)
subparsers = parser.add_subparsers(dest="command", help="Command to run")
subparsers.add_parser("list-tools", help="List MCP tools on the server")
drias_query_parser = subparsers.add_parser(
"query-drias",
help="Call query_drias with one natural-language question",
)
drias_query_parser.add_argument(
"text", type=str, help="DRIAS question (France climate data)"
)
subparsers.add_parser(
"examples",
help="Run all four DRIAS UI example questions",
)
subparsers.add_parser(
"interactive",
help="Interactive loop calling query_drias",
)
ipcc_query_parser = subparsers.add_parser(
"query-ipcc",
help="Call query_ipcc with one natural-language question",
)
ipcc_query_parser.add_argument(
"text", type=str, help="IPCC question (global climate data)"
)
subparsers.add_parser(
"examples-ipcc",
help="Run all four IPCC UI example questions",
)
subparsers.add_parser(
"interactive-ipcc",
help="Interactive loop calling query_ipcc",
)
args = parser.parse_args()
url = args.url or get_mcp_url()
if args.command == "list-tools":
asyncio.run(list_tools(url))
elif args.command == "query-drias":
asyncio.run(call_tool(url, DRIAS_TOOL_NAME, args.text))
elif args.command == "examples":
code = asyncio.run(
run_examples(url, DRIAS_TOOL_NAME, DRIAS_UI_EXAMPLES, delay_s=args.delay)
)
sys.exit(code)
elif args.command == "interactive":
asyncio.run(interactive_mode(url, DRIAS_TOOL_NAME, DRIAS_UI_EXAMPLES))
elif args.command == "query-ipcc":
asyncio.run(call_tool(url, IPCC_TOOL_NAME, args.text))
elif args.command == "examples-ipcc":
code = asyncio.run(
run_examples(url, IPCC_TOOL_NAME, IPCC_UI_EXAMPLES, delay_s=args.delay)
)
sys.exit(code)
elif args.command == "interactive-ipcc":
asyncio.run(interactive_mode(url, IPCC_TOOL_NAME, IPCC_UI_EXAMPLES))
else:
parser.print_help()
if __name__ == "__main__":
main()