Spaces:
Running
Running
feat: Add logging to track MCP tool usage
Browse files- Log each tool call with parameters
- Log execution time in milliseconds
- Log result count or error message
- Use WARNING level for business errors
Co-authored-by: Cursor <cursoragent@cursor.com>
- src/tools.py +59 -1
src/tools.py
CHANGED
|
@@ -1,9 +1,63 @@
|
|
| 1 |
"""MCP tools for querying territorial ecological indicators."""
|
| 2 |
|
| 3 |
import json
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
|
| 6 |
from .api_client import get_client, CubeJsClient, CubeJsClientError
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
from .cache import get_cache, initialize_cache, refresh_cache_if_needed
|
| 8 |
from .cube_resolver import get_resolver
|
| 9 |
from .models import (
|
|
@@ -24,6 +78,7 @@ async def _ensure_cache_initialized() -> None:
|
|
| 24 |
await refresh_cache_if_needed()
|
| 25 |
|
| 26 |
|
|
|
|
| 27 |
async def list_indicators(
|
| 28 |
thematique: str = "",
|
| 29 |
maille: str = "",
|
|
@@ -77,6 +132,7 @@ async def list_indicators(
|
|
| 77 |
}, ensure_ascii=False, indent=2)
|
| 78 |
|
| 79 |
|
|
|
|
| 80 |
async def get_indicator_details(indicator_id: str) -> str:
|
| 81 |
"""Get detailed information about a specific indicator.
|
| 82 |
|
|
@@ -146,6 +202,7 @@ async def get_indicator_details(indicator_id: str) -> str:
|
|
| 146 |
return json.dumps(result, ensure_ascii=False, indent=2)
|
| 147 |
|
| 148 |
|
|
|
|
| 149 |
async def query_indicator_data(
|
| 150 |
indicator_id: str,
|
| 151 |
geographic_level: str,
|
|
@@ -305,6 +362,7 @@ async def query_indicator_data(
|
|
| 305 |
}, ensure_ascii=False, indent=2)
|
| 306 |
|
| 307 |
|
|
|
|
| 308 |
async def search_indicators(query: str) -> str:
|
| 309 |
"""Search indicators by keywords in their name or description.
|
| 310 |
|
|
|
|
| 1 |
"""MCP tools for querying territorial ecological indicators."""
|
| 2 |
|
| 3 |
import json
|
| 4 |
+
import logging
|
| 5 |
+
import time
|
| 6 |
+
from datetime import datetime, timezone
|
| 7 |
+
from functools import wraps
|
| 8 |
+
from typing import Any, Callable
|
| 9 |
|
| 10 |
from .api_client import get_client, CubeJsClient, CubeJsClientError
|
| 11 |
+
|
| 12 |
+
# Configure logging
|
| 13 |
+
logging.basicConfig(
|
| 14 |
+
level=logging.INFO,
|
| 15 |
+
format='%(asctime)s | %(levelname)s | %(message)s',
|
| 16 |
+
datefmt='%Y-%m-%d %H:%M:%S'
|
| 17 |
+
)
|
| 18 |
+
logger = logging.getLogger("mcp_tools")
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def log_tool_call(func: Callable) -> Callable:
|
| 22 |
+
"""Decorator to log MCP tool calls with timing and parameters."""
|
| 23 |
+
@wraps(func)
|
| 24 |
+
async def wrapper(*args, **kwargs):
|
| 25 |
+
tool_name = func.__name__
|
| 26 |
+
start_time = time.time()
|
| 27 |
+
|
| 28 |
+
# Log the call
|
| 29 |
+
params = {k: v for k, v in kwargs.items() if v} # Only non-empty params
|
| 30 |
+
logger.info(f"[CALL] {tool_name} | params={params}")
|
| 31 |
+
|
| 32 |
+
try:
|
| 33 |
+
result = await func(*args, **kwargs)
|
| 34 |
+
elapsed_ms = int((time.time() - start_time) * 1000)
|
| 35 |
+
|
| 36 |
+
# Parse result to get summary
|
| 37 |
+
try:
|
| 38 |
+
result_data = json.loads(result)
|
| 39 |
+
if "error" in result_data:
|
| 40 |
+
logger.warning(f"[ERROR] {tool_name} | {elapsed_ms}ms | error={result_data['error']}")
|
| 41 |
+
elif "count" in result_data:
|
| 42 |
+
logger.info(f"[OK] {tool_name} | {elapsed_ms}ms | count={result_data['count']}")
|
| 43 |
+
elif "total_count" in result_data:
|
| 44 |
+
logger.info(f"[OK] {tool_name} | {elapsed_ms}ms | count={result_data['total_count']}")
|
| 45 |
+
elif "metadata" in result_data:
|
| 46 |
+
ind_id = result_data.get("metadata", {}).get("id", "?")
|
| 47 |
+
logger.info(f"[OK] {tool_name} | {elapsed_ms}ms | indicator_id={ind_id}")
|
| 48 |
+
else:
|
| 49 |
+
logger.info(f"[OK] {tool_name} | {elapsed_ms}ms")
|
| 50 |
+
except json.JSONDecodeError:
|
| 51 |
+
logger.info(f"[OK] {tool_name} | {elapsed_ms}ms | raw_response")
|
| 52 |
+
|
| 53 |
+
return result
|
| 54 |
+
|
| 55 |
+
except Exception as e:
|
| 56 |
+
elapsed_ms = int((time.time() - start_time) * 1000)
|
| 57 |
+
logger.error(f"[EXCEPTION] {tool_name} | {elapsed_ms}ms | {type(e).__name__}: {e}")
|
| 58 |
+
raise
|
| 59 |
+
|
| 60 |
+
return wrapper
|
| 61 |
from .cache import get_cache, initialize_cache, refresh_cache_if_needed
|
| 62 |
from .cube_resolver import get_resolver
|
| 63 |
from .models import (
|
|
|
|
| 78 |
await refresh_cache_if_needed()
|
| 79 |
|
| 80 |
|
| 81 |
+
@log_tool_call
|
| 82 |
async def list_indicators(
|
| 83 |
thematique: str = "",
|
| 84 |
maille: str = "",
|
|
|
|
| 132 |
}, ensure_ascii=False, indent=2)
|
| 133 |
|
| 134 |
|
| 135 |
+
@log_tool_call
|
| 136 |
async def get_indicator_details(indicator_id: str) -> str:
|
| 137 |
"""Get detailed information about a specific indicator.
|
| 138 |
|
|
|
|
| 202 |
return json.dumps(result, ensure_ascii=False, indent=2)
|
| 203 |
|
| 204 |
|
| 205 |
+
@log_tool_call
|
| 206 |
async def query_indicator_data(
|
| 207 |
indicator_id: str,
|
| 208 |
geographic_level: str,
|
|
|
|
| 362 |
}, ensure_ascii=False, indent=2)
|
| 363 |
|
| 364 |
|
| 365 |
+
@log_tool_call
|
| 366 |
async def search_indicators(query: str) -> str:
|
| 367 |
"""Search indicators by keywords in their name or description.
|
| 368 |
|