mcp-agent-trace 1.0.0__tar.gz
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.
- mcp_agent_trace-1.0.0/LICENSE +17 -0
- mcp_agent_trace-1.0.0/PKG-INFO +66 -0
- mcp_agent_trace-1.0.0/README.md +45 -0
- mcp_agent_trace-1.0.0/mcp_agent_trace.egg-info/PKG-INFO +66 -0
- mcp_agent_trace-1.0.0/mcp_agent_trace.egg-info/SOURCES.txt +14 -0
- mcp_agent_trace-1.0.0/mcp_agent_trace.egg-info/dependency_links.txt +1 -0
- mcp_agent_trace-1.0.0/mcp_agent_trace.egg-info/top_level.txt +1 -0
- mcp_agent_trace-1.0.0/pyproject.toml +30 -0
- mcp_agent_trace-1.0.0/setup.cfg +4 -0
- mcp_agent_trace-1.0.0/src/__init__.py +5 -0
- mcp_agent_trace-1.0.0/src/agent_trace_engine.py +269 -0
- mcp_agent_trace-1.0.0/src/server.py +71 -0
- mcp_agent_trace-1.0.0/tests/test_agent_trace.py +190 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
Copyright (c) 2026 AMEOBIUS
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
The above copyright notice and this permission notice shall be included in all
|
|
10
|
+
copies or substantial portions of the Software.
|
|
11
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
12
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
13
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
14
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
15
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
16
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
17
|
+
SOFTWARE.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-agent-trace
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Agent tracing: step-by-step execution traces for AI agent debugging
|
|
5
|
+
Author: aaameobius-crypto
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aaameobius-crypto/darkbot-ai-templates
|
|
8
|
+
Project-URL: Repository, https://github.com/aaameobius-crypto/darkbot-ai-templates
|
|
9
|
+
Keywords: mcp,ai,agent,harness,tools
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# MCP Agent Trace — Observability for AI Agent Loops
|
|
23
|
+
|
|
24
|
+
> Record agent events, build trace trees, compute metrics, detect loops, and export traces. Zero dependencies, pure Python stdlib.
|
|
25
|
+
|
|
26
|
+
## The Problem
|
|
27
|
+
|
|
28
|
+
Agent decisions are black boxes. No way to trace what happened, what tokens were spent on, where loops occurred. Debugging agent failures requires guessing.
|
|
29
|
+
|
|
30
|
+
## The Solution
|
|
31
|
+
|
|
32
|
+
**MCP Agent Trace** records structured events throughout the agent loop, builds hierarchical trace trees, computes token/latency metrics, detects repeated action patterns, and exports full traces as JSON.
|
|
33
|
+
|
|
34
|
+
## Tools (12)
|
|
35
|
+
|
|
36
|
+
| Tool | What it does |
|
|
37
|
+
|------|-------------|
|
|
38
|
+
| `start_trace` | Begin a new trace session |
|
|
39
|
+
| `end_trace` | End session, compute summary metrics |
|
|
40
|
+
| `log_event` | Record a structured event (tool_call, decision, error, milestone) |
|
|
41
|
+
| `get_trace` | Get full trace tree for a session |
|
|
42
|
+
| `get_metrics` | Token usage, tool calls, latency, loop detection |
|
|
43
|
+
| `detect_loops` | Find repeated tool-call patterns |
|
|
44
|
+
| `export_trace` | Export as JSON for external analysis |
|
|
45
|
+
| `list_sessions` | List all trace sessions |
|
|
46
|
+
| `get_timeline` | Chronological event timeline |
|
|
47
|
+
| `annotate` | Add human annotation to an event |
|
|
48
|
+
| `get_stats` | Aggregate statistics across sessions |
|
|
49
|
+
| `reset` | Clear all sessions and traces |
|
|
50
|
+
|
|
51
|
+
## Tests
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
python -m pytest tests/ -v # 28 tests, all passing
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Inspiration
|
|
58
|
+
|
|
59
|
+
- [AgentDoG](https://arxiv.org/abs/2601.18491) — Diagnostic guardrails
|
|
60
|
+
- [Azure SRE Agent observability](https://techcommunity.microsoft.com/blog/appsonazureblog/how-we-build-azure-sre-agent-with-agentic-workflows/4508753) — 35k incidents traced
|
|
61
|
+
- [LangChain middleware](https://blog.langchain.com/how-middleware-lets-you-customize-your-agent-harness/) — Agent loop hooks
|
|
62
|
+
- [The Design Space of Today's AI Agent Systems](https://arxiv.org/abs/2604.14228) — 27-event-type hook pipeline
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT — aaameobius-crypto
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# MCP Agent Trace — Observability for AI Agent Loops
|
|
2
|
+
|
|
3
|
+
> Record agent events, build trace trees, compute metrics, detect loops, and export traces. Zero dependencies, pure Python stdlib.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
Agent decisions are black boxes. No way to trace what happened, what tokens were spent on, where loops occurred. Debugging agent failures requires guessing.
|
|
8
|
+
|
|
9
|
+
## The Solution
|
|
10
|
+
|
|
11
|
+
**MCP Agent Trace** records structured events throughout the agent loop, builds hierarchical trace trees, computes token/latency metrics, detects repeated action patterns, and exports full traces as JSON.
|
|
12
|
+
|
|
13
|
+
## Tools (12)
|
|
14
|
+
|
|
15
|
+
| Tool | What it does |
|
|
16
|
+
|------|-------------|
|
|
17
|
+
| `start_trace` | Begin a new trace session |
|
|
18
|
+
| `end_trace` | End session, compute summary metrics |
|
|
19
|
+
| `log_event` | Record a structured event (tool_call, decision, error, milestone) |
|
|
20
|
+
| `get_trace` | Get full trace tree for a session |
|
|
21
|
+
| `get_metrics` | Token usage, tool calls, latency, loop detection |
|
|
22
|
+
| `detect_loops` | Find repeated tool-call patterns |
|
|
23
|
+
| `export_trace` | Export as JSON for external analysis |
|
|
24
|
+
| `list_sessions` | List all trace sessions |
|
|
25
|
+
| `get_timeline` | Chronological event timeline |
|
|
26
|
+
| `annotate` | Add human annotation to an event |
|
|
27
|
+
| `get_stats` | Aggregate statistics across sessions |
|
|
28
|
+
| `reset` | Clear all sessions and traces |
|
|
29
|
+
|
|
30
|
+
## Tests
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
python -m pytest tests/ -v # 28 tests, all passing
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Inspiration
|
|
37
|
+
|
|
38
|
+
- [AgentDoG](https://arxiv.org/abs/2601.18491) — Diagnostic guardrails
|
|
39
|
+
- [Azure SRE Agent observability](https://techcommunity.microsoft.com/blog/appsonazureblog/how-we-build-azure-sre-agent-with-agentic-workflows/4508753) — 35k incidents traced
|
|
40
|
+
- [LangChain middleware](https://blog.langchain.com/how-middleware-lets-you-customize-your-agent-harness/) — Agent loop hooks
|
|
41
|
+
- [The Design Space of Today's AI Agent Systems](https://arxiv.org/abs/2604.14228) — 27-event-type hook pipeline
|
|
42
|
+
|
|
43
|
+
## License
|
|
44
|
+
|
|
45
|
+
MIT — aaameobius-crypto
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-agent-trace
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Agent tracing: step-by-step execution traces for AI agent debugging
|
|
5
|
+
Author: aaameobius-crypto
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/aaameobius-crypto/darkbot-ai-templates
|
|
8
|
+
Project-URL: Repository, https://github.com/aaameobius-crypto/darkbot-ai-templates
|
|
9
|
+
Keywords: mcp,ai,agent,harness,tools
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
License-File: LICENSE
|
|
20
|
+
Dynamic: license-file
|
|
21
|
+
|
|
22
|
+
# MCP Agent Trace — Observability for AI Agent Loops
|
|
23
|
+
|
|
24
|
+
> Record agent events, build trace trees, compute metrics, detect loops, and export traces. Zero dependencies, pure Python stdlib.
|
|
25
|
+
|
|
26
|
+
## The Problem
|
|
27
|
+
|
|
28
|
+
Agent decisions are black boxes. No way to trace what happened, what tokens were spent on, where loops occurred. Debugging agent failures requires guessing.
|
|
29
|
+
|
|
30
|
+
## The Solution
|
|
31
|
+
|
|
32
|
+
**MCP Agent Trace** records structured events throughout the agent loop, builds hierarchical trace trees, computes token/latency metrics, detects repeated action patterns, and exports full traces as JSON.
|
|
33
|
+
|
|
34
|
+
## Tools (12)
|
|
35
|
+
|
|
36
|
+
| Tool | What it does |
|
|
37
|
+
|------|-------------|
|
|
38
|
+
| `start_trace` | Begin a new trace session |
|
|
39
|
+
| `end_trace` | End session, compute summary metrics |
|
|
40
|
+
| `log_event` | Record a structured event (tool_call, decision, error, milestone) |
|
|
41
|
+
| `get_trace` | Get full trace tree for a session |
|
|
42
|
+
| `get_metrics` | Token usage, tool calls, latency, loop detection |
|
|
43
|
+
| `detect_loops` | Find repeated tool-call patterns |
|
|
44
|
+
| `export_trace` | Export as JSON for external analysis |
|
|
45
|
+
| `list_sessions` | List all trace sessions |
|
|
46
|
+
| `get_timeline` | Chronological event timeline |
|
|
47
|
+
| `annotate` | Add human annotation to an event |
|
|
48
|
+
| `get_stats` | Aggregate statistics across sessions |
|
|
49
|
+
| `reset` | Clear all sessions and traces |
|
|
50
|
+
|
|
51
|
+
## Tests
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
python -m pytest tests/ -v # 28 tests, all passing
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Inspiration
|
|
58
|
+
|
|
59
|
+
- [AgentDoG](https://arxiv.org/abs/2601.18491) — Diagnostic guardrails
|
|
60
|
+
- [Azure SRE Agent observability](https://techcommunity.microsoft.com/blog/appsonazureblog/how-we-build-azure-sre-agent-with-agentic-workflows/4508753) — 35k incidents traced
|
|
61
|
+
- [LangChain middleware](https://blog.langchain.com/how-middleware-lets-you-customize-your-agent-harness/) — Agent loop hooks
|
|
62
|
+
- [The Design Space of Today's AI Agent Systems](https://arxiv.org/abs/2604.14228) — 27-event-type hook pipeline
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT — aaameobius-crypto
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
./src/__init__.py
|
|
5
|
+
./src/agent_trace_engine.py
|
|
6
|
+
./src/server.py
|
|
7
|
+
mcp_agent_trace.egg-info/PKG-INFO
|
|
8
|
+
mcp_agent_trace.egg-info/SOURCES.txt
|
|
9
|
+
mcp_agent_trace.egg-info/dependency_links.txt
|
|
10
|
+
mcp_agent_trace.egg-info/top_level.txt
|
|
11
|
+
src/__init__.py
|
|
12
|
+
src/agent_trace_engine.py
|
|
13
|
+
src/server.py
|
|
14
|
+
tests/test_agent_trace.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
src
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=64"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mcp-agent-trace"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Agent tracing: step-by-step execution traces for AI agent debugging"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = {text = "MIT"}
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [{name = "aaameobius-crypto"}]
|
|
13
|
+
keywords = ["mcp", "ai", "agent", "harness", "tools"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/aaameobius-crypto/darkbot-ai-templates"
|
|
26
|
+
Repository = "https://github.com/aaameobius-crypto/darkbot-ai-templates"
|
|
27
|
+
|
|
28
|
+
[tool.setuptools]
|
|
29
|
+
packages = ["src"]
|
|
30
|
+
package-dir = {"" = "."}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"""Agent Trace engine — zero dependencies.
|
|
2
|
+
Records agent loop events, builds trace trees, computes metrics,
|
|
3
|
+
detects loops, and exports traces for external analysis.
|
|
4
|
+
"""
|
|
5
|
+
import json, time, collections, hashlib
|
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
7
|
+
|
|
8
|
+
class AgentTrace:
|
|
9
|
+
@staticmethod
|
|
10
|
+
def create_store() -> Dict:
|
|
11
|
+
return {
|
|
12
|
+
"stats": {"events_logged": 0, "sessions_started": 0, "sessions_ended": 0, "loops_detected": 0, "errors": 0},
|
|
13
|
+
"sessions": {}, # session_id -> {start, end, events, metadata}
|
|
14
|
+
"active_session": None,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
@staticmethod
|
|
18
|
+
def _track(store: Dict, op: str):
|
|
19
|
+
store["stats"][op] = store["stats"].get(op, 0) + 1
|
|
20
|
+
|
|
21
|
+
@staticmethod
|
|
22
|
+
def _gen_id() -> str:
|
|
23
|
+
return hashlib.sha256(str(time.time()).encode()).hexdigest()[:12]
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def start_trace(store: Dict, name: str = "", metadata: Dict = None) -> Dict:
|
|
27
|
+
AgentTrace._track(store, "sessions_started")
|
|
28
|
+
session_id = AgentTrace._gen_id()
|
|
29
|
+
store["sessions"][session_id] = {
|
|
30
|
+
"id": session_id,
|
|
31
|
+
"name": name or f"trace_{session_id[:8]}",
|
|
32
|
+
"start_time": time.time(),
|
|
33
|
+
"end_time": None,
|
|
34
|
+
"events": [],
|
|
35
|
+
"metadata": metadata or {},
|
|
36
|
+
"status": "active",
|
|
37
|
+
}
|
|
38
|
+
store["active_session"] = session_id
|
|
39
|
+
return {"success": True, "session_id": session_id, "name": store["sessions"][session_id]["name"]}
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def end_trace(store: Dict, session_id: str = None) -> Dict:
|
|
43
|
+
AgentTrace._track(store, "sessions_ended")
|
|
44
|
+
sid = session_id or store["active_session"]
|
|
45
|
+
if not sid or sid not in store["sessions"]:
|
|
46
|
+
return {"success": False, "error": "Session not found"}
|
|
47
|
+
session = store["sessions"][sid]
|
|
48
|
+
session["end_time"] = time.time()
|
|
49
|
+
session["status"] = "ended"
|
|
50
|
+
if store["active_session"] == sid:
|
|
51
|
+
store["active_session"] = None
|
|
52
|
+
|
|
53
|
+
# Compute summary metrics
|
|
54
|
+
events = session["events"]
|
|
55
|
+
duration = session["end_time"] - session["start_time"]
|
|
56
|
+
tool_calls = [e for e in events if e.get("type") == "tool_call"]
|
|
57
|
+
errors = [e for e in events if e.get("type") == "error"]
|
|
58
|
+
decisions = [e for e in events if e.get("type") == "decision"]
|
|
59
|
+
|
|
60
|
+
# Estimate token usage from event data
|
|
61
|
+
total_tokens = sum(e.get("metadata", {}).get("tokens", 0) for e in events)
|
|
62
|
+
|
|
63
|
+
# Detect loops
|
|
64
|
+
loops = AgentTrace._detect_loops_in_events(events)
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
"success": True,
|
|
68
|
+
"session_id": sid,
|
|
69
|
+
"duration_seconds": round(duration, 3),
|
|
70
|
+
"total_events": len(events),
|
|
71
|
+
"tool_calls": len(tool_calls),
|
|
72
|
+
"errors": len(errors),
|
|
73
|
+
"decisions": len(decisions),
|
|
74
|
+
"estimated_tokens": total_tokens,
|
|
75
|
+
"loops_detected": len(loops),
|
|
76
|
+
"status": "ended",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def log_event(store: Dict, event_type: str, data: Dict = None, session_id: str = None) -> Dict:
|
|
81
|
+
AgentTrace._track(store, "events_logged")
|
|
82
|
+
sid = session_id or store["active_session"]
|
|
83
|
+
if not sid or sid not in store["sessions"]:
|
|
84
|
+
# Auto-create a session
|
|
85
|
+
r = AgentTrace.start_trace(store)
|
|
86
|
+
sid = r["session_id"]
|
|
87
|
+
|
|
88
|
+
event = {
|
|
89
|
+
"id": AgentTrace._gen_id(),
|
|
90
|
+
"timestamp": time.time(),
|
|
91
|
+
"type": event_type,
|
|
92
|
+
"data": data or {},
|
|
93
|
+
"sequence": len(store["sessions"][sid]["events"]),
|
|
94
|
+
}
|
|
95
|
+
store["sessions"][sid]["events"].append(event)
|
|
96
|
+
if event_type == "error":
|
|
97
|
+
AgentTrace._track(store, "errors")
|
|
98
|
+
return {"success": True, "event_id": event["id"], "session_id": sid, "sequence": event["sequence"]}
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def get_trace(store: Dict, session_id: str) -> Dict:
|
|
102
|
+
if session_id not in store["sessions"]:
|
|
103
|
+
return {"success": False, "error": "Session not found"}
|
|
104
|
+
session = store["sessions"][session_id]
|
|
105
|
+
# Build tree from flat events
|
|
106
|
+
events = session["events"]
|
|
107
|
+
tree = AgentTrace._build_tree(events)
|
|
108
|
+
return {
|
|
109
|
+
"success": True,
|
|
110
|
+
"session": {
|
|
111
|
+
"id": session["id"],
|
|
112
|
+
"name": session["name"],
|
|
113
|
+
"start_time": session["start_time"],
|
|
114
|
+
"end_time": session["end_time"],
|
|
115
|
+
"status": session["status"],
|
|
116
|
+
"metadata": session["metadata"],
|
|
117
|
+
},
|
|
118
|
+
"events": events,
|
|
119
|
+
"tree": tree,
|
|
120
|
+
"total_events": len(events),
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
@staticmethod
|
|
124
|
+
def _build_tree(events: List[Dict]) -> Dict:
|
|
125
|
+
"""Build a simple tree from flat events based on parent_id in data."""
|
|
126
|
+
root = {"id": "root", "children": []}
|
|
127
|
+
nodes = {"root": root}
|
|
128
|
+
for e in events:
|
|
129
|
+
parent_id = e.get("data", {}).get("parent_id", "root")
|
|
130
|
+
node = {"id": e["id"], "type": e["type"], "timestamp": e["timestamp"], "children": []}
|
|
131
|
+
nodes[e["id"]] = node
|
|
132
|
+
if parent_id in nodes:
|
|
133
|
+
nodes[parent_id]["children"].append(node)
|
|
134
|
+
else:
|
|
135
|
+
root["children"].append(node)
|
|
136
|
+
return root
|
|
137
|
+
|
|
138
|
+
@staticmethod
|
|
139
|
+
def get_metrics(store: Dict, session_id: str) -> Dict:
|
|
140
|
+
if session_id not in store["sessions"]:
|
|
141
|
+
return {"success": False, "error": "Session not found"}
|
|
142
|
+
session = store["sessions"][session_id]
|
|
143
|
+
events = session["events"]
|
|
144
|
+
end = session["end_time"] or time.time()
|
|
145
|
+
duration = end - session["start_time"]
|
|
146
|
+
|
|
147
|
+
type_counts = dict(collections.Counter(e["type"] for e in events))
|
|
148
|
+
|
|
149
|
+
# Tool call latency
|
|
150
|
+
tool_calls = [e for e in events if e.get("type") == "tool_call"]
|
|
151
|
+
latencies = [e.get("data", {}).get("latency_ms", 0) for e in tool_calls if "latency_ms" in e.get("data", {})]
|
|
152
|
+
|
|
153
|
+
# Token usage by type
|
|
154
|
+
tokens_by_type = collections.defaultdict(int)
|
|
155
|
+
for e in events:
|
|
156
|
+
tokens_by_type[e["type"]] += e.get("data", {}).get("tokens", 0)
|
|
157
|
+
|
|
158
|
+
loops = AgentTrace._detect_loops_in_events(events)
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
"success": True,
|
|
162
|
+
"session_id": session_id,
|
|
163
|
+
"duration_seconds": round(duration, 3),
|
|
164
|
+
"total_events": len(events),
|
|
165
|
+
"events_by_type": type_counts,
|
|
166
|
+
"tokens_by_type": dict(tokens_by_type),
|
|
167
|
+
"total_tokens": sum(tokens_by_type.values()),
|
|
168
|
+
"tool_calls": len(tool_calls),
|
|
169
|
+
"avg_latency_ms": round(sum(latencies) / len(latencies), 2) if latencies else 0,
|
|
170
|
+
"max_latency_ms": max(latencies) if latencies else 0,
|
|
171
|
+
"loops_detected": len(loops),
|
|
172
|
+
"events_per_second": round(len(events) / max(duration, 0.001), 2),
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def _detect_loops_in_events(events: List[Dict]) -> List[Dict]:
|
|
177
|
+
"""Detect repeated action patterns in events."""
|
|
178
|
+
loops = []
|
|
179
|
+
# Simple: look for repeated tool_call sequences of length 2-5
|
|
180
|
+
tool_calls = [(e["sequence"], e.get("data", {}).get("tool", "")) for e in events if e.get("type") == "tool_call"]
|
|
181
|
+
if len(tool_calls) < 4:
|
|
182
|
+
return loops
|
|
183
|
+
|
|
184
|
+
for seq_len in (2, 3):
|
|
185
|
+
seen = {}
|
|
186
|
+
for i in range(len(tool_calls) - seq_len + 1):
|
|
187
|
+
key = tuple(tc[1] for tc in tool_calls[i:i + seq_len])
|
|
188
|
+
if key in seen:
|
|
189
|
+
loops.append({
|
|
190
|
+
"pattern": list(key),
|
|
191
|
+
"first_at": seen[key],
|
|
192
|
+
"repeated_at": tool_calls[i][0],
|
|
193
|
+
"sequence_length": seq_len,
|
|
194
|
+
})
|
|
195
|
+
else:
|
|
196
|
+
seen[key] = tool_calls[i][0]
|
|
197
|
+
|
|
198
|
+
return loops
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
def detect_loops(store: Dict, session_id: str = None) -> Dict:
|
|
202
|
+
sid = session_id or store["active_session"]
|
|
203
|
+
if not sid or sid not in store["sessions"]:
|
|
204
|
+
return {"success": False, "error": "No session"}
|
|
205
|
+
loops = AgentTrace._detect_loops_in_events(store["sessions"][sid]["events"])
|
|
206
|
+
store["stats"]["loops_detected"] += len(loops)
|
|
207
|
+
return {"success": True, "loops": loops, "count": len(loops)}
|
|
208
|
+
|
|
209
|
+
@staticmethod
|
|
210
|
+
def export_trace(store: Dict, session_id: str, format: str = "json") -> Dict:
|
|
211
|
+
trace = AgentTrace.get_trace(store, session_id)
|
|
212
|
+
if not trace["success"]:
|
|
213
|
+
return trace
|
|
214
|
+
if format == "json":
|
|
215
|
+
return {"success": True, "format": "json", "data": json.dumps(trace, indent=2)}
|
|
216
|
+
return trace
|
|
217
|
+
|
|
218
|
+
@staticmethod
|
|
219
|
+
def list_sessions(store: Dict) -> Dict:
|
|
220
|
+
sessions = []
|
|
221
|
+
for sid, s in store["sessions"].items():
|
|
222
|
+
sessions.append({
|
|
223
|
+
"id": sid,
|
|
224
|
+
"name": s["name"],
|
|
225
|
+
"status": s["status"],
|
|
226
|
+
"start_time": s["start_time"],
|
|
227
|
+
"end_time": s["end_time"],
|
|
228
|
+
"events": len(s["events"]),
|
|
229
|
+
})
|
|
230
|
+
sessions.sort(key=lambda x: -x["start_time"])
|
|
231
|
+
return {"success": True, "sessions": sessions, "total": len(sessions)}
|
|
232
|
+
|
|
233
|
+
@staticmethod
|
|
234
|
+
def get_timeline(store: Dict, session_id: str, limit: int = 50) -> Dict:
|
|
235
|
+
if session_id not in store["sessions"]:
|
|
236
|
+
return {"success": False, "error": "Session not found"}
|
|
237
|
+
events = store["sessions"][session_id]["events"][-limit:]
|
|
238
|
+
start = store["sessions"][session_id]["start_time"]
|
|
239
|
+
timeline = []
|
|
240
|
+
for e in events:
|
|
241
|
+
timeline.append({
|
|
242
|
+
"t": round(e["timestamp"] - start, 3),
|
|
243
|
+
"type": e["type"],
|
|
244
|
+
"data": e["data"],
|
|
245
|
+
"sequence": e["sequence"],
|
|
246
|
+
})
|
|
247
|
+
return {"success": True, "timeline": timeline, "count": len(timeline)}
|
|
248
|
+
|
|
249
|
+
@staticmethod
|
|
250
|
+
def annotate(store: Dict, session_id: str, event_id: str, annotation: str) -> Dict:
|
|
251
|
+
if session_id not in store["sessions"]:
|
|
252
|
+
return {"success": False, "error": "Session not found"}
|
|
253
|
+
for e in store["sessions"][session_id]["events"]:
|
|
254
|
+
if e["id"] == event_id:
|
|
255
|
+
e.setdefault("annotations", []).append({"text": annotation, "timestamp": time.time()})
|
|
256
|
+
return {"success": True, "event_id": event_id, "annotation": annotation}
|
|
257
|
+
return {"success": False, "error": "Event not found"}
|
|
258
|
+
|
|
259
|
+
@staticmethod
|
|
260
|
+
def get_stats(store: Dict) -> Dict:
|
|
261
|
+
return {"success": True, **store["stats"], "total_sessions": len(store["sessions"])}
|
|
262
|
+
|
|
263
|
+
@staticmethod
|
|
264
|
+
def reset(store: Dict) -> Dict:
|
|
265
|
+
old = AgentTrace.get_stats(store)
|
|
266
|
+
store["stats"] = {"events_logged": 0, "sessions_started": 0, "sessions_ended": 0, "loops_detected": 0, "errors": 0}
|
|
267
|
+
store["sessions"] = {}
|
|
268
|
+
store["active_session"] = None
|
|
269
|
+
return {"success": True, "reset": old}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""MCP Server for Agent Trace — observability and tracing for agent loops."""
|
|
2
|
+
import json, sys, argparse
|
|
3
|
+
from .agent_trace_engine import AgentTrace
|
|
4
|
+
|
|
5
|
+
_store = AgentTrace.create_store()
|
|
6
|
+
|
|
7
|
+
TOOL_DEFS = [
|
|
8
|
+
{"name":"start_trace","description":"Start a new trace session.","inputSchema":{"type":"object","properties":{"name":{"type":"string","default":""},"metadata":{"type":"object"}},"required":[]}},
|
|
9
|
+
{"name":"end_trace","description":"End trace session and compute summary.","inputSchema":{"type":"object","properties":{"session_id":{"type":"string"}},"required":[]}},
|
|
10
|
+
{"name":"log_event","description":"Record a structured trace event.","inputSchema":{"type":"object","properties":{"event_type":{"type":"string"},"data":{"type":"object"},"session_id":{"type":"string"}},"required":["event_type"]}},
|
|
11
|
+
{"name":"get_trace","description":"Get full trace tree for a session.","inputSchema":{"type":"object","properties":{"session_id":{"type":"string"}},"required":["session_id"]}},
|
|
12
|
+
{"name":"get_metrics","description":"Get token usage, tool calls, latency, loops.","inputSchema":{"type":"object","properties":{"session_id":{"type":"string"}},"required":["session_id"]}},
|
|
13
|
+
{"name":"detect_loops","description":"Find repeated action patterns.","inputSchema":{"type":"object","properties":{"session_id":{"type":"string"}},"required":[]}},
|
|
14
|
+
{"name":"export_trace","description":"Export trace as JSON.","inputSchema":{"type":"object","properties":{"session_id":{"type":"string"},"format":{"type":"string","default":"json"}},"required":["session_id"]}},
|
|
15
|
+
{"name":"list_sessions","description":"List all trace sessions.","inputSchema":{"type":"object","properties":{},"required":[]}},
|
|
16
|
+
{"name":"get_timeline","description":"Chronological event timeline.","inputSchema":{"type":"object","properties":{"session_id":{"type":"string"},"limit":{"type":"integer","default":50}},"required":["session_id"]}},
|
|
17
|
+
{"name":"annotate","description":"Add human annotation to an event.","inputSchema":{"type":"object","properties":{"session_id":{"type":"string"},"event_id":{"type":"string"},"annotation":{"type":"string"}},"required":["session_id","event_id","annotation"]}},
|
|
18
|
+
{"name":"get_stats","description":"Get aggregate trace statistics.","inputSchema":{"type":"object","properties":{},"required":[]}},
|
|
19
|
+
{"name":"reset","description":"Clear all sessions and traces.","inputSchema":{"type":"object","properties":{},"required":[]}},
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
class MCPAgentTraceServer:
|
|
23
|
+
def __init__(self,name="mcp-agent-trace",version="1.0.0"):
|
|
24
|
+
self.name=name;self.version=version
|
|
25
|
+
def list_tools(self):return TOOL_DEFS
|
|
26
|
+
def manifest(self):return{"server":{"name":self.name,"version":self.version},"capabilities":{"tools":{"listChanged":False}},"tools":self.list_tools()}
|
|
27
|
+
def handle_tool_call(self,name,args):
|
|
28
|
+
try:
|
|
29
|
+
if name=="start_trace":return json.dumps(AgentTrace.start_trace(_store,args.get("name",""),args.get("metadata")))
|
|
30
|
+
elif name=="end_trace":return json.dumps(AgentTrace.end_trace(_store,args.get("session_id")))
|
|
31
|
+
elif name=="log_event":return json.dumps(AgentTrace.log_event(_store,args["event_type"],args.get("data"),args.get("session_id")))
|
|
32
|
+
elif name=="get_trace":return json.dumps(AgentTrace.get_trace(_store,args["session_id"]))
|
|
33
|
+
elif name=="get_metrics":return json.dumps(AgentTrace.get_metrics(_store,args["session_id"]))
|
|
34
|
+
elif name=="detect_loops":return json.dumps(AgentTrace.detect_loops(_store,args.get("session_id")))
|
|
35
|
+
elif name=="export_trace":return json.dumps(AgentTrace.export_trace(_store,args["session_id"],args.get("format","json")))
|
|
36
|
+
elif name=="list_sessions":return json.dumps(AgentTrace.list_sessions(_store))
|
|
37
|
+
elif name=="get_timeline":return json.dumps(AgentTrace.get_timeline(_store,args["session_id"],args.get("limit",50)))
|
|
38
|
+
elif name=="annotate":return json.dumps(AgentTrace.annotate(_store,args["session_id"],args["event_id"],args["annotation"]))
|
|
39
|
+
elif name=="get_stats":return json.dumps(AgentTrace.get_stats(_store))
|
|
40
|
+
elif name=="reset":return json.dumps(AgentTrace.reset(_store))
|
|
41
|
+
else:return json.dumps({"error":f"Unknown tool: {name}"})
|
|
42
|
+
except KeyError as e:return json.dumps({"error":f"Missing required parameter: {e}","tool":name})
|
|
43
|
+
except Exception as e:return json.dumps({"error":str(e),"tool":name})
|
|
44
|
+
|
|
45
|
+
def _run_stdio():
|
|
46
|
+
server=MCPAgentTraceServer()
|
|
47
|
+
for line in sys.stdin:
|
|
48
|
+
line=line.strip()
|
|
49
|
+
if not line:continue
|
|
50
|
+
try:request=json.loads(line)
|
|
51
|
+
except json.JSONDecodeError:print(json.dumps({"jsonrpc":"2.0","error":{"code":-32700,"message":"Parse error"}}),flush=True);continue
|
|
52
|
+
method=request.get("method","");req_id=request.get("id");params=request.get("params",{})
|
|
53
|
+
if method=="initialize":response={"jsonrpc":"2.0","id":req_id,"result":{"server":server.name,"version":server.version}}
|
|
54
|
+
elif method=="tools/list":response={"jsonrpc":"2.0","id":req_id,"result":{"tools":server.list_tools()}}
|
|
55
|
+
elif method=="tools/call":
|
|
56
|
+
result=server.handle_tool_call(params.get("name",""),params.get("arguments",{}))
|
|
57
|
+
response={"jsonrpc":"2.0","id":req_id,"result":{"content":[{"type":"text","text":result}]}}
|
|
58
|
+
elif method=="shutdown":response={"jsonrpc":"2.0","id":req_id,"result":{}};print(json.dumps(response),flush=True);break
|
|
59
|
+
else:response={"jsonrpc":"2.0","id":req_id,"error":{"code":-32601,"message":f"Method not found: {method}"}}
|
|
60
|
+
print(json.dumps(response),flush=True)
|
|
61
|
+
|
|
62
|
+
def main():
|
|
63
|
+
parser=argparse.ArgumentParser(description="MCP Agent Trace Server")
|
|
64
|
+
parser.add_argument("--stdio",action="store_true")
|
|
65
|
+
parser.add_argument("--manifest",action="store_true")
|
|
66
|
+
args=parser.parse_args()
|
|
67
|
+
if args.manifest:print(json.dumps(MCPAgentTraceServer().manifest(),indent=2))
|
|
68
|
+
elif args.stdio:_run_stdio()
|
|
69
|
+
else:parser.print_help()
|
|
70
|
+
|
|
71
|
+
if __name__=="__main__":main()
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
"""Tests for MCP Agent Trace — sessions, events, metrics, loops, timeline."""
|
|
2
|
+
import json, pytest, os, sys, time
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
5
|
+
from src.server import MCPAgentTraceServer, TOOL_DEFS
|
|
6
|
+
from src.agent_trace_engine import AgentTrace
|
|
7
|
+
|
|
8
|
+
class TestToolDefs:
|
|
9
|
+
def test_names(self):
|
|
10
|
+
for t in TOOL_DEFS: assert "name" in t and len(t["name"])>0
|
|
11
|
+
def test_descs(self):
|
|
12
|
+
for t in TOOL_DEFS: assert "description" in t and len(t["description"])>15
|
|
13
|
+
def test_schema(self):
|
|
14
|
+
for t in TOOL_DEFS: assert "inputSchema" in t and t["inputSchema"]["type"]=="object"
|
|
15
|
+
def test_count(self):
|
|
16
|
+
assert len(TOOL_DEFS)==12
|
|
17
|
+
def test_required(self):
|
|
18
|
+
names={t["name"] for t in TOOL_DEFS}
|
|
19
|
+
expected={"start_trace","end_trace","log_event","get_trace","get_metrics","detect_loops","export_trace","list_sessions","get_timeline","annotate","get_stats","reset"}
|
|
20
|
+
assert names==expected
|
|
21
|
+
|
|
22
|
+
class TestManifest:
|
|
23
|
+
def test_manifest(self):
|
|
24
|
+
s=MCPAgentTraceServer();m=s.manifest()
|
|
25
|
+
assert m["server"]["name"]=="mcp-agent-trace"
|
|
26
|
+
assert len(m["tools"])==12
|
|
27
|
+
|
|
28
|
+
class TestStartEndTrace:
|
|
29
|
+
def test_start(self):
|
|
30
|
+
s=AgentTrace.create_store()
|
|
31
|
+
r=AgentTrace.start_trace(s,name="test_session")
|
|
32
|
+
assert r["success"] is True
|
|
33
|
+
assert len(r["session_id"])>0
|
|
34
|
+
assert r["name"]=="test_session"
|
|
35
|
+
def test_end(self):
|
|
36
|
+
s=AgentTrace.create_store()
|
|
37
|
+
r=AgentTrace.start_trace(s,name="test")
|
|
38
|
+
time.sleep(0.01)
|
|
39
|
+
end=AgentTrace.end_trace(s,r["session_id"])
|
|
40
|
+
assert end["success"] is True
|
|
41
|
+
assert end["duration_seconds"]>0
|
|
42
|
+
assert end["status"]=="ended"
|
|
43
|
+
def test_end_nonexistent(self):
|
|
44
|
+
s=AgentTrace.create_store()
|
|
45
|
+
r=AgentTrace.end_trace(s,"nonexistent")
|
|
46
|
+
assert r["success"] is False
|
|
47
|
+
|
|
48
|
+
class TestLogEvent:
|
|
49
|
+
def test_basic(self):
|
|
50
|
+
s=AgentTrace.create_store()
|
|
51
|
+
AgentTrace.start_trace(s)
|
|
52
|
+
r=AgentTrace.log_event(s,"tool_call",{"tool":"read_file","args":{"path":"/tmp"}})
|
|
53
|
+
assert r["success"] is True
|
|
54
|
+
assert r["sequence"]==0
|
|
55
|
+
def test_multiple(self):
|
|
56
|
+
s=AgentTrace.create_store()
|
|
57
|
+
AgentTrace.start_trace(s)
|
|
58
|
+
AgentTrace.log_event(s,"tool_call",{"tool":"a"})
|
|
59
|
+
AgentTrace.log_event(s,"decision",{"action":"proceed"})
|
|
60
|
+
r=AgentTrace.log_event(s,"tool_call",{"tool":"b"})
|
|
61
|
+
assert r["sequence"]==2
|
|
62
|
+
def test_auto_session(self):
|
|
63
|
+
s=AgentTrace.create_store()
|
|
64
|
+
r=AgentTrace.log_event(s,"milestone",{"msg":"started"})
|
|
65
|
+
assert r["success"] is True # auto-creates session
|
|
66
|
+
|
|
67
|
+
class TestGetTrace:
|
|
68
|
+
def test_basic(self):
|
|
69
|
+
s=AgentTrace.create_store()
|
|
70
|
+
sid=AgentTrace.start_trace(s,name="test")["session_id"]
|
|
71
|
+
AgentTrace.log_event(s,"tool_call",{"tool":"read"},session_id=sid)
|
|
72
|
+
r=AgentTrace.get_trace(s,sid)
|
|
73
|
+
assert r["success"] is True
|
|
74
|
+
assert r["total_events"]==1
|
|
75
|
+
assert "tree" in r
|
|
76
|
+
def test_not_found(self):
|
|
77
|
+
s=AgentTrace.create_store()
|
|
78
|
+
r=AgentTrace.get_trace(s,"nonexistent")
|
|
79
|
+
assert r["success"] is False
|
|
80
|
+
|
|
81
|
+
class TestGetMetrics:
|
|
82
|
+
def test_basic(self):
|
|
83
|
+
s=AgentTrace.create_store()
|
|
84
|
+
sid=AgentTrace.start_trace(s)["session_id"]
|
|
85
|
+
AgentTrace.log_event(s,"tool_call",{"tool":"a","latency_ms":100,"tokens":50},session_id=sid)
|
|
86
|
+
AgentTrace.log_event(s,"tool_call",{"tool":"b","latency_ms":200,"tokens":30},session_id=sid)
|
|
87
|
+
AgentTrace.log_event(s,"error",{"msg":"failed"},session_id=sid)
|
|
88
|
+
r=AgentTrace.get_metrics(s,sid)
|
|
89
|
+
assert r["success"] is True
|
|
90
|
+
assert r["tool_calls"]==2
|
|
91
|
+
assert r["avg_latency_ms"]==150
|
|
92
|
+
assert r["total_tokens"]==80
|
|
93
|
+
assert r["events_by_type"]["error"]==1
|
|
94
|
+
|
|
95
|
+
class TestDetectLoops:
|
|
96
|
+
def test_loop(self):
|
|
97
|
+
s=AgentTrace.create_store()
|
|
98
|
+
sid=AgentTrace.start_trace(s)["session_id"]
|
|
99
|
+
# Create a repeated pattern: a,b,a,b
|
|
100
|
+
for tool in ["read","write","read","write","read","write"]:
|
|
101
|
+
AgentTrace.log_event(s,"tool_call",{"tool":tool},session_id=sid)
|
|
102
|
+
r=AgentTrace.detect_loops(s,sid)
|
|
103
|
+
assert r["success"] is True
|
|
104
|
+
assert r["count"]>0
|
|
105
|
+
def test_no_loop(self):
|
|
106
|
+
s=AgentTrace.create_store()
|
|
107
|
+
sid=AgentTrace.start_trace(s)["session_id"]
|
|
108
|
+
for tool in ["read","write","delete","search"]:
|
|
109
|
+
AgentTrace.log_event(s,"tool_call",{"tool":tool},session_id=sid)
|
|
110
|
+
r=AgentTrace.detect_loops(s,sid)
|
|
111
|
+
assert r["count"]==0
|
|
112
|
+
|
|
113
|
+
class TestExportTrace:
|
|
114
|
+
def test_json(self):
|
|
115
|
+
s=AgentTrace.create_store()
|
|
116
|
+
sid=AgentTrace.start_trace(s)["session_id"]
|
|
117
|
+
AgentTrace.log_event(s,"milestone",{"msg":"test"},session_id=sid)
|
|
118
|
+
r=AgentTrace.export_trace(s,sid,format="json")
|
|
119
|
+
assert r["success"] is True
|
|
120
|
+
assert r["format"]=="json"
|
|
121
|
+
assert isinstance(r["data"],str)
|
|
122
|
+
parsed=json.loads(r["data"])
|
|
123
|
+
assert parsed["total_events"]==1
|
|
124
|
+
|
|
125
|
+
class TestListSessions:
|
|
126
|
+
def test_list(self):
|
|
127
|
+
s=AgentTrace.create_store()
|
|
128
|
+
AgentTrace.start_trace(s,name="s1")
|
|
129
|
+
AgentTrace.start_trace(s,name="s2")
|
|
130
|
+
r=AgentTrace.list_sessions(s)
|
|
131
|
+
assert r["total"]==2
|
|
132
|
+
assert all("name" in sess for sess in r["sessions"])
|
|
133
|
+
|
|
134
|
+
class TestGetTimeline:
|
|
135
|
+
def test_basic(self):
|
|
136
|
+
s=AgentTrace.create_store()
|
|
137
|
+
sid=AgentTrace.start_trace(s)["session_id"]
|
|
138
|
+
AgentTrace.log_event(s,"tool_call",{"tool":"a"},session_id=sid)
|
|
139
|
+
AgentTrace.log_event(s,"decision",{"action":"b"},session_id=sid)
|
|
140
|
+
r=AgentTrace.get_timeline(s,sid)
|
|
141
|
+
assert r["count"]==2
|
|
142
|
+
assert r["timeline"][0]["t"]<=r["timeline"][1]["t"]
|
|
143
|
+
assert "type" in r["timeline"][0]
|
|
144
|
+
|
|
145
|
+
class TestAnnotate:
|
|
146
|
+
def test_basic(self):
|
|
147
|
+
s=AgentTrace.create_store()
|
|
148
|
+
sid=AgentTrace.start_trace(s)["session_id"]
|
|
149
|
+
eid=AgentTrace.log_event(s,"milestone",{"msg":"done"},session_id=sid)["event_id"]
|
|
150
|
+
r=AgentTrace.annotate(s,sid,eid,"This was important")
|
|
151
|
+
assert r["success"] is True
|
|
152
|
+
def test_event_not_found(self):
|
|
153
|
+
s=AgentTrace.create_store()
|
|
154
|
+
sid=AgentTrace.start_trace(s)["session_id"]
|
|
155
|
+
r=AgentTrace.annotate(s,sid,"nonexistent","note")
|
|
156
|
+
assert r["success"] is False
|
|
157
|
+
|
|
158
|
+
class TestStatsReset:
|
|
159
|
+
def test_stats(self):
|
|
160
|
+
s=AgentTrace.create_store()
|
|
161
|
+
AgentTrace.start_trace(s)
|
|
162
|
+
AgentTrace.log_event(s,"tool_call")
|
|
163
|
+
r=AgentTrace.get_stats(s)
|
|
164
|
+
assert r["sessions_started"]==1
|
|
165
|
+
assert r["events_logged"]==1
|
|
166
|
+
def test_reset(self):
|
|
167
|
+
s=AgentTrace.create_store()
|
|
168
|
+
AgentTrace.start_trace(s)
|
|
169
|
+
AgentTrace.log_event(s,"tool_call")
|
|
170
|
+
r=AgentTrace.reset(s)
|
|
171
|
+
assert r["reset"]["events_logged"]==1
|
|
172
|
+
assert AgentTrace.get_stats(s)["events_logged"]==0
|
|
173
|
+
assert AgentTrace.list_sessions(s)["total"]==0
|
|
174
|
+
|
|
175
|
+
class TestDispatch:
|
|
176
|
+
def test_unknown(self):
|
|
177
|
+
srv=MCPAgentTraceServer();assert "error" in json.loads(srv.handle_tool_call("nope",{}))
|
|
178
|
+
def test_missing(self):
|
|
179
|
+
srv=MCPAgentTraceServer();assert "error" in json.loads(srv.handle_tool_call("get_trace",{}))
|
|
180
|
+
def test_start_dispatch(self):
|
|
181
|
+
srv=MCPAgentTraceServer()
|
|
182
|
+
r=json.loads(srv.handle_tool_call("start_trace",{"name":"test"}))
|
|
183
|
+
assert r["success"] is True
|
|
184
|
+
|
|
185
|
+
class TestSTDIO:
|
|
186
|
+
def test_manifest_flag(self,capsys):
|
|
187
|
+
from src.server import main
|
|
188
|
+
with patch("sys.argv",["server","--manifest"]):main()
|
|
189
|
+
parsed=json.loads(capsys.readouterr().out.strip())
|
|
190
|
+
assert parsed["server"]["name"]=="mcp-agent-trace"
|