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.
@@ -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,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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,5 @@
1
+ """mcp-agent-trace package — MCP server for agent loop observability."""
2
+ from .agent_trace_engine import AgentTrace
3
+ from .server import MCPAgentTraceServer, TOOL_DEFS
4
+ __all__ = ["AgentTrace", "MCPAgentTraceServer", "TOOL_DEFS"]
5
+ __version__ = "1.0.0"
@@ -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"