@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/README.md +131 -0
- package/bin/cdp.js +96 -0
- package/bin/devtools.js +11 -0
- package/bin/server.js +9 -0
- package/dist/index.cjs +310 -0
- package/dist/index.d.cts +44 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +280 -0
- package/package.json +61 -0
- package/ui/app.css +238 -0
- package/ui/app.js +273 -0
- package/ui/index.html +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# @oimdb/mcp
|
|
2
|
+
|
|
3
|
+
Connects AI assistants to a live OIMDB store running in the browser. Provides MCP tools for Claude Code and an optional CDP inspector for any AI that can run a shell command.
|
|
4
|
+
|
|
5
|
+
## For AI Assistants
|
|
6
|
+
|
|
7
|
+
If you are an AI assistant and this package is present in the project, you can get live OIMDB state from the running browser tab.
|
|
8
|
+
|
|
9
|
+
**Check if MCP is already configured** — look for `mcpServers.oimdb` in `.claude/settings.json` or `~/.claude/settings.json`. If it is, the tools `oimdb_inspect`, `oimdb_dump`, `oimdb_collection`, and `oimdb_computed` are available. Call `oimdb_inspect` now to see the current state.
|
|
10
|
+
|
|
11
|
+
**If MCP is not configured yet**, add this to the project's MCP config:
|
|
12
|
+
|
|
13
|
+
- Claude Code: `.claude/settings.json`
|
|
14
|
+
- Cursor: `.cursor/mcp.json`
|
|
15
|
+
|
|
16
|
+
```json
|
|
17
|
+
{
|
|
18
|
+
"mcpServers": {
|
|
19
|
+
"oimdb": {
|
|
20
|
+
"command": "npx",
|
|
21
|
+
"args": ["-y", "@oimdb/mcp"]
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Then ask the user to:
|
|
28
|
+
1. Reload Claude Code so the MCP server starts
|
|
29
|
+
2. Make sure `debug.ts` calls `registry.connect()` and the app is running in the browser
|
|
30
|
+
|
|
31
|
+
**If the user just wants a one-shot snapshot** without MCP setup, run this via Bash (requires Chrome launched with `--remote-debugging-port=9222`):
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx @oimdb/mcp cdp
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Human Setup
|
|
38
|
+
|
|
39
|
+
### Standalone DevTools UI (for humans)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx @oimdb/mcp devtools
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Opens `http://localhost:7433` automatically. Shows live collections and computeds from the running browser tab. No Chrome extension needed.
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
OIMDB_WS_PORT=7433 OIMDB_UI_PORT=7434 npx @oimdb/mcp devtools # custom ports
|
|
49
|
+
npx @oimdb/mcp devtools --no-open # don't open browser
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 1. Add MCP config (for AI tools)
|
|
53
|
+
|
|
54
|
+
Claude Code — `.claude/settings.json`:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"oimdb": {
|
|
60
|
+
"command": "npx",
|
|
61
|
+
"args": ["-y", "@oimdb/mcp"]
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
Cursor — `.cursor/mcp.json`:
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"mcpServers": {
|
|
72
|
+
"oimdb": {
|
|
73
|
+
"command": "npx",
|
|
74
|
+
"args": ["-y", "@oimdb/mcp"]
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### 2. Connect the browser
|
|
81
|
+
|
|
82
|
+
```ts
|
|
83
|
+
// src/debug.ts
|
|
84
|
+
import './store/users.debug';
|
|
85
|
+
import './store/tasks.debug';
|
|
86
|
+
import { registry } from '@oimdb/devtools';
|
|
87
|
+
|
|
88
|
+
if (typeof window !== 'undefined') {
|
|
89
|
+
(window as Record<string, unknown>).__OIMDB_DEV__ = registry;
|
|
90
|
+
registry.connect(); // connects to ws://localhost:7432
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### 3. Use from Claude
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
Call oimdb_inspect to see what's in the store.
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Available MCP Tools
|
|
101
|
+
|
|
102
|
+
| Tool | Description |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `oimdb_inspect` | Full structured state — all collections, computeds, dep graph |
|
|
105
|
+
| `oimdb_dump` | Human-readable text summary (same as `registry.dump()`) |
|
|
106
|
+
| `oimdb_collection` | Details of one collection by name |
|
|
107
|
+
| `oimdb_computed` | Status + deps of one computed by name |
|
|
108
|
+
|
|
109
|
+
## CDP Inspector (no browser-side code needed)
|
|
110
|
+
|
|
111
|
+
Launch Chrome with `--remote-debugging-port=9222`, then:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
npx @oimdb/mcp cdp
|
|
115
|
+
npx @oimdb/mcp cdp | jq .collections
|
|
116
|
+
OIMDB_CDP_URL=http://localhost:9222 npx @oimdb/mcp cdp
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Port
|
|
120
|
+
|
|
121
|
+
The MCP bridge WebSocket runs on port `7432` by default. Change it:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
OIMDB_WS_PORT=7433 npx @oimdb/mcp
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Update `registry.connect()` accordingly:
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
registry.connect('ws://localhost:7433');
|
|
131
|
+
```
|
package/bin/cdp.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* One-shot CDP inspector: connects to Chrome, evaluates __OIMDB_DEV__.inspect(),
|
|
4
|
+
* prints JSON to stdout. Pipe to jq or paste into an AI prompt.
|
|
5
|
+
*
|
|
6
|
+
* Requirements:
|
|
7
|
+
* Launch Chrome with: --remote-debugging-port=9222
|
|
8
|
+
* (or set CDP_URL env var to point to a different address)
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* npx @oimdb/mcp cdp
|
|
12
|
+
* OIMDB_CDP_URL=http://localhost:9222 npx @oimdb/mcp cdp
|
|
13
|
+
* npx @oimdb/mcp cdp | jq .collections
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const cdpBase = process.env.OIMDB_CDP_URL || 'http://localhost:9222';
|
|
17
|
+
|
|
18
|
+
async function run() {
|
|
19
|
+
// 1. List open tabs
|
|
20
|
+
let tabs;
|
|
21
|
+
try {
|
|
22
|
+
const res = await fetch(`${cdpBase}/json/list`);
|
|
23
|
+
tabs = await res.json();
|
|
24
|
+
} catch {
|
|
25
|
+
process.stderr.write(
|
|
26
|
+
`[OIMDB CDP] Cannot reach Chrome at ${cdpBase}\n` +
|
|
27
|
+
`Launch Chrome with: --remote-debugging-port=9222\n`
|
|
28
|
+
);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Find first tab that has a debugger URL
|
|
33
|
+
const tab = tabs.find((t) => t.webSocketDebuggerUrl);
|
|
34
|
+
if (!tab) {
|
|
35
|
+
process.stderr.write('[OIMDB CDP] No debuggable tabs found.\n');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
process.stderr.write(`[OIMDB CDP] Connected to: ${tab.title} (${tab.url})\n`);
|
|
40
|
+
|
|
41
|
+
// 2. Open CDP WebSocket
|
|
42
|
+
const { WebSocket } = await import('ws');
|
|
43
|
+
const ws = new WebSocket(tab.webSocketDebuggerUrl);
|
|
44
|
+
|
|
45
|
+
await new Promise((resolve, reject) => {
|
|
46
|
+
const timeout = setTimeout(() => {
|
|
47
|
+
ws.close();
|
|
48
|
+
reject(new Error('CDP timeout (5s)'));
|
|
49
|
+
}, 5000);
|
|
50
|
+
|
|
51
|
+
ws.once('open', () => {
|
|
52
|
+
ws.send(JSON.stringify({
|
|
53
|
+
id: 1,
|
|
54
|
+
method: 'Runtime.evaluate',
|
|
55
|
+
params: {
|
|
56
|
+
expression: [
|
|
57
|
+
'(function(){',
|
|
58
|
+
' if(!window.__OIMDB_DEV__) return null;',
|
|
59
|
+
' return JSON.parse(JSON.stringify(window.__OIMDB_DEV__.inspect()));',
|
|
60
|
+
'})()',
|
|
61
|
+
].join(''),
|
|
62
|
+
returnByValue: true,
|
|
63
|
+
awaitPromise: false,
|
|
64
|
+
},
|
|
65
|
+
}));
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
ws.on('message', (raw) => {
|
|
69
|
+
const msg = JSON.parse(raw.toString());
|
|
70
|
+
if (msg.id !== 1) return;
|
|
71
|
+
clearTimeout(timeout);
|
|
72
|
+
ws.close();
|
|
73
|
+
|
|
74
|
+
const val = msg.result?.result?.value;
|
|
75
|
+
if (val === null || val === undefined) {
|
|
76
|
+
process.stderr.write(
|
|
77
|
+
'[OIMDB CDP] __OIMDB_DEV__ not found on this page.\n' +
|
|
78
|
+
'Make sure debug.ts sets: window.__OIMDB_DEV__ = registry\n'
|
|
79
|
+
);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
process.stdout.write(JSON.stringify(val, null, 2) + '\n');
|
|
83
|
+
resolve(undefined);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
ws.once('error', (err) => {
|
|
87
|
+
clearTimeout(timeout);
|
|
88
|
+
reject(err);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
run().catch((err) => {
|
|
94
|
+
process.stderr.write('[OIMDB CDP] Error: ' + err.message + '\n');
|
|
95
|
+
process.exit(1);
|
|
96
|
+
});
|
package/bin/devtools.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { OIMDevUIServer } = require('../dist/index.cjs');
|
|
4
|
+
|
|
5
|
+
const wsPort = process.env.OIMDB_WS_PORT ? Number(process.env.OIMDB_WS_PORT) : 7432;
|
|
6
|
+
const uiPort = process.env.OIMDB_UI_PORT ? Number(process.env.OIMDB_UI_PORT) : 7433;
|
|
7
|
+
const noOpen = process.argv.includes('--no-open');
|
|
8
|
+
const uiDir = path.join(__dirname, '../ui');
|
|
9
|
+
|
|
10
|
+
const server = new OIMDevUIServer({ wsPort, uiPort, uiDir, open: !noOpen });
|
|
11
|
+
server.start();
|
package/bin/server.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { OIMDevMcpServer } = require('../dist/index.cjs');
|
|
3
|
+
|
|
4
|
+
const wsPort = process.env.OIMDB_WS_PORT ? Number(process.env.OIMDB_WS_PORT) : 7432;
|
|
5
|
+
const server = new OIMDevMcpServer({ wsPort });
|
|
6
|
+
server.start().catch((err) => {
|
|
7
|
+
process.stderr.write('[OIMDB MCP] Fatal: ' + err.message + '\n');
|
|
8
|
+
process.exit(1);
|
|
9
|
+
});
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
OIMDevMcpServer: () => OIMDevMcpServer,
|
|
24
|
+
OIMDevUIServer: () => OIMDevUIServer,
|
|
25
|
+
createOIMDevMcpServer: () => createOIMDevMcpServer,
|
|
26
|
+
createOIMDevUIServer: () => createOIMDevUIServer
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/core/OIMDevMcpServer.ts
|
|
31
|
+
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
32
|
+
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
33
|
+
var import_ws = require("ws");
|
|
34
|
+
var import_zod = require("zod");
|
|
35
|
+
var OIMDevMcpServer = class {
|
|
36
|
+
constructor(options = {}) {
|
|
37
|
+
this.wss = null;
|
|
38
|
+
this.latestBrowser = null;
|
|
39
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
40
|
+
this.wsPort = options.wsPort ?? 7432;
|
|
41
|
+
this.mcpServer = new import_mcp.McpServer({ name: "oimdb", version: "1.0.0" });
|
|
42
|
+
this.registerTools();
|
|
43
|
+
}
|
|
44
|
+
registerTools() {
|
|
45
|
+
const server = this.mcpServer;
|
|
46
|
+
const nameSchema = { name: import_zod.z.string().describe("Registered name") };
|
|
47
|
+
server.registerTool("oimdb_inspect", {
|
|
48
|
+
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."
|
|
49
|
+
}, async () => {
|
|
50
|
+
const data = await this.queryBrowser("inspect");
|
|
51
|
+
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
52
|
+
});
|
|
53
|
+
server.registerTool("oimdb_dump", {
|
|
54
|
+
description: "Get a human-readable text summary of OIMDB state. Same output as registry.dump() in the browser."
|
|
55
|
+
}, async () => {
|
|
56
|
+
const text = await this.queryBrowser("dump");
|
|
57
|
+
return { content: [{ type: "text", text }] };
|
|
58
|
+
});
|
|
59
|
+
server.registerTool("oimdb_collection", {
|
|
60
|
+
description: "Get details of a specific registered collection: entity count, field names, index keys, and relations.",
|
|
61
|
+
inputSchema: nameSchema
|
|
62
|
+
}, async ({ name }) => {
|
|
63
|
+
const data = await this.queryBrowser("inspect");
|
|
64
|
+
const info = data.collections[name];
|
|
65
|
+
if (!info) {
|
|
66
|
+
const available = Object.keys(data.collections).join(", ") || "(none registered)";
|
|
67
|
+
return { content: [{ type: "text", text: `Collection '${name}' not found. Registered: ${available}` }] };
|
|
68
|
+
}
|
|
69
|
+
return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
|
|
70
|
+
});
|
|
71
|
+
server.registerTool("oimdb_computed", {
|
|
72
|
+
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.",
|
|
73
|
+
inputSchema: nameSchema
|
|
74
|
+
}, async ({ name }) => {
|
|
75
|
+
const data = await this.queryBrowser("inspect");
|
|
76
|
+
const info = data.computeds[name];
|
|
77
|
+
if (!info) {
|
|
78
|
+
const available = Object.keys(data.computeds).join(", ") || "(none registered)";
|
|
79
|
+
return { content: [{ type: "text", text: `Computed '${name}' not found. Registered: ${available}` }] };
|
|
80
|
+
}
|
|
81
|
+
return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
async queryBrowser(action) {
|
|
85
|
+
if (!this.latestBrowser) {
|
|
86
|
+
throw new Error(
|
|
87
|
+
`No browser tab connected to the OIMDB MCP bridge.
|
|
88
|
+
|
|
89
|
+
Add to your debug entry point:
|
|
90
|
+
import { registry } from '@oimdb/devtools';
|
|
91
|
+
registry.connect(); // connects to ws://localhost:${this.wsPort}
|
|
92
|
+
|
|
93
|
+
Then reload the page.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const id = Math.random().toString(36).slice(2);
|
|
98
|
+
const timer = setTimeout(() => {
|
|
99
|
+
this.pending.delete(id);
|
|
100
|
+
reject(new Error("Browser response timeout (5s). Is the page active?"));
|
|
101
|
+
}, 5e3);
|
|
102
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
103
|
+
this.latestBrowser.ws.send(JSON.stringify({ type: "request", id, action }));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
setupWebSocketServer() {
|
|
107
|
+
this.wss = new import_ws.WebSocketServer({ port: this.wsPort });
|
|
108
|
+
this.wss.on("error", (err) => {
|
|
109
|
+
if (err.code === "EADDRINUSE") {
|
|
110
|
+
process.stderr.write(
|
|
111
|
+
`[OIMDB MCP] Port ${this.wsPort} is already in use.
|
|
112
|
+
Set a different port: OIMDB_WS_PORT=7433 npx @oimdb/mcp
|
|
113
|
+
`
|
|
114
|
+
);
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
process.stderr.write(`[OIMDB MCP] WebSocket server error: ${err.message}
|
|
118
|
+
`);
|
|
119
|
+
process.exit(1);
|
|
120
|
+
});
|
|
121
|
+
this.wss.on("connection", (ws) => {
|
|
122
|
+
ws.on("message", (raw) => {
|
|
123
|
+
const msg = JSON.parse(raw.toString());
|
|
124
|
+
if (msg.type === "hello") {
|
|
125
|
+
this.latestBrowser = { ws, url: msg.url, title: msg.title };
|
|
126
|
+
process.stderr.write(`[OIMDB MCP] Browser connected: ${msg.title} (${msg.url})
|
|
127
|
+
`);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (msg.type === "response") {
|
|
131
|
+
const pending = this.pending.get(msg.id);
|
|
132
|
+
if (!pending) return;
|
|
133
|
+
this.pending.delete(msg.id);
|
|
134
|
+
clearTimeout(pending.timer);
|
|
135
|
+
if (msg.error) {
|
|
136
|
+
pending.reject(new Error(msg.error));
|
|
137
|
+
} else {
|
|
138
|
+
pending.resolve(msg.data);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
ws.on("close", () => {
|
|
143
|
+
if (this.latestBrowser?.ws === ws) {
|
|
144
|
+
this.latestBrowser = null;
|
|
145
|
+
process.stderr.write("[OIMDB MCP] Browser disconnected\n");
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
process.stderr.write(`[OIMDB MCP] Bridge ready on ws://localhost:${this.wsPort}
|
|
150
|
+
`);
|
|
151
|
+
}
|
|
152
|
+
async start() {
|
|
153
|
+
this.setupWebSocketServer();
|
|
154
|
+
const transport = new import_stdio.StdioServerTransport();
|
|
155
|
+
await this.mcpServer.connect(transport);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
// src/core/OIMDevUIServer.ts
|
|
160
|
+
var import_http = require("http");
|
|
161
|
+
var import_fs = require("fs");
|
|
162
|
+
var import_path = require("path");
|
|
163
|
+
var import_child_process = require("child_process");
|
|
164
|
+
var import_ws2 = require("ws");
|
|
165
|
+
var MIME = {
|
|
166
|
+
".html": "text/html; charset=utf-8",
|
|
167
|
+
".js": "text/javascript; charset=utf-8",
|
|
168
|
+
".css": "text/css; charset=utf-8"
|
|
169
|
+
};
|
|
170
|
+
var OIMDevUIServer = class {
|
|
171
|
+
constructor(options) {
|
|
172
|
+
this.wss = null;
|
|
173
|
+
this.latestBrowser = null;
|
|
174
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
175
|
+
this.wsPort = options.wsPort ?? 7432;
|
|
176
|
+
this.uiPort = options.uiPort ?? 7433;
|
|
177
|
+
this.uiDir = options.uiDir;
|
|
178
|
+
this.shouldOpen = options.open ?? true;
|
|
179
|
+
}
|
|
180
|
+
async queryBrowser(action) {
|
|
181
|
+
if (!this.latestBrowser) return null;
|
|
182
|
+
return new Promise((resolve, reject) => {
|
|
183
|
+
const id = Math.random().toString(36).slice(2);
|
|
184
|
+
const timer = setTimeout(() => {
|
|
185
|
+
this.pending.delete(id);
|
|
186
|
+
reject(new Error("timeout"));
|
|
187
|
+
}, 5e3);
|
|
188
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
189
|
+
this.latestBrowser.ws.send(JSON.stringify({ type: "request", id, action }));
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
setupBridge() {
|
|
193
|
+
this.wss = new import_ws2.WebSocketServer({ port: this.wsPort });
|
|
194
|
+
this.wss.on("error", (err) => {
|
|
195
|
+
if (err.code === "EADDRINUSE") {
|
|
196
|
+
process.stderr.write(
|
|
197
|
+
`[OIMDB DevTools] Port ${this.wsPort} is already in use.
|
|
198
|
+
Set a different port: OIMDB_WS_PORT=7433 npx @oimdb/mcp devtools
|
|
199
|
+
`
|
|
200
|
+
);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
process.stderr.write(`[OIMDB DevTools] Bridge error: ${err.message}
|
|
204
|
+
`);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
});
|
|
207
|
+
this.wss.on("connection", (ws) => {
|
|
208
|
+
ws.on("message", (raw) => {
|
|
209
|
+
const msg = JSON.parse(raw.toString());
|
|
210
|
+
if (msg.type === "hello") {
|
|
211
|
+
this.latestBrowser = { ws, url: msg.url, title: msg.title };
|
|
212
|
+
process.stderr.write(`[OIMDB DevTools] Browser connected: ${msg.title} (${msg.url})
|
|
213
|
+
`);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (msg.type === "response") {
|
|
217
|
+
const req = this.pending.get(msg.id);
|
|
218
|
+
if (!req) return;
|
|
219
|
+
this.pending.delete(msg.id);
|
|
220
|
+
clearTimeout(req.timer);
|
|
221
|
+
if (msg.error) req.reject(new Error(msg.error));
|
|
222
|
+
else req.resolve(msg.data);
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
ws.on("close", () => {
|
|
226
|
+
if (this.latestBrowser?.ws === ws) {
|
|
227
|
+
this.latestBrowser = null;
|
|
228
|
+
process.stderr.write("[OIMDB DevTools] Browser disconnected\n");
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
process.stderr.write(`[OIMDB DevTools] Bridge on ws://localhost:${this.wsPort}
|
|
233
|
+
`);
|
|
234
|
+
}
|
|
235
|
+
setupHTTP() {
|
|
236
|
+
const server = (0, import_http.createServer)((req, res) => {
|
|
237
|
+
const url = req.url ?? "/";
|
|
238
|
+
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
239
|
+
if (url === "/api/inspect") {
|
|
240
|
+
this.queryBrowser("inspect").then((data) => {
|
|
241
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
242
|
+
res.end(JSON.stringify({
|
|
243
|
+
connected: !!this.latestBrowser,
|
|
244
|
+
tab: this.latestBrowser ? { url: this.latestBrowser.url, title: this.latestBrowser.title } : null,
|
|
245
|
+
data
|
|
246
|
+
}));
|
|
247
|
+
}).catch(() => {
|
|
248
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
249
|
+
res.end(JSON.stringify({ connected: false, tab: null, data: null }));
|
|
250
|
+
});
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
const filePath = (0, import_path.join)(this.uiDir, url === "/" ? "index.html" : url);
|
|
254
|
+
try {
|
|
255
|
+
const content = (0, import_fs.readFileSync)(filePath);
|
|
256
|
+
res.writeHead(200, { "Content-Type": MIME[(0, import_path.extname)(filePath)] ?? "text/plain" });
|
|
257
|
+
res.end(content);
|
|
258
|
+
} catch {
|
|
259
|
+
res.writeHead(404);
|
|
260
|
+
res.end("Not found");
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
server.on("error", (err) => {
|
|
264
|
+
if (err.code === "EADDRINUSE") {
|
|
265
|
+
process.stderr.write(
|
|
266
|
+
`[OIMDB DevTools] UI port ${this.uiPort} is already in use.
|
|
267
|
+
Set a different port: OIMDB_UI_PORT=7434 npx @oimdb/mcp devtools
|
|
268
|
+
`
|
|
269
|
+
);
|
|
270
|
+
process.exit(1);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
server.listen(this.uiPort, () => {
|
|
274
|
+
const uiUrl = `http://localhost:${this.uiPort}`;
|
|
275
|
+
process.stderr.write(`[OIMDB DevTools] UI ready \u2192 ${uiUrl}
|
|
276
|
+
`);
|
|
277
|
+
if (this.shouldOpen) this.openURL(uiUrl);
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
openURL(url) {
|
|
281
|
+
const cmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
282
|
+
(0, import_child_process.exec)(cmd, (err) => {
|
|
283
|
+
if (err) process.stderr.write(`[OIMDB DevTools] Could not open browser: ${err.message}
|
|
284
|
+
`);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
start() {
|
|
288
|
+
this.setupBridge();
|
|
289
|
+
this.setupHTTP();
|
|
290
|
+
process.stderr.write(
|
|
291
|
+
`[OIMDB DevTools] Add to debug.ts: registry.connect();
|
|
292
|
+
`
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
|
|
297
|
+
// src/index.ts
|
|
298
|
+
function createOIMDevMcpServer(options) {
|
|
299
|
+
return new OIMDevMcpServer(options);
|
|
300
|
+
}
|
|
301
|
+
function createOIMDevUIServer(options) {
|
|
302
|
+
return new OIMDevUIServer(options);
|
|
303
|
+
}
|
|
304
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
305
|
+
0 && (module.exports = {
|
|
306
|
+
OIMDevMcpServer,
|
|
307
|
+
OIMDevUIServer,
|
|
308
|
+
createOIMDevMcpServer,
|
|
309
|
+
createOIMDevUIServer
|
|
310
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
type TOIMDevMcpOptions = {
|
|
2
|
+
/** Port for the WebSocket bridge that browser tabs connect to. Default: 7432 */
|
|
3
|
+
wsPort?: number;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
declare class OIMDevMcpServer {
|
|
7
|
+
private readonly mcpServer;
|
|
8
|
+
private readonly wsPort;
|
|
9
|
+
private wss;
|
|
10
|
+
private latestBrowser;
|
|
11
|
+
private readonly pending;
|
|
12
|
+
constructor(options?: TOIMDevMcpOptions);
|
|
13
|
+
private registerTools;
|
|
14
|
+
private queryBrowser;
|
|
15
|
+
private setupWebSocketServer;
|
|
16
|
+
start(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type TOIMDevUIServerOptions = {
|
|
20
|
+
wsPort?: number;
|
|
21
|
+
uiPort?: number;
|
|
22
|
+
uiDir: string;
|
|
23
|
+
open?: boolean;
|
|
24
|
+
};
|
|
25
|
+
declare class OIMDevUIServer {
|
|
26
|
+
private readonly wsPort;
|
|
27
|
+
private readonly uiPort;
|
|
28
|
+
private readonly uiDir;
|
|
29
|
+
private readonly shouldOpen;
|
|
30
|
+
private wss;
|
|
31
|
+
private latestBrowser;
|
|
32
|
+
private readonly pending;
|
|
33
|
+
constructor(options: TOIMDevUIServerOptions);
|
|
34
|
+
private queryBrowser;
|
|
35
|
+
private setupBridge;
|
|
36
|
+
private setupHTTP;
|
|
37
|
+
private openURL;
|
|
38
|
+
start(): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
declare function createOIMDevMcpServer(options?: TOIMDevMcpOptions): OIMDevMcpServer;
|
|
42
|
+
declare function createOIMDevUIServer(options: TOIMDevUIServerOptions): OIMDevUIServer;
|
|
43
|
+
|
|
44
|
+
export { OIMDevMcpServer, OIMDevUIServer, type TOIMDevMcpOptions, type TOIMDevUIServerOptions, createOIMDevMcpServer, createOIMDevUIServer };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
type TOIMDevMcpOptions = {
|
|
2
|
+
/** Port for the WebSocket bridge that browser tabs connect to. Default: 7432 */
|
|
3
|
+
wsPort?: number;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
declare class OIMDevMcpServer {
|
|
7
|
+
private readonly mcpServer;
|
|
8
|
+
private readonly wsPort;
|
|
9
|
+
private wss;
|
|
10
|
+
private latestBrowser;
|
|
11
|
+
private readonly pending;
|
|
12
|
+
constructor(options?: TOIMDevMcpOptions);
|
|
13
|
+
private registerTools;
|
|
14
|
+
private queryBrowser;
|
|
15
|
+
private setupWebSocketServer;
|
|
16
|
+
start(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type TOIMDevUIServerOptions = {
|
|
20
|
+
wsPort?: number;
|
|
21
|
+
uiPort?: number;
|
|
22
|
+
uiDir: string;
|
|
23
|
+
open?: boolean;
|
|
24
|
+
};
|
|
25
|
+
declare class OIMDevUIServer {
|
|
26
|
+
private readonly wsPort;
|
|
27
|
+
private readonly uiPort;
|
|
28
|
+
private readonly uiDir;
|
|
29
|
+
private readonly shouldOpen;
|
|
30
|
+
private wss;
|
|
31
|
+
private latestBrowser;
|
|
32
|
+
private readonly pending;
|
|
33
|
+
constructor(options: TOIMDevUIServerOptions);
|
|
34
|
+
private queryBrowser;
|
|
35
|
+
private setupBridge;
|
|
36
|
+
private setupHTTP;
|
|
37
|
+
private openURL;
|
|
38
|
+
start(): void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
declare function createOIMDevMcpServer(options?: TOIMDevMcpOptions): OIMDevMcpServer;
|
|
42
|
+
declare function createOIMDevUIServer(options: TOIMDevUIServerOptions): OIMDevUIServer;
|
|
43
|
+
|
|
44
|
+
export { OIMDevMcpServer, OIMDevUIServer, type TOIMDevMcpOptions, type TOIMDevUIServerOptions, createOIMDevMcpServer, createOIMDevUIServer };
|