@oimdb/mcp 1.2.0 → 2.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 CHANGED
@@ -1,14 +1,14 @@
1
1
  # @oimdb/mcp
2
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.
3
+ Connects AI assistants to a live OIMDB store running in the browser. It provides MCP tools for Claude Code, plus an optional CDP inspector for any AI that can run a shell command.
4
4
 
5
- 📖 **[Full documentation](https://abaikov.github.io/oimdb/)**
5
+ 📖 **[Full documentation](https://oimdb.org/)**
6
6
 
7
7
  ## For AI Assistants
8
8
 
9
9
  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.
10
10
 
11
- **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.
11
+ **Check whether MCP is already configured.** Look for `mcpServers.oimdb` in `.claude/settings.json` or `~/.claude/settings.json`. If it is there, the tools `oimdb_inspect`, `oimdb_dump`, `oimdb_collection` and `oimdb_computed` are available. Call `oimdb_inspect` now to see the current state.
12
12
 
13
13
  **If MCP is not configured yet**, add this to the project's MCP config:
14
14
 
@@ -28,7 +28,7 @@ If you are an AI assistant and this package is present in the project, you can g
28
28
 
29
29
  Then ask the user to:
30
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
31
+ 2. Make sure `debug.ts` calls `oimDevRegistry.connect()` and the app is running in the browser
32
32
 
33
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
34
 
@@ -44,7 +44,7 @@ npx @oimdb/mcp cdp
44
44
  npx @oimdb/mcp devtools
45
45
  ```
46
46
 
47
- Opens `http://localhost:7433` automatically. Shows live collections and computeds from the running browser tab. No Chrome extension needed.
47
+ Opens `http://localhost:7433` automatically and shows live collections and computeds from the running browser tab. No Chrome extension is needed.
48
48
 
49
49
  ```bash
50
50
  OIMDB_WS_PORT=7433 OIMDB_UI_PORT=7434 npx @oimdb/mcp devtools # custom ports
@@ -81,15 +81,19 @@ Cursor — `.cursor/mcp.json`:
81
81
 
82
82
  ### 2. Connect the browser
83
83
 
84
+ The app calls `oimInitDevtools()` once at startup, before it builds its first
85
+ service, where it builds the database (see the `@oimdb/devtools` README, Setup).
86
+ The debug entry point then only connects:
87
+
84
88
  ```ts
85
89
  // src/debug.ts
86
90
  import './store/users.debug';
87
91
  import './store/tasks.debug';
88
- import { registry } from '@oimdb/devtools';
92
+ import { oimDevRegistry } from '@oimdb/devtools';
89
93
 
90
94
  if (typeof window !== 'undefined') {
91
- (window as Record<string, unknown>).__OIMDB_DEV__ = registry;
92
- registry.connect(); // connects to ws://localhost:7432
95
+ (window as unknown as Record<string, unknown>).__OIMDB_DEV__ = oimDevRegistry;
96
+ oimDevRegistry.connect(); // connects to ws://localhost:7432
93
97
  }
94
98
  ```
95
99
 
@@ -103,47 +107,68 @@ Call oimdb_inspect to see what's in the store.
103
107
 
104
108
  | Tool | Description |
105
109
  |---|---|
106
- | `oimdb_inspect` | Full structured state — all collections, computeds, dep graph |
107
- | `oimdb_dump` | Human-readable text summary (same as `registry.dump()`) |
110
+ | `oimdb_inspect` | Full structured state — collections with their index keys and relations, computeds, flush history, errors |
111
+ | `oimdb_dump` | Human-readable text summary — what `oimDevRegistry.dumpString()` returns (`oimDevRegistry.dump()` is the sibling that console-logs it) |
108
112
  | `oimdb_collection` | Details of one collection by name |
109
- | `oimdb_computed` | Status + deps of one computed by name |
113
+ | `oimdb_computed` | Current value of one registered computed, by name |
110
114
 
111
115
  ## Offline / static mode (no browser)
112
116
 
113
117
  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.
118
+ server at an **in-process model** so it answers offline. This suits "what's the
119
+ data model?" questions when you don't want to run the app.
116
120
 
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):
121
+ **CLI: a model module.** Set `OIMDB_MODEL_MODULE` to a module that builds the
122
+ model in Node and exports an `oimDevRegistry` (or a default export, or a zero-arg
123
+ factory that returns one). It calls `oimInitDevtools()` before it builds the first
124
+ service, as every app using `@oimdb/devtools` does:
120
125
 
121
126
  ```ts
122
127
  // 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: { /* ... */ } });
128
+ import {
129
+ OIMDB, createOIMBucketService, createOIMEntityService, type TOIMEntitySlot,
130
+ } from '@oimdb/core';
131
+ import { OIMDevRegistry, oimInitDevtools } from '@oimdb/devtools';
132
+
133
+ oimInitDevtools();
134
+
135
+ type TUser = { id: string; name: string; role: string };
136
+
137
+ const db = new OIMDB();
138
+ const usersService = createOIMEntityService<TUser>(db, { keyOf: (user) => user.id });
139
+ const users = usersService.createStoreSlot(db);
140
+ // A bucket's slots are the caller's — a write to a user moves the value and
141
+ // files nothing, so whoever writes the user also calls
142
+ // `usersByRoleService.slotContentService.writeBucketMemberSlots(bucketSlot, entitySlots)`.
143
+ const usersByRoleService = createOIMBucketService<TOIMEntitySlot<TUser>>(db);
144
+ const usersByRole = usersByRoleService.createStoreSlot(db);
145
+
146
+ export const oimDevRegistry = new OIMDevRegistry();
147
+ // The bucket collection and its store slot — you constructed them, so you hand them over.
148
+ oimDevRegistry.collection('users', usersService, users, {
149
+ indexes: { byRole: { bucketCollectionService: usersByRoleService, bucketStoreSlot: usersByRole } },
150
+ });
131
151
  ```
132
152
 
153
+ The server asks a registry for only two methods, `inspect()` and `dumpString()`,
154
+ declared here as `IOIMDevRegistry`, so this package depends on no `@oimdb`
155
+ package at all. `OIMDevRegistry` from `@oimdb/devtools` satisfies it as it
156
+ stands, and so does anything else that answers those two.
157
+
133
158
  ```bash
134
159
  OIMDB_MODEL_MODULE=./oimdb-model.js npx @oimdb/mcp
135
160
  ```
136
161
 
137
- No WebSocket port is opened in this mode — no browser required.
162
+ No WebSocket port is opened in this mode, and no browser is required.
138
163
 
139
- **Embedded — inject a registry.** When running the server from your own Node
164
+ **Embedded: inject a registry.** When you run the server from your own Node
140
165
  process, pass the registry directly:
141
166
 
142
167
  ```ts
143
168
  import { createOIMDevMcpServer } from '@oimdb/mcp';
144
- import { registry } from './oimdb-model';
169
+ import { oimDevRegistry } from '@oimdb/devtools';
145
170
 
146
- await createOIMDevMcpServer({ registry }).start();
171
+ await createOIMDevMcpServer({ registry: oimDevRegistry }).start();
147
172
  ```
148
173
 
149
174
  > **Fields offline:** structure (collections, indexes, relations) is available
@@ -161,16 +186,91 @@ npx @oimdb/mcp cdp | jq .collections
161
186
  OIMDB_CDP_URL=http://localhost:9222 npx @oimdb/mcp cdp
162
187
  ```
163
188
 
164
- ## Port
189
+ ## Ports, host and origins
190
+
191
+ Everything here is configured through **environment variables**. The only CLI
192
+ flag in the package is `devtools --no-open`.
193
+
194
+ | Variable | Default | What it sets |
195
+ |---|---|---|
196
+ | `OIMDB_WS_PORT` | `7432` | The bridge WebSocket port |
197
+ | `OIMDB_UI_PORT` | `7433` | The `devtools` UI HTTP port |
198
+ | `OIMDB_HOST` | `127.0.0.1` | The interface both servers bind to |
199
+ | `OIMDB_ALLOWED_ORIGINS` | *(empty)* | Extra page origins allowed onto the bridge, comma-separated |
200
+ | `OIMDB_MODEL_MODULE` | — | Offline model module (see above) |
201
+ | `OIMDB_CDP_URL` | `http://localhost:9222` | The CDP endpoint the `cdp` subcommand reads |
202
+
203
+ The same two settings are `host` and `allowedOrigins` on `TOIMDevMcpOptions` and
204
+ `TOIMDevUIServerOptions` when you construct a server yourself.
165
205
 
166
- The MCP bridge WebSocket runs on port `7432` by default. Change it:
206
+ **It binds to loopback by default, on purpose.** This port hands out the
207
+ application's entire data model, and a `WebSocketServer` built with no host
208
+ binds every interface, which would put the model on the café Wi-Fi.
209
+ Set `OIMDB_HOST=0.0.0.0` when the browser really runs
210
+ somewhere else (a container, a VM, a phone), and accept that everyone who can
211
+ route there can talk to it. Both servers print the bound host at startup.
212
+
213
+ **Origins are checked, and `'*'` is never allowed.** A request with no `Origin` header is
214
+ allowed (that is a non-browser client). A loopback origin is allowed
215
+ (`localhost`, `127.0.0.1`, `::1`, `[::1]`, `*.localhost`). Anything else is
216
+ allowed only if you listed it exactly in `OIMDB_ALLOWED_ORIGINS`. The literal
217
+ string `null` is always rejected, because a sandboxed iframe on any site in the world
218
+ sends exactly that. Only an allow-listed origin gets
219
+ `Access-Control-Allow-Origin` echoed back.
220
+
221
+ While bound to a loopback host, the UI server also rejects a request whose
222
+ `Host` header is not a loopback authority, with **421 Misdirected request**.
223
+ This is the DNS-rebinding guard, and it is switched off when you bind elsewhere.
224
+
225
+ Change the bridge port and update `oimDevRegistry.connect()` accordingly:
167
226
 
168
227
  ```bash
169
228
  OIMDB_WS_PORT=7433 npx @oimdb/mcp
170
229
  ```
171
230
 
172
- Update `registry.connect()` accordingly:
231
+ ```ts
232
+ import { oimDevRegistry } from '@oimdb/devtools';
233
+
234
+ oimDevRegistry.connect('ws://localhost:7433');
235
+ ```
236
+
237
+ ## Running a server from your own process
173
238
 
174
239
  ```ts
175
- registry.connect('ws://localhost:7433');
240
+ import { createOIMDevMcpServer, createOIMDevUIServer } from '@oimdb/mcp';
241
+ ```
242
+
243
+ - **`createOIMDevMcpServer(options?)`** → `OIMDevMcpServer`. Options: `wsPort?`
244
+ (7432), `registry?`, `modelModulePath?`, `host?` (`127.0.0.1`),
245
+ `allowedOrigins?` (`[]`).
246
+ - **`createOIMDevUIServer(options)`** → `OIMDevUIServer`. Options: `wsPort?`
247
+ (7432), `uiPort?` (7433), **`uiDir` — required, no default**, `open?` (true),
248
+ `host?` (`127.0.0.1`), `allowedOrigins?` (`[]`).
249
+
250
+ Both classes are exported too, along with `TOIMDevMcpOptions`,
251
+ `TOIMDevUIServerOptions` and `IOIMDevRegistry`.
252
+
253
+ Know these three behaviours before you wire this into anything:
254
+
255
+ - **The newest browser tab takes the bridge**, and the previous socket is closed
256
+ with "Replaced by a newer OIMDB DevTools tab". Only the socket that currently
257
+ holds the bridge may answer a request; anything else is ignored.
258
+ - **A query times out after 5 seconds**, reported as "Browser response timeout
259
+ (5s). Is the page active?".
260
+ - **A malformed frame drops that client** with close code 1007, and `EADDRINUSE`
261
+ exits the process with a message naming the env var to change.
262
+
263
+ The `devtools` UI serves `/`, `/index.html`, `/app.js`, `/app.css`, plus
264
+ `/api/inspect`, which answers `{ connected, tab, data }`:
265
+
266
+ ```bash
267
+ curl http://127.0.0.1:7433/api/inspect
268
+ ```
269
+
270
+ ## Subcommands
271
+
272
+ ```bash
273
+ npx @oimdb/mcp # the MCP server; `mcp` and `serve` are accepted aliases
274
+ npx @oimdb/mcp devtools # the standalone UI
275
+ npx @oimdb/mcp cdp # one-shot snapshot over the Chrome DevTools Protocol
176
276
  ```
package/bin/cdp.cjs CHANGED
@@ -74,7 +74,7 @@ async function run() {
74
74
  if (val === null || val === undefined) {
75
75
  process.stderr.write(
76
76
  '[OIMDB CDP] __OIMDB_DEV__ not found on this page.\n' +
77
- 'Make sure debug.ts sets: window.__OIMDB_DEV__ = registry\n'
77
+ 'Make sure debug.ts sets: window.__OIMDB_DEV__ = oimDevRegistry\n'
78
78
  );
79
79
  process.exit(1);
80
80
  }
package/bin/devtools.cjs CHANGED
@@ -11,7 +11,16 @@ module.exports = function runDevtools() {
11
11
  : 7433;
12
12
  const noOpen = process.argv.includes('--no-open');
13
13
  const uiDir = path.join(__dirname, '../ui');
14
+ // Loopback unless told otherwise. This port hands out the application's
15
+ // whole data model, so binding every interface — which is what happens when
16
+ // no host is given — puts it on the café Wi-Fi.
17
+ const host = process.env.OIMDB_HOST || '127.0.0.1';
18
+ // Exact page origins, comma-separated. Never '*'.
19
+ const allowedOrigins = (process.env.OIMDB_ALLOWED_ORIGINS || '')
20
+ .split(',')
21
+ .map(s => s.trim())
22
+ .filter(Boolean);
14
23
 
15
- const server = new OIMDevUIServer({ wsPort, uiPort, uiDir, open: !noOpen });
24
+ const server = new OIMDevUIServer({ wsPort, uiPort, uiDir, open: !noOpen, host, allowedOrigins });
16
25
  server.start();
17
26
  };
package/bin/oimdb-mcp.cjs CHANGED
@@ -1,4 +1,15 @@
1
1
  #!/usr/bin/env node
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
+ });
2
13
  /**
3
14
  * Single entry point for @oimdb/mcp. Dispatches on the first argument so all of
4
15
  * these work via npx (one bin → npx can always resolve it):
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
- const server = new OIMDevMcpServer({ wsPort, modelModulePath });
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);