@turingspark/mcp-debug 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 +109 -0
- package/dist/index.js +440 -0
- package/dist/index.js.map +1 -0
- package/package.json +48 -0
- package/web-dist/assets/index-Dhx4EXuk.css +1 -0
- package/web-dist/assets/index-zMycMyhm.js +49 -0
- package/web-dist/index.html +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# @turingspark/mcp-debug
|
|
2
|
+
|
|
3
|
+
A transparent proxy and live dashboard for debugging [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) servers.
|
|
4
|
+
|
|
5
|
+
Sits between your MCP host (e.g. Claude Desktop) and any MCP server, intercepting every JSON-RPC message in both directions. All traffic is displayed in a real-time web dashboard and persisted to a local SQLite database.
|
|
6
|
+
|
|
7
|
+
## Quick Start
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx @turingspark/mcp-debug --cmd "node my-mcp-server.js" --open
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This starts your MCP server, proxies all stdio traffic, and opens the dashboard at `http://localhost:8100`.
|
|
14
|
+
|
|
15
|
+
## Use with Claude Desktop
|
|
16
|
+
|
|
17
|
+
Wrap any existing MCP server in your Claude Desktop config (`claude_desktop_config.json`):
|
|
18
|
+
|
|
19
|
+
**Before:**
|
|
20
|
+
```json
|
|
21
|
+
{
|
|
22
|
+
"mcpServers": {
|
|
23
|
+
"my-server": {
|
|
24
|
+
"command": "node",
|
|
25
|
+
"args": ["my-mcp-server.js"]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**After:**
|
|
32
|
+
```json
|
|
33
|
+
{
|
|
34
|
+
"mcpServers": {
|
|
35
|
+
"my-server": {
|
|
36
|
+
"command": "npx",
|
|
37
|
+
"args": [
|
|
38
|
+
"@turingspark/mcp-debug",
|
|
39
|
+
"--cmd", "node my-mcp-server.js",
|
|
40
|
+
"--open"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Works with any MCP server — Node.js, Python, or any other runtime:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
{
|
|
51
|
+
"mcpServers": {
|
|
52
|
+
"python-server": {
|
|
53
|
+
"command": "npx",
|
|
54
|
+
"args": [
|
|
55
|
+
"@turingspark/mcp-debug",
|
|
56
|
+
"--cmd", "python3 my_server.py",
|
|
57
|
+
"--open"
|
|
58
|
+
]
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Dashboard
|
|
65
|
+
|
|
66
|
+
The web dashboard at `http://localhost:8100` provides three views:
|
|
67
|
+
|
|
68
|
+
- **Messages** — All JSON-RPC requests and responses with full payloads. Click any message to see the complete JSON.
|
|
69
|
+
- **Timeline** — Visual timeline of request/response pairs with latency measurements.
|
|
70
|
+
- **Stderr** — Server stderr output captured in real-time.
|
|
71
|
+
|
|
72
|
+
The dashboard connects via WebSocket and replays message history, so you can open it at any time without missing earlier traffic.
|
|
73
|
+
|
|
74
|
+
## CLI Options
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Usage: mcp-debug [options]
|
|
78
|
+
|
|
79
|
+
Options:
|
|
80
|
+
--cmd <command> The MCP server command to proxy (required)
|
|
81
|
+
--port <number> Dashboard port (default: 8100)
|
|
82
|
+
--db <path> SQLite database path (default: ~/.mcp-debug/data.db)
|
|
83
|
+
--quiet Suppress stderr output (default: false)
|
|
84
|
+
--open Auto-open dashboard in browser (default: false)
|
|
85
|
+
-h, --help Display help
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## How It Works
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
MCP Host (Claude Desktop)
|
|
92
|
+
|
|
|
93
|
+
| stdin/stdout (JSON-RPC)
|
|
94
|
+
v
|
|
95
|
+
mcp-debug proxy
|
|
96
|
+
| - Intercepts all messages
|
|
97
|
+
| - Stores in SQLite
|
|
98
|
+
| - Broadcasts to dashboard via WebSocket
|
|
99
|
+
|
|
|
100
|
+
| stdin/stdout (JSON-RPC)
|
|
101
|
+
v
|
|
102
|
+
MCP Server (your server)
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
The proxy is fully transparent — it passes all messages through without modification. Your MCP host and server don't know it's there.
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
MIT
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import { join as join2 } from "path";
|
|
6
|
+
import { homedir } from "os";
|
|
7
|
+
|
|
8
|
+
// src/proxy.ts
|
|
9
|
+
import { spawn } from "child_process";
|
|
10
|
+
|
|
11
|
+
// src/interceptor.ts
|
|
12
|
+
import { EventEmitter } from "events";
|
|
13
|
+
import { Transform } from "stream";
|
|
14
|
+
var Interceptor = class extends EventEmitter {
|
|
15
|
+
sessionId;
|
|
16
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
17
|
+
messageCounter = 0;
|
|
18
|
+
constructor(sessionId) {
|
|
19
|
+
super();
|
|
20
|
+
this.sessionId = sessionId;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates a Transform stream that intercepts JSON-RPC messages
|
|
24
|
+
* while passing the raw data through unmodified.
|
|
25
|
+
*/
|
|
26
|
+
createTransform(direction) {
|
|
27
|
+
let buffer = "";
|
|
28
|
+
return new Transform({
|
|
29
|
+
transform: (chunk, _encoding, callback) => {
|
|
30
|
+
callback(null, chunk);
|
|
31
|
+
buffer += chunk.toString();
|
|
32
|
+
const lines = buffer.split("\n");
|
|
33
|
+
buffer = lines.pop() ?? "";
|
|
34
|
+
for (const line of lines) {
|
|
35
|
+
const trimmed = line.trim();
|
|
36
|
+
if (!trimmed) continue;
|
|
37
|
+
this.processLine(trimmed, direction);
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
flush: (callback) => {
|
|
41
|
+
if (buffer.trim()) {
|
|
42
|
+
this.processLine(buffer.trim(), direction);
|
|
43
|
+
}
|
|
44
|
+
callback();
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
processLine(line, direction) {
|
|
49
|
+
let parsed;
|
|
50
|
+
try {
|
|
51
|
+
parsed = JSON.parse(line);
|
|
52
|
+
} catch {
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (parsed.jsonrpc !== "2.0") return;
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
const timestamp = new Date(now).toISOString();
|
|
58
|
+
let rpcId = null;
|
|
59
|
+
let method = null;
|
|
60
|
+
let latencyMs = null;
|
|
61
|
+
if ("method" in parsed) {
|
|
62
|
+
method = parsed.method;
|
|
63
|
+
if ("id" in parsed && parsed.id != null) {
|
|
64
|
+
rpcId = parsed.id;
|
|
65
|
+
this.pendingRequests.set(rpcId, { method, timestamp: now });
|
|
66
|
+
}
|
|
67
|
+
} else if ("id" in parsed) {
|
|
68
|
+
rpcId = parsed.id;
|
|
69
|
+
const pending = this.pendingRequests.get(rpcId);
|
|
70
|
+
if (pending) {
|
|
71
|
+
latencyMs = now - pending.timestamp;
|
|
72
|
+
method = pending.method;
|
|
73
|
+
this.pendingRequests.delete(rpcId);
|
|
74
|
+
}
|
|
75
|
+
if ("error" in parsed && parsed.error) {
|
|
76
|
+
method = method ? `${method} (error)` : "(error)";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const message = {
|
|
80
|
+
id: ++this.messageCounter,
|
|
81
|
+
sessionId: this.sessionId,
|
|
82
|
+
direction,
|
|
83
|
+
rpcId,
|
|
84
|
+
method,
|
|
85
|
+
rawJson: line,
|
|
86
|
+
timestamp,
|
|
87
|
+
latencyMs
|
|
88
|
+
};
|
|
89
|
+
this.emit("message", message);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
// src/store.ts
|
|
94
|
+
import Database from "better-sqlite3";
|
|
95
|
+
import { mkdirSync } from "fs";
|
|
96
|
+
import { dirname } from "path";
|
|
97
|
+
var Store = class {
|
|
98
|
+
db;
|
|
99
|
+
constructor(dbPath) {
|
|
100
|
+
mkdirSync(dirname(dbPath), { recursive: true });
|
|
101
|
+
this.db = new Database(dbPath);
|
|
102
|
+
this.db.pragma("journal_mode = WAL");
|
|
103
|
+
this.db.pragma("synchronous = NORMAL");
|
|
104
|
+
this.migrate();
|
|
105
|
+
}
|
|
106
|
+
migrate() {
|
|
107
|
+
this.db.exec(`
|
|
108
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
109
|
+
id TEXT PRIMARY KEY,
|
|
110
|
+
server_cmd TEXT NOT NULL,
|
|
111
|
+
started_at TEXT NOT NULL,
|
|
112
|
+
ended_at TEXT
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
116
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
117
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
118
|
+
direction TEXT NOT NULL CHECK(direction IN ('host_to_server', 'server_to_host')),
|
|
119
|
+
rpc_id TEXT,
|
|
120
|
+
method TEXT,
|
|
121
|
+
raw_json TEXT NOT NULL,
|
|
122
|
+
timestamp TEXT NOT NULL,
|
|
123
|
+
latency_ms INTEGER
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
CREATE TABLE IF NOT EXISTS stderr_logs (
|
|
127
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
128
|
+
session_id TEXT NOT NULL REFERENCES sessions(id),
|
|
129
|
+
line TEXT NOT NULL,
|
|
130
|
+
timestamp TEXT NOT NULL
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);
|
|
134
|
+
CREATE INDEX IF NOT EXISTS idx_messages_method ON messages(method);
|
|
135
|
+
CREATE INDEX IF NOT EXISTS idx_stderr_session ON stderr_logs(session_id);
|
|
136
|
+
`);
|
|
137
|
+
}
|
|
138
|
+
createSession(session) {
|
|
139
|
+
this.db.prepare(
|
|
140
|
+
"INSERT INTO sessions (id, server_cmd, started_at, ended_at) VALUES (?, ?, ?, ?)"
|
|
141
|
+
).run(session.id, session.serverCmd, session.startedAt, session.endedAt);
|
|
142
|
+
}
|
|
143
|
+
endSession(sessionId) {
|
|
144
|
+
this.db.prepare(
|
|
145
|
+
"UPDATE sessions SET ended_at = ? WHERE id = ?"
|
|
146
|
+
).run((/* @__PURE__ */ new Date()).toISOString(), sessionId);
|
|
147
|
+
}
|
|
148
|
+
insertMessage(msg) {
|
|
149
|
+
this.db.prepare(
|
|
150
|
+
`INSERT INTO messages (session_id, direction, rpc_id, method, raw_json, timestamp, latency_ms)
|
|
151
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
152
|
+
).run(
|
|
153
|
+
msg.sessionId,
|
|
154
|
+
msg.direction,
|
|
155
|
+
msg.rpcId != null ? String(msg.rpcId) : null,
|
|
156
|
+
msg.method,
|
|
157
|
+
msg.rawJson,
|
|
158
|
+
msg.timestamp,
|
|
159
|
+
msg.latencyMs
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
insertStderrLog(sessionId, line) {
|
|
163
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
|
|
164
|
+
const result = this.db.prepare(
|
|
165
|
+
"INSERT INTO stderr_logs (session_id, line, timestamp) VALUES (?, ?, ?)"
|
|
166
|
+
).run(sessionId, line, timestamp);
|
|
167
|
+
return {
|
|
168
|
+
id: Number(result.lastInsertRowid),
|
|
169
|
+
sessionId,
|
|
170
|
+
line,
|
|
171
|
+
timestamp
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
getRecentMessages(sessionId, limit = 100) {
|
|
175
|
+
return this.db.prepare(
|
|
176
|
+
`SELECT id, session_id as sessionId, direction, rpc_id as rpcId, method, raw_json as rawJson, timestamp, latency_ms as latencyMs
|
|
177
|
+
FROM messages WHERE session_id = ? ORDER BY id DESC LIMIT ?`
|
|
178
|
+
).all(sessionId, limit);
|
|
179
|
+
}
|
|
180
|
+
getSession(sessionId) {
|
|
181
|
+
return this.db.prepare(
|
|
182
|
+
"SELECT id, server_cmd as serverCmd, started_at as startedAt, ended_at as endedAt FROM sessions WHERE id = ?"
|
|
183
|
+
).get(sessionId);
|
|
184
|
+
}
|
|
185
|
+
close() {
|
|
186
|
+
this.db.close();
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// src/ws-server.ts
|
|
191
|
+
import { createServer } from "http";
|
|
192
|
+
import { readFileSync, existsSync } from "fs";
|
|
193
|
+
import { join, extname } from "path";
|
|
194
|
+
import { fileURLToPath } from "url";
|
|
195
|
+
import { WebSocketServer } from "ws";
|
|
196
|
+
var MIME_TYPES = {
|
|
197
|
+
".html": "text/html",
|
|
198
|
+
".js": "application/javascript",
|
|
199
|
+
".css": "text/css",
|
|
200
|
+
".json": "application/json",
|
|
201
|
+
".svg": "image/svg+xml",
|
|
202
|
+
".png": "image/png",
|
|
203
|
+
".ico": "image/x-icon"
|
|
204
|
+
};
|
|
205
|
+
var WsServer = class {
|
|
206
|
+
httpServer;
|
|
207
|
+
wss;
|
|
208
|
+
port;
|
|
209
|
+
clients = /* @__PURE__ */ new Set();
|
|
210
|
+
messageHistory = [];
|
|
211
|
+
constructor(port) {
|
|
212
|
+
this.port = port;
|
|
213
|
+
this.httpServer = createServer((req, res) => {
|
|
214
|
+
const webDistDir = this.getWebDistDir();
|
|
215
|
+
if (!webDistDir) {
|
|
216
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
217
|
+
res.end(`
|
|
218
|
+
<!DOCTYPE html>
|
|
219
|
+
<html>
|
|
220
|
+
<body style="font-family: system-ui; padding: 2rem; background: #1a1a2e; color: #e0e0e0;">
|
|
221
|
+
<h1>MCP Debug Dashboard</h1>
|
|
222
|
+
<p>Web dashboard not built yet. Run <code>npm run build</code> in the web package.</p>
|
|
223
|
+
<p>WebSocket is active on this port \u2014 the dashboard will connect when built.</p>
|
|
224
|
+
</body>
|
|
225
|
+
</html>
|
|
226
|
+
`);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
let filePath = join(webDistDir, req.url === "/" ? "index.html" : req.url);
|
|
230
|
+
if (!existsSync(filePath)) {
|
|
231
|
+
filePath = join(webDistDir, "index.html");
|
|
232
|
+
}
|
|
233
|
+
try {
|
|
234
|
+
const content = readFileSync(filePath);
|
|
235
|
+
const ext = extname(filePath);
|
|
236
|
+
res.writeHead(200, { "Content-Type": MIME_TYPES[ext] || "application/octet-stream" });
|
|
237
|
+
res.end(content);
|
|
238
|
+
} catch {
|
|
239
|
+
res.writeHead(404);
|
|
240
|
+
res.end("Not found");
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
this.wss = new WebSocketServer({ server: this.httpServer });
|
|
244
|
+
this.wss.on("connection", (ws) => {
|
|
245
|
+
this.clients.add(ws);
|
|
246
|
+
for (const msg of this.messageHistory) {
|
|
247
|
+
ws.send(msg);
|
|
248
|
+
}
|
|
249
|
+
ws.on("close", () => this.clients.delete(ws));
|
|
250
|
+
ws.on("error", () => this.clients.delete(ws));
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
getWebDistDir() {
|
|
254
|
+
const __dirname = fileURLToPath(new URL(".", import.meta.url));
|
|
255
|
+
const candidates = [
|
|
256
|
+
join(__dirname, "..", "web-dist"),
|
|
257
|
+
// npm: packages/cli/web-dist (bundled for distribution)
|
|
258
|
+
join(__dirname, "..", "..", "web", "dist"),
|
|
259
|
+
// dev: packages/cli/src -> packages/web/dist
|
|
260
|
+
join(__dirname, "..", "..", "..", "web", "dist")
|
|
261
|
+
// built: packages/cli/dist -> packages/web/dist
|
|
262
|
+
];
|
|
263
|
+
for (const dir of candidates) {
|
|
264
|
+
if (existsSync(join(dir, "index.html"))) {
|
|
265
|
+
return dir;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
async start() {
|
|
271
|
+
return new Promise((resolve, reject) => {
|
|
272
|
+
this.httpServer.on("error", (err) => {
|
|
273
|
+
if (err.code === "EADDRINUSE") {
|
|
274
|
+
process.stderr.write(
|
|
275
|
+
`[mcp-debug] Port ${this.port} is in use. Try --port <number>
|
|
276
|
+
`
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
reject(err);
|
|
280
|
+
});
|
|
281
|
+
this.httpServer.listen(this.port, () => resolve());
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
broadcast(msg) {
|
|
285
|
+
const data = JSON.stringify(msg);
|
|
286
|
+
this.messageHistory.push(data);
|
|
287
|
+
for (const client of this.clients) {
|
|
288
|
+
if (client.readyState === client.OPEN) {
|
|
289
|
+
client.send(data);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
close() {
|
|
294
|
+
for (const client of this.clients) {
|
|
295
|
+
client.close();
|
|
296
|
+
}
|
|
297
|
+
this.wss.close();
|
|
298
|
+
this.httpServer.close();
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// src/proxy.ts
|
|
303
|
+
var Proxy = class {
|
|
304
|
+
child = null;
|
|
305
|
+
interceptor;
|
|
306
|
+
store;
|
|
307
|
+
wsServer;
|
|
308
|
+
sessionId;
|
|
309
|
+
options;
|
|
310
|
+
constructor(options) {
|
|
311
|
+
this.options = options;
|
|
312
|
+
this.sessionId = `session_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
313
|
+
this.store = new Store(options.dbPath);
|
|
314
|
+
this.interceptor = new Interceptor(this.sessionId);
|
|
315
|
+
this.wsServer = new WsServer(options.port);
|
|
316
|
+
}
|
|
317
|
+
async start() {
|
|
318
|
+
this.store.createSession({
|
|
319
|
+
id: this.sessionId,
|
|
320
|
+
serverCmd: this.options.cmd,
|
|
321
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
322
|
+
endedAt: null
|
|
323
|
+
});
|
|
324
|
+
await this.wsServer.start();
|
|
325
|
+
this.interceptor.on("message", (msg) => {
|
|
326
|
+
this.store.insertMessage(msg);
|
|
327
|
+
this.wsServer.broadcast({ type: "message", message: msg });
|
|
328
|
+
if (!this.options.quiet) {
|
|
329
|
+
this.logToStderr(msg);
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
const [command, ...args] = this.parseCommand(this.options.cmd);
|
|
333
|
+
this.child = spawn(command, args, {
|
|
334
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
335
|
+
env: { ...process.env }
|
|
336
|
+
});
|
|
337
|
+
const hostToServer = this.interceptor.createTransform("host_to_server");
|
|
338
|
+
const serverToHost = this.interceptor.createTransform("server_to_host");
|
|
339
|
+
process.stdin.pipe(hostToServer).pipe(this.child.stdin);
|
|
340
|
+
this.child.stdout.pipe(serverToHost).pipe(process.stdout);
|
|
341
|
+
this.child.stderr.on("data", (chunk) => {
|
|
342
|
+
const lines = chunk.toString().split("\n").filter((l) => l.trim());
|
|
343
|
+
for (const line of lines) {
|
|
344
|
+
const log = this.store.insertStderrLog(this.sessionId, line);
|
|
345
|
+
this.wsServer.broadcast({ type: "stderr", log });
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
this.child.on("exit", (code, signal) => {
|
|
349
|
+
if (!this.options.quiet) {
|
|
350
|
+
process.stderr.write(
|
|
351
|
+
`[mcp-debug] Server exited with ${signal ? `signal ${signal}` : `code ${code}`}
|
|
352
|
+
`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
this.shutdown();
|
|
356
|
+
});
|
|
357
|
+
this.child.on("error", (err) => {
|
|
358
|
+
process.stderr.write(`[mcp-debug] Failed to start server: ${err.message}
|
|
359
|
+
`);
|
|
360
|
+
this.shutdown(1);
|
|
361
|
+
});
|
|
362
|
+
process.on("SIGTERM", () => this.shutdown());
|
|
363
|
+
process.on("SIGINT", () => this.shutdown());
|
|
364
|
+
process.stdin.on("end", () => {
|
|
365
|
+
this.shutdown();
|
|
366
|
+
});
|
|
367
|
+
if (!this.options.quiet) {
|
|
368
|
+
process.stderr.write(`[mcp-debug] Proxying: ${this.options.cmd}
|
|
369
|
+
`);
|
|
370
|
+
process.stderr.write(`[mcp-debug] Dashboard: http://localhost:${this.options.port}
|
|
371
|
+
`);
|
|
372
|
+
process.stderr.write(`[mcp-debug] Session: ${this.sessionId}
|
|
373
|
+
`);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
parseCommand(cmd) {
|
|
377
|
+
const parts = [];
|
|
378
|
+
let current = "";
|
|
379
|
+
let inQuote = null;
|
|
380
|
+
for (const char of cmd) {
|
|
381
|
+
if (inQuote) {
|
|
382
|
+
if (char === inQuote) {
|
|
383
|
+
inQuote = null;
|
|
384
|
+
} else {
|
|
385
|
+
current += char;
|
|
386
|
+
}
|
|
387
|
+
} else if (char === '"' || char === "'") {
|
|
388
|
+
inQuote = char;
|
|
389
|
+
} else if (char === " ") {
|
|
390
|
+
if (current) {
|
|
391
|
+
parts.push(current);
|
|
392
|
+
current = "";
|
|
393
|
+
}
|
|
394
|
+
} else {
|
|
395
|
+
current += char;
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
if (current) parts.push(current);
|
|
399
|
+
return parts;
|
|
400
|
+
}
|
|
401
|
+
logToStderr(msg) {
|
|
402
|
+
const arrow = msg.direction === "host_to_server" ? "\u2192" : "\u2190";
|
|
403
|
+
const method = msg.method ?? "(response)";
|
|
404
|
+
const latency = msg.latencyMs != null ? ` [${msg.latencyMs}ms]` : "";
|
|
405
|
+
process.stderr.write(`[mcp-debug] ${arrow} ${method}${latency}
|
|
406
|
+
`);
|
|
407
|
+
}
|
|
408
|
+
shuttingDown = false;
|
|
409
|
+
shutdown(exitCode = 0) {
|
|
410
|
+
if (this.shuttingDown) return;
|
|
411
|
+
this.shuttingDown = true;
|
|
412
|
+
this.store.endSession(this.sessionId);
|
|
413
|
+
this.wsServer.broadcast({ type: "session_end", sessionId: this.sessionId });
|
|
414
|
+
if (this.child && !this.child.killed) {
|
|
415
|
+
this.child.kill("SIGTERM");
|
|
416
|
+
}
|
|
417
|
+
this.wsServer.close();
|
|
418
|
+
this.store.close();
|
|
419
|
+
process.exit(exitCode);
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// src/index.ts
|
|
424
|
+
var DEFAULT_DB_PATH = join2(homedir(), ".mcp-debug", "data.db");
|
|
425
|
+
var DEFAULT_PORT = 8100;
|
|
426
|
+
var program = new Command().name("mcp-debug").description("MCP Inspector & Debugger \u2014 transparent proxy for MCP stdio traffic").requiredOption("--cmd <command>", 'The MCP server command to proxy (e.g. "node server.js")').option("--port <number>", "Dashboard port", String(DEFAULT_PORT)).option("--db <path>", "SQLite database path", DEFAULT_DB_PATH).option("--quiet", "Suppress stderr output", false).option("--open", "Auto-open dashboard in browser", false).action(async (opts) => {
|
|
427
|
+
const proxy = new Proxy({
|
|
428
|
+
cmd: opts.cmd,
|
|
429
|
+
dbPath: opts.db,
|
|
430
|
+
port: parseInt(opts.port, 10),
|
|
431
|
+
quiet: opts.quiet
|
|
432
|
+
});
|
|
433
|
+
await proxy.start();
|
|
434
|
+
if (opts.open) {
|
|
435
|
+
const open = (await import("open")).default;
|
|
436
|
+
await open(`http://localhost:${opts.port}`);
|
|
437
|
+
}
|
|
438
|
+
});
|
|
439
|
+
program.parse();
|
|
440
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/proxy.ts","../src/interceptor.ts","../src/store.ts","../src/ws-server.ts"],"sourcesContent":["import { Command } from 'commander';\nimport { join } from 'node:path';\nimport { homedir } from 'node:os';\nimport { Proxy } from './proxy.js';\n\nconst DEFAULT_DB_PATH = join(homedir(), '.mcp-debug', 'data.db');\nconst DEFAULT_PORT = 8100;\n\nconst program = new Command()\n .name('mcp-debug')\n .description('MCP Inspector & Debugger — transparent proxy for MCP stdio traffic')\n .requiredOption('--cmd <command>', 'The MCP server command to proxy (e.g. \"node server.js\")')\n .option('--port <number>', 'Dashboard port', String(DEFAULT_PORT))\n .option('--db <path>', 'SQLite database path', DEFAULT_DB_PATH)\n .option('--quiet', 'Suppress stderr output', false)\n .option('--open', 'Auto-open dashboard in browser', false)\n .action(async (opts) => {\n const proxy = new Proxy({\n cmd: opts.cmd,\n dbPath: opts.db,\n port: parseInt(opts.port, 10),\n quiet: opts.quiet,\n });\n\n await proxy.start();\n\n if (opts.open) {\n const open = (await import('open')).default;\n await open(`http://localhost:${opts.port}`);\n }\n });\n\nprogram.parse();\n","import { spawn, type ChildProcess } from 'node:child_process';\nimport { Interceptor } from './interceptor.js';\nimport { Store } from './store.js';\nimport { WsServer } from './ws-server.js';\nimport type { InterceptedMessage, StderrLog } from './types.js';\n\nexport interface ProxyOptions {\n cmd: string;\n dbPath: string;\n port: number;\n quiet: boolean;\n}\n\nexport class Proxy {\n private child: ChildProcess | null = null;\n private interceptor: Interceptor;\n private store: Store;\n private wsServer: WsServer;\n private sessionId: string;\n private options: ProxyOptions;\n\n constructor(options: ProxyOptions) {\n this.options = options;\n this.sessionId = `session_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;\n this.store = new Store(options.dbPath);\n this.interceptor = new Interceptor(this.sessionId);\n this.wsServer = new WsServer(options.port);\n }\n\n async start(): Promise<void> {\n // Create session\n this.store.createSession({\n id: this.sessionId,\n serverCmd: this.options.cmd,\n startedAt: new Date().toISOString(),\n endedAt: null,\n });\n\n // Start WebSocket server for dashboard\n await this.wsServer.start();\n\n // Listen for intercepted messages\n this.interceptor.on('message', (msg: InterceptedMessage) => {\n this.store.insertMessage(msg);\n this.wsServer.broadcast({ type: 'message', message: msg });\n\n if (!this.options.quiet) {\n this.logToStderr(msg);\n }\n });\n\n // Spawn the real MCP server\n const [command, ...args] = this.parseCommand(this.options.cmd);\n this.child = spawn(command, args, {\n stdio: ['pipe', 'pipe', 'pipe'],\n env: { ...process.env },\n });\n\n // Create transform streams for interception\n const hostToServer = this.interceptor.createTransform('host_to_server');\n const serverToHost = this.interceptor.createTransform('server_to_host');\n\n // Wire up pipes:\n // Host stdin → interceptor → Server stdin\n process.stdin.pipe(hostToServer).pipe(this.child.stdin!);\n\n // Server stdout → interceptor → Host stdout\n this.child.stdout!.pipe(serverToHost).pipe(process.stdout);\n\n // Server stderr → capture for logging\n this.child.stderr!.on('data', (chunk: Buffer) => {\n const lines = chunk.toString().split('\\n').filter((l) => l.trim());\n for (const line of lines) {\n const log = this.store.insertStderrLog(this.sessionId, line);\n this.wsServer.broadcast({ type: 'stderr', log });\n }\n });\n\n // Handle child process exit\n this.child.on('exit', (code, signal) => {\n if (!this.options.quiet) {\n process.stderr.write(\n `[mcp-debug] Server exited with ${signal ? `signal ${signal}` : `code ${code}`}\\n`\n );\n }\n this.shutdown();\n });\n\n this.child.on('error', (err) => {\n process.stderr.write(`[mcp-debug] Failed to start server: ${err.message}\\n`);\n this.shutdown(1);\n });\n\n // Handle signals for clean shutdown\n process.on('SIGTERM', () => this.shutdown());\n process.on('SIGINT', () => this.shutdown());\n\n // Handle host stdin closing (host disconnected)\n process.stdin.on('end', () => {\n this.shutdown();\n });\n\n if (!this.options.quiet) {\n process.stderr.write(`[mcp-debug] Proxying: ${this.options.cmd}\\n`);\n process.stderr.write(`[mcp-debug] Dashboard: http://localhost:${this.options.port}\\n`);\n process.stderr.write(`[mcp-debug] Session: ${this.sessionId}\\n`);\n }\n }\n\n private parseCommand(cmd: string): string[] {\n // Simple shell-like parsing: split on spaces, respecting quotes\n const parts: string[] = [];\n let current = '';\n let inQuote: string | null = null;\n\n for (const char of cmd) {\n if (inQuote) {\n if (char === inQuote) {\n inQuote = null;\n } else {\n current += char;\n }\n } else if (char === '\"' || char === \"'\") {\n inQuote = char;\n } else if (char === ' ') {\n if (current) {\n parts.push(current);\n current = '';\n }\n } else {\n current += char;\n }\n }\n if (current) parts.push(current);\n\n return parts;\n }\n\n private logToStderr(msg: InterceptedMessage): void {\n const arrow = msg.direction === 'host_to_server' ? '→' : '←';\n const method = msg.method ?? '(response)';\n const latency = msg.latencyMs != null ? ` [${msg.latencyMs}ms]` : '';\n process.stderr.write(`[mcp-debug] ${arrow} ${method}${latency}\\n`);\n }\n\n private shuttingDown = false;\n private shutdown(exitCode = 0): void {\n if (this.shuttingDown) return;\n this.shuttingDown = true;\n\n this.store.endSession(this.sessionId);\n this.wsServer.broadcast({ type: 'session_end', sessionId: this.sessionId });\n\n if (this.child && !this.child.killed) {\n this.child.kill('SIGTERM');\n }\n\n this.wsServer.close();\n this.store.close();\n process.exit(exitCode);\n }\n}\n","import { EventEmitter } from 'node:events';\nimport { Transform, type TransformCallback } from 'node:stream';\nimport type { MessageDirection, JsonRpcMessage, InterceptedMessage } from './types.js';\n\ninterface InterceptorEvents {\n message: [InterceptedMessage];\n}\n\nexport class Interceptor extends EventEmitter<InterceptorEvents> {\n private sessionId: string;\n private pendingRequests = new Map<string | number, { method: string; timestamp: number }>();\n private messageCounter = 0;\n\n constructor(sessionId: string) {\n super();\n this.sessionId = sessionId;\n }\n\n /**\n * Creates a Transform stream that intercepts JSON-RPC messages\n * while passing the raw data through unmodified.\n */\n createTransform(direction: MessageDirection): Transform {\n let buffer = '';\n\n return new Transform({\n transform: (chunk: Buffer, _encoding: string, callback: TransformCallback) => {\n // Always pass the raw data through immediately\n callback(null, chunk);\n\n // Parse asynchronously so we don't block the pipe\n buffer += chunk.toString();\n const lines = buffer.split('\\n');\n // Keep the last incomplete line in the buffer\n buffer = lines.pop() ?? '';\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n this.processLine(trimmed, direction);\n }\n },\n flush: (callback: TransformCallback) => {\n // Process any remaining data\n if (buffer.trim()) {\n this.processLine(buffer.trim(), direction);\n }\n callback();\n },\n });\n }\n\n private processLine(line: string, direction: MessageDirection): void {\n let parsed: JsonRpcMessage;\n try {\n parsed = JSON.parse(line);\n } catch {\n // Not valid JSON — skip (could be partial data or non-JSON stderr)\n return;\n }\n\n if (parsed.jsonrpc !== '2.0') return;\n\n const now = Date.now();\n const timestamp = new Date(now).toISOString();\n\n let rpcId: string | number | null = null;\n let method: string | null = null;\n let latencyMs: number | null = null;\n\n if ('method' in parsed) {\n // Request or notification\n method = parsed.method;\n if ('id' in parsed && parsed.id != null) {\n rpcId = parsed.id;\n this.pendingRequests.set(rpcId, { method, timestamp: now });\n }\n } else if ('id' in parsed) {\n // Response\n rpcId = parsed.id;\n const pending = this.pendingRequests.get(rpcId);\n if (pending) {\n latencyMs = now - pending.timestamp;\n method = pending.method;\n this.pendingRequests.delete(rpcId);\n }\n if ('error' in parsed && parsed.error) {\n method = method ? `${method} (error)` : '(error)';\n }\n }\n\n const message: InterceptedMessage = {\n id: ++this.messageCounter,\n sessionId: this.sessionId,\n direction,\n rpcId,\n method,\n rawJson: line,\n timestamp,\n latencyMs,\n };\n\n this.emit('message', message);\n }\n}\n","import Database from 'better-sqlite3';\nimport { mkdirSync } from 'node:fs';\nimport { dirname } from 'node:path';\nimport type { InterceptedMessage, Session, StderrLog } from './types.js';\n\nexport class Store {\n private db: Database.Database;\n\n constructor(dbPath: string) {\n mkdirSync(dirname(dbPath), { recursive: true });\n this.db = new Database(dbPath);\n this.db.pragma('journal_mode = WAL');\n this.db.pragma('synchronous = NORMAL');\n this.migrate();\n }\n\n private migrate(): void {\n this.db.exec(`\n CREATE TABLE IF NOT EXISTS sessions (\n id TEXT PRIMARY KEY,\n server_cmd TEXT NOT NULL,\n started_at TEXT NOT NULL,\n ended_at TEXT\n );\n\n CREATE TABLE IF NOT EXISTS messages (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL REFERENCES sessions(id),\n direction TEXT NOT NULL CHECK(direction IN ('host_to_server', 'server_to_host')),\n rpc_id TEXT,\n method TEXT,\n raw_json TEXT NOT NULL,\n timestamp TEXT NOT NULL,\n latency_ms INTEGER\n );\n\n CREATE TABLE IF NOT EXISTS stderr_logs (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n session_id TEXT NOT NULL REFERENCES sessions(id),\n line TEXT NOT NULL,\n timestamp TEXT NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_messages_session ON messages(session_id);\n CREATE INDEX IF NOT EXISTS idx_messages_method ON messages(method);\n CREATE INDEX IF NOT EXISTS idx_stderr_session ON stderr_logs(session_id);\n `);\n }\n\n createSession(session: Session): void {\n this.db.prepare(\n 'INSERT INTO sessions (id, server_cmd, started_at, ended_at) VALUES (?, ?, ?, ?)'\n ).run(session.id, session.serverCmd, session.startedAt, session.endedAt);\n }\n\n endSession(sessionId: string): void {\n this.db.prepare(\n 'UPDATE sessions SET ended_at = ? WHERE id = ?'\n ).run(new Date().toISOString(), sessionId);\n }\n\n insertMessage(msg: InterceptedMessage): void {\n this.db.prepare(\n `INSERT INTO messages (session_id, direction, rpc_id, method, raw_json, timestamp, latency_ms)\n VALUES (?, ?, ?, ?, ?, ?, ?)`\n ).run(\n msg.sessionId,\n msg.direction,\n msg.rpcId != null ? String(msg.rpcId) : null,\n msg.method,\n msg.rawJson,\n msg.timestamp,\n msg.latencyMs,\n );\n }\n\n insertStderrLog(sessionId: string, line: string): StderrLog {\n const timestamp = new Date().toISOString();\n const result = this.db.prepare(\n 'INSERT INTO stderr_logs (session_id, line, timestamp) VALUES (?, ?, ?)'\n ).run(sessionId, line, timestamp);\n\n return {\n id: Number(result.lastInsertRowid),\n sessionId,\n line,\n timestamp,\n };\n }\n\n getRecentMessages(sessionId: string, limit = 100): InterceptedMessage[] {\n return this.db.prepare(\n `SELECT id, session_id as sessionId, direction, rpc_id as rpcId, method, raw_json as rawJson, timestamp, latency_ms as latencyMs\n FROM messages WHERE session_id = ? ORDER BY id DESC LIMIT ?`\n ).all(sessionId, limit) as InterceptedMessage[];\n }\n\n getSession(sessionId: string): Session | undefined {\n return this.db.prepare(\n 'SELECT id, server_cmd as serverCmd, started_at as startedAt, ended_at as endedAt FROM sessions WHERE id = ?'\n ).get(sessionId) as Session | undefined;\n }\n\n close(): void {\n this.db.close();\n }\n}\n","import { createServer, type Server as HttpServer } from 'node:http';\nimport { readFileSync, existsSync } from 'node:fs';\nimport { join, extname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { WebSocketServer, type WebSocket } from 'ws';\nimport type { WsMessage } from './types.js';\n\nconst MIME_TYPES: Record<string, string> = {\n '.html': 'text/html',\n '.js': 'application/javascript',\n '.css': 'text/css',\n '.json': 'application/json',\n '.svg': 'image/svg+xml',\n '.png': 'image/png',\n '.ico': 'image/x-icon',\n};\n\nexport class WsServer {\n private httpServer: HttpServer;\n private wss: WebSocketServer;\n private port: number;\n private clients = new Set<WebSocket>();\n private messageHistory: string[] = [];\n\n constructor(port: number) {\n this.port = port;\n\n // Serve static dashboard files\n this.httpServer = createServer((req, res) => {\n const webDistDir = this.getWebDistDir();\n if (!webDistDir) {\n res.writeHead(200, { 'Content-Type': 'text/html' });\n res.end(`\n <!DOCTYPE html>\n <html>\n <body style=\"font-family: system-ui; padding: 2rem; background: #1a1a2e; color: #e0e0e0;\">\n <h1>MCP Debug Dashboard</h1>\n <p>Web dashboard not built yet. Run <code>npm run build</code> in the web package.</p>\n <p>WebSocket is active on this port — the dashboard will connect when built.</p>\n </body>\n </html>\n `);\n return;\n }\n\n let filePath = join(webDistDir, req.url === '/' ? 'index.html' : req.url!);\n if (!existsSync(filePath)) {\n // SPA fallback\n filePath = join(webDistDir, 'index.html');\n }\n\n try {\n const content = readFileSync(filePath);\n const ext = extname(filePath);\n res.writeHead(200, { 'Content-Type': MIME_TYPES[ext] || 'application/octet-stream' });\n res.end(content);\n } catch {\n res.writeHead(404);\n res.end('Not found');\n }\n });\n\n this.wss = new WebSocketServer({ server: this.httpServer });\n\n this.wss.on('connection', (ws) => {\n this.clients.add(ws);\n\n // Replay message history to new clients\n for (const msg of this.messageHistory) {\n ws.send(msg);\n }\n\n ws.on('close', () => this.clients.delete(ws));\n ws.on('error', () => this.clients.delete(ws));\n });\n }\n\n private getWebDistDir(): string | null {\n const __dirname = fileURLToPath(new URL('.', import.meta.url));\n const candidates = [\n join(__dirname, '..', 'web-dist'), // npm: packages/cli/web-dist (bundled for distribution)\n join(__dirname, '..', '..', 'web', 'dist'), // dev: packages/cli/src -> packages/web/dist\n join(__dirname, '..', '..', '..', 'web', 'dist'), // built: packages/cli/dist -> packages/web/dist\n ];\n for (const dir of candidates) {\n if (existsSync(join(dir, 'index.html'))) {\n return dir;\n }\n }\n return null;\n }\n\n async start(): Promise<void> {\n return new Promise((resolve, reject) => {\n this.httpServer.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n process.stderr.write(\n `[mcp-debug] Port ${this.port} is in use. Try --port <number>\\n`\n );\n }\n reject(err);\n });\n this.httpServer.listen(this.port, () => resolve());\n });\n }\n\n broadcast(msg: WsMessage): void {\n const data = JSON.stringify(msg);\n this.messageHistory.push(data);\n for (const client of this.clients) {\n if (client.readyState === client.OPEN) {\n client.send(data);\n }\n }\n }\n\n close(): void {\n for (const client of this.clients) {\n client.close();\n }\n this.wss.close();\n this.httpServer.close();\n }\n}\n"],"mappings":"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","names":["join","join"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@turingspark/mcp-debug",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MCP Inspector & Debugger — transparent proxy with a live dashboard for MCP stdio traffic",
|
|
5
|
+
"author": "TuringSpark <team@turingspark.com>",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"bin": {
|
|
9
|
+
"mcp-debug": "./dist/index.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"web-dist",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"scripts": {
|
|
17
|
+
"dev": "tsx src/index.ts",
|
|
18
|
+
"build": "tsup",
|
|
19
|
+
"build:all": "npm run --workspace=mcp-debug-web build && rm -rf web-dist && cp -r ../web/dist web-dist && tsup",
|
|
20
|
+
"prepublishOnly": "npm run build:all"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"mcp",
|
|
24
|
+
"model-context-protocol",
|
|
25
|
+
"debugger",
|
|
26
|
+
"inspector",
|
|
27
|
+
"proxy",
|
|
28
|
+
"ai",
|
|
29
|
+
"llm"
|
|
30
|
+
],
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "https://github.com/turingspark/mcp-debug"
|
|
34
|
+
},
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"better-sqlite3": "^11.0.0",
|
|
37
|
+
"commander": "^12.0.0",
|
|
38
|
+
"open": "^10.0.0",
|
|
39
|
+
"ws": "^8.16.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/better-sqlite3": "^7.6.0",
|
|
43
|
+
"@types/ws": "^8.5.0",
|
|
44
|
+
"tsup": "^8.0.0",
|
|
45
|
+
"tsx": "^4.0.0",
|
|
46
|
+
"typescript": "^5.4.0"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*,:before,:after{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x: 0;--tw-border-spacing-y: 0;--tw-translate-x: 0;--tw-translate-y: 0;--tw-rotate: 0;--tw-skew-x: 0;--tw-skew-y: 0;--tw-scale-x: 1;--tw-scale-y: 1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness: proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width: 0px;--tw-ring-offset-color: #fff;--tw-ring-color: rgb(59 130 246 / .5);--tw-ring-offset-shadow: 0 0 #0000;--tw-ring-shadow: 0 0 #0000;--tw-shadow: 0 0 #0000;--tw-shadow-colored: 0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }*,:before,:after{box-sizing:border-box;border-width:0;border-style:solid;border-color:#e5e7eb}:before,:after{--tw-content: ""}html,:host{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dl,dd,h1,h2,h3,h4,h5,h6,hr,figure,p,pre{margin:0}fieldset{margin:0;padding:0}legend{padding:0}ol,ul,menu{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}button,[role=button]{cursor:pointer}:disabled{cursor:default}img,svg,video,canvas,audio,iframe,embed,object{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]:where(:not([hidden=until-found])){display:none}.sticky{position:sticky}.top-0{top:0}.z-10{z-index:10}.mb-3{margin-bottom:.75rem}.ml-auto{margin-left:auto}.inline-block{display:inline-block}.flex{display:flex}.table{display:table}.h-2{height:.5rem}.h-4{height:1rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-96{max-height:24rem}.w-12{width:3rem}.w-16{width:4rem}.w-2{width:.5rem}.w-20{width:5rem}.w-24{width:6rem}.w-32{width:8rem}.w-48{width:12rem}.w-64{width:16rem}.w-full{width:100%}.flex-1{flex:1 1 0%}.cursor-pointer{cursor:pointer}.flex-col{flex-direction:column}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-3{gap:.75rem}.gap-4{gap:1rem}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse: 0;margin-top:calc(.25rem * calc(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem * var(--tw-space-y-reverse))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.border{border-width:1px}.border-b{border-bottom-width:1px}.border-t{border-top-width:1px}.border-gray-700{--tw-border-opacity: 1;border-color:rgb(55 65 81 / var(--tw-border-opacity, 1))}.border-gray-800{--tw-border-opacity: 1;border-color:rgb(31 41 55 / var(--tw-border-opacity, 1))}.border-gray-800\/50{border-color:#1f293780}.bg-gray-700{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.bg-gray-800{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.bg-gray-900{--tw-bg-opacity: 1;background-color:rgb(17 24 39 / var(--tw-bg-opacity, 1))}.bg-gray-900\/50{background-color:#11182780}.bg-gray-950{--tw-bg-opacity: 1;background-color:rgb(3 7 18 / var(--tw-bg-opacity, 1))}.bg-green-400{--tw-bg-opacity: 1;background-color:rgb(74 222 128 / var(--tw-bg-opacity, 1))}.bg-green-500{--tw-bg-opacity: 1;background-color:rgb(34 197 94 / var(--tw-bg-opacity, 1))}.bg-red-400{--tw-bg-opacity: 1;background-color:rgb(248 113 113 / var(--tw-bg-opacity, 1))}.bg-red-500{--tw-bg-opacity: 1;background-color:rgb(239 68 68 / var(--tw-bg-opacity, 1))}.bg-red-950\/20{background-color:#450a0a33}.bg-yellow-400{--tw-bg-opacity: 1;background-color:rgb(250 204 21 / var(--tw-bg-opacity, 1))}.bg-yellow-500{--tw-bg-opacity: 1;background-color:rgb(234 179 8 / var(--tw-bg-opacity, 1))}.p-0{padding:0}.p-4{padding:1rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-1\.5{padding-top:.375rem;padding-bottom:.375rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.text-left{text-align:left}.text-right{text-align:right}.font-mono{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.uppercase{text-transform:uppercase}.leading-relaxed{line-height:1.625}.tracking-tight{letter-spacing:-.025em}.tracking-wider{letter-spacing:.05em}.text-blue-400{--tw-text-opacity: 1;color:rgb(96 165 250 / var(--tw-text-opacity, 1))}.text-gray-100{--tw-text-opacity: 1;color:rgb(243 244 246 / var(--tw-text-opacity, 1))}.text-gray-300{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.text-gray-400{--tw-text-opacity: 1;color:rgb(156 163 175 / var(--tw-text-opacity, 1))}.text-gray-500{--tw-text-opacity: 1;color:rgb(107 114 128 / var(--tw-text-opacity, 1))}.text-gray-600{--tw-text-opacity: 1;color:rgb(75 85 99 / var(--tw-text-opacity, 1))}.text-green-400{--tw-text-opacity: 1;color:rgb(74 222 128 / var(--tw-text-opacity, 1))}.text-red-400{--tw-text-opacity: 1;color:rgb(248 113 113 / var(--tw-text-opacity, 1))}.text-white{--tw-text-opacity: 1;color:rgb(255 255 255 / var(--tw-text-opacity, 1))}.text-yellow-300\/80{color:#fde047cc}.text-yellow-400{--tw-text-opacity: 1;color:rgb(250 204 21 / var(--tw-text-opacity, 1))}.placeholder-gray-600::-moz-placeholder{--tw-placeholder-opacity: 1;color:rgb(75 85 99 / var(--tw-placeholder-opacity, 1))}.placeholder-gray-600::placeholder{--tw-placeholder-opacity: 1;color:rgb(75 85 99 / var(--tw-placeholder-opacity, 1))}.\!filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)!important}.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-colors{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}body{margin:0;font-family:SF Mono,Fira Code,Fira Mono,Menlo,Consolas,monospace}::-webkit-scrollbar{width:6px}::-webkit-scrollbar-track{background:#1a1a2e}::-webkit-scrollbar-thumb{background:#374151;border-radius:3px}::-webkit-scrollbar-thumb:hover{background:#4b5563}.hover\:bg-gray-700:hover{--tw-bg-opacity: 1;background-color:rgb(55 65 81 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-800:hover{--tw-bg-opacity: 1;background-color:rgb(31 41 55 / var(--tw-bg-opacity, 1))}.hover\:bg-gray-800\/50:hover{background-color:#1f293780}.hover\:bg-red-950\/40:hover{background-color:#450a0a66}.hover\:text-gray-200:hover{--tw-text-opacity: 1;color:rgb(229 231 235 / var(--tw-text-opacity, 1))}.hover\:text-gray-300:hover{--tw-text-opacity: 1;color:rgb(209 213 219 / var(--tw-text-opacity, 1))}.focus\:border-gray-500:focus{--tw-border-opacity: 1;border-color:rgb(107 114 128 / var(--tw-border-opacity, 1))}.focus\:outline-none:focus{outline:2px solid transparent;outline-offset:2px}
|