@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 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,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/cli.js'
@@ -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":[]}