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
package/.env.example
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# mcp-agent-trace-inspector environment variables
|
|
2
|
+
# Copy this file to .env and fill in values as needed.
|
|
3
|
+
|
|
4
|
+
# API key for HTTP endpoint authentication (optional).
|
|
5
|
+
# When set, all HTTP requests must include the header: X-API-Key: <value>
|
|
6
|
+
# MCP_API_KEY=your-api-key-here
|
|
7
|
+
|
|
8
|
+
# JWT secret for Bearer token authentication (optional).
|
|
9
|
+
# When set, all HTTP requests must include: Authorization: Bearer <jwt>
|
|
10
|
+
# MCP_JWT_SECRET=your-jwt-secret-here
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to MCP Agent Trace Inspector will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [1.0.0] - 2026-03-23
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- `@modelcontextprotocol/sdk` upgraded from `^1.0.0` to `^1.12.0`.
|
|
15
|
+
- `@types/node` upgraded from `^22.x` to `^24.12.0` (Node 24 LTS).
|
|
16
|
+
- `eslint` upgraded from `^9.x` to `^10.0.3`; `eslint-config-prettier` from `^9.x` to `^10.1.8`.
|
|
17
|
+
- `yargs` upgraded from `^17.x` to `^18.0.0`.
|
|
18
|
+
- Added `author`, `repository`, and `homepage` fields to `package.json` for npm registry and marketplace metadata.
|
|
19
|
+
- Added `.env.example` documenting `MCP_API_KEY` and `MCP_JWT_SECRET`.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- Removed two unused function references (`checkAndAlert`, `loadAlertRules`) from `src/server.ts`.
|
|
24
|
+
- Prefixed unused test helper variables with `_` in `tests/auth.test.ts` and `tests/trace.test.ts` to satisfy `no-unused-vars` lint rule.
|
|
25
|
+
|
|
26
|
+
### Security
|
|
27
|
+
|
|
28
|
+
- Resolved **GHSA-67mh-4wv8-2f99** (`esbuild` ≤ 0.24.2 dev-server cross-origin exposure) by upgrading `vitest` and `@vitest/coverage-v8` to `^4.1.0`. Affects local development only; not a production runtime concern.
|
|
29
|
+
|
|
30
|
+
## [0.2.0] - 2026-03-23
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **Alerting** (`src/alerting.ts`): configurable alert rules on latency, error rate, and total cost. Fires to Slack webhooks or generic HTTP endpoints. Alert rules persist to SQLite between restarts.
|
|
35
|
+
- **Audit log** (`src/audit-log.ts`): append-only JSONL audit trail written to `~/.mcp/trace-inspector-audit.jsonl`; queryable by time range via `export_compliance_log`.
|
|
36
|
+
- **JWT / API-key auth middleware** (`src/auth.ts`): HTTP transport protected via `MCP_API_KEY` (X-API-Key header) or `MCP_JWT_SECRET` (HMAC-SHA256 Bearer token). stdio transport is unaffected.
|
|
37
|
+
- **Per-client rate limiter** (`src/rate-limiter.ts`): sliding-window request throttle on the HTTP transport.
|
|
38
|
+
- **OpenTelemetry exporter** (`src/otel-exporter.ts`): export traces to any OTLP-compatible backend (Jaeger, Grafana Tempo, etc.). Supports single-trace and all-traces export with `format=json`.
|
|
39
|
+
- **Retention policy** (`src/retention.ts`): archive or delete trace records older than a configurable number of days. Archives traces past the threshold; deletes archived traces past 2× the threshold.
|
|
40
|
+
- **New tools**: `configure_alerts`, `set_retention_policy`, `apply_retention`, `export_otel`, `export_compliance_log`.
|
|
41
|
+
- **`npm run inspect` script**: launches MCP Inspector (`npx @modelcontextprotocol/inspector node dist/index.js`) for interactive pre-publish verification.
|
|
42
|
+
- MCP Inspector verification instructions added to README.
|
|
43
|
+
- Tests for alerting, audit log, auth, OpenTelemetry exporter, rate limiter, and retention.
|
|
44
|
+
|
|
45
|
+
### Fixed
|
|
46
|
+
|
|
47
|
+
- `export_compliance_log` was always returning an empty array. The shared `AuditLog` instance was never written to because `trace_step` did not call `record()`. Fixed by creating a single `AuditLog` instance in `server.ts` and calling `_auditLog.record()` from the `trace_step` handler. Audit entries are now correctly appended to `~/.mcp/trace-inspector-audit.jsonl` on every recorded step.
|
|
48
|
+
- `apply_retention` now returns a clear error (`InvalidParams`) when called before `set_retention_policy` has been invoked in the current session, rather than silently operating on a null policy.
|
|
49
|
+
|
|
50
|
+
## [0.1.0] - 2026-03-12
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- Initial public release of `mcp-agent-trace-inspector`.
|
|
55
|
+
- `trace_start`, `trace_step`, and `trace_end` tools for full trace lifecycle management.
|
|
56
|
+
- `get_trace_summary`, `list_traces`, and `compare_traces` inspection tools.
|
|
57
|
+
- `export_dashboard` tool generating a self-contained single-file HTML dashboard.
|
|
58
|
+
- Persistent SQLite storage with configurable path via `--db`.
|
|
59
|
+
- Token cost estimation using a built-in model pricing table (no external API calls required).
|
|
60
|
+
- Automatic trace retention via `--retention-days`.
|
|
61
|
+
- Custom pricing table support via `--pricing-table`.
|
|
62
|
+
- `--no-token-count` flag to disable token counting.
|
|
63
|
+
- Streamable HTTP transport via `--http-port` flag (default: disabled, uses stdio).
|
|
64
|
+
- GitHub Actions CI workflow running build, test, and lint on push/PR to `main`.
|
|
65
|
+
- Vitest test suite with coverage via `@vitest/coverage-v8`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 MCP Agent Trace Inspector contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# MCP Agent Trace Inspector
|
|
2
|
+
|
|
3
|
+
npm `mcp-agent-trace-inspector` package
|
|
4
|
+
|
|
5
|
+
Local-first, MCP-native observability for agent workflows. Every tool call, prompt transformation, latency, and token count is recorded in a local SQLite database — no cloud account, no API key, no traces leaving your machine. Built specifically for MCP rather than bolted onto a generic LLM proxy.
|
|
6
|
+
|
|
7
|
+
[Tool reference](#tools) | [Configuration](#configuration) | [Contributing](#contributing) | [Troubleshooting](#troubleshooting) | [Design principles](#design-principles)
|
|
8
|
+
|
|
9
|
+
## Key features
|
|
10
|
+
|
|
11
|
+
- **Tool call tracing**: Captures inputs, outputs, latency, and token usage for every step in a workflow.
|
|
12
|
+
- **Persistent storage**: Traces survive session restarts; stored locally in SQLite with no external dependencies.
|
|
13
|
+
- **HTML dashboard**: Generates a self-contained single-file dashboard with an interactive step timeline.
|
|
14
|
+
- **Token cost estimation**: Calculates USD cost per trace using a configurable model pricing table — no API calls required.
|
|
15
|
+
- **Trace comparison**: Diff two traces side by side to measure the impact of prompt or tool changes.
|
|
16
|
+
- **Low overhead**: Adds less than 5ms per step; never becomes the bottleneck.
|
|
17
|
+
|
|
18
|
+
## Why this over LangSmith / AgentOps?
|
|
19
|
+
|
|
20
|
+
| | mcp-agent-trace-inspector | LangSmith / AgentOps |
|
|
21
|
+
| --------------- | ------------------------------------------------ | --------------------------------------------- |
|
|
22
|
+
| Data location | Local SQLite — never leaves your machine | Cloud-hosted; traces sent to external servers |
|
|
23
|
+
| Setup | `npx` one-liner, zero config | Account signup, API key, SDK instrumentation |
|
|
24
|
+
| MCP-aware | Native — records tool calls as first-class steps | Generic LLM proxy; MCP structure is opaque |
|
|
25
|
+
| Run diffs | Built-in `compare_traces` diff | Separate paid feature or manual export |
|
|
26
|
+
| Cost estimation | Offline tiktoken + configurable pricing table | Requires live API traffic through their proxy |
|
|
27
|
+
| Overhead | <5ms per step | Network round-trip per event |
|
|
28
|
+
|
|
29
|
+
If your traces contain sensitive tool outputs, proprietary prompts, or data that must stay on-device, this is the right tool. If you need cross-team trace sharing or a managed SaaS, use LangSmith.
|
|
30
|
+
|
|
31
|
+
## Disclaimers
|
|
32
|
+
|
|
33
|
+
`mcp-agent-trace-inspector` stores tool call inputs and outputs locally in a SQLite database. Traces may contain sensitive information passed to or returned from your tools. Review trace contents before sharing dashboard exports. Traces are not automatically transmitted; optional alert webhooks are available.
|
|
34
|
+
|
|
35
|
+
## Requirements
|
|
36
|
+
|
|
37
|
+
- Node.js v22.5.0 or newer.
|
|
38
|
+
- npm.
|
|
39
|
+
|
|
40
|
+
## Getting started
|
|
41
|
+
|
|
42
|
+
Add the following config to your MCP client:
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
{
|
|
46
|
+
"mcpServers": {
|
|
47
|
+
"trace-inspector": {
|
|
48
|
+
"command": "npx",
|
|
49
|
+
"args": ["-y", "mcp-agent-trace-inspector@latest"]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
To set a custom storage path:
|
|
56
|
+
|
|
57
|
+
```json
|
|
58
|
+
{
|
|
59
|
+
"mcpServers": {
|
|
60
|
+
"trace-inspector": {
|
|
61
|
+
"command": "npx",
|
|
62
|
+
"args": [
|
|
63
|
+
"-y",
|
|
64
|
+
"mcp-agent-trace-inspector@latest",
|
|
65
|
+
"--db=~/traces/my-project.db"
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### MCP Client configuration
|
|
73
|
+
|
|
74
|
+
Amp · Claude Code · Cline · Cursor · VS Code · Windsurf · Zed
|
|
75
|
+
|
|
76
|
+
## Your first prompt
|
|
77
|
+
|
|
78
|
+
Enter the following in your MCP client to verify everything is working:
|
|
79
|
+
|
|
80
|
+
```
|
|
81
|
+
Start a trace called "test-run", then list the files in the current directory, then end the trace and show me the summary.
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Your client should return a summary showing step count, total tokens, and latency.
|
|
85
|
+
|
|
86
|
+
## Tools
|
|
87
|
+
|
|
88
|
+
### Trace lifecycle (3 tools)
|
|
89
|
+
|
|
90
|
+
- `trace_start` — begin a new trace; returns a `trace_id` for subsequent calls
|
|
91
|
+
- `trace_step` — record one tool call step (inputs, outputs, optional token count and latency)
|
|
92
|
+
- `trace_end` — mark a trace as completed
|
|
93
|
+
|
|
94
|
+
### Inspection (4 tools)
|
|
95
|
+
|
|
96
|
+
- `list_traces` — list stored traces with names, statuses, and timestamps
|
|
97
|
+
- `get_trace_summary` — token totals, step count, latency, and cost estimate for a trace
|
|
98
|
+
- `compare_traces` — diff two traces side by side (step counts, tokens, latency)
|
|
99
|
+
- `extract_reasoning_chain` — extract only reasoning/thinking steps from a trace
|
|
100
|
+
|
|
101
|
+
### Export (3 tools)
|
|
102
|
+
|
|
103
|
+
- `export_dashboard` — generate a self-contained single-file HTML dashboard with latency waterfall
|
|
104
|
+
- `export_otel` — export one or all traces in OpenTelemetry OTLP JSON span format
|
|
105
|
+
- `export_compliance_log` — export the compliance audit log as JSON or CSV, with optional date range filtering
|
|
106
|
+
|
|
107
|
+
### Operations (3 tools)
|
|
108
|
+
|
|
109
|
+
- `configure_alerts` — configure alert rules on latency, error rate, or cost; fire to Slack or generic webhooks
|
|
110
|
+
- `set_retention_policy` — set how many days to keep traces (in-memory; must be called before `apply_retention`)
|
|
111
|
+
- `apply_retention` — archive traces older than the configured threshold; delete traces past 2x the threshold
|
|
112
|
+
|
|
113
|
+
## Configuration
|
|
114
|
+
|
|
115
|
+
### `--db` / `--db-path`
|
|
116
|
+
|
|
117
|
+
Path to the SQLite database file used to store traces.
|
|
118
|
+
|
|
119
|
+
Type: `string`
|
|
120
|
+
Default: `~/.mcp/traces.db`
|
|
121
|
+
|
|
122
|
+
### `--retention-days`
|
|
123
|
+
|
|
124
|
+
Automatically delete traces older than N days. Set to `0` to disable.
|
|
125
|
+
|
|
126
|
+
Type: `number`
|
|
127
|
+
Default: `0`
|
|
128
|
+
|
|
129
|
+
### `--pricing-table`
|
|
130
|
+
|
|
131
|
+
Path to a JSON file containing custom model pricing ($/1K tokens). Overrides the built-in table.
|
|
132
|
+
|
|
133
|
+
Type: `string`
|
|
134
|
+
|
|
135
|
+
### `--no-token-count`
|
|
136
|
+
|
|
137
|
+
Disable tiktoken-based token counting. Traces will omit token usage metrics.
|
|
138
|
+
|
|
139
|
+
Type: `boolean`
|
|
140
|
+
Default: `false`
|
|
141
|
+
|
|
142
|
+
Pass flags via the `args` property in your JSON config:
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"mcpServers": {
|
|
147
|
+
"trace-inspector": {
|
|
148
|
+
"command": "npx",
|
|
149
|
+
"args": ["-y", "mcp-agent-trace-inspector@latest", "--retention-days=30"]
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Design principles
|
|
156
|
+
|
|
157
|
+
- **Append-only traces**: Steps are immutable once recorded. Trust requires integrity.
|
|
158
|
+
- **Local-first**: All core functionality works without a network connection.
|
|
159
|
+
- **Portable dashboards**: HTML exports are always single-file; no server required to view them.
|
|
160
|
+
|
|
161
|
+
## Verification
|
|
162
|
+
|
|
163
|
+
Before publishing a new version, verify the server with MCP Inspector to confirm all tools are exposed correctly and the protocol handshake succeeds.
|
|
164
|
+
|
|
165
|
+
**Interactive UI** (opens browser):
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
npm run build && npm run inspect
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
**CLI mode** (scripted / CI-friendly):
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# List all tools
|
|
175
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js --method tools/list
|
|
176
|
+
|
|
177
|
+
# List resources and prompts
|
|
178
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js --method resources/list
|
|
179
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js --method prompts/list
|
|
180
|
+
|
|
181
|
+
# Call a tool (example — replace with a relevant read-only tool for this plugin)
|
|
182
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js \
|
|
183
|
+
--method tools/call --tool-name list_traces
|
|
184
|
+
|
|
185
|
+
# Call a tool with arguments
|
|
186
|
+
npx @modelcontextprotocol/inspector --cli node dist/index.js \
|
|
187
|
+
--method tools/call --tool-name list_traces --tool-arg key=value
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Run before publishing to catch regressions in tool registration and runtime startup.
|
|
191
|
+
|
|
192
|
+
## Contributing
|
|
193
|
+
|
|
194
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md) for full contribution guidelines.
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
npm install && npm test
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
## MCP Registry & Marketplace
|
|
201
|
+
|
|
202
|
+
This plugin is available on:
|
|
203
|
+
|
|
204
|
+
- [MCP Registry](https://registry.modelcontextprotocol.io)
|
|
205
|
+
- [MCP Marketplace](https://marketplace.modelcontextprotocol.io)
|
|
206
|
+
|
|
207
|
+
Search for `mcp-agent-trace-inspector`.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
export interface AlertChannels {
|
|
3
|
+
slackWebhook?: string;
|
|
4
|
+
genericWebhook?: string;
|
|
5
|
+
}
|
|
6
|
+
export interface AlertRule {
|
|
7
|
+
type: "latency" | "error_rate" | "cost";
|
|
8
|
+
threshold: number;
|
|
9
|
+
}
|
|
10
|
+
export interface AlertFired {
|
|
11
|
+
rule: AlertRule;
|
|
12
|
+
value: number;
|
|
13
|
+
message: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function checkAndAlert(db: DatabaseSync, rules: AlertRule[], channels: AlertChannels): Promise<AlertFired[]>;
|
|
16
|
+
export declare function initAlertRulesTable(db: DatabaseSync): void;
|
|
17
|
+
export declare function saveAlertRules(db: DatabaseSync, rules: AlertRule[]): void;
|
|
18
|
+
export declare function loadAlertRules(db: DatabaseSync): AlertRule[];
|
package/dist/alerting.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { request as httpsRequest } from "node:https";
|
|
2
|
+
import { request as httpRequest } from "node:http";
|
|
3
|
+
import { URL } from "node:url";
|
|
4
|
+
import { listTraces, getSteps } from "./db.js";
|
|
5
|
+
// Pricing per 1K tokens (input+output average) — rough estimate
|
|
6
|
+
const COST_PER_1K_TOKENS_USD = 0.003;
|
|
7
|
+
function computeAverageLatencyMs(db) {
|
|
8
|
+
const traces = listTraces(db);
|
|
9
|
+
if (traces.length === 0)
|
|
10
|
+
return 0;
|
|
11
|
+
let totalLatency = 0;
|
|
12
|
+
let count = 0;
|
|
13
|
+
for (const trace of traces) {
|
|
14
|
+
const steps = getSteps(db, trace.id);
|
|
15
|
+
for (const step of steps) {
|
|
16
|
+
if (step.latency_ms !== null && step.latency_ms !== undefined) {
|
|
17
|
+
totalLatency += step.latency_ms;
|
|
18
|
+
count++;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return count > 0 ? totalLatency / count : 0;
|
|
23
|
+
}
|
|
24
|
+
function computeErrorRatePct(db) {
|
|
25
|
+
const traces = listTraces(db);
|
|
26
|
+
if (traces.length === 0)
|
|
27
|
+
return 0;
|
|
28
|
+
let totalSteps = 0;
|
|
29
|
+
let errorSteps = 0;
|
|
30
|
+
for (const trace of traces) {
|
|
31
|
+
const steps = getSteps(db, trace.id);
|
|
32
|
+
for (const step of steps) {
|
|
33
|
+
totalSteps++;
|
|
34
|
+
try {
|
|
35
|
+
const output = JSON.parse(step.output_json);
|
|
36
|
+
if (output.error || output.isError === true) {
|
|
37
|
+
errorSteps++;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// ignore
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return totalSteps > 0 ? (errorSteps / totalSteps) * 100 : 0;
|
|
46
|
+
}
|
|
47
|
+
function computeTotalCostUsd(db) {
|
|
48
|
+
const traces = listTraces(db);
|
|
49
|
+
let totalTokens = 0;
|
|
50
|
+
for (const trace of traces) {
|
|
51
|
+
const steps = getSteps(db, trace.id);
|
|
52
|
+
for (const step of steps) {
|
|
53
|
+
if (step.token_count !== null && step.token_count !== undefined) {
|
|
54
|
+
totalTokens += step.token_count;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return (totalTokens / 1000) * COST_PER_1K_TOKENS_USD;
|
|
59
|
+
}
|
|
60
|
+
function postJson(url, body) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const parsed = new URL(url);
|
|
63
|
+
const data = JSON.stringify(body);
|
|
64
|
+
const isHttps = parsed.protocol === "https:";
|
|
65
|
+
const reqFn = isHttps ? httpsRequest : httpRequest;
|
|
66
|
+
const options = {
|
|
67
|
+
hostname: parsed.hostname,
|
|
68
|
+
port: parsed.port || (isHttps ? 443 : 80),
|
|
69
|
+
path: parsed.pathname + parsed.search,
|
|
70
|
+
method: "POST",
|
|
71
|
+
headers: {
|
|
72
|
+
"Content-Type": "application/json",
|
|
73
|
+
"Content-Length": Buffer.byteLength(data),
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
const req = reqFn(options, (res) => {
|
|
77
|
+
res.resume(); // drain response
|
|
78
|
+
res.on("end", resolve);
|
|
79
|
+
});
|
|
80
|
+
req.on("error", reject);
|
|
81
|
+
req.write(data);
|
|
82
|
+
req.end();
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
function buildSlackBlocks(alert) {
|
|
86
|
+
return {
|
|
87
|
+
blocks: [
|
|
88
|
+
{
|
|
89
|
+
type: "section",
|
|
90
|
+
text: {
|
|
91
|
+
type: "mrkdwn",
|
|
92
|
+
text: `*Alert Fired: ${alert.rule.type}*\n${alert.message}\nValue: ${alert.value.toFixed(2)}, Threshold: ${alert.rule.threshold}`,
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export async function checkAndAlert(db, rules, channels) {
|
|
99
|
+
const fired = [];
|
|
100
|
+
for (const rule of rules) {
|
|
101
|
+
let value = 0;
|
|
102
|
+
let message = "";
|
|
103
|
+
switch (rule.type) {
|
|
104
|
+
case "latency":
|
|
105
|
+
value = computeAverageLatencyMs(db);
|
|
106
|
+
message = `Average step latency ${value.toFixed(0)}ms exceeds threshold ${rule.threshold}ms`;
|
|
107
|
+
break;
|
|
108
|
+
case "error_rate":
|
|
109
|
+
value = computeErrorRatePct(db);
|
|
110
|
+
message = `Error rate ${value.toFixed(1)}% exceeds threshold ${rule.threshold}%`;
|
|
111
|
+
break;
|
|
112
|
+
case "cost":
|
|
113
|
+
value = computeTotalCostUsd(db);
|
|
114
|
+
message = `Total cost $${value.toFixed(4)} exceeds threshold $${rule.threshold}`;
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
if (value > rule.threshold) {
|
|
118
|
+
const alert = { rule, value, message };
|
|
119
|
+
fired.push(alert);
|
|
120
|
+
const sendPromises = [];
|
|
121
|
+
if (channels.slackWebhook) {
|
|
122
|
+
sendPromises.push(postJson(channels.slackWebhook, buildSlackBlocks(alert)).catch((err) => {
|
|
123
|
+
console.error("[alerting] Slack webhook failed:", err);
|
|
124
|
+
}));
|
|
125
|
+
}
|
|
126
|
+
if (channels.genericWebhook) {
|
|
127
|
+
sendPromises.push(postJson(channels.genericWebhook, {
|
|
128
|
+
alert_type: rule.type,
|
|
129
|
+
threshold: rule.threshold,
|
|
130
|
+
value,
|
|
131
|
+
message,
|
|
132
|
+
timestamp: new Date().toISOString(),
|
|
133
|
+
}).catch((err) => {
|
|
134
|
+
console.error("[alerting] Generic webhook failed:", err);
|
|
135
|
+
}));
|
|
136
|
+
}
|
|
137
|
+
await Promise.all(sendPromises);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return fired;
|
|
141
|
+
}
|
|
142
|
+
// Database helpers for persisting alert rules
|
|
143
|
+
export function initAlertRulesTable(db) {
|
|
144
|
+
db.exec(`
|
|
145
|
+
CREATE TABLE IF NOT EXISTS alert_rules (
|
|
146
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
147
|
+
type TEXT NOT NULL,
|
|
148
|
+
threshold REAL NOT NULL
|
|
149
|
+
);
|
|
150
|
+
`);
|
|
151
|
+
}
|
|
152
|
+
export function saveAlertRules(db, rules) {
|
|
153
|
+
initAlertRulesTable(db);
|
|
154
|
+
db.exec("DELETE FROM alert_rules");
|
|
155
|
+
const stmt = db.prepare("INSERT INTO alert_rules (type, threshold) VALUES (?, ?)");
|
|
156
|
+
for (const rule of rules) {
|
|
157
|
+
stmt.run(rule.type, rule.threshold);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
export function loadAlertRules(db) {
|
|
161
|
+
initAlertRulesTable(db);
|
|
162
|
+
const rows = db
|
|
163
|
+
.prepare("SELECT type, threshold FROM alert_rules")
|
|
164
|
+
.all();
|
|
165
|
+
return rows.map((r) => ({
|
|
166
|
+
type: r.type,
|
|
167
|
+
threshold: r.threshold,
|
|
168
|
+
}));
|
|
169
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface AuditEntry {
|
|
2
|
+
timestamp: string;
|
|
3
|
+
trace_id: string;
|
|
4
|
+
tool_name: string;
|
|
5
|
+
user_id: string;
|
|
6
|
+
token_count: number;
|
|
7
|
+
cost_usd: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class AuditLog {
|
|
10
|
+
private readonly filePath;
|
|
11
|
+
constructor(filePath?: string);
|
|
12
|
+
private ensureDir;
|
|
13
|
+
record(entry: AuditEntry): void;
|
|
14
|
+
export(from?: string, to?: string): AuditEntry[];
|
|
15
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { appendFileSync, readFileSync, mkdirSync, existsSync } from "node:fs";
|
|
2
|
+
import { join, dirname } from "node:path";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
const DEFAULT_AUDIT_PATH = join(homedir(), ".mcp", "trace-inspector-audit.jsonl");
|
|
5
|
+
export class AuditLog {
|
|
6
|
+
filePath;
|
|
7
|
+
constructor(filePath = DEFAULT_AUDIT_PATH) {
|
|
8
|
+
this.filePath = filePath;
|
|
9
|
+
this.ensureDir();
|
|
10
|
+
}
|
|
11
|
+
ensureDir() {
|
|
12
|
+
const dir = dirname(this.filePath);
|
|
13
|
+
if (!existsSync(dir)) {
|
|
14
|
+
mkdirSync(dir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
record(entry) {
|
|
18
|
+
this.ensureDir();
|
|
19
|
+
const line = JSON.stringify(entry) + "\n";
|
|
20
|
+
appendFileSync(this.filePath, line, "utf8");
|
|
21
|
+
}
|
|
22
|
+
export(from, to) {
|
|
23
|
+
if (!existsSync(this.filePath)) {
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
const raw = readFileSync(this.filePath, "utf8");
|
|
27
|
+
const lines = raw.split("\n").filter((l) => l.trim() !== "");
|
|
28
|
+
const entries = [];
|
|
29
|
+
for (const line of lines) {
|
|
30
|
+
try {
|
|
31
|
+
const entry = JSON.parse(line);
|
|
32
|
+
entries.push(entry);
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// skip malformed lines
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const fromDate = from ? new Date(from).getTime() : null;
|
|
39
|
+
const toDate = to ? new Date(to).getTime() : null;
|
|
40
|
+
return entries.filter((e) => {
|
|
41
|
+
const ts = new Date(e.timestamp).getTime();
|
|
42
|
+
if (fromDate !== null && ts < fromDate)
|
|
43
|
+
return false;
|
|
44
|
+
if (toDate !== null && ts > toDate)
|
|
45
|
+
return false;
|
|
46
|
+
return true;
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
package/dist/auth.d.ts
ADDED
package/dist/auth.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { createHmac } from "node:crypto";
|
|
2
|
+
function base64urlDecode(str) {
|
|
3
|
+
// Convert base64url to base64
|
|
4
|
+
const base64 = str.replace(/-/g, "+").replace(/_/g, "/");
|
|
5
|
+
const padded = base64 + "=".repeat((4 - (base64.length % 4)) % 4);
|
|
6
|
+
return Buffer.from(padded, "base64");
|
|
7
|
+
}
|
|
8
|
+
function verifyJwt(token, secret) {
|
|
9
|
+
const parts = token.split(".");
|
|
10
|
+
if (parts.length !== 3)
|
|
11
|
+
return false;
|
|
12
|
+
const [headerB64, payloadB64, signatureB64] = parts;
|
|
13
|
+
const signingInput = `${headerB64}.${payloadB64}`;
|
|
14
|
+
const expectedSig = createHmac("sha256", secret)
|
|
15
|
+
.update(signingInput)
|
|
16
|
+
.digest("base64url");
|
|
17
|
+
// Constant-time comparison
|
|
18
|
+
const expected = Buffer.from(expectedSig);
|
|
19
|
+
const actual = Buffer.from(signatureB64);
|
|
20
|
+
if (expected.length !== actual.length)
|
|
21
|
+
return false;
|
|
22
|
+
let diff = 0;
|
|
23
|
+
for (let i = 0; i < expected.length; i++) {
|
|
24
|
+
diff |= expected[i] ^ actual[i];
|
|
25
|
+
}
|
|
26
|
+
return diff === 0;
|
|
27
|
+
}
|
|
28
|
+
function verifyPayloadExpiry(token) {
|
|
29
|
+
const parts = token.split(".");
|
|
30
|
+
if (parts.length !== 3)
|
|
31
|
+
return false;
|
|
32
|
+
try {
|
|
33
|
+
const payloadJson = base64urlDecode(parts[1]).toString("utf8");
|
|
34
|
+
const payload = JSON.parse(payloadJson);
|
|
35
|
+
if (typeof payload.exp === "number" &&
|
|
36
|
+
payload.exp < Math.floor(Date.now() / 1000)) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// If payload can't be parsed, don't block — just ignore expiry check
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
export function createAuthMiddleware() {
|
|
46
|
+
return (req, res, next) => {
|
|
47
|
+
const apiKeyEnv = process.env.MCP_API_KEY;
|
|
48
|
+
const jwtSecretEnv = process.env.MCP_JWT_SECRET;
|
|
49
|
+
// If neither env var is set, pass through
|
|
50
|
+
if (!apiKeyEnv && !jwtSecretEnv) {
|
|
51
|
+
next();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Validate X-API-Key if MCP_API_KEY is set
|
|
55
|
+
if (apiKeyEnv) {
|
|
56
|
+
const providedKey = req.headers["x-api-key"];
|
|
57
|
+
if (typeof providedKey !== "string" || providedKey !== apiKeyEnv) {
|
|
58
|
+
res.status(401).json({ error: "Unauthorized: invalid API key" });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Validate Authorization: Bearer <token> if MCP_JWT_SECRET is set
|
|
63
|
+
if (jwtSecretEnv) {
|
|
64
|
+
const authHeader = req.headers["authorization"];
|
|
65
|
+
if (typeof authHeader !== "string" || !authHeader.startsWith("Bearer ")) {
|
|
66
|
+
res.status(401).json({
|
|
67
|
+
error: "Unauthorized: missing or malformed Authorization header",
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const token = authHeader.slice("Bearer ".length).trim();
|
|
72
|
+
if (!verifyJwt(token, jwtSecretEnv)) {
|
|
73
|
+
res.status(401).json({ error: "Unauthorized: invalid JWT signature" });
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!verifyPayloadExpiry(token)) {
|
|
77
|
+
res.status(401).json({ error: "Unauthorized: JWT has expired" });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
next();
|
|
82
|
+
};
|
|
83
|
+
}
|
package/dist/db.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
export type Db = DatabaseSync;
|
|
3
|
+
export interface TraceRow {
|
|
4
|
+
id: string;
|
|
5
|
+
name: string;
|
|
6
|
+
status: string;
|
|
7
|
+
started_at: number;
|
|
8
|
+
ended_at: number | null;
|
|
9
|
+
metadata: string | null;
|
|
10
|
+
}
|
|
11
|
+
export interface StepRow {
|
|
12
|
+
id: string;
|
|
13
|
+
trace_id: string;
|
|
14
|
+
tool_name: string;
|
|
15
|
+
input_json: string;
|
|
16
|
+
output_json: string;
|
|
17
|
+
token_count: number | null;
|
|
18
|
+
latency_ms: number | null;
|
|
19
|
+
created_at: number;
|
|
20
|
+
}
|
|
21
|
+
export declare function expandPath(p: string): string;
|
|
22
|
+
export declare function openDatabase(dbPath: string): DatabaseSync;
|
|
23
|
+
export declare function insertTrace(db: DatabaseSync, id: string, name: string): void;
|
|
24
|
+
export declare function endTrace(db: DatabaseSync, id: string): void;
|
|
25
|
+
export declare function insertStep(db: DatabaseSync, step: Omit<StepRow, "created_at">): void;
|
|
26
|
+
export declare function getTrace(db: DatabaseSync, id: string): TraceRow | undefined;
|
|
27
|
+
export declare function getSteps(db: DatabaseSync, traceId: string): StepRow[];
|
|
28
|
+
export declare function listTraces(db: DatabaseSync, limit?: number): TraceRow[];
|
|
29
|
+
export declare function deleteOldTraces(db: DatabaseSync, retentionDays: number): number;
|
|
30
|
+
export interface TraceSummary {
|
|
31
|
+
trace: TraceRow;
|
|
32
|
+
stepCount: number;
|
|
33
|
+
totalTokens: number;
|
|
34
|
+
totalLatencyMs: number;
|
|
35
|
+
steps: StepRow[];
|
|
36
|
+
}
|
|
37
|
+
export declare function computeSummary(db: DatabaseSync, traceId: string): TraceSummary | null;
|