trodo-python 2.2.0__py3-none-any.whl → 2.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
trodo/__init__.py CHANGED
@@ -40,7 +40,7 @@ Downstream microservice (join the caller's run instead of making a new one):
40
40
 
41
41
  from __future__ import annotations
42
42
 
43
- __version__ = "2.2.0"
43
+ __version__ = "2.3.1"
44
44
 
45
45
  from typing import Any, Callable, Dict, List, Optional, Union
46
46
 
@@ -88,6 +88,7 @@ __all__ = [
88
88
  "start_run",
89
89
  "end_run",
90
90
  "track_llm_call",
91
+ "track_mcp",
91
92
  "feedback",
92
93
  "get_tracer",
93
94
  # Cross-service propagation
@@ -425,6 +426,65 @@ def track_llm_call(
425
426
  )
426
427
 
427
428
 
429
+ def track_mcp(
430
+ *,
431
+ tool: str,
432
+ distinct_id: str,
433
+ input: Any = None,
434
+ output: Any = None,
435
+ error: Optional[str] = None,
436
+ duration_ms: Optional[int] = None,
437
+ session_id: Optional[str] = None,
438
+ client_label: Optional[str] = None,
439
+ attributes: Optional[Dict[str, Any]] = None,
440
+ agent_name: str = "MCP",
441
+ started_at: Optional[str] = None,
442
+ ended_at: Optional[str] = None,
443
+ ) -> str:
444
+ """Record one MCP tool call as a runless span (no parent run).
445
+
446
+ Use this from inside an MCP server's ``tools/call`` handler. The MCP
447
+ server proxies tool calls but never sees the user's prompt or the
448
+ LLM's final answer, so a parent run carries no analytical value.
449
+ Each call to ``track_mcp`` writes one self-contained span row tagged
450
+ with ``agent_name='MCP'`` (overridable), ``distinct_id`` (the user),
451
+ and ``conversation_id`` (the MCP-Session-Id, auto-uuid'd if omitted).
452
+
453
+ Required:
454
+ tool: the tool name. Becomes ``name='tool.<tool>'``.
455
+ distinct_id: end-user attribution (email or stable user id).
456
+
457
+ Optional:
458
+ input, output: full args + result. Truncated at 64KB by the SDK.
459
+ error: if set, marks ``status='error'``.
460
+ duration_ms: tool wall time. Defaults to 0 if omitted.
461
+ session_id: the ``Mcp-Session-Id`` from the client; auto-uuid'd
462
+ if omitted (each call becomes its own grouping bucket).
463
+ client_label: 'anthropic' / 'cursor' / 'chatgpt' / etc.; merged
464
+ into attributes as ``mcp_client_label``.
465
+ attributes: free-form extras.
466
+ agent_name: defaults to 'MCP'; override only for custom tags.
467
+ started_at / ended_at: ISO 8601; defaults derived from now() and
468
+ ``duration_ms`` so callers usually skip them.
469
+
470
+ Returns the span_id (str) for caller-side correlation/logging.
471
+ """
472
+ return _get_client().track_mcp(
473
+ tool=tool,
474
+ distinct_id=distinct_id,
475
+ input=input,
476
+ output=output,
477
+ error=error,
478
+ duration_ms=duration_ms,
479
+ session_id=session_id,
480
+ client_label=client_label,
481
+ attributes=attributes,
482
+ agent_name=agent_name,
483
+ started_at=started_at,
484
+ ended_at=ended_at,
485
+ )
486
+
487
+
428
488
  def feedback(
429
489
  run_id: str,
430
490
  *,
trodo/api/http_client.py CHANGED
@@ -107,6 +107,11 @@ class HttpClient:
107
107
  {"spans": spans},
108
108
  )
109
109
 
110
+ def post_runless_spans(self, spans: list) -> ApiResult:
111
+ """Ingest spans with no parent run (e.g. MCP tool calls). Each span
112
+ MUST carry distinct_id and agent_name; the server gates on that."""
113
+ return self._request("/api/sdk/spans/append", {"spans": spans})
114
+
110
115
  def post_run_feedback(self, run_id: str, payload: Dict[str, Any]) -> ApiResult:
111
116
  from urllib.parse import quote
112
117
  return self._request(
trodo/client.py CHANGED
@@ -26,6 +26,7 @@ from .otel.auto_instrument import enable_auto_instrument
26
26
  from .otel.helpers import (
27
27
  tool as tool_decorator,
28
28
  track_llm_call as track_llm_call_fn,
29
+ track_mcp as track_mcp_fn,
29
30
  fastapi_middleware as fastapi_middleware_fn,
30
31
  propagation_headers as propagation_headers_fn,
31
32
  )
@@ -395,6 +396,65 @@ class TrodoClient:
395
396
  metadata=metadata,
396
397
  )
397
398
 
399
+ def track_mcp(
400
+ self,
401
+ *,
402
+ tool: str,
403
+ distinct_id: str,
404
+ input: Any = None,
405
+ output: Any = None,
406
+ error: Optional[str] = None,
407
+ duration_ms: Optional[int] = None,
408
+ session_id: Optional[str] = None,
409
+ client_label: Optional[str] = None,
410
+ attributes: Optional[Dict[str, Any]] = None,
411
+ agent_name: str = "MCP",
412
+ started_at: Optional[str] = None,
413
+ ended_at: Optional[str] = None,
414
+ ) -> str:
415
+ """Record a single MCP tool call as a runless span.
416
+
417
+ Use this from inside an MCP server's ``tools/call`` handler — one
418
+ call to ``track_mcp`` = one row in ``agent_spans`` with ``run_id``
419
+ NULL. The MCP server proxies tool calls but never sees the user's
420
+ prompt or the LLM's final answer, so a parent run carries no
421
+ analytical value; the span is the unit of analytics.
422
+
423
+ Required:
424
+ tool: the tool name (becomes ``name = "tool.<tool>"``).
425
+ distinct_id: end-user attribution (email or stable user id).
426
+
427
+ Optional but recommended:
428
+ input / output: the tool's args + full result. Truncated at 64KB.
429
+ error: if set, marks ``status="error"`` and stored as
430
+ ``error_message``.
431
+ duration_ms: tool wall time. If omitted, falls back to 0.
432
+ session_id: the ``Mcp-Session-Id`` from the client; stored as
433
+ ``conversation_id`` for grouping. Auto-uuid'd if
434
+ omitted (each call becomes its own bucket).
435
+ client_label: which MCP client (anthropic / cursor / chatgpt /
436
+ etc.); merged into attributes.
437
+ attributes: any extra free-form key/values.
438
+ agent_name: defaults to "MCP"; override only for custom tags.
439
+
440
+ Returns the span_id (str) so the caller can correlate.
441
+ """
442
+ return track_mcp_fn(
443
+ http=self._http,
444
+ tool=tool,
445
+ distinct_id=distinct_id,
446
+ input=input,
447
+ output=output,
448
+ error=error,
449
+ duration_ms=duration_ms,
450
+ session_id=session_id,
451
+ client_label=client_label,
452
+ attributes=attributes,
453
+ agent_name=agent_name,
454
+ started_at=started_at,
455
+ ended_at=ended_at,
456
+ )
457
+
398
458
  def fastapi_middleware(self) -> Callable:
399
459
  """Return a FastAPI/Starlette middleware that auto-joins runs."""
400
460
  return fastapi_middleware_fn(self)
trodo/otel/helpers.py CHANGED
@@ -286,6 +286,96 @@ def llm(
286
286
  )
287
287
 
288
288
 
289
+ def track_mcp(
290
+ *,
291
+ http: Any,
292
+ tool: str,
293
+ distinct_id: str,
294
+ input: Any = None,
295
+ output: Any = None,
296
+ error: Optional[str] = None,
297
+ duration_ms: Optional[int] = None,
298
+ session_id: Optional[str] = None,
299
+ client_label: Optional[str] = None,
300
+ attributes: Optional[Dict[str, Any]] = None,
301
+ agent_name: str = "MCP",
302
+ started_at: Optional[str] = None,
303
+ ended_at: Optional[str] = None,
304
+ ) -> str:
305
+ """Record one MCP tool call as a runless span. See ``Client.track_mcp``
306
+ for the customer-facing docstring; this is the implementation that
307
+ builds the span payload and posts it via the HttpClient.
308
+
309
+ Returns the span_id.
310
+ """
311
+ import json as _json
312
+ import uuid as _uuid
313
+ from datetime import datetime, timedelta, timezone
314
+
315
+ if not tool:
316
+ raise ValueError("track_mcp: 'tool' is required")
317
+ if not distinct_id:
318
+ raise ValueError("track_mcp: 'distinct_id' is required (runless spans must attribute)")
319
+
320
+ span_id = str(_uuid.uuid4())
321
+ conv_id = session_id or str(_uuid.uuid4())
322
+
323
+ now = datetime.now(timezone.utc)
324
+ if ended_at is None:
325
+ ended_at = now.isoformat()
326
+ if started_at is None:
327
+ offset_ms = duration_ms if isinstance(duration_ms, int) and duration_ms > 0 else 0
328
+ started_at = (now - timedelta(milliseconds=offset_ms)).isoformat()
329
+ if duration_ms is None:
330
+ duration_ms = 0
331
+
332
+ status = "error" if error else "ok"
333
+ if error:
334
+ error_payload: Any = {"error": error}
335
+ output_to_record = output if output is not None else error_payload
336
+ else:
337
+ output_to_record = output
338
+
339
+ def _stringify(value: Any) -> Any:
340
+ if value is None or isinstance(value, str):
341
+ return value
342
+ try:
343
+ return _json.dumps(value, default=str)
344
+ except Exception:
345
+ return str(value)
346
+
347
+ merged_attributes: Dict[str, Any] = dict(attributes or {})
348
+ if client_label:
349
+ merged_attributes.setdefault("mcp_client_label", client_label)
350
+
351
+ span_payload: Dict[str, Any] = {
352
+ "span_id": span_id,
353
+ "kind": "tool",
354
+ "name": f"tool.{tool}",
355
+ "status": status,
356
+ "input": _stringify({"tool": tool, "params": input}) if input is not None else None,
357
+ "output": _stringify(output_to_record),
358
+ "tool_name": tool,
359
+ "started_at": started_at,
360
+ "ended_at": ended_at,
361
+ "duration_ms": duration_ms,
362
+ "distinct_id": distinct_id,
363
+ "conversation_id": conv_id,
364
+ "agent_name": agent_name,
365
+ "error_message": error,
366
+ "attributes": merged_attributes,
367
+ }
368
+
369
+ try:
370
+ http.post_runless_spans([span_payload])
371
+ except Exception:
372
+ # Span loss must never break the caller's request flow. The
373
+ # underlying HttpClient already retries; failures here mean the
374
+ # backend rejected (rare) or the network is permanently down.
375
+ pass
376
+ return span_id
377
+
378
+
289
379
  def track_llm_call(
290
380
  *,
291
381
  model: Optional[str] = None,
@@ -71,4 +71,5 @@ def build_session_payload(session: ServerSession) -> Dict[str, Any]:
71
71
  "utm_id": None,
72
72
  "visited_pages": [],
73
73
  "active_time_ms": 0,
74
+ "is_server_session": True,
74
75
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: trodo-python
3
- Version: 2.2.0
3
+ Version: 2.3.1
4
4
  Summary: Trodo Analytics SDK for Python — server-side event tracking
5
5
  License: ISC
6
6
  Keywords: analytics,tracking,trodo,server-side
@@ -1,11 +1,11 @@
1
- trodo/__init__.py,sha256=lo_QFNEk_4ylQ_4-v0RBfdA8ed30M5QjlA7ylJAiAjU,12870
2
- trodo/client.py,sha256=x_HjIyLMowUU-w73GYETEZWWHKjWBC-4LpG6lfLBRmU,15499
1
+ trodo/__init__.py,sha256=e3-d_o3DdaXK6L2AxIKJgcLWeUxGLpY5pE-yL5vmWJ4,15121
2
+ trodo/client.py,sha256=5sT3mKh-AqHnB9_duTVwvnQcPmJ90hc2GJIe0Z7j1BI,17865
3
3
  trodo/types.py,sha256=eySgUvCXROG2TxtxgiU0MNr5iH0DEcduK8bmYtTKG44,3138
4
4
  trodo/user_context.py,sha256=9la6azzwEanVmdP4ps_xMoufbeWVeIGU-M8ychmgajg,7859
5
5
  trodo/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  trodo/api/async_client.py,sha256=rZN4aJ2QiKyrHBK260bApCUB9JaMWU6BQtzoSJZh7xk,3408
7
7
  trodo/api/endpoints.py,sha256=HKQ3d_Mxf0y4HwlHor0XkSAwUVj-4Xvv--rzE9njxjM,1027
8
- trodo/api/http_client.py,sha256=AELbKiABR066px2Uhq_dtkSsiVqMMP23p8QeoywzeHg,4391
8
+ trodo/api/http_client.py,sha256=bu_tRAhlsqHnLYZhRChs3ltNWuOXzQLArNbWG0P3whg,4676
9
9
  trodo/auto/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  trodo/auto/auto_event_manager.py,sha256=cztuRsRkNoJE5R4NfSfTrTJTGl4jx2Yb-Ncy0aVAPo8,4247
11
11
  trodo/managers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -14,16 +14,16 @@ trodo/managers/people_manager.py,sha256=mMVnx40Mlifx6NGgChvohC9ViK6dQu2mkXNHbV8p
14
14
  trodo/otel/__init__.py,sha256=yiRFXWUU45bAM2CV37XeO7zf1hmnmjufdP4XO50yEyE,624
15
15
  trodo/otel/auto_instrument.py,sha256=J_neFxvO-3YACUvtetY4RdM8xYA_79SZUgPry6hXrm8,9434
16
16
  trodo/otel/context.py,sha256=iJ1rE42-SbO8VZHAxhIl2ZJXgNwLIVps5xLg8GKgfFc,1165
17
- trodo/otel/helpers.py,sha256=cvgFrdT8yP92P9mttloiHPr_eTCe8cC4NVrxrJo_I-A,13234
17
+ trodo/otel/helpers.py,sha256=_ImekkJOWWT0pD8Vm1NuKBIps4wjCjx-72SUu7MpAyk,16172
18
18
  trodo/otel/processor.py,sha256=jVZkslZlw50G5uRAa7-GMRgn_yvae58EmlWTZL8tMkQ,6285
19
19
  trodo/otel/wrap_agent.py,sha256=mwHYwxtg9A9VUBdlbCKLswrNP0v5oY8ELpvt1JVYE8Q,17495
20
20
  trodo/queue/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
21
  trodo/queue/batch_flusher.py,sha256=4Lg6T3Urwi9U0Q4FpFGPmjDYKg4ZliCTR-ND8BJvWaY,1298
22
22
  trodo/queue/event_queue.py,sha256=EVFZrhlq_kwC3jJ2GK0wMhHISf9UzLCZNDnT_aZ2I2A,872
23
23
  trodo/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
- trodo/session/server_session.py,sha256=uBAq1QSYPUUaHFSeoOyM5Yr65dLb8T82OOx3D1BrdrE,1970
24
+ trodo/session/server_session.py,sha256=4bQZc_Zxktmu8RVoyh0qI7tvr8AKsHI5xkGf3jEpWVE,2005
25
25
  trodo/session/session_manager.py,sha256=JrgH1VeicmtlxPR4dXEuJbxhi23OelkgwW3-9Slv80o,2525
26
- trodo_python-2.2.0.dist-info/METADATA,sha256=2WsoZx5P03S5LG7JDUsCfBwTLVGSCVYa3R8ZS8KKLqw,16353
27
- trodo_python-2.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
28
- trodo_python-2.2.0.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
29
- trodo_python-2.2.0.dist-info/RECORD,,
26
+ trodo_python-2.3.1.dist-info/METADATA,sha256=-aDktgYzcnpau7fLB3IMXdl1EapanQz-mMIuoEjb_Ps,16353
27
+ trodo_python-2.3.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
28
+ trodo_python-2.3.1.dist-info/top_level.txt,sha256=VCQu1CJWFmNsqTs1YxMcw4Mq35Tc3z3uI9RwHEXAayQ,6
29
+ trodo_python-2.3.1.dist-info/RECORD,,