mcp-agent-trace-inspector 1.0.0
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.
- package/.env.example +10 -0
- package/CHANGELOG.md +65 -0
- package/LICENSE +21 -0
- package/README.md +207 -0
- package/dist/alerting.d.ts +18 -0
- package/dist/alerting.js +169 -0
- package/dist/audit-log.d.ts +15 -0
- package/dist/audit-log.js +49 -0
- package/dist/auth.d.ts +2 -0
- package/dist/auth.js +83 -0
- package/dist/db.d.ts +37 -0
- package/dist/db.js +107 -0
- package/dist/http-server.d.ts +1 -0
- package/dist/http-server.js +34 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +74 -0
- package/dist/otel-exporter.d.ts +20 -0
- package/dist/otel-exporter.js +98 -0
- package/dist/pricing.d.ts +10 -0
- package/dist/pricing.js +38 -0
- package/dist/prompts.d.ts +21 -0
- package/dist/prompts.js +66 -0
- package/dist/rate-limiter.d.ts +2 -0
- package/dist/rate-limiter.js +34 -0
- package/dist/resources.d.ts +16 -0
- package/dist/resources.js +55 -0
- package/dist/retention.d.ts +13 -0
- package/dist/retention.js +43 -0
- package/dist/server.d.ts +9 -0
- package/dist/server.js +673 -0
- package/dist/tools/compare.d.ts +11 -0
- package/dist/tools/compare.js +121 -0
- package/dist/tools/export.d.ts +11 -0
- package/dist/tools/export.js +373 -0
- package/dist/tools/inspect.d.ts +20 -0
- package/dist/tools/inspect.js +149 -0
- package/dist/tools/trace.d.ts +33 -0
- package/dist/tools/trace.js +146 -0
- package/package.json +62 -0
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { computeSummary } from "../db.js";
|
|
3
|
+
export function handleCompareTraces(db, args) {
|
|
4
|
+
const { trace_id_a, trace_id_b } = args;
|
|
5
|
+
if (!trace_id_a || typeof trace_id_a !== "string") {
|
|
6
|
+
throw new McpError(ErrorCode.InvalidParams, "trace_id_a must be a non-empty string");
|
|
7
|
+
}
|
|
8
|
+
if (!trace_id_b || typeof trace_id_b !== "string") {
|
|
9
|
+
throw new McpError(ErrorCode.InvalidParams, "trace_id_b must be a non-empty string");
|
|
10
|
+
}
|
|
11
|
+
try {
|
|
12
|
+
const summaryA = computeSummary(db, trace_id_a);
|
|
13
|
+
if (!summaryA) {
|
|
14
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown trace_id_a: ${trace_id_a}`);
|
|
15
|
+
}
|
|
16
|
+
const summaryB = computeSummary(db, trace_id_b);
|
|
17
|
+
if (!summaryB) {
|
|
18
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown trace_id_b: ${trace_id_b}`);
|
|
19
|
+
}
|
|
20
|
+
const stepCountDiff = summaryB.stepCount - summaryA.stepCount;
|
|
21
|
+
const tokenDiff = summaryB.totalTokens - summaryA.totalTokens;
|
|
22
|
+
const latencyDiff = summaryB.totalLatencyMs - summaryA.totalLatencyMs;
|
|
23
|
+
// Build tool name sets for step-by-step comparison
|
|
24
|
+
const toolsA = summaryA.steps.map((s) => s.tool_name);
|
|
25
|
+
const toolsB = summaryB.steps.map((s) => s.tool_name);
|
|
26
|
+
const setA = new Set(toolsA);
|
|
27
|
+
const setB = new Set(toolsB);
|
|
28
|
+
const onlyInA = toolsA.filter((t) => !setB.has(t));
|
|
29
|
+
const onlyInB = toolsB.filter((t) => !setA.has(t));
|
|
30
|
+
const inBoth = toolsA.filter((t) => setB.has(t));
|
|
31
|
+
// Per-position diff (for steps that exist in both by index)
|
|
32
|
+
const minLen = Math.min(summaryA.steps.length, summaryB.steps.length);
|
|
33
|
+
const stepDiffs = [];
|
|
34
|
+
for (let i = 0; i < minLen; i++) {
|
|
35
|
+
const sA = summaryA.steps[i];
|
|
36
|
+
const sB = summaryB.steps[i];
|
|
37
|
+
const sameTool = sA.tool_name === sB.tool_name;
|
|
38
|
+
const tDiff = sA.token_count != null && sB.token_count != null
|
|
39
|
+
? sB.token_count - sA.token_count
|
|
40
|
+
: null;
|
|
41
|
+
const lDiff = sA.latency_ms != null && sB.latency_ms != null
|
|
42
|
+
? sB.latency_ms - sA.latency_ms
|
|
43
|
+
: null;
|
|
44
|
+
stepDiffs.push({
|
|
45
|
+
index: i + 1,
|
|
46
|
+
tool_a: sA.tool_name,
|
|
47
|
+
tool_b: sB.tool_name,
|
|
48
|
+
same_tool: sameTool,
|
|
49
|
+
token_diff: tDiff,
|
|
50
|
+
latency_diff: lDiff,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
const sign = (n) => (n >= 0 ? `+${n}` : `${n}`);
|
|
54
|
+
const lines = [
|
|
55
|
+
`Trace Comparison`,
|
|
56
|
+
``,
|
|
57
|
+
` Trace A: ${summaryA.trace.name} (${trace_id_a})`,
|
|
58
|
+
` Trace B: ${summaryB.trace.name} (${trace_id_b})`,
|
|
59
|
+
``,
|
|
60
|
+
`Aggregate Diff (B - A):`,
|
|
61
|
+
` Steps: ${sign(stepCountDiff)} (A=${summaryA.stepCount}, B=${summaryB.stepCount})`,
|
|
62
|
+
` Tokens: ${sign(tokenDiff)} (A=${summaryA.totalTokens}, B=${summaryB.totalTokens})`,
|
|
63
|
+
` Latency: ${sign(latencyDiff)}ms (A=${summaryA.totalLatencyMs}ms, B=${summaryB.totalLatencyMs}ms)`,
|
|
64
|
+
``,
|
|
65
|
+
`Tool Coverage:`,
|
|
66
|
+
` In both: ${inBoth.length > 0 ? inBoth.join(", ") : "(none)"}`,
|
|
67
|
+
` Only in A: ${onlyInA.length > 0 ? onlyInA.join(", ") : "(none)"}`,
|
|
68
|
+
` Only in B: ${onlyInB.length > 0 ? onlyInB.join(", ") : "(none)"}`,
|
|
69
|
+
``,
|
|
70
|
+
`Step-by-Step (overlapping positions):`,
|
|
71
|
+
...stepDiffs.map((d) => {
|
|
72
|
+
const toolPart = d.same_tool
|
|
73
|
+
? ` ${d.index}. ${d.tool_a} [same]`
|
|
74
|
+
: ` ${d.index}. ${d.tool_a} → ${d.tool_b} [DIFFERENT]`;
|
|
75
|
+
const extras = [];
|
|
76
|
+
if (d.token_diff !== null)
|
|
77
|
+
extras.push(`tokens ${sign(d.token_diff)}`);
|
|
78
|
+
if (d.latency_diff !== null)
|
|
79
|
+
extras.push(`latency ${sign(d.latency_diff)}ms`);
|
|
80
|
+
return toolPart + (extras.length ? ` (${extras.join(", ")})` : "");
|
|
81
|
+
}),
|
|
82
|
+
];
|
|
83
|
+
const json = {
|
|
84
|
+
trace_a: {
|
|
85
|
+
id: trace_id_a,
|
|
86
|
+
name: summaryA.trace.name,
|
|
87
|
+
step_count: summaryA.stepCount,
|
|
88
|
+
total_tokens: summaryA.totalTokens,
|
|
89
|
+
total_latency_ms: summaryA.totalLatencyMs,
|
|
90
|
+
},
|
|
91
|
+
trace_b: {
|
|
92
|
+
id: trace_id_b,
|
|
93
|
+
name: summaryB.trace.name,
|
|
94
|
+
step_count: summaryB.stepCount,
|
|
95
|
+
total_tokens: summaryB.totalTokens,
|
|
96
|
+
total_latency_ms: summaryB.totalLatencyMs,
|
|
97
|
+
},
|
|
98
|
+
diff: {
|
|
99
|
+
step_count: stepCountDiff,
|
|
100
|
+
total_tokens: tokenDiff,
|
|
101
|
+
total_latency_ms: latencyDiff,
|
|
102
|
+
},
|
|
103
|
+
tools_only_in_a: onlyInA,
|
|
104
|
+
tools_only_in_b: onlyInB,
|
|
105
|
+
tools_in_both: inBoth,
|
|
106
|
+
step_diffs: stepDiffs,
|
|
107
|
+
};
|
|
108
|
+
return {
|
|
109
|
+
content: [
|
|
110
|
+
{ type: "text", text: lines.join("\n") },
|
|
111
|
+
{ type: "text", text: "\n\nJSON:\n" + JSON.stringify(json, null, 2) },
|
|
112
|
+
],
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
if (err instanceof McpError)
|
|
117
|
+
throw err;
|
|
118
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
119
|
+
throw new McpError(ErrorCode.InternalError, `Failed to compare traces: ${message}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
3
|
+
export interface ExportDashboardArgs {
|
|
4
|
+
trace_id: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function handleExportDashboard(db: DatabaseSync, args: ExportDashboardArgs, server?: Server): Promise<{
|
|
7
|
+
content: Array<{
|
|
8
|
+
type: string;
|
|
9
|
+
text: string;
|
|
10
|
+
}>;
|
|
11
|
+
}>;
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { computeSummary } from "../db.js";
|
|
3
|
+
function escapeHtml(str) {
|
|
4
|
+
return str
|
|
5
|
+
.replace(/&/g, "&")
|
|
6
|
+
.replace(/</g, "<")
|
|
7
|
+
.replace(/>/g, ">")
|
|
8
|
+
.replace(/"/g, """)
|
|
9
|
+
.replace(/'/g, "'");
|
|
10
|
+
}
|
|
11
|
+
function formatDuration(ms) {
|
|
12
|
+
if (ms < 1000)
|
|
13
|
+
return `${ms}ms`;
|
|
14
|
+
if (ms < 60000)
|
|
15
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
16
|
+
const minutes = Math.floor(ms / 60000);
|
|
17
|
+
const seconds = ((ms % 60000) / 1000).toFixed(1);
|
|
18
|
+
return `${minutes}m ${seconds}s`;
|
|
19
|
+
}
|
|
20
|
+
function isErrorOutput(outputJson) {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(outputJson);
|
|
23
|
+
if (typeof parsed !== "object" || parsed === null)
|
|
24
|
+
return false;
|
|
25
|
+
const obj = parsed;
|
|
26
|
+
if (obj.error !== undefined)
|
|
27
|
+
return true;
|
|
28
|
+
if (obj.isError === true)
|
|
29
|
+
return true;
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function handleExportDashboard(db, args, server) {
|
|
37
|
+
const { trace_id } = args;
|
|
38
|
+
if (!trace_id || typeof trace_id !== "string") {
|
|
39
|
+
throw new McpError(ErrorCode.InvalidParams, "trace_id must be a non-empty string");
|
|
40
|
+
}
|
|
41
|
+
// Emit progress: starting generation
|
|
42
|
+
if (server) {
|
|
43
|
+
await server.notification({
|
|
44
|
+
method: "notifications/progress",
|
|
45
|
+
params: {
|
|
46
|
+
progressToken: `export_${trace_id}`,
|
|
47
|
+
progress: 0,
|
|
48
|
+
total: 100,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
try {
|
|
53
|
+
const summary = computeSummary(db, trace_id);
|
|
54
|
+
if (!summary) {
|
|
55
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown trace_id: ${trace_id}`);
|
|
56
|
+
}
|
|
57
|
+
const { trace, stepCount, totalTokens, totalLatencyMs, steps } = summary;
|
|
58
|
+
const durationMs = trace.ended_at != null
|
|
59
|
+
? trace.ended_at - trace.started_at
|
|
60
|
+
: Date.now() - trace.started_at;
|
|
61
|
+
const statusColor = trace.status === "completed"
|
|
62
|
+
? "#22c55e"
|
|
63
|
+
: trace.status === "running"
|
|
64
|
+
? "#f59e0b"
|
|
65
|
+
: "#ef4444";
|
|
66
|
+
// Calculate max latency for waterfall proportions
|
|
67
|
+
const maxLatency = steps.reduce((max, s) => Math.max(max, s.latency_ms ?? 0), 0);
|
|
68
|
+
const stepsRows = steps
|
|
69
|
+
.map((s, i) => {
|
|
70
|
+
let inputStr = "";
|
|
71
|
+
let outputStr = "";
|
|
72
|
+
try {
|
|
73
|
+
inputStr = JSON.stringify(JSON.parse(s.input_json), null, 2);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
inputStr = s.input_json;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
outputStr = JSON.stringify(JSON.parse(s.output_json), null, 2);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
outputStr = s.output_json;
|
|
83
|
+
}
|
|
84
|
+
const latency = s.latency_ms != null ? formatDuration(s.latency_ms) : "—";
|
|
85
|
+
const tokens = s.token_count != null ? s.token_count.toString() : "—";
|
|
86
|
+
const ts = new Date(s.created_at).toISOString();
|
|
87
|
+
const hasError = isErrorOutput(s.output_json);
|
|
88
|
+
const rowStyle = hasError ? ' style="background:#3b0a0a;"' : "";
|
|
89
|
+
const errorBadge = hasError
|
|
90
|
+
? ' <span class="error-badge">ERROR</span>'
|
|
91
|
+
: "";
|
|
92
|
+
// Waterfall bar width as percentage
|
|
93
|
+
const waterfallWidth = maxLatency > 0 && s.latency_ms != null
|
|
94
|
+
? Math.max(1, Math.round((s.latency_ms / maxLatency) * 100))
|
|
95
|
+
: 0;
|
|
96
|
+
const waterfallBar = waterfallWidth > 0
|
|
97
|
+
? `<div class="waterfall-bar" style="width:${waterfallWidth}%;"></div>`
|
|
98
|
+
: `<div class="waterfall-empty">—</div>`;
|
|
99
|
+
return `
|
|
100
|
+
<tr${rowStyle}>
|
|
101
|
+
<td class="step-num">${i + 1}</td>
|
|
102
|
+
<td class="tool-name">${escapeHtml(s.tool_name)}${errorBadge}</td>
|
|
103
|
+
<td class="ts">${escapeHtml(ts)}</td>
|
|
104
|
+
<td class="latency">
|
|
105
|
+
${escapeHtml(latency)}
|
|
106
|
+
${waterfallBar}
|
|
107
|
+
</td>
|
|
108
|
+
<td class="tokens">${escapeHtml(tokens)}</td>
|
|
109
|
+
<td>
|
|
110
|
+
<details>
|
|
111
|
+
<summary>View</summary>
|
|
112
|
+
<pre class="json-block">${escapeHtml(inputStr)}</pre>
|
|
113
|
+
</details>
|
|
114
|
+
</td>
|
|
115
|
+
<td>
|
|
116
|
+
<details>
|
|
117
|
+
<summary>View</summary>
|
|
118
|
+
<pre class="json-block${hasError ? " error-output" : ""}">${escapeHtml(outputStr)}</pre>
|
|
119
|
+
</details>
|
|
120
|
+
</td>
|
|
121
|
+
</tr>`;
|
|
122
|
+
})
|
|
123
|
+
.join("\n");
|
|
124
|
+
const html = `<!DOCTYPE html>
|
|
125
|
+
<html lang="en">
|
|
126
|
+
<head>
|
|
127
|
+
<meta charset="UTF-8" />
|
|
128
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
129
|
+
<title>Trace Dashboard: ${escapeHtml(trace.name)}</title>
|
|
130
|
+
<style>
|
|
131
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
132
|
+
body {
|
|
133
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
134
|
+
background: #0f172a;
|
|
135
|
+
color: #e2e8f0;
|
|
136
|
+
line-height: 1.6;
|
|
137
|
+
padding: 2rem;
|
|
138
|
+
}
|
|
139
|
+
.container { max-width: 1200px; margin: 0 auto; }
|
|
140
|
+
header {
|
|
141
|
+
background: #1e293b;
|
|
142
|
+
border: 1px solid #334155;
|
|
143
|
+
border-radius: 12px;
|
|
144
|
+
padding: 1.5rem 2rem;
|
|
145
|
+
margin-bottom: 1.5rem;
|
|
146
|
+
}
|
|
147
|
+
header h1 {
|
|
148
|
+
font-size: 1.75rem;
|
|
149
|
+
font-weight: 700;
|
|
150
|
+
color: #f1f5f9;
|
|
151
|
+
margin-bottom: 0.5rem;
|
|
152
|
+
}
|
|
153
|
+
.badge {
|
|
154
|
+
display: inline-block;
|
|
155
|
+
padding: 0.2rem 0.75rem;
|
|
156
|
+
border-radius: 9999px;
|
|
157
|
+
font-size: 0.8rem;
|
|
158
|
+
font-weight: 600;
|
|
159
|
+
color: #0f172a;
|
|
160
|
+
background: ${statusColor};
|
|
161
|
+
margin-left: 0.5rem;
|
|
162
|
+
vertical-align: middle;
|
|
163
|
+
}
|
|
164
|
+
.error-badge {
|
|
165
|
+
display: inline-block;
|
|
166
|
+
padding: 0.1rem 0.4rem;
|
|
167
|
+
border-radius: 4px;
|
|
168
|
+
font-size: 0.7rem;
|
|
169
|
+
font-weight: 700;
|
|
170
|
+
color: #fff;
|
|
171
|
+
background: #ef4444;
|
|
172
|
+
margin-left: 0.4rem;
|
|
173
|
+
vertical-align: middle;
|
|
174
|
+
}
|
|
175
|
+
.meta {
|
|
176
|
+
display: grid;
|
|
177
|
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
178
|
+
gap: 0.5rem 1.5rem;
|
|
179
|
+
font-size: 0.875rem;
|
|
180
|
+
color: #94a3b8;
|
|
181
|
+
margin-top: 0.75rem;
|
|
182
|
+
}
|
|
183
|
+
.meta span strong { color: #cbd5e1; }
|
|
184
|
+
.stats-grid {
|
|
185
|
+
display: grid;
|
|
186
|
+
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
|
187
|
+
gap: 1rem;
|
|
188
|
+
margin-bottom: 1.5rem;
|
|
189
|
+
}
|
|
190
|
+
.stat-card {
|
|
191
|
+
background: #1e293b;
|
|
192
|
+
border: 1px solid #334155;
|
|
193
|
+
border-radius: 10px;
|
|
194
|
+
padding: 1rem 1.25rem;
|
|
195
|
+
text-align: center;
|
|
196
|
+
}
|
|
197
|
+
.stat-card .value {
|
|
198
|
+
font-size: 1.75rem;
|
|
199
|
+
font-weight: 700;
|
|
200
|
+
color: #60a5fa;
|
|
201
|
+
}
|
|
202
|
+
.stat-card .label {
|
|
203
|
+
font-size: 0.75rem;
|
|
204
|
+
color: #64748b;
|
|
205
|
+
text-transform: uppercase;
|
|
206
|
+
letter-spacing: 0.05em;
|
|
207
|
+
margin-top: 0.25rem;
|
|
208
|
+
}
|
|
209
|
+
.card {
|
|
210
|
+
background: #1e293b;
|
|
211
|
+
border: 1px solid #334155;
|
|
212
|
+
border-radius: 12px;
|
|
213
|
+
overflow: hidden;
|
|
214
|
+
margin-bottom: 1.5rem;
|
|
215
|
+
}
|
|
216
|
+
.card-header {
|
|
217
|
+
padding: 1rem 1.5rem;
|
|
218
|
+
border-bottom: 1px solid #334155;
|
|
219
|
+
font-weight: 600;
|
|
220
|
+
font-size: 1rem;
|
|
221
|
+
color: #f1f5f9;
|
|
222
|
+
}
|
|
223
|
+
table {
|
|
224
|
+
width: 100%;
|
|
225
|
+
border-collapse: collapse;
|
|
226
|
+
font-size: 0.875rem;
|
|
227
|
+
}
|
|
228
|
+
th {
|
|
229
|
+
background: #0f172a;
|
|
230
|
+
padding: 0.75rem 1rem;
|
|
231
|
+
text-align: left;
|
|
232
|
+
font-size: 0.75rem;
|
|
233
|
+
text-transform: uppercase;
|
|
234
|
+
letter-spacing: 0.05em;
|
|
235
|
+
color: #64748b;
|
|
236
|
+
font-weight: 600;
|
|
237
|
+
}
|
|
238
|
+
td {
|
|
239
|
+
padding: 0.75rem 1rem;
|
|
240
|
+
border-top: 1px solid #1e293b;
|
|
241
|
+
vertical-align: top;
|
|
242
|
+
color: #cbd5e1;
|
|
243
|
+
}
|
|
244
|
+
tr:hover td { background: #162032; }
|
|
245
|
+
tr[style*="background:#3b0a0a"] td { background: #3b0a0a; }
|
|
246
|
+
tr[style*="background:#3b0a0a"]:hover td { background: #4c1010; }
|
|
247
|
+
.step-num { color: #64748b; font-weight: 600; width: 3rem; }
|
|
248
|
+
.tool-name { font-family: monospace; color: #7dd3fc; font-weight: 600; }
|
|
249
|
+
.ts { color: #64748b; font-size: 0.8rem; white-space: nowrap; }
|
|
250
|
+
.latency { color: #86efac; font-family: monospace; }
|
|
251
|
+
.tokens { color: #fcd34d; font-family: monospace; }
|
|
252
|
+
.waterfall-bar {
|
|
253
|
+
height: 6px;
|
|
254
|
+
background: #60a5fa;
|
|
255
|
+
border-radius: 3px;
|
|
256
|
+
margin-top: 4px;
|
|
257
|
+
min-width: 2px;
|
|
258
|
+
transition: width 0.3s ease;
|
|
259
|
+
}
|
|
260
|
+
.waterfall-empty { color: #64748b; font-size: 0.75rem; margin-top: 4px; }
|
|
261
|
+
details summary {
|
|
262
|
+
cursor: pointer;
|
|
263
|
+
color: #60a5fa;
|
|
264
|
+
font-size: 0.8rem;
|
|
265
|
+
user-select: none;
|
|
266
|
+
}
|
|
267
|
+
details summary:hover { color: #93c5fd; }
|
|
268
|
+
.json-block {
|
|
269
|
+
margin-top: 0.5rem;
|
|
270
|
+
background: #0f172a;
|
|
271
|
+
border: 1px solid #334155;
|
|
272
|
+
border-radius: 6px;
|
|
273
|
+
padding: 0.75rem;
|
|
274
|
+
font-family: "Fira Code", "Cascadia Code", monospace;
|
|
275
|
+
font-size: 0.75rem;
|
|
276
|
+
color: #94a3b8;
|
|
277
|
+
overflow-x: auto;
|
|
278
|
+
white-space: pre;
|
|
279
|
+
max-height: 300px;
|
|
280
|
+
overflow-y: auto;
|
|
281
|
+
}
|
|
282
|
+
.json-block.error-output {
|
|
283
|
+
border-color: #ef4444;
|
|
284
|
+
background: #1a0505;
|
|
285
|
+
color: #fca5a5;
|
|
286
|
+
}
|
|
287
|
+
footer {
|
|
288
|
+
text-align: center;
|
|
289
|
+
font-size: 0.75rem;
|
|
290
|
+
color: #334155;
|
|
291
|
+
margin-top: 2rem;
|
|
292
|
+
}
|
|
293
|
+
</style>
|
|
294
|
+
</head>
|
|
295
|
+
<body>
|
|
296
|
+
<div class="container">
|
|
297
|
+
<header>
|
|
298
|
+
<h1>${escapeHtml(trace.name)}<span class="badge">${escapeHtml(trace.status)}</span></h1>
|
|
299
|
+
<div class="meta">
|
|
300
|
+
<span><strong>Trace ID:</strong> ${escapeHtml(trace.id)}</span>
|
|
301
|
+
<span><strong>Started:</strong> ${escapeHtml(new Date(trace.started_at).toISOString())}</span>
|
|
302
|
+
<span><strong>Ended:</strong> ${trace.ended_at ? escapeHtml(new Date(trace.ended_at).toISOString()) : "—"}</span>
|
|
303
|
+
<span><strong>Duration:</strong> ${escapeHtml(formatDuration(durationMs))}</span>
|
|
304
|
+
</div>
|
|
305
|
+
</header>
|
|
306
|
+
|
|
307
|
+
<div class="stats-grid">
|
|
308
|
+
<div class="stat-card">
|
|
309
|
+
<div class="value">${stepCount}</div>
|
|
310
|
+
<div class="label">Total Steps</div>
|
|
311
|
+
</div>
|
|
312
|
+
<div class="stat-card">
|
|
313
|
+
<div class="value">${totalTokens.toLocaleString()}</div>
|
|
314
|
+
<div class="label">Total Tokens</div>
|
|
315
|
+
</div>
|
|
316
|
+
<div class="stat-card">
|
|
317
|
+
<div class="value">${escapeHtml(formatDuration(totalLatencyMs))}</div>
|
|
318
|
+
<div class="label">Total Latency</div>
|
|
319
|
+
</div>
|
|
320
|
+
<div class="stat-card">
|
|
321
|
+
<div class="value">${escapeHtml(formatDuration(durationMs))}</div>
|
|
322
|
+
<div class="label">Wall Duration</div>
|
|
323
|
+
</div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<div class="card">
|
|
327
|
+
<div class="card-header">Step Timeline</div>
|
|
328
|
+
<table>
|
|
329
|
+
<thead>
|
|
330
|
+
<tr>
|
|
331
|
+
<th>#</th>
|
|
332
|
+
<th>Tool</th>
|
|
333
|
+
<th>Timestamp</th>
|
|
334
|
+
<th>Latency / Waterfall</th>
|
|
335
|
+
<th>Tokens</th>
|
|
336
|
+
<th>Input</th>
|
|
337
|
+
<th>Output</th>
|
|
338
|
+
</tr>
|
|
339
|
+
</thead>
|
|
340
|
+
<tbody>
|
|
341
|
+
${stepsRows || '<tr><td colspan="7" style="text-align:center;color:#64748b;padding:2rem;">No steps recorded</td></tr>'}
|
|
342
|
+
</tbody>
|
|
343
|
+
</table>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
<footer>
|
|
347
|
+
Generated by mcp-agent-trace-inspector • ${new Date().toISOString()}
|
|
348
|
+
</footer>
|
|
349
|
+
</div>
|
|
350
|
+
</body>
|
|
351
|
+
</html>`;
|
|
352
|
+
// Emit progress: done
|
|
353
|
+
if (server) {
|
|
354
|
+
await server.notification({
|
|
355
|
+
method: "notifications/progress",
|
|
356
|
+
params: {
|
|
357
|
+
progressToken: `export_${trace_id}`,
|
|
358
|
+
progress: 100,
|
|
359
|
+
total: 100,
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
content: [{ type: "text", text: html }],
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
catch (err) {
|
|
368
|
+
if (err instanceof McpError)
|
|
369
|
+
throw err;
|
|
370
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
371
|
+
throw new McpError(ErrorCode.InternalError, `Failed to export dashboard: ${message}`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
export interface GetTraceSummaryArgs {
|
|
3
|
+
trace_id: string;
|
|
4
|
+
model?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface ListTracesArgs {
|
|
7
|
+
limit?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare function handleGetTraceSummary(db: DatabaseSync, args: GetTraceSummaryArgs): {
|
|
10
|
+
content: Array<{
|
|
11
|
+
type: string;
|
|
12
|
+
text: string;
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
15
|
+
export declare function handleListTraces(db: DatabaseSync, args: ListTracesArgs): {
|
|
16
|
+
content: Array<{
|
|
17
|
+
type: string;
|
|
18
|
+
text: string;
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { computeSummary, listTraces } from "../db.js";
|
|
3
|
+
import { DEFAULT_PRICING, estimateCost } from "../pricing.js";
|
|
4
|
+
/**
|
|
5
|
+
* Detect prompt→reasoning→action patterns in step tool names.
|
|
6
|
+
* Returns true if the sequence contains at least one instance of the pattern.
|
|
7
|
+
*/
|
|
8
|
+
function detectReasoningChain(toolNames) {
|
|
9
|
+
const PROMPT_PATTERNS = /prompt|input|query|ask|request/i;
|
|
10
|
+
const REASONING_PATTERNS = /reason|think|plan|reflect|analyz|consider/i;
|
|
11
|
+
const ACTION_PATTERNS = /action|execute|run|call|invoke|output|respond|answer/i;
|
|
12
|
+
const patterns = [];
|
|
13
|
+
for (let i = 0; i < toolNames.length - 2; i++) {
|
|
14
|
+
const a = toolNames[i];
|
|
15
|
+
const b = toolNames[i + 1];
|
|
16
|
+
const c = toolNames[i + 2];
|
|
17
|
+
if (PROMPT_PATTERNS.test(a) &&
|
|
18
|
+
REASONING_PATTERNS.test(b) &&
|
|
19
|
+
ACTION_PATTERNS.test(c)) {
|
|
20
|
+
patterns.push(`${a} → ${b} → ${c}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
// Also flag standalone reasoning steps even without the full triple
|
|
24
|
+
const standaloneReasoning = toolNames.filter((t) => REASONING_PATTERNS.test(t));
|
|
25
|
+
return {
|
|
26
|
+
detected: patterns.length > 0 || standaloneReasoning.length > 0,
|
|
27
|
+
patterns: patterns.length > 0 ? patterns : standaloneReasoning.map((t) => t),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export function handleGetTraceSummary(db, args) {
|
|
31
|
+
const { trace_id, model } = args;
|
|
32
|
+
if (!trace_id || typeof trace_id !== "string") {
|
|
33
|
+
throw new McpError(ErrorCode.InvalidParams, "trace_id must be a non-empty string");
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const summary = computeSummary(db, trace_id);
|
|
37
|
+
if (!summary) {
|
|
38
|
+
throw new McpError(ErrorCode.InvalidParams, `Unknown trace_id: ${trace_id}`);
|
|
39
|
+
}
|
|
40
|
+
const { trace, stepCount, totalTokens, totalLatencyMs, steps } = summary;
|
|
41
|
+
const durationMs = trace.ended_at != null
|
|
42
|
+
? trace.ended_at - trace.started_at
|
|
43
|
+
: Date.now() - trace.started_at;
|
|
44
|
+
// Cost estimation
|
|
45
|
+
const pricingModel = model ?? "claude-sonnet-4-6";
|
|
46
|
+
const estimatedCost = totalTokens > 0
|
|
47
|
+
? estimateCost(totalTokens, pricingModel, DEFAULT_PRICING)
|
|
48
|
+
: null;
|
|
49
|
+
// Reasoning chain detection
|
|
50
|
+
const toolNames = steps.map((s) => s.tool_name);
|
|
51
|
+
const reasoningResult = detectReasoningChain(toolNames);
|
|
52
|
+
const costLine = estimatedCost !== null
|
|
53
|
+
? ` Est. Cost: $${estimatedCost.toFixed(6)} (model: ${pricingModel})`
|
|
54
|
+
: ` Est. Cost: N/A (model "${pricingModel}" not in pricing table)`;
|
|
55
|
+
const reasoningLine = reasoningResult.detected
|
|
56
|
+
? ` Reasoning: DETECTED — ${reasoningResult.patterns.join("; ")}`
|
|
57
|
+
: ` Reasoning: No prompt→reasoning→action pattern detected`;
|
|
58
|
+
const text = [
|
|
59
|
+
`Trace Summary: ${trace.name}`,
|
|
60
|
+
` ID: ${trace.id}`,
|
|
61
|
+
` Status: ${trace.status}`,
|
|
62
|
+
` Started: ${new Date(trace.started_at).toISOString()}`,
|
|
63
|
+
` Ended: ${trace.ended_at ? new Date(trace.ended_at).toISOString() : "—"}`,
|
|
64
|
+
` Duration: ${durationMs}ms`,
|
|
65
|
+
` Steps: ${stepCount}`,
|
|
66
|
+
` Total Tokens: ${totalTokens}`,
|
|
67
|
+
` Total Latency: ${totalLatencyMs}ms`,
|
|
68
|
+
costLine,
|
|
69
|
+
reasoningLine,
|
|
70
|
+
"",
|
|
71
|
+
"Steps:",
|
|
72
|
+
...steps.map((s, i) => ` ${i + 1}. ${s.tool_name}` +
|
|
73
|
+
(s.latency_ms != null ? ` [${s.latency_ms}ms]` : "") +
|
|
74
|
+
(s.token_count != null ? ` [${s.token_count} tokens]` : "")),
|
|
75
|
+
].join("\n");
|
|
76
|
+
const json = {
|
|
77
|
+
trace_id: trace.id,
|
|
78
|
+
name: trace.name,
|
|
79
|
+
status: trace.status,
|
|
80
|
+
started_at: trace.started_at,
|
|
81
|
+
ended_at: trace.ended_at,
|
|
82
|
+
duration_ms: durationMs,
|
|
83
|
+
step_count: stepCount,
|
|
84
|
+
total_tokens: totalTokens,
|
|
85
|
+
total_latency_ms: totalLatencyMs,
|
|
86
|
+
estimated_cost_usd: estimatedCost,
|
|
87
|
+
cost_model: pricingModel,
|
|
88
|
+
reasoning_chain_detected: reasoningResult.detected,
|
|
89
|
+
reasoning_patterns: reasoningResult.patterns,
|
|
90
|
+
steps: steps.map((s) => ({
|
|
91
|
+
id: s.id,
|
|
92
|
+
tool_name: s.tool_name,
|
|
93
|
+
token_count: s.token_count,
|
|
94
|
+
latency_ms: s.latency_ms,
|
|
95
|
+
created_at: s.created_at,
|
|
96
|
+
})),
|
|
97
|
+
};
|
|
98
|
+
return {
|
|
99
|
+
content: [
|
|
100
|
+
{ type: "text", text },
|
|
101
|
+
{ type: "text", text: "\n\nJSON:\n" + JSON.stringify(json, null, 2) },
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
if (err instanceof McpError)
|
|
107
|
+
throw err;
|
|
108
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
109
|
+
throw new McpError(ErrorCode.InternalError, `Failed to get trace summary: ${message}`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export function handleListTraces(db, args) {
|
|
113
|
+
const { limit } = args;
|
|
114
|
+
if (limit !== undefined && (typeof limit !== "number" || limit < 1)) {
|
|
115
|
+
throw new McpError(ErrorCode.InvalidParams, "limit must be a positive integer");
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const traces = listTraces(db, limit);
|
|
119
|
+
const lines = [
|
|
120
|
+
`Found ${traces.length} trace(s):`,
|
|
121
|
+
...traces.map((t) => {
|
|
122
|
+
const started = new Date(t.started_at).toISOString();
|
|
123
|
+
const ended = t.ended_at
|
|
124
|
+
? new Date(t.ended_at).toISOString()
|
|
125
|
+
: "running";
|
|
126
|
+
return ` [${t.status.toUpperCase()}] ${t.name} (${t.id})\n Started: ${started} Ended: ${ended}`;
|
|
127
|
+
}),
|
|
128
|
+
];
|
|
129
|
+
const json = traces.map((t) => ({
|
|
130
|
+
id: t.id,
|
|
131
|
+
name: t.name,
|
|
132
|
+
status: t.status,
|
|
133
|
+
started_at: t.started_at,
|
|
134
|
+
ended_at: t.ended_at,
|
|
135
|
+
}));
|
|
136
|
+
return {
|
|
137
|
+
content: [
|
|
138
|
+
{ type: "text", text: lines.join("\n") },
|
|
139
|
+
{ type: "text", text: "\n\nJSON:\n" + JSON.stringify(json, null, 2) },
|
|
140
|
+
],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
if (err instanceof McpError)
|
|
145
|
+
throw err;
|
|
146
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
147
|
+
throw new McpError(ErrorCode.InternalError, `Failed to list traces: ${message}`);
|
|
148
|
+
}
|
|
149
|
+
}
|