From 571c56ef7102ceb2966dbbd5f8ffa454012c4fd7 Mon Sep 17 00:00:00 2001 From: Colin Maudry Date: Thu, 9 Jul 2026 19:57:09 +0200 Subject: [PATCH] feat(mcp): helper track_mcp_tool pour tracer les appels MCP dans Matomo --- src/utils/tracking.py | 32 +++++++++++++++++++++++++++++ tests/mcp/test_tracking.py | 42 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) create mode 100644 tests/mcp/test_tracking.py diff --git a/src/utils/tracking.py b/src/utils/tracking.py index 86e5377..e7ba173 100644 --- a/src/utils/tracking.py +++ b/src/utils/tracking.py @@ -28,3 +28,35 @@ def track_search(query, category): url=f"https://{os.getenv('MATOMO_DOMAIN')}/matomo.php", params=params, ).raise_for_status() + + +def track_mcp_tool(tool_name: str, query: str | None = None) -> None: + """Enregistre un appel d'outil MCP dans Matomo (best-effort, prod uniquement). + + `action_name="MCP / "`, `dimension1=`. Si l'outil porte une + requête texte, elle est envoyée en `search`. Nécessite un Custom Dimension + slot 1 (scope Action) configuré côté Matomo — sinon `dimension1` est ignoré. + Ne lève jamais : une panne Matomo ne doit pas casser l'appel du tool. + """ + if DEVELOPMENT or not os.getenv("MATOMO_DOMAIN"): + return + params = { + "idsite": os.getenv("MATOMO_ID_SITE"), + "url": "https://colibre.fr/_mcp", + "rec": "1", + "action_name": f"MCP / {tool_name}", + "dimension1": tool_name, + "rand": uuid.uuid4().hex, + "apiv": "1", + "h": localtime().tm_hour, + "m": localtime().tm_min, + "s": localtime().tm_sec, + "token_auth": os.getenv("MATOMO_TOKEN"), + } + if query: + params["search"] = query + params["search_cat"] = "mcp" + try: + post(url=f"https://{os.getenv('MATOMO_DOMAIN')}/matomo.php", params=params) + except Exception: + pass diff --git a/tests/mcp/test_tracking.py b/tests/mcp/test_tracking.py new file mode 100644 index 0000000..e4067b9 --- /dev/null +++ b/tests/mcp/test_tracking.py @@ -0,0 +1,42 @@ +import src.utils.tracking as tracking + + +def test_track_mcp_tool_sends_action_and_dimension(monkeypatch): + captured = {} + + def fake_post(url, params): + captured["url"] = url + captured["params"] = params + + class _R: + def raise_for_status(self): + pass + + return _R() + + monkeypatch.setattr(tracking, "DEVELOPMENT", False) + monkeypatch.setattr(tracking, "post", fake_post) + monkeypatch.setenv("MATOMO_DOMAIN", "matomo.example") + monkeypatch.setenv("MATOMO_ID_SITE", "1") + + tracking.track_mcp_tool("rechercher_marches", query="informatique") + + assert captured["params"]["action_name"] == "MCP / rechercher_marches" + assert captured["params"]["dimension1"] == "rechercher_marches" + assert captured["params"]["search"] == "informatique" + + +def test_track_mcp_tool_noop_in_development(monkeypatch): + called = False + + def fake_post(url, params): + nonlocal called + called = True + + monkeypatch.setattr(tracking, "DEVELOPMENT", True) + monkeypatch.setattr(tracking, "post", fake_post) + monkeypatch.setenv("MATOMO_DOMAIN", "matomo.example") + + tracking.track_mcp_tool("stats_acheteur") + + assert called is False