@oimdb/mcp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,280 @@
1
+ // src/core/OIMDevMcpServer.ts
2
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
+ import { WebSocketServer } from "ws";
5
+ import { z } from "zod";
6
+ var OIMDevMcpServer = class {
7
+ constructor(options = {}) {
8
+ this.wss = null;
9
+ this.latestBrowser = null;
10
+ this.pending = /* @__PURE__ */ new Map();
11
+ this.wsPort = options.wsPort ?? 7432;
12
+ this.mcpServer = new McpServer({ name: "oimdb", version: "1.0.0" });
13
+ this.registerTools();
14
+ }
15
+ registerTools() {
16
+ const server = this.mcpServer;
17
+ const nameSchema = { name: z.string().describe("Registered name") };
18
+ server.registerTool("oimdb_inspect", {
19
+ description: "Get full state of all registered OIMDB collections and computeds from the connected browser tab. Returns structured JSON with entity counts, field names, index keys, relations, and computed statuses."
20
+ }, async () => {
21
+ const data = await this.queryBrowser("inspect");
22
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
23
+ });
24
+ server.registerTool("oimdb_dump", {
25
+ description: "Get a human-readable text summary of OIMDB state. Same output as registry.dump() in the browser."
26
+ }, async () => {
27
+ const text = await this.queryBrowser("dump");
28
+ return { content: [{ type: "text", text }] };
29
+ });
30
+ server.registerTool("oimdb_collection", {
31
+ description: "Get details of a specific registered collection: entity count, field names, index keys, and relations.",
32
+ inputSchema: nameSchema
33
+ }, async ({ name }) => {
34
+ const data = await this.queryBrowser("inspect");
35
+ const info = data.collections[name];
36
+ if (!info) {
37
+ const available = Object.keys(data.collections).join(", ") || "(none registered)";
38
+ return { content: [{ type: "text", text: `Collection '${name}' not found. Registered: ${available}` }] };
39
+ }
40
+ return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
41
+ });
42
+ server.registerTool("oimdb_computed", {
43
+ description: "Get status and resolved dependency names of a specific registered computed value. Shows fresh/stale/pending status, current value, and which registered sources it depends on.",
44
+ inputSchema: nameSchema
45
+ }, async ({ name }) => {
46
+ const data = await this.queryBrowser("inspect");
47
+ const info = data.computeds[name];
48
+ if (!info) {
49
+ const available = Object.keys(data.computeds).join(", ") || "(none registered)";
50
+ return { content: [{ type: "text", text: `Computed '${name}' not found. Registered: ${available}` }] };
51
+ }
52
+ return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
53
+ });
54
+ }
55
+ async queryBrowser(action) {
56
+ if (!this.latestBrowser) {
57
+ throw new Error(
58
+ `No browser tab connected to the OIMDB MCP bridge.
59
+
60
+ Add to your debug entry point:
61
+ import { registry } from '@oimdb/devtools';
62
+ registry.connect(); // connects to ws://localhost:${this.wsPort}
63
+
64
+ Then reload the page.`
65
+ );
66
+ }
67
+ return new Promise((resolve, reject) => {
68
+ const id = Math.random().toString(36).slice(2);
69
+ const timer = setTimeout(() => {
70
+ this.pending.delete(id);
71
+ reject(new Error("Browser response timeout (5s). Is the page active?"));
72
+ }, 5e3);
73
+ this.pending.set(id, { resolve, reject, timer });
74
+ this.latestBrowser.ws.send(JSON.stringify({ type: "request", id, action }));
75
+ });
76
+ }
77
+ setupWebSocketServer() {
78
+ this.wss = new WebSocketServer({ port: this.wsPort });
79
+ this.wss.on("error", (err) => {
80
+ if (err.code === "EADDRINUSE") {
81
+ process.stderr.write(
82
+ `[OIMDB MCP] Port ${this.wsPort} is already in use.
83
+ Set a different port: OIMDB_WS_PORT=7433 npx @oimdb/mcp
84
+ `
85
+ );
86
+ process.exit(1);
87
+ }
88
+ process.stderr.write(`[OIMDB MCP] WebSocket server error: ${err.message}
89
+ `);
90
+ process.exit(1);
91
+ });
92
+ this.wss.on("connection", (ws) => {
93
+ ws.on("message", (raw) => {
94
+ const msg = JSON.parse(raw.toString());
95
+ if (msg.type === "hello") {
96
+ this.latestBrowser = { ws, url: msg.url, title: msg.title };
97
+ process.stderr.write(`[OIMDB MCP] Browser connected: ${msg.title} (${msg.url})
98
+ `);
99
+ return;
100
+ }
101
+ if (msg.type === "response") {
102
+ const pending = this.pending.get(msg.id);
103
+ if (!pending) return;
104
+ this.pending.delete(msg.id);
105
+ clearTimeout(pending.timer);
106
+ if (msg.error) {
107
+ pending.reject(new Error(msg.error));
108
+ } else {
109
+ pending.resolve(msg.data);
110
+ }
111
+ }
112
+ });
113
+ ws.on("close", () => {
114
+ if (this.latestBrowser?.ws === ws) {
115
+ this.latestBrowser = null;
116
+ process.stderr.write("[OIMDB MCP] Browser disconnected\n");
117
+ }
118
+ });
119
+ });
120
+ process.stderr.write(`[OIMDB MCP] Bridge ready on ws://localhost:${this.wsPort}
121
+ `);
122
+ }
123
+ async start() {
124
+ this.setupWebSocketServer();
125
+ const transport = new StdioServerTransport();
126
+ await this.mcpServer.connect(transport);
127
+ }
128
+ };
129
+
130
+ // src/core/OIMDevUIServer.ts
131
+ import { createServer } from "http";
132
+ import { readFileSync } from "fs";
133
+ import { join, extname } from "path";
134
+ import { exec } from "child_process";
135
+ import { WebSocketServer as WebSocketServer2 } from "ws";
136
+ var MIME = {
137
+ ".html": "text/html; charset=utf-8",
138
+ ".js": "text/javascript; charset=utf-8",
139
+ ".css": "text/css; charset=utf-8"
140
+ };
141
+ var OIMDevUIServer = class {
142
+ constructor(options) {
143
+ this.wss = null;
144
+ this.latestBrowser = null;
145
+ this.pending = /* @__PURE__ */ new Map();
146
+ this.wsPort = options.wsPort ?? 7432;
147
+ this.uiPort = options.uiPort ?? 7433;
148
+ this.uiDir = options.uiDir;
149
+ this.shouldOpen = options.open ?? true;
150
+ }
151
+ async queryBrowser(action) {
152
+ if (!this.latestBrowser) return null;
153
+ return new Promise((resolve, reject) => {
154
+ const id = Math.random().toString(36).slice(2);
155
+ const timer = setTimeout(() => {
156
+ this.pending.delete(id);
157
+ reject(new Error("timeout"));
158
+ }, 5e3);
159
+ this.pending.set(id, { resolve, reject, timer });
160
+ this.latestBrowser.ws.send(JSON.stringify({ type: "request", id, action }));
161
+ });
162
+ }
163
+ setupBridge() {
164
+ this.wss = new WebSocketServer2({ port: this.wsPort });
165
+ this.wss.on("error", (err) => {
166
+ if (err.code === "EADDRINUSE") {
167
+ process.stderr.write(
168
+ `[OIMDB DevTools] Port ${this.wsPort} is already in use.
169
+ Set a different port: OIMDB_WS_PORT=7433 npx @oimdb/mcp devtools
170
+ `
171
+ );
172
+ process.exit(1);
173
+ }
174
+ process.stderr.write(`[OIMDB DevTools] Bridge error: ${err.message}
175
+ `);
176
+ process.exit(1);
177
+ });
178
+ this.wss.on("connection", (ws) => {
179
+ ws.on("message", (raw) => {
180
+ const msg = JSON.parse(raw.toString());
181
+ if (msg.type === "hello") {
182
+ this.latestBrowser = { ws, url: msg.url, title: msg.title };
183
+ process.stderr.write(`[OIMDB DevTools] Browser connected: ${msg.title} (${msg.url})
184
+ `);
185
+ return;
186
+ }
187
+ if (msg.type === "response") {
188
+ const req = this.pending.get(msg.id);
189
+ if (!req) return;
190
+ this.pending.delete(msg.id);
191
+ clearTimeout(req.timer);
192
+ if (msg.error) req.reject(new Error(msg.error));
193
+ else req.resolve(msg.data);
194
+ }
195
+ });
196
+ ws.on("close", () => {
197
+ if (this.latestBrowser?.ws === ws) {
198
+ this.latestBrowser = null;
199
+ process.stderr.write("[OIMDB DevTools] Browser disconnected\n");
200
+ }
201
+ });
202
+ });
203
+ process.stderr.write(`[OIMDB DevTools] Bridge on ws://localhost:${this.wsPort}
204
+ `);
205
+ }
206
+ setupHTTP() {
207
+ const server = createServer((req, res) => {
208
+ const url = req.url ?? "/";
209
+ res.setHeader("Access-Control-Allow-Origin", "*");
210
+ if (url === "/api/inspect") {
211
+ this.queryBrowser("inspect").then((data) => {
212
+ res.writeHead(200, { "Content-Type": "application/json" });
213
+ res.end(JSON.stringify({
214
+ connected: !!this.latestBrowser,
215
+ tab: this.latestBrowser ? { url: this.latestBrowser.url, title: this.latestBrowser.title } : null,
216
+ data
217
+ }));
218
+ }).catch(() => {
219
+ res.writeHead(200, { "Content-Type": "application/json" });
220
+ res.end(JSON.stringify({ connected: false, tab: null, data: null }));
221
+ });
222
+ return;
223
+ }
224
+ const filePath = join(this.uiDir, url === "/" ? "index.html" : url);
225
+ try {
226
+ const content = readFileSync(filePath);
227
+ res.writeHead(200, { "Content-Type": MIME[extname(filePath)] ?? "text/plain" });
228
+ res.end(content);
229
+ } catch {
230
+ res.writeHead(404);
231
+ res.end("Not found");
232
+ }
233
+ });
234
+ server.on("error", (err) => {
235
+ if (err.code === "EADDRINUSE") {
236
+ process.stderr.write(
237
+ `[OIMDB DevTools] UI port ${this.uiPort} is already in use.
238
+ Set a different port: OIMDB_UI_PORT=7434 npx @oimdb/mcp devtools
239
+ `
240
+ );
241
+ process.exit(1);
242
+ }
243
+ });
244
+ server.listen(this.uiPort, () => {
245
+ const uiUrl = `http://localhost:${this.uiPort}`;
246
+ process.stderr.write(`[OIMDB DevTools] UI ready \u2192 ${uiUrl}
247
+ `);
248
+ if (this.shouldOpen) this.openURL(uiUrl);
249
+ });
250
+ }
251
+ openURL(url) {
252
+ const cmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
253
+ exec(cmd, (err) => {
254
+ if (err) process.stderr.write(`[OIMDB DevTools] Could not open browser: ${err.message}
255
+ `);
256
+ });
257
+ }
258
+ start() {
259
+ this.setupBridge();
260
+ this.setupHTTP();
261
+ process.stderr.write(
262
+ `[OIMDB DevTools] Add to debug.ts: registry.connect();
263
+ `
264
+ );
265
+ }
266
+ };
267
+
268
+ // src/index.ts
269
+ function createOIMDevMcpServer(options) {
270
+ return new OIMDevMcpServer(options);
271
+ }
272
+ function createOIMDevUIServer(options) {
273
+ return new OIMDevUIServer(options);
274
+ }
275
+ export {
276
+ OIMDevMcpServer,
277
+ OIMDevUIServer,
278
+ createOIMDevMcpServer,
279
+ createOIMDevUIServer
280
+ };
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@oimdb/mcp",
3
+ "version": "1.0.0",
4
+ "description": "MCP server and CDP inspector for OIMDB — connects AI assistants to live browser state",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "sideEffects": false,
10
+ "publishConfig": {
11
+ "access": "public"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js",
17
+ "require": "./dist/index.cjs"
18
+ }
19
+ },
20
+ "bin": {
21
+ "oimdb-mcp": "bin/server.js",
22
+ "oimdb-devtools": "bin/devtools.js",
23
+ "oimdb-cdp": "bin/cdp.js"
24
+ },
25
+ "files": [
26
+ "dist",
27
+ "bin",
28
+ "ui"
29
+ ],
30
+ "scripts": {
31
+ "build": "tsup --config tsup.config.ts --tsconfig tsconfig.build.json",
32
+ "dev": "tsup --config tsup.config.ts --tsconfig tsconfig.build.json --watch",
33
+ "type-check": "tsc --noEmit",
34
+ "prepublishOnly": "npm run type-check && npm run build"
35
+ },
36
+ "dependencies": {
37
+ "@modelcontextprotocol/sdk": "^1.0.0",
38
+ "ws": "^8.18.0",
39
+ "zod": "^3.22.0"
40
+ },
41
+ "devDependencies": {
42
+ "@types/ws": "^8.5.10",
43
+ "tsup": "^8.0.0",
44
+ "typescript": "^5.2.2"
45
+ },
46
+ "keywords": [
47
+ "oimdb",
48
+ "mcp",
49
+ "devtools",
50
+ "debug",
51
+ "cdp",
52
+ "ai"
53
+ ],
54
+ "author": "Andrei Baikov",
55
+ "license": "MIT",
56
+ "repository": {
57
+ "type": "git",
58
+ "url": "git+https://github.com/abaikov/oimdb.git",
59
+ "directory": "packages/mcp"
60
+ }
61
+ }
package/ui/app.css ADDED
@@ -0,0 +1,238 @@
1
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2
+
3
+ :root {
4
+ --bg: #1e1e1e;
5
+ --bg-panel: #252526;
6
+ --bg-row: #2a2a2a;
7
+ --bg-hover: #2e2e2e;
8
+ --bg-input: #3c3c3c;
9
+ --border: #3e3e42;
10
+ --text: #d4d4d4;
11
+ --text-dim: #808080;
12
+ --text-hi: #f0f0f0;
13
+ --accent: #4fc3f7;
14
+ --green: #4caf50;
15
+ --yellow: #ffb74d;
16
+ --gray: #616161;
17
+ --tag-bg: #2d3f4a;
18
+ --tag-warn: #4a2d2d;
19
+ --radius: 4px;
20
+ --mono: 'Menlo', 'Consolas', 'DejaVu Sans Mono', monospace;
21
+ --toolbar-h: 40px;
22
+ }
23
+
24
+ html, body {
25
+ height: 100%;
26
+ background: var(--bg);
27
+ color: var(--text);
28
+ font: 13px/1.5 -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
29
+ overflow: hidden;
30
+ }
31
+
32
+ /* ── Toolbar ──────────────────────────────────── */
33
+
34
+ #toolbar {
35
+ display: flex;
36
+ align-items: center;
37
+ gap: 16px;
38
+ height: var(--toolbar-h);
39
+ padding: 0 16px;
40
+ background: var(--bg-panel);
41
+ border-bottom: 1px solid var(--border);
42
+ flex-shrink: 0;
43
+ }
44
+
45
+ .toolbar-brand { display: flex; align-items: baseline; gap: 6px; }
46
+ .brand-name { font-weight: 700; font-size: 15px; color: var(--accent); letter-spacing: -0.3px; }
47
+ .brand-sub { font-size: 11px; color: var(--text-dim); }
48
+
49
+ .toolbar-status { display: flex; align-items: center; gap: 7px; flex: 1; }
50
+
51
+ .conn-dot {
52
+ width: 8px; height: 8px; border-radius: 50%;
53
+ background: var(--gray); flex-shrink: 0;
54
+ transition: background 0.3s;
55
+ }
56
+ .conn-dot.connected { background: var(--green); }
57
+ .conn-dot.disconnected { background: var(--gray); }
58
+
59
+ #conn-label { font-size: 12px; color: var(--text-dim); }
60
+
61
+ .toolbar-actions {
62
+ display: flex; align-items: center; gap: 10px; margin-left: auto;
63
+ }
64
+ #last-updated { font-size: 11px; color: var(--text-dim); }
65
+
66
+ #btn-refresh {
67
+ background: none; border: 1px solid var(--border); border-radius: var(--radius);
68
+ color: var(--text); cursor: pointer; font-size: 16px; line-height: 1;
69
+ padding: 2px 8px; transition: background 0.15s;
70
+ }
71
+ #btn-refresh:hover { background: var(--bg-input); }
72
+
73
+ .auto-label {
74
+ display: flex; align-items: center; gap: 5px;
75
+ font-size: 12px; color: var(--text-dim); cursor: pointer; user-select: none;
76
+ }
77
+
78
+ /* ── Screens ──────────────────────────────────── */
79
+
80
+ .hidden { display: none !important; }
81
+
82
+ #screen-waiting {
83
+ height: calc(100vh - var(--toolbar-h));
84
+ display: flex; align-items: center; justify-content: center;
85
+ }
86
+
87
+ .waiting-card {
88
+ max-width: 420px; padding: 28px 32px;
89
+ background: var(--bg-panel); border: 1px solid var(--border);
90
+ border-radius: 8px; text-align: center;
91
+ }
92
+ .waiting-title {
93
+ font-size: 18px; font-weight: 600; color: var(--accent); margin-bottom: 14px;
94
+ }
95
+ .waiting-card p { color: var(--text-dim); margin-bottom: 10px; line-height: 1.6; }
96
+ .waiting-card code {
97
+ font-family: var(--mono); background: var(--bg-input);
98
+ padding: 2px 6px; border-radius: 3px; color: var(--accent);
99
+ }
100
+ .waiting-card pre {
101
+ font-family: var(--mono); font-size: 12px;
102
+ background: var(--bg-row); border: 1px solid var(--border);
103
+ border-radius: var(--radius); padding: 10px 14px;
104
+ margin: 10px 0; text-align: left; color: var(--text);
105
+ }
106
+ .waiting-hint { font-size: 12px !important; }
107
+
108
+ /* ── Two-column main ──────────────────────────── */
109
+
110
+ #screen-main {
111
+ display: flex;
112
+ height: calc(100vh - var(--toolbar-h));
113
+ }
114
+
115
+ .col {
116
+ flex: 1; display: flex; flex-direction: column;
117
+ min-width: 0; overflow: hidden;
118
+ border-right: 1px solid var(--border);
119
+ }
120
+ .col:last-child { border-right: none; }
121
+
122
+ .col-header {
123
+ display: flex; align-items: center; gap: 7px;
124
+ height: 32px; padding: 0 14px; flex-shrink: 0;
125
+ background: var(--bg-panel); border-bottom: 1px solid var(--border);
126
+ font-size: 11px; font-weight: 700; text-transform: uppercase;
127
+ letter-spacing: 0.06em; color: var(--text-dim);
128
+ }
129
+
130
+ .badge {
131
+ background: var(--bg-input); color: var(--text-dim);
132
+ border-radius: 10px; padding: 0 7px;
133
+ font-size: 10px; font-weight: 700; letter-spacing: 0;
134
+ }
135
+
136
+ .col-body { overflow-y: auto; flex: 1; }
137
+
138
+ /* ── Collection rows ──────────────────────────── */
139
+
140
+ .c-row { border-bottom: 1px solid var(--border); }
141
+
142
+ .c-summary {
143
+ display: flex; align-items: center; gap: 8px;
144
+ padding: 7px 14px; cursor: pointer; user-select: none;
145
+ background: var(--bg-row); transition: background 0.1s;
146
+ }
147
+ .c-summary:hover { background: var(--bg-hover); }
148
+
149
+ .c-arrow {
150
+ font-size: 9px; color: var(--text-dim); width: 10px;
151
+ transition: transform 0.15s; flex-shrink: 0;
152
+ }
153
+ .c-arrow.open { transform: rotate(90deg); }
154
+
155
+ .c-name {
156
+ font-family: var(--mono); font-size: 13px;
157
+ font-weight: 600; color: var(--text-hi); flex: 1;
158
+ }
159
+
160
+ .c-count { font-size: 11px; color: var(--text-dim); }
161
+
162
+ .c-detail {
163
+ display: none; padding: 8px 14px 10px 32px;
164
+ background: var(--bg); border-top: 1px solid var(--border);
165
+ font-size: 11px; font-family: var(--mono);
166
+ }
167
+ .c-detail.open { display: block; }
168
+
169
+ .d-row {
170
+ display: flex; gap: 8px; margin-bottom: 4px;
171
+ flex-wrap: wrap; align-items: flex-start;
172
+ }
173
+ .d-label { color: var(--text-dim); min-width: 72px; flex-shrink: 0; }
174
+
175
+ .tag {
176
+ display: inline-block; background: var(--tag-bg);
177
+ color: var(--text); border-radius: 3px;
178
+ padding: 1px 7px; margin: 1px 2px 1px 0; font-size: 11px;
179
+ }
180
+ .tag-warn { background: var(--tag-warn); color: #ffaa80; }
181
+ .tag-rel-arrow { color: var(--accent); margin: 0 3px; }
182
+
183
+ /* ── Computed rows ────────────────────────────── */
184
+
185
+ .comp-row {
186
+ padding: 8px 14px; border-bottom: 1px solid var(--border);
187
+ background: var(--bg-row); transition: background 0.1s;
188
+ }
189
+ .comp-row:hover { background: var(--bg-hover); }
190
+
191
+ .comp-top { display: flex; align-items: center; gap: 8px; }
192
+
193
+ .status-dot {
194
+ width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0;
195
+ }
196
+ .status-dot.fresh { background: var(--green); }
197
+ .status-dot.stale { background: var(--yellow); }
198
+ .status-dot.pending { background: var(--gray); }
199
+
200
+ .comp-name {
201
+ font-family: var(--mono); font-size: 13px;
202
+ font-weight: 600; color: var(--text-hi); flex: 1;
203
+ min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
204
+ }
205
+
206
+ .status-pill {
207
+ font-size: 10px; font-weight: 700; letter-spacing: 0.06em;
208
+ padding: 1px 8px; border-radius: 10px; flex-shrink: 0;
209
+ }
210
+ .status-pill.fresh { background: #1a3a1c; color: #81c784; }
211
+ .status-pill.stale { background: #3a2a10; color: #ffcc80; }
212
+ .status-pill.pending { background: #2a2a2a; color: #90a4ae; }
213
+
214
+ .comp-value {
215
+ font-family: var(--mono); font-size: 12px; color: var(--accent);
216
+ max-width: 200px; overflow: hidden; text-overflow: ellipsis;
217
+ white-space: nowrap; cursor: default;
218
+ }
219
+
220
+ .comp-deps {
221
+ display: flex; flex-wrap: wrap; align-items: center;
222
+ gap: 3px; margin-top: 5px; padding-left: 17px;
223
+ }
224
+ .comp-deps-label { font-size: 11px; color: var(--text-dim); margin-right: 4px; }
225
+
226
+ /* ── Scrollbar ────────────────────────────────── */
227
+
228
+ ::-webkit-scrollbar { width: 6px; }
229
+ ::-webkit-scrollbar-track { background: var(--bg); }
230
+ ::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; }
231
+ ::-webkit-scrollbar-thumb:hover { background: #555; }
232
+
233
+ /* ── Empty states ─────────────────────────────── */
234
+
235
+ .empty {
236
+ padding: 20px 16px; color: var(--text-dim);
237
+ font-size: 12px; text-align: center;
238
+ }