@cardor/heimdall-mcp 0.1.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/README.md +391 -0
- package/bin/heimdall-mcp.js +2 -0
- package/dist/MySqlStore-VOXUWDAJ.js +111 -0
- package/dist/MySqlStore-VOXUWDAJ.js.map +1 -0
- package/dist/PostgresStore-QUSLDMPH.js +111 -0
- package/dist/PostgresStore-QUSLDMPH.js.map +1 -0
- package/dist/SqliteStore-HMGNDB3O.js +109 -0
- package/dist/SqliteStore-HMGNDB3O.js.map +1 -0
- package/dist/chunk-VOBMDW4J.js +660 -0
- package/dist/chunk-VOBMDW4J.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +42 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +147 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
# heimdall-mcp
|
|
2
|
+
|
|
3
|
+
Transparent proxy for any MCP server. Intercepts all JSON-RPC messages, measures latency, and stores traces in a configurable database — without touching the original server.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [How it works](#how-it-works)
|
|
8
|
+
- [Installation](#installation)
|
|
9
|
+
- [Usage modes](#usage-modes)
|
|
10
|
+
- [Mode 1 — CLI wrapping a subprocess (stdio)](#mode-1--cli-wrapping-a-subprocess-stdio)
|
|
11
|
+
- [Mode 2 — CLI wrapping a remote HTTP server](#mode-2--cli-wrapping-a-remote-http-server)
|
|
12
|
+
- [Mode 3 — CLI wrapping a remote SSE server](#mode-3--cli-wrapping-a-remote-sse-server)
|
|
13
|
+
- [Mode 4 — Library for developers](#mode-4--library-for-developers)
|
|
14
|
+
- [Stores](#stores)
|
|
15
|
+
- [SQLite](#sqlite)
|
|
16
|
+
- [PostgreSQL](#postgresql)
|
|
17
|
+
- [MySQL](#mysql)
|
|
18
|
+
- [What gets recorded](#what-gets-recorded)
|
|
19
|
+
- [Custom interceptors](#custom-interceptors)
|
|
20
|
+
- [CLI reference](#cli-reference)
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## How it works
|
|
25
|
+
|
|
26
|
+
```mermaid
|
|
27
|
+
flowchart LR
|
|
28
|
+
A["MCP Client\n(Claude Desktop / OpenCode / Cursor)"]
|
|
29
|
+
|
|
30
|
+
subgraph proxy["heimdall-mcp"]
|
|
31
|
+
B["TelemetryInterceptor"]
|
|
32
|
+
C["ForwardInterceptor"]
|
|
33
|
+
D[("SQLite\nPostgres\nMySQL")]
|
|
34
|
+
B --> C
|
|
35
|
+
B -->|"saves span"| D
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
S["Real MCP server\n(subprocess / HTTP / SSE)"]
|
|
39
|
+
|
|
40
|
+
A -->|"stdio"| B
|
|
41
|
+
C -->|"stdio · http · sse"| S
|
|
42
|
+
S -->|"response"| C
|
|
43
|
+
C -->|"response"| A
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The proxy always exposes **stdio** to the MCP client and speaks the correct transport to the real server. Every request/response pair is converted into a span with timing, attributes, and the input/output body.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
npm install -g @cardor/heimdall-mcp
|
|
54
|
+
# or as a project dependency
|
|
55
|
+
npm install @cardor/heimdall-mcp
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Usage modes
|
|
61
|
+
|
|
62
|
+
### Mode 1 — CLI wrapping a subprocess (stdio)
|
|
63
|
+
|
|
64
|
+
The MCP client thinks it is talking to `heimdall-mcp`. The proxy spawns the real server as a child process and forwards all messages.
|
|
65
|
+
|
|
66
|
+
**`mcp.json` / Claude Desktop configuration:**
|
|
67
|
+
|
|
68
|
+
```json
|
|
69
|
+
{
|
|
70
|
+
"mcpServers": {
|
|
71
|
+
"my-server": {
|
|
72
|
+
"command": "heimdall-mcp",
|
|
73
|
+
"args": [
|
|
74
|
+
"--store", "sqlite://~/.mcp-traces/traces.db",
|
|
75
|
+
"--", "node", "my-server.js"
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The `--` separator divides heimdall-mcp flags from the real server command. Everything after it is executed as a subprocess.
|
|
83
|
+
|
|
84
|
+
**With a globally installed server:**
|
|
85
|
+
|
|
86
|
+
```json
|
|
87
|
+
{
|
|
88
|
+
"mcpServers": {
|
|
89
|
+
"filesystem": {
|
|
90
|
+
"command": "heimdall-mcp",
|
|
91
|
+
"args": [
|
|
92
|
+
"--store", "sqlite://~/.mcp-traces/traces.db",
|
|
93
|
+
"--", "npx", "@modelcontextprotocol/server-filesystem", "/tmp"
|
|
94
|
+
]
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**With Postgres instead of SQLite:**
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"mcpServers": {
|
|
105
|
+
"my-server": {
|
|
106
|
+
"command": "heimdall-mcp",
|
|
107
|
+
"args": [
|
|
108
|
+
"--store", "postgres://user:pass@localhost:5432/traces",
|
|
109
|
+
"--", "node", "my-server.js"
|
|
110
|
+
]
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### Mode 2 — CLI wrapping a remote HTTP server
|
|
119
|
+
|
|
120
|
+
When the MCP server is already running and exposes an HTTP endpoint.
|
|
121
|
+
|
|
122
|
+
```json
|
|
123
|
+
{
|
|
124
|
+
"mcpServers": {
|
|
125
|
+
"remote-server": {
|
|
126
|
+
"command": "heimdall-mcp",
|
|
127
|
+
"args": [
|
|
128
|
+
"--store", "sqlite://~/.mcp-traces/traces.db",
|
|
129
|
+
"--out", "http",
|
|
130
|
+
"--target", "http://localhost:3001"
|
|
131
|
+
]
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The proxy exposes **stdio** to the client and forwards each message as an HTTP `POST` to the target URL.
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
### Mode 3 — CLI wrapping a remote SSE server
|
|
142
|
+
|
|
143
|
+
For servers that use Server-Sent Events.
|
|
144
|
+
|
|
145
|
+
```json
|
|
146
|
+
{
|
|
147
|
+
"mcpServers": {
|
|
148
|
+
"sse-server": {
|
|
149
|
+
"command": "heimdall-mcp",
|
|
150
|
+
"args": [
|
|
151
|
+
"--store", "postgres://user:pass@host/db",
|
|
152
|
+
"--out", "sse",
|
|
153
|
+
"--target", "http://remote.example.com"
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
The proxy connects to `{target}/sse` to receive responses and sends requests as `POST` to `{target}`.
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
### Mode 4 — Library for developers
|
|
165
|
+
|
|
166
|
+
When you have access to the source code and want to integrate the proxy programmatically.
|
|
167
|
+
|
|
168
|
+
**Minimal setup:**
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
import { ProxyBuilder } from '@cardor/heimdall-mcp'
|
|
172
|
+
|
|
173
|
+
const proxy = await ProxyBuilder.create()
|
|
174
|
+
.inbound({ transport: 'stdio' })
|
|
175
|
+
.outbound({ transport: 'stdio', command: 'node', args: ['my-server.js'] })
|
|
176
|
+
.store('sqlite://./traces.db')
|
|
177
|
+
.build()
|
|
178
|
+
|
|
179
|
+
await proxy.start()
|
|
180
|
+
|
|
181
|
+
// clean shutdown
|
|
182
|
+
process.on('SIGINT', () => proxy.stop())
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**stdio → remote HTTP:**
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
const proxy = await ProxyBuilder.create()
|
|
189
|
+
.inbound({ transport: 'stdio' })
|
|
190
|
+
.outbound({ transport: 'http', url: 'http://localhost:3001' })
|
|
191
|
+
.store('postgres://user:pass@localhost/traces')
|
|
192
|
+
.build()
|
|
193
|
+
|
|
194
|
+
await proxy.start()
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
**HTTP inbound (proxy listens on a port):**
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
const proxy = await ProxyBuilder.create()
|
|
201
|
+
.inbound({ transport: 'http', port: 8080 })
|
|
202
|
+
.outbound({ transport: 'stdio', command: 'node', args: ['server.js'] })
|
|
203
|
+
.store('mysql://user:pass@localhost/traces')
|
|
204
|
+
.build()
|
|
205
|
+
|
|
206
|
+
await proxy.start()
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**With a custom interceptor:**
|
|
210
|
+
|
|
211
|
+
```ts
|
|
212
|
+
import type { Interceptor, InterceptorContext, JsonRpcMessage } from '@cardor/heimdall-mcp'
|
|
213
|
+
|
|
214
|
+
class LogAllInterceptor implements Interceptor {
|
|
215
|
+
name = 'LogAllInterceptor'
|
|
216
|
+
|
|
217
|
+
async intercept(
|
|
218
|
+
request: JsonRpcMessage,
|
|
219
|
+
context: InterceptorContext,
|
|
220
|
+
next: () => Promise<JsonRpcMessage>
|
|
221
|
+
): Promise<JsonRpcMessage> {
|
|
222
|
+
console.log('→', request.method, request.id)
|
|
223
|
+
const response = await next()
|
|
224
|
+
console.log('←', response.id, response.error ? 'ERROR' : 'OK')
|
|
225
|
+
return response
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const proxy = await ProxyBuilder.create()
|
|
230
|
+
.inbound({ transport: 'stdio' })
|
|
231
|
+
.outbound({ transport: 'stdio', command: 'node', args: ['server.js'] })
|
|
232
|
+
.store('sqlite://./traces.db')
|
|
233
|
+
.build()
|
|
234
|
+
|
|
235
|
+
proxy.addInterceptor(new LogAllInterceptor())
|
|
236
|
+
await proxy.start()
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Stores
|
|
242
|
+
|
|
243
|
+
### SQLite
|
|
244
|
+
|
|
245
|
+
No external server required — ideal for local development.
|
|
246
|
+
|
|
247
|
+
**Valid connection strings:**
|
|
248
|
+
|
|
249
|
+
```
|
|
250
|
+
sqlite://./traces.db
|
|
251
|
+
sqlite://~/.mcp-traces/traces.db
|
|
252
|
+
sqlite:///absolute/path/traces.db
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Driver: [`@libsql/client`](https://github.com/tursodatabase/libsql-client-ts) — pure WASM, no native compilation required.
|
|
256
|
+
|
|
257
|
+
**Schema:**
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
mcp_spans
|
|
261
|
+
id TEXT PRIMARY KEY → "{trace_id}-{span_id}"
|
|
262
|
+
trace_id TEXT NOT NULL
|
|
263
|
+
span_id TEXT NOT NULL
|
|
264
|
+
parent_id TEXT
|
|
265
|
+
name TEXT NOT NULL → "mcp.tool.call", "mcp.initialize", etc.
|
|
266
|
+
status TEXT NOT NULL → "ok" | "error"
|
|
267
|
+
started_at TEXT NOT NULL → ISO 8601
|
|
268
|
+
ended_at TEXT NOT NULL
|
|
269
|
+
duration_ms INT NOT NULL
|
|
270
|
+
attributes JSON → tool_name, client_version, etc.
|
|
271
|
+
events JSON → input/output bodies
|
|
272
|
+
|
|
273
|
+
mcp_metrics
|
|
274
|
+
id INT PRIMARY KEY
|
|
275
|
+
tool_name TEXT NOT NULL
|
|
276
|
+
call_count INT
|
|
277
|
+
error_count INT
|
|
278
|
+
avg_duration REAL
|
|
279
|
+
updated_at TEXT
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
### PostgreSQL
|
|
285
|
+
|
|
286
|
+
```
|
|
287
|
+
postgres://user:pass@localhost:5432/my_db
|
|
288
|
+
postgresql://user:pass@localhost:5432/my_db
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
Driver: [`postgres`](https://github.com/porsager/postgres) — pure JS, no node-gyp.
|
|
292
|
+
|
|
293
|
+
Same schema as SQLite but with native Postgres types (`TIMESTAMP`, `INTEGER`, `JSON`).
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
### MySQL
|
|
298
|
+
|
|
299
|
+
```
|
|
300
|
+
mysql://user:pass@localhost:3306/my_db
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
Driver: [`mysql2`](https://github.com/sidorares/node-mysql2).
|
|
304
|
+
|
|
305
|
+
Same schema adapted to MySQL types (`FLOAT`, `INT`, `JSON`, `TIMESTAMP`).
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## What gets recorded
|
|
310
|
+
|
|
311
|
+
Every JSON-RPC message produces a span in the `mcp_spans` table. Attributes vary by method:
|
|
312
|
+
|
|
313
|
+
| MCP method | Span name | Key attributes | Events |
|
|
314
|
+
|------------------|-----------------------|-------------------------------------------------------------|-------------------------------|
|
|
315
|
+
| `initialize` | `mcp.initialize` | `mcp.client_version`, `mcp.server_version`, capabilities | — |
|
|
316
|
+
| `tools/list` | `mcp.tools.list` | `mcp.tools_count`, `mcp.duration_ms` | tools list as JSON |
|
|
317
|
+
| `tools/call` | `mcp.tool.call` | `gen_ai.tool.name`, `gen_ai.tool.call.id`, duration | `tool.input` + `tool.output` |
|
|
318
|
+
| `resources/read` | `mcp.resource.read` | `url.full`, `mcp.duration_ms` | — |
|
|
319
|
+
| `resources/list` | `mcp.resources.list` | `mcp.duration_ms` | — |
|
|
320
|
+
| `prompts/get` | `mcp.prompt.get` | `mcp.prompt_name`, `mcp.duration_ms` | rendered prompt body |
|
|
321
|
+
| `prompts/list` | `mcp.prompts.list` | `mcp.duration_ms` | — |
|
|
322
|
+
| `shutdown` | `mcp.shutdown` | `mcp.duration_ms` | — |
|
|
323
|
+
| any other | `mcp.{method}` | `gen_ai.operation.name`, `mcp.duration_ms` | — |
|
|
324
|
+
|
|
325
|
+
Attributes use the `gen_ai.*` prefix following the [OpenTelemetry semantic conventions](https://opentelemetry.io/docs/specs/semconv/gen-ai/) for generative AI.
|
|
326
|
+
|
|
327
|
+
---
|
|
328
|
+
|
|
329
|
+
## Custom interceptors
|
|
330
|
+
|
|
331
|
+
The `Interceptor` interface is public. You can add your own logic into the pipeline before the telemetry interceptor:
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
interface Interceptor {
|
|
335
|
+
name: string
|
|
336
|
+
intercept(
|
|
337
|
+
request: JsonRpcMessage,
|
|
338
|
+
context: InterceptorContext,
|
|
339
|
+
next: () => Promise<JsonRpcMessage>
|
|
340
|
+
): Promise<JsonRpcMessage>
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
interface InterceptorContext {
|
|
344
|
+
startedAt: Date
|
|
345
|
+
traceId: string
|
|
346
|
+
spanId: string
|
|
347
|
+
metadata: Record<string, unknown>
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
Calling `next()` passes control to the next interceptor in the chain. `ForwardInterceptor` is always last — it makes the actual call to the real server.
|
|
352
|
+
|
|
353
|
+
---
|
|
354
|
+
|
|
355
|
+
## CLI reference
|
|
356
|
+
|
|
357
|
+
```
|
|
358
|
+
heimdall-mcp [options] [-- command [args...]]
|
|
359
|
+
|
|
360
|
+
Options:
|
|
361
|
+
--store <url> Store connection string (required)
|
|
362
|
+
sqlite://./traces.db
|
|
363
|
+
postgres://user:pass@host/db
|
|
364
|
+
mysql://user:pass@host/db
|
|
365
|
+
|
|
366
|
+
--out <transport> Transport to the real server (default: stdio)
|
|
367
|
+
stdio | http | sse
|
|
368
|
+
|
|
369
|
+
--target <url> Server URL when --out is http or sse
|
|
370
|
+
|
|
371
|
+
--in <transport> Inbound transport (default: stdio)
|
|
372
|
+
stdio | http | sse
|
|
373
|
+
|
|
374
|
+
--in-port <port> Port for --in http or --in sse
|
|
375
|
+
|
|
376
|
+
-V, --version Print version
|
|
377
|
+
-h, --help Print this help
|
|
378
|
+
|
|
379
|
+
-- Separates proxy flags from the subprocess command
|
|
380
|
+
(required when --out is stdio)
|
|
381
|
+
|
|
382
|
+
Examples:
|
|
383
|
+
# stdio proxy → subprocess
|
|
384
|
+
heimdall-mcp --store sqlite://./t.db -- node server.js
|
|
385
|
+
|
|
386
|
+
# stdio proxy → remote HTTP server
|
|
387
|
+
heimdall-mcp --store sqlite://./t.db --out http --target http://localhost:3001
|
|
388
|
+
|
|
389
|
+
# stdio proxy → remote SSE server with Postgres
|
|
390
|
+
heimdall-mcp --store postgres://user:pass@host/db --out sse --target http://remote.com
|
|
391
|
+
```
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// src/store/MySqlStore.ts
|
|
2
|
+
import { and, between, eq, sql } from "drizzle-orm";
|
|
3
|
+
import { drizzle } from "drizzle-orm/mysql2";
|
|
4
|
+
import mysql from "mysql2/promise";
|
|
5
|
+
|
|
6
|
+
// src/schema/mysql.schema.ts
|
|
7
|
+
import { float, int, json, mysqlTable, serial, timestamp, varchar } from "drizzle-orm/mysql-core";
|
|
8
|
+
var spans = mysqlTable("mcp_spans", {
|
|
9
|
+
id: varchar("id", { length: 64 }).primaryKey(),
|
|
10
|
+
traceId: varchar("trace_id", { length: 32 }).notNull(),
|
|
11
|
+
spanId: varchar("span_id", { length: 16 }).notNull(),
|
|
12
|
+
parentId: varchar("parent_id", { length: 16 }),
|
|
13
|
+
name: varchar("name", { length: 128 }).notNull(),
|
|
14
|
+
status: varchar("status", { length: 16 }).notNull(),
|
|
15
|
+
startedAt: timestamp("started_at").notNull(),
|
|
16
|
+
endedAt: timestamp("ended_at").notNull(),
|
|
17
|
+
durationMs: int("duration_ms").notNull(),
|
|
18
|
+
attributes: json("attributes"),
|
|
19
|
+
events: json("events")
|
|
20
|
+
});
|
|
21
|
+
var metrics = mysqlTable("mcp_metrics", {
|
|
22
|
+
id: serial("id").primaryKey(),
|
|
23
|
+
toolName: varchar("tool_name", { length: 128 }).notNull(),
|
|
24
|
+
callCount: int("call_count").default(0),
|
|
25
|
+
errorCount: int("error_count").default(0),
|
|
26
|
+
avgDuration: float("avg_duration"),
|
|
27
|
+
updatedAt: timestamp("updated_at").notNull()
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// src/store/MySqlStore.ts
|
|
31
|
+
var MySqlStore = class {
|
|
32
|
+
pool;
|
|
33
|
+
db;
|
|
34
|
+
constructor(connectionString) {
|
|
35
|
+
this.pool = mysql.createPool(connectionString);
|
|
36
|
+
this.db = drizzle(this.pool);
|
|
37
|
+
}
|
|
38
|
+
async init() {
|
|
39
|
+
await this.db.execute(sql`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS mcp_spans (
|
|
41
|
+
id VARCHAR(64) NOT NULL PRIMARY KEY,
|
|
42
|
+
trace_id VARCHAR(32) NOT NULL,
|
|
43
|
+
span_id VARCHAR(16) NOT NULL,
|
|
44
|
+
parent_id VARCHAR(16),
|
|
45
|
+
name VARCHAR(128) NOT NULL,
|
|
46
|
+
status VARCHAR(16) NOT NULL,
|
|
47
|
+
started_at TIMESTAMP(3) NOT NULL,
|
|
48
|
+
ended_at TIMESTAMP(3) NOT NULL,
|
|
49
|
+
duration_ms INT NOT NULL,
|
|
50
|
+
attributes JSON,
|
|
51
|
+
events JSON
|
|
52
|
+
)
|
|
53
|
+
`);
|
|
54
|
+
await this.db.execute(sql`
|
|
55
|
+
CREATE TABLE IF NOT EXISTS mcp_metrics (
|
|
56
|
+
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
|
57
|
+
tool_name VARCHAR(128) NOT NULL,
|
|
58
|
+
call_count INT DEFAULT 0,
|
|
59
|
+
error_count INT DEFAULT 0,
|
|
60
|
+
avg_duration FLOAT,
|
|
61
|
+
updated_at TIMESTAMP(3) NOT NULL
|
|
62
|
+
)
|
|
63
|
+
`);
|
|
64
|
+
}
|
|
65
|
+
async save(span) {
|
|
66
|
+
await this.db.insert(spans).values({
|
|
67
|
+
id: span.id,
|
|
68
|
+
traceId: span.traceId,
|
|
69
|
+
spanId: span.spanId,
|
|
70
|
+
parentId: span.parentId,
|
|
71
|
+
name: span.name,
|
|
72
|
+
status: span.status,
|
|
73
|
+
startedAt: span.startedAt,
|
|
74
|
+
endedAt: span.endedAt,
|
|
75
|
+
durationMs: span.durationMs,
|
|
76
|
+
attributes: span.attributes ?? null,
|
|
77
|
+
events: span.events ?? null
|
|
78
|
+
}).onDuplicateKeyUpdate({ set: { id: span.id } });
|
|
79
|
+
}
|
|
80
|
+
async query(filters) {
|
|
81
|
+
const conditions = [];
|
|
82
|
+
if (filters.traceId) conditions.push(eq(spans.traceId, filters.traceId));
|
|
83
|
+
if (filters.spanId) conditions.push(eq(spans.spanId, filters.spanId));
|
|
84
|
+
if (filters.name) conditions.push(eq(spans.name, filters.name));
|
|
85
|
+
if (filters.status) conditions.push(eq(spans.status, filters.status));
|
|
86
|
+
if (filters.from && filters.to) {
|
|
87
|
+
conditions.push(between(spans.startedAt, filters.from, filters.to));
|
|
88
|
+
}
|
|
89
|
+
const rows = await this.db.select().from(spans).where(conditions.length ? and(...conditions) : void 0).limit(filters.limit ?? 100);
|
|
90
|
+
return rows.map((r) => ({
|
|
91
|
+
id: r.id,
|
|
92
|
+
traceId: r.traceId,
|
|
93
|
+
spanId: r.spanId,
|
|
94
|
+
parentId: r.parentId ?? void 0,
|
|
95
|
+
name: r.name,
|
|
96
|
+
status: r.status,
|
|
97
|
+
startedAt: r.startedAt,
|
|
98
|
+
endedAt: r.endedAt,
|
|
99
|
+
durationMs: r.durationMs,
|
|
100
|
+
attributes: r.attributes,
|
|
101
|
+
events: r.events
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
async close() {
|
|
105
|
+
await this.pool.end();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
export {
|
|
109
|
+
MySqlStore
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=MySqlStore-VOXUWDAJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store/MySqlStore.ts","../src/schema/mysql.schema.ts"],"sourcesContent":["import { and, between, eq, sql } from 'drizzle-orm'\nimport { drizzle } from 'drizzle-orm/mysql2'\nimport mysql from 'mysql2/promise'\n\nimport { spans } from '@/schema/mysql.schema'\n\nimport type { TraceStore } from './TraceStore'\nimport type { McpSpan, SpanFilters } from '@/types'\n\nexport class MySqlStore implements TraceStore {\n private pool\n private db\n\n constructor(connectionString: string) {\n this.pool = mysql.createPool(connectionString)\n this.db = drizzle(this.pool)\n }\n\n async init(): Promise<void> {\n await this.db.execute(sql`\n CREATE TABLE IF NOT EXISTS mcp_spans (\n id VARCHAR(64) NOT NULL PRIMARY KEY,\n trace_id VARCHAR(32) NOT NULL,\n span_id VARCHAR(16) NOT NULL,\n parent_id VARCHAR(16),\n name VARCHAR(128) NOT NULL,\n status VARCHAR(16) NOT NULL,\n started_at TIMESTAMP(3) NOT NULL,\n ended_at TIMESTAMP(3) NOT NULL,\n duration_ms INT NOT NULL,\n attributes JSON,\n events JSON\n )\n `)\n await this.db.execute(sql`\n CREATE TABLE IF NOT EXISTS mcp_metrics (\n id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,\n tool_name VARCHAR(128) NOT NULL,\n call_count INT DEFAULT 0,\n error_count INT DEFAULT 0,\n avg_duration FLOAT,\n updated_at TIMESTAMP(3) NOT NULL\n )\n `)\n }\n\n async save(span: McpSpan): Promise<void> {\n await this.db.insert(spans).values({\n id: span.id,\n traceId: span.traceId,\n spanId: span.spanId,\n parentId: span.parentId,\n name: span.name,\n status: span.status,\n startedAt: span.startedAt,\n endedAt: span.endedAt,\n durationMs: span.durationMs,\n attributes: span.attributes ?? null,\n events: span.events ?? null,\n }).onDuplicateKeyUpdate({ set: { id: span.id } })\n }\n\n async query(filters: SpanFilters): Promise<McpSpan[]> {\n const conditions = []\n if (filters.traceId) conditions.push(eq(spans.traceId, filters.traceId))\n if (filters.spanId) conditions.push(eq(spans.spanId, filters.spanId))\n if (filters.name) conditions.push(eq(spans.name, filters.name))\n if (filters.status) conditions.push(eq(spans.status, filters.status))\n if (filters.from && filters.to) {\n conditions.push(between(spans.startedAt, filters.from, filters.to))\n }\n\n const rows = await this.db\n .select()\n .from(spans)\n .where(conditions.length ? and(...conditions) : undefined)\n .limit(filters.limit ?? 100)\n\n return rows.map((r) => ({\n id: r.id,\n traceId: r.traceId,\n spanId: r.spanId,\n parentId: r.parentId ?? undefined,\n name: r.name,\n status: r.status as McpSpan['status'],\n startedAt: r.startedAt,\n endedAt: r.endedAt,\n durationMs: r.durationMs,\n attributes: r.attributes as Record<string, unknown> | undefined,\n events: r.events as McpSpan['events'],\n }))\n }\n\n async close(): Promise<void> {\n await this.pool.end()\n }\n}\n","import { float, int, json, mysqlTable, serial, timestamp, varchar } from 'drizzle-orm/mysql-core'\n\nexport const spans = mysqlTable('mcp_spans', {\n id: varchar('id', { length: 64 }).primaryKey(),\n traceId: varchar('trace_id', { length: 32 }).notNull(),\n spanId: varchar('span_id', { length: 16 }).notNull(),\n parentId: varchar('parent_id', { length: 16 }),\n name: varchar('name', { length: 128 }).notNull(),\n status: varchar('status', { length: 16 }).notNull(),\n startedAt: timestamp('started_at').notNull(),\n endedAt: timestamp('ended_at').notNull(),\n durationMs: int('duration_ms').notNull(),\n attributes: json('attributes'),\n events: json('events'),\n})\n\nexport const metrics = mysqlTable('mcp_metrics', {\n id: serial('id').primaryKey(),\n toolName: varchar('tool_name', { length: 128 }).notNull(),\n callCount: int('call_count').default(0),\n errorCount: int('error_count').default(0),\n avgDuration: float('avg_duration'),\n updatedAt: timestamp('updated_at').notNull(),\n})\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// src/store/PostgresStore.ts
|
|
2
|
+
import { and, between, eq, sql } from "drizzle-orm";
|
|
3
|
+
import { drizzle } from "drizzle-orm/postgres-js";
|
|
4
|
+
import postgres from "postgres";
|
|
5
|
+
|
|
6
|
+
// src/schema/pg.schema.ts
|
|
7
|
+
import { integer, json, pgTable, real, serial, timestamp, varchar } from "drizzle-orm/pg-core";
|
|
8
|
+
var spans = pgTable("mcp_spans", {
|
|
9
|
+
id: varchar("id", { length: 64 }).primaryKey(),
|
|
10
|
+
traceId: varchar("trace_id", { length: 32 }).notNull(),
|
|
11
|
+
spanId: varchar("span_id", { length: 16 }).notNull(),
|
|
12
|
+
parentId: varchar("parent_id", { length: 16 }),
|
|
13
|
+
name: varchar("name", { length: 128 }).notNull(),
|
|
14
|
+
status: varchar("status", { length: 16 }).notNull(),
|
|
15
|
+
startedAt: timestamp("started_at").notNull(),
|
|
16
|
+
endedAt: timestamp("ended_at").notNull(),
|
|
17
|
+
durationMs: integer("duration_ms").notNull(),
|
|
18
|
+
attributes: json("attributes"),
|
|
19
|
+
events: json("events")
|
|
20
|
+
});
|
|
21
|
+
var metrics = pgTable("mcp_metrics", {
|
|
22
|
+
id: serial("id").primaryKey(),
|
|
23
|
+
toolName: varchar("tool_name", { length: 128 }).notNull(),
|
|
24
|
+
callCount: integer("call_count").default(0),
|
|
25
|
+
errorCount: integer("error_count").default(0),
|
|
26
|
+
avgDuration: real("avg_duration"),
|
|
27
|
+
updatedAt: timestamp("updated_at").notNull()
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// src/store/PostgresStore.ts
|
|
31
|
+
var PostgresStore = class {
|
|
32
|
+
sql;
|
|
33
|
+
db;
|
|
34
|
+
constructor(connectionString) {
|
|
35
|
+
this.sql = postgres(connectionString);
|
|
36
|
+
this.db = drizzle(this.sql);
|
|
37
|
+
}
|
|
38
|
+
async init() {
|
|
39
|
+
await this.db.execute(sql`
|
|
40
|
+
CREATE TABLE IF NOT EXISTS mcp_spans (
|
|
41
|
+
id VARCHAR(64) NOT NULL PRIMARY KEY,
|
|
42
|
+
trace_id VARCHAR(32) NOT NULL,
|
|
43
|
+
span_id VARCHAR(16) NOT NULL,
|
|
44
|
+
parent_id VARCHAR(16),
|
|
45
|
+
name VARCHAR(128) NOT NULL,
|
|
46
|
+
status VARCHAR(16) NOT NULL,
|
|
47
|
+
started_at TIMESTAMP NOT NULL,
|
|
48
|
+
ended_at TIMESTAMP NOT NULL,
|
|
49
|
+
duration_ms INTEGER NOT NULL,
|
|
50
|
+
attributes JSON,
|
|
51
|
+
events JSON
|
|
52
|
+
)
|
|
53
|
+
`);
|
|
54
|
+
await this.db.execute(sql`
|
|
55
|
+
CREATE TABLE IF NOT EXISTS mcp_metrics (
|
|
56
|
+
id SERIAL PRIMARY KEY,
|
|
57
|
+
tool_name VARCHAR(128) NOT NULL,
|
|
58
|
+
call_count INTEGER DEFAULT 0,
|
|
59
|
+
error_count INTEGER DEFAULT 0,
|
|
60
|
+
avg_duration REAL,
|
|
61
|
+
updated_at TIMESTAMP NOT NULL
|
|
62
|
+
)
|
|
63
|
+
`);
|
|
64
|
+
}
|
|
65
|
+
async save(span) {
|
|
66
|
+
await this.db.insert(spans).values({
|
|
67
|
+
id: span.id,
|
|
68
|
+
traceId: span.traceId,
|
|
69
|
+
spanId: span.spanId,
|
|
70
|
+
parentId: span.parentId,
|
|
71
|
+
name: span.name,
|
|
72
|
+
status: span.status,
|
|
73
|
+
startedAt: span.startedAt,
|
|
74
|
+
endedAt: span.endedAt,
|
|
75
|
+
durationMs: span.durationMs,
|
|
76
|
+
attributes: span.attributes ?? null,
|
|
77
|
+
events: span.events ?? null
|
|
78
|
+
}).onConflictDoNothing();
|
|
79
|
+
}
|
|
80
|
+
async query(filters) {
|
|
81
|
+
const conditions = [];
|
|
82
|
+
if (filters.traceId) conditions.push(eq(spans.traceId, filters.traceId));
|
|
83
|
+
if (filters.spanId) conditions.push(eq(spans.spanId, filters.spanId));
|
|
84
|
+
if (filters.name) conditions.push(eq(spans.name, filters.name));
|
|
85
|
+
if (filters.status) conditions.push(eq(spans.status, filters.status));
|
|
86
|
+
if (filters.from && filters.to) {
|
|
87
|
+
conditions.push(between(spans.startedAt, filters.from, filters.to));
|
|
88
|
+
}
|
|
89
|
+
const rows = await this.db.select().from(spans).where(conditions.length ? and(...conditions) : void 0).limit(filters.limit ?? 100);
|
|
90
|
+
return rows.map((r) => ({
|
|
91
|
+
id: r.id,
|
|
92
|
+
traceId: r.traceId,
|
|
93
|
+
spanId: r.spanId,
|
|
94
|
+
parentId: r.parentId ?? void 0,
|
|
95
|
+
name: r.name,
|
|
96
|
+
status: r.status,
|
|
97
|
+
startedAt: r.startedAt,
|
|
98
|
+
endedAt: r.endedAt,
|
|
99
|
+
durationMs: r.durationMs,
|
|
100
|
+
attributes: r.attributes,
|
|
101
|
+
events: r.events
|
|
102
|
+
}));
|
|
103
|
+
}
|
|
104
|
+
async close() {
|
|
105
|
+
await this.sql.end();
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
export {
|
|
109
|
+
PostgresStore
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=PostgresStore-QUSLDMPH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/store/PostgresStore.ts","../src/schema/pg.schema.ts"],"sourcesContent":["import { and, between, eq, sql } from 'drizzle-orm'\nimport { drizzle } from 'drizzle-orm/postgres-js'\nimport postgres from 'postgres'\n\nimport { spans } from '@/schema/pg.schema'\n\nimport type { TraceStore } from './TraceStore'\nimport type { McpSpan, SpanFilters } from '@/types'\n\nexport class PostgresStore implements TraceStore {\n private sql\n private db\n\n constructor(connectionString: string) {\n this.sql = postgres(connectionString)\n this.db = drizzle(this.sql)\n }\n\n async init(): Promise<void> {\n await this.db.execute(sql`\n CREATE TABLE IF NOT EXISTS mcp_spans (\n id VARCHAR(64) NOT NULL PRIMARY KEY,\n trace_id VARCHAR(32) NOT NULL,\n span_id VARCHAR(16) NOT NULL,\n parent_id VARCHAR(16),\n name VARCHAR(128) NOT NULL,\n status VARCHAR(16) NOT NULL,\n started_at TIMESTAMP NOT NULL,\n ended_at TIMESTAMP NOT NULL,\n duration_ms INTEGER NOT NULL,\n attributes JSON,\n events JSON\n )\n `)\n await this.db.execute(sql`\n CREATE TABLE IF NOT EXISTS mcp_metrics (\n id SERIAL PRIMARY KEY,\n tool_name VARCHAR(128) NOT NULL,\n call_count INTEGER DEFAULT 0,\n error_count INTEGER DEFAULT 0,\n avg_duration REAL,\n updated_at TIMESTAMP NOT NULL\n )\n `)\n }\n\n async save(span: McpSpan): Promise<void> {\n await this.db.insert(spans).values({\n id: span.id,\n traceId: span.traceId,\n spanId: span.spanId,\n parentId: span.parentId,\n name: span.name,\n status: span.status,\n startedAt: span.startedAt,\n endedAt: span.endedAt,\n durationMs: span.durationMs,\n attributes: span.attributes ?? null,\n events: span.events ?? null,\n }).onConflictDoNothing()\n }\n\n async query(filters: SpanFilters): Promise<McpSpan[]> {\n const conditions = []\n if (filters.traceId) conditions.push(eq(spans.traceId, filters.traceId))\n if (filters.spanId) conditions.push(eq(spans.spanId, filters.spanId))\n if (filters.name) conditions.push(eq(spans.name, filters.name))\n if (filters.status) conditions.push(eq(spans.status, filters.status))\n if (filters.from && filters.to) {\n conditions.push(between(spans.startedAt, filters.from, filters.to))\n }\n\n const rows = await this.db\n .select()\n .from(spans)\n .where(conditions.length ? and(...conditions) : undefined)\n .limit(filters.limit ?? 100)\n\n return rows.map((r) => ({\n id: r.id,\n traceId: r.traceId,\n spanId: r.spanId,\n parentId: r.parentId ?? undefined,\n name: r.name,\n status: r.status as McpSpan['status'],\n startedAt: r.startedAt,\n endedAt: r.endedAt,\n durationMs: r.durationMs,\n attributes: r.attributes as Record<string, unknown> | undefined,\n events: r.events as McpSpan['events'],\n }))\n }\n\n async close(): Promise<void> {\n await this.sql.end()\n }\n}\n","import { integer, json, pgTable, real, serial, timestamp, varchar } from 'drizzle-orm/pg-core'\n\nexport const spans = pgTable('mcp_spans', {\n id: varchar('id', { length: 64 }).primaryKey(),\n traceId: varchar('trace_id', { length: 32 }).notNull(),\n spanId: varchar('span_id', { length: 16 }).notNull(),\n parentId: varchar('parent_id', { length: 16 }),\n name: varchar('name', { length: 128 }).notNull(),\n status: varchar('status', { length: 16 }).notNull(),\n startedAt: timestamp('started_at').notNull(),\n endedAt: timestamp('ended_at').notNull(),\n durationMs: integer('duration_ms').notNull(),\n attributes: json('attributes'),\n events: json('events'),\n})\n\nexport const metrics = pgTable('mcp_metrics', {\n id: serial('id').primaryKey(),\n toolName: varchar('tool_name', { length: 128 }).notNull(),\n callCount: integer('call_count').default(0),\n errorCount: integer('error_count').default(0),\n avgDuration: real('avg_duration'),\n updatedAt: timestamp('updated_at').notNull(),\n})\n"],"mappings":"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","names":[]}
|