@oimdb/mcp 1.2.0 → 3.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 +25 -129
- package/bin/oimdb-mcp.cjs +13 -14
- package/bin/server.cjs +10 -1
- package/dist/index.cjs +32 -305
- package/dist/index.d.cts +13 -65
- package/dist/index.d.ts +13 -65
- package/dist/index.js +37 -302
- package/package.json +15 -8
- package/bin/cdp.cjs +0 -98
- package/bin/devtools.cjs +0 -17
- package/ui/app.css +0 -323
- package/ui/app.js +0 -409
- package/ui/index.html +0 -63
package/README.md
CHANGED
|
@@ -1,59 +1,19 @@
|
|
|
1
1
|
# @oimdb/mcp
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The MCP server for OIMDB. It gives an AI assistant the tools `oimdb_inspect`, `oimdb_dump`,
|
|
4
|
+
`oimdb_collection` and `oimdb_computed` over a live browser tab or an in-process model. It is built on
|
|
5
|
+
`@oimdb/cli`, which answers the same four questions from a shell: an assistant that can run a command
|
|
6
|
+
can use `npx @oimdb/cli inspect` instead.
|
|
4
7
|
|
|
5
|
-
📖 **[Full documentation](https://
|
|
8
|
+
📖 **[Full documentation](https://oimdb.org/)**
|
|
6
9
|
|
|
7
10
|
## For AI Assistants
|
|
8
11
|
|
|
9
|
-
|
|
12
|
+
**Check whether MCP is already configured.** Look for `mcpServers.oimdb` in `.claude/settings.json` or
|
|
13
|
+
`~/.claude/settings.json`. If it is there, call `oimdb_inspect` now to see the current state.
|
|
10
14
|
|
|
11
|
-
**
|
|
12
|
-
|
|
13
|
-
**If MCP is not configured yet**, add this to the project's MCP config:
|
|
14
|
-
|
|
15
|
-
- Claude Code: `.claude/settings.json`
|
|
16
|
-
- Cursor: `.cursor/mcp.json`
|
|
17
|
-
|
|
18
|
-
```json
|
|
19
|
-
{
|
|
20
|
-
"mcpServers": {
|
|
21
|
-
"oimdb": {
|
|
22
|
-
"command": "npx",
|
|
23
|
-
"args": ["-y", "@oimdb/mcp"]
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
Then ask the user to:
|
|
30
|
-
1. Reload Claude Code so the MCP server starts
|
|
31
|
-
2. Make sure `debug.ts` calls `registry.connect()` and the app is running in the browser
|
|
32
|
-
|
|
33
|
-
**If the user just wants a one-shot snapshot** without MCP setup, run this via Bash (requires Chrome launched with `--remote-debugging-port=9222`):
|
|
34
|
-
|
|
35
|
-
```bash
|
|
36
|
-
npx @oimdb/mcp cdp
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
## Human Setup
|
|
40
|
-
|
|
41
|
-
### Standalone DevTools UI (for humans)
|
|
42
|
-
|
|
43
|
-
```bash
|
|
44
|
-
npx @oimdb/mcp devtools
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Opens `http://localhost:7433` automatically. Shows live collections and computeds from the running browser tab. No Chrome extension needed.
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
OIMDB_WS_PORT=7433 OIMDB_UI_PORT=7434 npx @oimdb/mcp devtools # custom ports
|
|
51
|
-
npx @oimdb/mcp devtools --no-open # don't open browser
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
### 1. Add MCP config (for AI tools)
|
|
55
|
-
|
|
56
|
-
Claude Code — `.claude/settings.json`:
|
|
15
|
+
**If MCP is not configured yet**, add this to the project's MCP config (Claude Code: `.claude/settings.json`,
|
|
16
|
+
Cursor: `.cursor/mcp.json`):
|
|
57
17
|
|
|
58
18
|
```json
|
|
59
19
|
{
|
|
@@ -66,111 +26,47 @@ Claude Code — `.claude/settings.json`:
|
|
|
66
26
|
}
|
|
67
27
|
```
|
|
68
28
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
```json
|
|
72
|
-
{
|
|
73
|
-
"mcpServers": {
|
|
74
|
-
"oimdb": {
|
|
75
|
-
"command": "npx",
|
|
76
|
-
"args": ["-y", "@oimdb/mcp"]
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### 2. Connect the browser
|
|
29
|
+
Then ask the user to reload the assistant so the server starts, and to make sure the page connects to the
|
|
30
|
+
bridge:
|
|
83
31
|
|
|
84
32
|
```ts
|
|
85
33
|
// src/debug.ts
|
|
86
34
|
import './store/users.debug';
|
|
87
35
|
import './store/tasks.debug';
|
|
88
|
-
import {
|
|
36
|
+
import { oimDevRegistry } from '@oimdb/devtools';
|
|
89
37
|
|
|
90
38
|
if (typeof window !== 'undefined') {
|
|
91
|
-
(window as Record<string, unknown>).__OIMDB_DEV__ =
|
|
92
|
-
|
|
39
|
+
(window as unknown as Record<string, unknown>).__OIMDB_DEV__ = oimDevRegistry;
|
|
40
|
+
oimDevRegistry.connect(); // connects to ws://localhost:7432
|
|
93
41
|
}
|
|
94
42
|
```
|
|
95
43
|
|
|
96
|
-
### 3. Use from Claude
|
|
97
|
-
|
|
98
|
-
```
|
|
99
|
-
Call oimdb_inspect to see what's in the store.
|
|
100
|
-
```
|
|
101
|
-
|
|
102
44
|
## Available MCP Tools
|
|
103
45
|
|
|
104
46
|
| Tool | Description |
|
|
105
47
|
|---|---|
|
|
106
|
-
| `oimdb_inspect` | Full structured state —
|
|
107
|
-
| `oimdb_dump` | Human-readable text summary
|
|
48
|
+
| `oimdb_inspect` | Full structured state — collections with their index keys and relations, computeds, flush history, errors |
|
|
49
|
+
| `oimdb_dump` | Human-readable text summary — what `oimDevRegistry.dumpString()` returns |
|
|
108
50
|
| `oimdb_collection` | Details of one collection by name |
|
|
109
|
-
| `oimdb_computed` |
|
|
51
|
+
| `oimdb_computed` | Current value of one registered computed, by name |
|
|
110
52
|
|
|
111
53
|
## Offline / static mode (no browser)
|
|
112
54
|
|
|
113
|
-
By default the tools read a **live browser tab**. You can instead point the
|
|
114
|
-
server at an **in-process model** so it answers offline — ideal for "what's the
|
|
115
|
-
data model?" questions without running the app.
|
|
116
|
-
|
|
117
|
-
**CLI — a model module.** Set `OIMDB_MODEL_MODULE` to a module that builds the
|
|
118
|
-
model in Node and exports a `registry` (or a default export, or a zero-arg
|
|
119
|
-
factory returning one):
|
|
120
|
-
|
|
121
|
-
```ts
|
|
122
|
-
// oimdb-model.js (built JS, or run the server under a TS loader)
|
|
123
|
-
import { OIMEventQueue, OIMReactiveCollection } from '@oimdb/core';
|
|
124
|
-
import { OIMDevRegistry } from '@oimdb/devtools';
|
|
125
|
-
|
|
126
|
-
const queue = new OIMEventQueue();
|
|
127
|
-
const users = new OIMReactiveCollection(queue, { selectPk: u => u.id });
|
|
128
|
-
|
|
129
|
-
export const registry = new OIMDevRegistry();
|
|
130
|
-
registry.collection('users', users, { indexes: { /* ... */ } });
|
|
131
|
-
```
|
|
132
|
-
|
|
133
55
|
```bash
|
|
134
56
|
OIMDB_MODEL_MODULE=./oimdb-model.js npx @oimdb/mcp
|
|
135
57
|
```
|
|
136
58
|
|
|
137
|
-
No WebSocket port is opened in this
|
|
138
|
-
|
|
139
|
-
**Embedded — inject a registry.** When running the server from your own Node
|
|
140
|
-
process, pass the registry directly:
|
|
59
|
+
The module is the one `@oimdb/cli` reads with `--model`; see its README. No WebSocket port is opened in this
|
|
60
|
+
mode. From your own Node process, pass the registry directly:
|
|
141
61
|
|
|
142
62
|
```ts
|
|
143
63
|
import { createOIMDevMcpServer } from '@oimdb/mcp';
|
|
144
|
-
import {
|
|
145
|
-
|
|
146
|
-
await createOIMDevMcpServer({ registry }).start();
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
> **Fields offline:** structure (collections, indexes, relations) is available
|
|
150
|
-
> as soon as the model is constructed. Entity **field names** appear only when
|
|
151
|
-
> the model holds at least one sample entity (TypeScript types are erased at
|
|
152
|
-
> runtime) — seed a representative row in the model module if you want them.
|
|
153
|
-
|
|
154
|
-
## CDP Inspector (no browser-side code needed)
|
|
155
|
-
|
|
156
|
-
Launch Chrome with `--remote-debugging-port=9222`, then:
|
|
157
|
-
|
|
158
|
-
```bash
|
|
159
|
-
npx @oimdb/mcp cdp
|
|
160
|
-
npx @oimdb/mcp cdp | jq .collections
|
|
161
|
-
OIMDB_CDP_URL=http://localhost:9222 npx @oimdb/mcp cdp
|
|
162
|
-
```
|
|
163
|
-
|
|
164
|
-
## Port
|
|
165
|
-
|
|
166
|
-
The MCP bridge WebSocket runs on port `7432` by default. Change it:
|
|
64
|
+
import { oimDevRegistry } from '@oimdb/devtools';
|
|
167
65
|
|
|
168
|
-
|
|
169
|
-
OIMDB_WS_PORT=7433 npx @oimdb/mcp
|
|
66
|
+
await createOIMDevMcpServer({ registry: oimDevRegistry }).start();
|
|
170
67
|
```
|
|
171
68
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
```
|
|
69
|
+
`createOIMDevMcpServer(options?)` takes `wsPort?` (7432), `registry?`, `modelModulePath?`, `host?`
|
|
70
|
+
(`127.0.0.1`) and `allowedOrigins?` (`[]`). The bridge, ports, host and origin rules are `@oimdb/cli`'s and are
|
|
71
|
+
described in its README; `OIMDB_WS_PORT`, `OIMDB_HOST`, `OIMDB_ALLOWED_ORIGINS` and `OIMDB_MODEL_MODULE` set them
|
|
72
|
+
here too.
|
package/bin/oimdb-mcp.cjs
CHANGED
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
2
|
+
// The servers catch what they can reach; this is the net for what they cannot.
|
|
3
|
+
// It lives in the CLI and NOT in the library classes, because those also run
|
|
4
|
+
// embedded in somebody else's process, where deciding that the process survives
|
|
5
|
+
// its own uncaught errors is not ours to make. stderr only — MCP mode speaks
|
|
6
|
+
// protocol on stdout.
|
|
7
|
+
process.on('uncaughtException', err => {
|
|
8
|
+
process.stderr.write(`[OIMDB] Uncaught error (continuing): ${err && err.stack ? err.stack : String(err)}\n`);
|
|
9
|
+
});
|
|
10
|
+
process.on('unhandledRejection', reason => {
|
|
11
|
+
process.stderr.write(`[OIMDB] Unhandled rejection (continuing): ${String(reason)}\n`);
|
|
12
|
+
});
|
|
10
13
|
const sub = process.argv[2];
|
|
11
14
|
|
|
12
|
-
if (sub === '
|
|
13
|
-
require('./devtools.cjs')();
|
|
14
|
-
} else if (sub === 'cdp') {
|
|
15
|
-
require('./cdp.cjs')();
|
|
16
|
-
} else if (sub === undefined || sub === 'mcp' || sub === 'serve') {
|
|
15
|
+
if (sub === undefined || sub === 'mcp' || sub === 'serve') {
|
|
17
16
|
require('./server.cjs')();
|
|
18
17
|
} else {
|
|
19
18
|
process.stderr.write(
|
|
20
19
|
`[OIMDB] Unknown command: ${sub}\n` +
|
|
21
|
-
'Usage: oimdb-mcp
|
|
20
|
+
'Usage: oimdb-mcp (the MCP server). The DevTools UI and the CDP snapshot are in @oimdb/cli: oimdb serve, oimdb inspect --cdp.\n'
|
|
22
21
|
);
|
|
23
22
|
process.exit(1);
|
|
24
23
|
}
|
package/bin/server.cjs
CHANGED
|
@@ -7,7 +7,16 @@ module.exports = function runMcpServer() {
|
|
|
7
7
|
: 7432;
|
|
8
8
|
// Offline/static mode: point at a module that builds the model in-process.
|
|
9
9
|
const modelModulePath = process.env.OIMDB_MODEL_MODULE || undefined;
|
|
10
|
-
|
|
10
|
+
// Loopback unless told otherwise. This port hands out the application's
|
|
11
|
+
// whole data model, so binding every interface — which is what happens when
|
|
12
|
+
// no host is given — puts it on the café Wi-Fi.
|
|
13
|
+
const host = process.env.OIMDB_HOST || '127.0.0.1';
|
|
14
|
+
// Exact page origins, comma-separated. Never '*'.
|
|
15
|
+
const allowedOrigins = (process.env.OIMDB_ALLOWED_ORIGINS || '')
|
|
16
|
+
.split(',')
|
|
17
|
+
.map(s => s.trim())
|
|
18
|
+
.filter(Boolean);
|
|
19
|
+
const server = new OIMDevMcpServer({ wsPort, modelModulePath, host, allowedOrigins });
|
|
11
20
|
server.start().catch(err => {
|
|
12
21
|
process.stderr.write('[OIMDB MCP] Fatal: ' + err.message + '\n');
|
|
13
22
|
process.exit(1);
|
package/dist/index.cjs
CHANGED
|
@@ -21,42 +21,36 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
OIMDevMcpServer: () => OIMDevMcpServer,
|
|
24
|
-
|
|
25
|
-
createOIMDevMcpServer: () => createOIMDevMcpServer,
|
|
26
|
-
createOIMDevUIServer: () => createOIMDevUIServer
|
|
24
|
+
createOIMDevMcpServer: () => createOIMDevMcpServer
|
|
27
25
|
});
|
|
28
26
|
module.exports = __toCommonJS(index_exports);
|
|
29
27
|
|
|
30
28
|
// src/core/OIMDevMcpServer.ts
|
|
31
29
|
var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
32
30
|
var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
33
|
-
var import_ws = require("ws");
|
|
34
|
-
var import_node_url = require("url");
|
|
35
|
-
var import_node_path = require("path");
|
|
36
31
|
var import_zod = require("zod");
|
|
37
|
-
|
|
38
|
-
const url = (0, import_node_url.pathToFileURL)((0, import_node_path.resolve)(path)).href;
|
|
39
|
-
const mod = await import(url);
|
|
40
|
-
let candidate = mod.registry ?? mod.default ?? mod;
|
|
41
|
-
if (typeof candidate === "function") {
|
|
42
|
-
candidate = await candidate();
|
|
43
|
-
}
|
|
44
|
-
if (!candidate || typeof candidate.inspect !== "function" || typeof candidate.dumpString !== "function") {
|
|
45
|
-
throw new Error(
|
|
46
|
-
`[OIMDB MCP] Module "${path}" must export a registry exposing inspect() and dumpString() (as a "registry" or default export, or a zero-arg factory returning one).`
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
return candidate;
|
|
50
|
-
}
|
|
32
|
+
var import_cli = require("@oimdb/cli");
|
|
51
33
|
var OIMDevMcpServer = class {
|
|
52
34
|
constructor(options = {}) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
35
|
+
const wsPort = options.wsPort ?? 7432;
|
|
36
|
+
let devStateSource;
|
|
37
|
+
if (options.registry !== void 0) {
|
|
38
|
+
devStateSource = new import_cli.OIMDevRegistryStateSource(options.registry);
|
|
39
|
+
} else if (options.modelModulePath !== void 0) {
|
|
40
|
+
this.devModuleStateSource = new import_cli.OIMDevModuleStateSource(options.modelModulePath);
|
|
41
|
+
devStateSource = this.devModuleStateSource;
|
|
42
|
+
} else {
|
|
43
|
+
this.devBridge = new import_cli.OIMDevBridge({
|
|
44
|
+
wsPort,
|
|
45
|
+
host: options.host ?? "127.0.0.1",
|
|
46
|
+
allowedOrigins: new Set(options.allowedOrigins ?? []),
|
|
47
|
+
logPrefix: "[OIMDB MCP]",
|
|
48
|
+
portInUseHint: "npx @oimdb/mcp",
|
|
49
|
+
requestTimeoutMessage: "Browser response timeout (5s). Is the page active?"
|
|
50
|
+
});
|
|
51
|
+
devStateSource = new import_cli.OIMDevBridgeStateSource(this.devBridge, wsPort);
|
|
52
|
+
}
|
|
53
|
+
this.devStateReader = new import_cli.OIMDevStateReader(devStateSource);
|
|
60
54
|
this.mcpServer = new import_mcp.McpServer(
|
|
61
55
|
{ name: "oimdb", version: "1.0.0" },
|
|
62
56
|
{
|
|
@@ -65,292 +59,30 @@ var OIMDevMcpServer = class {
|
|
|
65
59
|
);
|
|
66
60
|
this.registerTools();
|
|
67
61
|
}
|
|
68
|
-
/**
|
|
69
|
-
* Resolves the in-process registry, loading the model module on first use.
|
|
70
|
-
* Returns null when no in-process source is configured (browser-bridge mode).
|
|
71
|
-
*/
|
|
72
|
-
async getInProcessRegistry() {
|
|
73
|
-
if (this.inProcessRegistry) return this.inProcessRegistry;
|
|
74
|
-
if (this.modelModulePath && !this.modelModuleLoaded) {
|
|
75
|
-
this.modelModuleLoaded = true;
|
|
76
|
-
this.inProcessRegistry = await loadRegistryFromModule(
|
|
77
|
-
this.modelModulePath
|
|
78
|
-
);
|
|
79
|
-
}
|
|
80
|
-
return this.inProcessRegistry;
|
|
81
|
-
}
|
|
82
|
-
/** Reads the inspect snapshot from the in-process model or the browser. */
|
|
83
|
-
async readInspect() {
|
|
84
|
-
const registry = await this.getInProcessRegistry();
|
|
85
|
-
if (registry) return registry.inspect();
|
|
86
|
-
return this.queryBrowser("inspect");
|
|
87
|
-
}
|
|
88
|
-
/** Reads the human-readable dump from the in-process model or the browser. */
|
|
89
|
-
async readDump() {
|
|
90
|
-
const registry = await this.getInProcessRegistry();
|
|
91
|
-
if (registry) return registry.dumpString();
|
|
92
|
-
return await this.queryBrowser("dump");
|
|
93
|
-
}
|
|
94
62
|
registerTools() {
|
|
95
63
|
const server = this.mcpServer;
|
|
96
64
|
const nameSchema = { name: import_zod.z.string().describe("Registered name") };
|
|
65
|
+
const asText = (text) => ({ content: [{ type: "text", text }] });
|
|
97
66
|
server.registerTool("oimdb_inspect", {
|
|
98
67
|
description: "Get full state of all registered OIMDB collections and computeds. Returns structured JSON with entity counts, field names, index keys, relations, and computed statuses. Source is the connected browser tab, or an in-process model when one is configured (offline/static \u2014 field names appear only if the model holds sample entities)."
|
|
99
|
-
}, async () =>
|
|
100
|
-
const data = await this.readInspect();
|
|
101
|
-
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
102
|
-
});
|
|
68
|
+
}, async () => asText(await this.devStateReader.readInspectText()));
|
|
103
69
|
server.registerTool("oimdb_dump", {
|
|
104
|
-
description: "Get a human-readable text summary of OIMDB state. Same output as
|
|
105
|
-
}, async () =>
|
|
106
|
-
const text = await this.readDump();
|
|
107
|
-
return { content: [{ type: "text", text }] };
|
|
108
|
-
});
|
|
70
|
+
description: "Get a human-readable text summary of OIMDB state. Same output as oimDevRegistry.dump(). Reads the connected browser tab or an in-process model when configured."
|
|
71
|
+
}, async () => asText(await this.devStateReader.readDumpText()));
|
|
109
72
|
server.registerTool("oimdb_collection", {
|
|
110
73
|
description: "Get details of a specific registered collection: entity count, field names, index keys, and relations.",
|
|
111
74
|
inputSchema: nameSchema
|
|
112
|
-
}, async ({ name }) =>
|
|
113
|
-
const data = await this.readInspect();
|
|
114
|
-
const info = data.collections[name];
|
|
115
|
-
if (!info) {
|
|
116
|
-
const available = Object.keys(data.collections).join(", ") || "(none registered)";
|
|
117
|
-
return { content: [{ type: "text", text: `Collection '${name}' not found. Registered: ${available}` }] };
|
|
118
|
-
}
|
|
119
|
-
return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
|
|
120
|
-
});
|
|
75
|
+
}, async ({ name }) => asText(await this.devStateReader.readCollectionText(name)));
|
|
121
76
|
server.registerTool("oimdb_computed", {
|
|
122
77
|
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.",
|
|
123
78
|
inputSchema: nameSchema
|
|
124
|
-
}, async ({ name }) =>
|
|
125
|
-
const data = await this.readInspect();
|
|
126
|
-
const info = data.computeds[name];
|
|
127
|
-
if (!info) {
|
|
128
|
-
const available = Object.keys(data.computeds).join(", ") || "(none registered)";
|
|
129
|
-
return { content: [{ type: "text", text: `Computed '${name}' not found. Registered: ${available}` }] };
|
|
130
|
-
}
|
|
131
|
-
return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
|
|
132
|
-
});
|
|
133
|
-
}
|
|
134
|
-
async queryBrowser(action) {
|
|
135
|
-
if (!this.latestBrowser) {
|
|
136
|
-
throw new Error(
|
|
137
|
-
`No browser tab connected to the OIMDB MCP bridge.
|
|
138
|
-
|
|
139
|
-
Live mode \u2014 add to your debug entry point:
|
|
140
|
-
import { registry } from '@oimdb/devtools';
|
|
141
|
-
registry.connect(); // connects to ws://localhost:${this.wsPort}
|
|
142
|
-
Then reload the page.
|
|
143
|
-
|
|
144
|
-
Offline mode \u2014 point the server at a model module instead:
|
|
145
|
-
OIMDB_MODEL_MODULE=./path/to/model.js npx @oimdb/mcp
|
|
146
|
-
(the module exports a "registry" with inspect()/dumpString()).`
|
|
147
|
-
);
|
|
148
|
-
}
|
|
149
|
-
return new Promise((resolve, reject) => {
|
|
150
|
-
const id = Math.random().toString(36).slice(2);
|
|
151
|
-
const timer = setTimeout(() => {
|
|
152
|
-
this.pending.delete(id);
|
|
153
|
-
reject(new Error("Browser response timeout (5s). Is the page active?"));
|
|
154
|
-
}, 5e3);
|
|
155
|
-
this.pending.set(id, { resolve, reject, timer });
|
|
156
|
-
this.latestBrowser.ws.send(JSON.stringify({ type: "request", id, action }));
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
setupWebSocketServer() {
|
|
160
|
-
this.wss = new import_ws.WebSocketServer({ port: this.wsPort });
|
|
161
|
-
this.wss.on("error", (err) => {
|
|
162
|
-
if (err.code === "EADDRINUSE") {
|
|
163
|
-
process.stderr.write(
|
|
164
|
-
`[OIMDB MCP] Port ${this.wsPort} is already in use.
|
|
165
|
-
Set a different port: OIMDB_WS_PORT=7433 npx @oimdb/mcp
|
|
166
|
-
`
|
|
167
|
-
);
|
|
168
|
-
process.exit(1);
|
|
169
|
-
}
|
|
170
|
-
process.stderr.write(`[OIMDB MCP] WebSocket server error: ${err.message}
|
|
171
|
-
`);
|
|
172
|
-
process.exit(1);
|
|
173
|
-
});
|
|
174
|
-
this.wss.on("connection", (ws) => {
|
|
175
|
-
ws.on("message", (raw) => {
|
|
176
|
-
const msg = JSON.parse(raw.toString());
|
|
177
|
-
if (msg.type === "hello") {
|
|
178
|
-
this.latestBrowser = { ws, url: msg.url, title: msg.title };
|
|
179
|
-
process.stderr.write(`[OIMDB MCP] Browser connected: ${msg.title} (${msg.url})
|
|
180
|
-
`);
|
|
181
|
-
return;
|
|
182
|
-
}
|
|
183
|
-
if (msg.type === "response") {
|
|
184
|
-
const pending = this.pending.get(msg.id);
|
|
185
|
-
if (!pending) return;
|
|
186
|
-
this.pending.delete(msg.id);
|
|
187
|
-
clearTimeout(pending.timer);
|
|
188
|
-
if (msg.error) {
|
|
189
|
-
pending.reject(new Error(msg.error));
|
|
190
|
-
} else {
|
|
191
|
-
pending.resolve(msg.data);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
ws.on("close", () => {
|
|
196
|
-
if (this.latestBrowser?.ws === ws) {
|
|
197
|
-
this.latestBrowser = null;
|
|
198
|
-
process.stderr.write("[OIMDB MCP] Browser disconnected\n");
|
|
199
|
-
}
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
process.stderr.write(`[OIMDB MCP] Bridge ready on ws://localhost:${this.wsPort}
|
|
203
|
-
`);
|
|
79
|
+
}, async ({ name }) => asText(await this.devStateReader.readComputedText(name)));
|
|
204
80
|
}
|
|
205
81
|
async start() {
|
|
206
|
-
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
);
|
|
211
|
-
} else {
|
|
212
|
-
this.setupWebSocketServer();
|
|
213
|
-
}
|
|
214
|
-
const transport = new import_stdio.StdioServerTransport();
|
|
215
|
-
await this.mcpServer.connect(transport);
|
|
216
|
-
}
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
// src/core/OIMDevUIServer.ts
|
|
220
|
-
var import_http = require("http");
|
|
221
|
-
var import_fs = require("fs");
|
|
222
|
-
var import_path = require("path");
|
|
223
|
-
var import_child_process = require("child_process");
|
|
224
|
-
var import_ws2 = require("ws");
|
|
225
|
-
var MIME = {
|
|
226
|
-
".html": "text/html; charset=utf-8",
|
|
227
|
-
".js": "text/javascript; charset=utf-8",
|
|
228
|
-
".css": "text/css; charset=utf-8"
|
|
229
|
-
};
|
|
230
|
-
var OIMDevUIServer = class {
|
|
231
|
-
constructor(options) {
|
|
232
|
-
this.wss = null;
|
|
233
|
-
this.latestBrowser = null;
|
|
234
|
-
this.pending = /* @__PURE__ */ new Map();
|
|
235
|
-
this.wsPort = options.wsPort ?? 7432;
|
|
236
|
-
this.uiPort = options.uiPort ?? 7433;
|
|
237
|
-
this.uiDir = options.uiDir;
|
|
238
|
-
this.shouldOpen = options.open ?? true;
|
|
239
|
-
}
|
|
240
|
-
async queryBrowser(action) {
|
|
241
|
-
if (!this.latestBrowser) return null;
|
|
242
|
-
return new Promise((resolve, reject) => {
|
|
243
|
-
const id = Math.random().toString(36).slice(2);
|
|
244
|
-
const timer = setTimeout(() => {
|
|
245
|
-
this.pending.delete(id);
|
|
246
|
-
reject(new Error("timeout"));
|
|
247
|
-
}, 5e3);
|
|
248
|
-
this.pending.set(id, { resolve, reject, timer });
|
|
249
|
-
this.latestBrowser.ws.send(JSON.stringify({ type: "request", id, action }));
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
setupBridge() {
|
|
253
|
-
this.wss = new import_ws2.WebSocketServer({ port: this.wsPort });
|
|
254
|
-
this.wss.on("error", (err) => {
|
|
255
|
-
if (err.code === "EADDRINUSE") {
|
|
256
|
-
process.stderr.write(
|
|
257
|
-
`[OIMDB DevTools] Port ${this.wsPort} is already in use.
|
|
258
|
-
Set a different port: OIMDB_WS_PORT=7433 npx @oimdb/mcp devtools
|
|
259
|
-
`
|
|
260
|
-
);
|
|
261
|
-
process.exit(1);
|
|
262
|
-
}
|
|
263
|
-
process.stderr.write(`[OIMDB DevTools] Bridge error: ${err.message}
|
|
264
|
-
`);
|
|
265
|
-
process.exit(1);
|
|
266
|
-
});
|
|
267
|
-
this.wss.on("connection", (ws) => {
|
|
268
|
-
ws.on("message", (raw) => {
|
|
269
|
-
const msg = JSON.parse(raw.toString());
|
|
270
|
-
if (msg.type === "hello") {
|
|
271
|
-
this.latestBrowser = { ws, url: msg.url, title: msg.title };
|
|
272
|
-
process.stderr.write(`[OIMDB DevTools] Browser connected: ${msg.title} (${msg.url})
|
|
273
|
-
`);
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
if (msg.type === "response") {
|
|
277
|
-
const req = this.pending.get(msg.id);
|
|
278
|
-
if (!req) return;
|
|
279
|
-
this.pending.delete(msg.id);
|
|
280
|
-
clearTimeout(req.timer);
|
|
281
|
-
if (msg.error) req.reject(new Error(msg.error));
|
|
282
|
-
else req.resolve(msg.data);
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
ws.on("close", () => {
|
|
286
|
-
if (this.latestBrowser?.ws === ws) {
|
|
287
|
-
this.latestBrowser = null;
|
|
288
|
-
process.stderr.write("[OIMDB DevTools] Browser disconnected\n");
|
|
289
|
-
}
|
|
290
|
-
});
|
|
291
|
-
});
|
|
292
|
-
process.stderr.write(`[OIMDB DevTools] Bridge on ws://localhost:${this.wsPort}
|
|
293
|
-
`);
|
|
294
|
-
}
|
|
295
|
-
setupHTTP() {
|
|
296
|
-
const server = (0, import_http.createServer)((req, res) => {
|
|
297
|
-
const url = req.url ?? "/";
|
|
298
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
299
|
-
if (url === "/api/inspect") {
|
|
300
|
-
this.queryBrowser("inspect").then((data) => {
|
|
301
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
302
|
-
res.end(JSON.stringify({
|
|
303
|
-
connected: !!this.latestBrowser,
|
|
304
|
-
tab: this.latestBrowser ? { url: this.latestBrowser.url, title: this.latestBrowser.title } : null,
|
|
305
|
-
data
|
|
306
|
-
}));
|
|
307
|
-
}).catch(() => {
|
|
308
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
309
|
-
res.end(JSON.stringify({ connected: false, tab: null, data: null }));
|
|
310
|
-
});
|
|
311
|
-
return;
|
|
312
|
-
}
|
|
313
|
-
const filePath = (0, import_path.join)(this.uiDir, url === "/" ? "index.html" : url);
|
|
314
|
-
try {
|
|
315
|
-
const content = (0, import_fs.readFileSync)(filePath);
|
|
316
|
-
res.writeHead(200, { "Content-Type": MIME[(0, import_path.extname)(filePath)] ?? "text/plain" });
|
|
317
|
-
res.end(content);
|
|
318
|
-
} catch {
|
|
319
|
-
res.writeHead(404);
|
|
320
|
-
res.end("Not found");
|
|
321
|
-
}
|
|
322
|
-
});
|
|
323
|
-
server.on("error", (err) => {
|
|
324
|
-
if (err.code === "EADDRINUSE") {
|
|
325
|
-
process.stderr.write(
|
|
326
|
-
`[OIMDB DevTools] UI port ${this.uiPort} is already in use.
|
|
327
|
-
Set a different port: OIMDB_UI_PORT=7434 npx @oimdb/mcp devtools
|
|
328
|
-
`
|
|
329
|
-
);
|
|
330
|
-
process.exit(1);
|
|
331
|
-
}
|
|
332
|
-
});
|
|
333
|
-
server.listen(this.uiPort, () => {
|
|
334
|
-
const uiUrl = `http://localhost:${this.uiPort}`;
|
|
335
|
-
process.stderr.write(`[OIMDB DevTools] UI ready \u2192 ${uiUrl}
|
|
336
|
-
`);
|
|
337
|
-
if (this.shouldOpen) this.openURL(uiUrl);
|
|
338
|
-
});
|
|
339
|
-
}
|
|
340
|
-
openURL(url) {
|
|
341
|
-
const cmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
342
|
-
(0, import_child_process.exec)(cmd, (err) => {
|
|
343
|
-
if (err) process.stderr.write(`[OIMDB DevTools] Could not open browser: ${err.message}
|
|
344
|
-
`);
|
|
345
|
-
});
|
|
346
|
-
}
|
|
347
|
-
start() {
|
|
348
|
-
this.setupBridge();
|
|
349
|
-
this.setupHTTP();
|
|
350
|
-
process.stderr.write(
|
|
351
|
-
`[OIMDB DevTools] Add to debug.ts: registry.connect();
|
|
352
|
-
`
|
|
353
|
-
);
|
|
82
|
+
if (this.devModuleStateSource !== void 0) await this.devModuleStateSource.loadDevRegistry();
|
|
83
|
+
if (this.devBridge !== void 0) this.devBridge.start();
|
|
84
|
+
else process.stderr.write("[OIMDB MCP] In-process model loaded \u2014 serving offline (no browser bridge).\n");
|
|
85
|
+
await this.mcpServer.connect(new import_stdio.StdioServerTransport());
|
|
354
86
|
}
|
|
355
87
|
};
|
|
356
88
|
|
|
@@ -358,13 +90,8 @@ Set a different port: OIMDB_UI_PORT=7434 npx @oimdb/mcp devtools
|
|
|
358
90
|
function createOIMDevMcpServer(options) {
|
|
359
91
|
return new OIMDevMcpServer(options);
|
|
360
92
|
}
|
|
361
|
-
function createOIMDevUIServer(options) {
|
|
362
|
-
return new OIMDevUIServer(options);
|
|
363
|
-
}
|
|
364
93
|
// Annotate the CommonJS export names for ESM import in node:
|
|
365
94
|
0 && (module.exports = {
|
|
366
95
|
OIMDevMcpServer,
|
|
367
|
-
|
|
368
|
-
createOIMDevMcpServer,
|
|
369
|
-
createOIMDevUIServer
|
|
96
|
+
createOIMDevMcpServer
|
|
370
97
|
});
|