Qdonnars Cursor commited on
Commit
831a68e
·
1 Parent(s): dbdc8a5

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>

Files changed (1) hide show
  1. 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
- from typing import Any
 
 
 
 
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