@oimdb/mcp 1.0.3 → 1.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 CHANGED
@@ -108,6 +108,49 @@ Call oimdb_inspect to see what's in the store.
108
108
  | `oimdb_collection` | Details of one collection by name |
109
109
  | `oimdb_computed` | Status + deps of one computed by name |
110
110
 
111
+ ## Offline / static mode (no browser)
112
+
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
+ ```bash
134
+ OIMDB_MODEL_MODULE=./oimdb-model.js npx @oimdb/mcp
135
+ ```
136
+
137
+ No WebSocket port is opened in this mode — no browser required.
138
+
139
+ **Embedded — inject a registry.** When running the server from your own Node
140
+ process, pass the registry directly:
141
+
142
+ ```ts
143
+ import { createOIMDevMcpServer } from '@oimdb/mcp';
144
+ import { registry } from './oimdb-model';
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
+
111
154
  ## CDP Inspector (no browser-side code needed)
112
155
 
113
156
  Launch Chrome with `--remote-debugging-port=9222`, then:
package/bin/server.cjs CHANGED
@@ -2,7 +2,9 @@
2
2
  const { OIMDevMcpServer } = require('../dist/index.cjs');
3
3
 
4
4
  const wsPort = process.env.OIMDB_WS_PORT ? Number(process.env.OIMDB_WS_PORT) : 7432;
5
- const server = new OIMDevMcpServer({ wsPort });
5
+ // Offline/static mode: point at a module that builds the model in-process.
6
+ const modelModulePath = process.env.OIMDB_MODEL_MODULE || undefined;
7
+ const server = new OIMDevMcpServer({ wsPort, modelModulePath });
6
8
  server.start().catch((err) => {
7
9
  process.stderr.write('[OIMDB MCP] Fatal: ' + err.message + '\n');
8
10
  process.exit(1);
package/dist/index.cjs CHANGED
@@ -31,36 +31,86 @@ module.exports = __toCommonJS(index_exports);
31
31
  var import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
32
32
  var import_stdio = require("@modelcontextprotocol/sdk/server/stdio.js");
33
33
  var import_ws = require("ws");
34
+ var import_node_url = require("url");
35
+ var import_node_path = require("path");
34
36
  var import_zod = require("zod");
37
+ async function loadRegistryFromModule(path) {
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
+ }
35
51
  var OIMDevMcpServer = class {
36
52
  constructor(options = {}) {
37
53
  this.wss = null;
38
54
  this.latestBrowser = null;
39
55
  this.pending = /* @__PURE__ */ new Map();
56
+ this.modelModuleLoaded = false;
40
57
  this.wsPort = options.wsPort ?? 7432;
41
- this.mcpServer = new import_mcp.McpServer({ name: "oimdb", version: "1.0.0" });
58
+ this.inProcessRegistry = options.registry ?? null;
59
+ this.modelModulePath = options.modelModulePath;
60
+ this.mcpServer = new import_mcp.McpServer(
61
+ { name: "oimdb", version: "1.0.0" },
62
+ {
63
+ instructions: "Before using these tools, read the OIMDB API reference so you know how to write correct code: node_modules/@oimdb/core/llms.txt. Then use oimdb_inspect to see the data model (collections, indexes, computeds). The source is either a live browser tab (WebSocket bridge) or an in-process model (when a registry / model module is provided) for offline/static introspection."
64
+ }
65
+ );
42
66
  this.registerTools();
43
67
  }
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
+ }
44
94
  registerTools() {
45
95
  const server = this.mcpServer;
46
96
  const nameSchema = { name: import_zod.z.string().describe("Registered name") };
47
97
  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."
98
+ 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)."
49
99
  }, async () => {
50
- const data = await this.queryBrowser("inspect");
100
+ const data = await this.readInspect();
51
101
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
52
102
  });
53
103
  server.registerTool("oimdb_dump", {
54
- description: "Get a human-readable text summary of OIMDB state. Same output as registry.dump() in the browser."
104
+ description: "Get a human-readable text summary of OIMDB state. Same output as registry.dump(). Reads the connected browser tab or an in-process model when configured."
55
105
  }, async () => {
56
- const text = await this.queryBrowser("dump");
106
+ const text = await this.readDump();
57
107
  return { content: [{ type: "text", text }] };
58
108
  });
59
109
  server.registerTool("oimdb_collection", {
60
110
  description: "Get details of a specific registered collection: entity count, field names, index keys, and relations.",
61
111
  inputSchema: nameSchema
62
112
  }, async ({ name }) => {
63
- const data = await this.queryBrowser("inspect");
113
+ const data = await this.readInspect();
64
114
  const info = data.collections[name];
65
115
  if (!info) {
66
116
  const available = Object.keys(data.collections).join(", ") || "(none registered)";
@@ -72,7 +122,7 @@ var OIMDevMcpServer = class {
72
122
  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
123
  inputSchema: nameSchema
74
124
  }, async ({ name }) => {
75
- const data = await this.queryBrowser("inspect");
125
+ const data = await this.readInspect();
76
126
  const info = data.computeds[name];
77
127
  if (!info) {
78
128
  const available = Object.keys(data.computeds).join(", ") || "(none registered)";
@@ -86,11 +136,14 @@ var OIMDevMcpServer = class {
86
136
  throw new Error(
87
137
  `No browser tab connected to the OIMDB MCP bridge.
88
138
 
89
- Add to your debug entry point:
139
+ Live mode \u2014 add to your debug entry point:
90
140
  import { registry } from '@oimdb/devtools';
91
141
  registry.connect(); // connects to ws://localhost:${this.wsPort}
142
+ Then reload the page.
92
143
 
93
- Then reload the page.`
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()).`
94
147
  );
95
148
  }
96
149
  return new Promise((resolve, reject) => {
@@ -150,7 +203,14 @@ Set a different port: OIMDB_WS_PORT=7433 npx @oimdb/mcp
150
203
  `);
151
204
  }
152
205
  async start() {
153
- this.setupWebSocketServer();
206
+ const registry = await this.getInProcessRegistry();
207
+ if (registry) {
208
+ process.stderr.write(
209
+ "[OIMDB MCP] In-process model loaded \u2014 serving offline (no browser bridge).\n"
210
+ );
211
+ } else {
212
+ this.setupWebSocketServer();
213
+ }
154
214
  const transport = new import_stdio.StdioServerTransport();
155
215
  await this.mcpServer.connect(transport);
156
216
  }
package/dist/index.d.cts CHANGED
@@ -1,6 +1,29 @@
1
+ /**
2
+ * The readable shape the MCP tools need from a registry — `OIMDevRegistry` from
3
+ * `@oimdb/devtools` already satisfies it. Declared structurally (following the
4
+ * existing `IOIMDev*Like` convention) so `@oimdb/mcp` need not depend on
5
+ * `@oimdb/devtools`; it is not a separate registry implementation.
6
+ */
7
+ interface IOIMDevRegistryLike {
8
+ inspect(): unknown;
9
+ dumpString(): string;
10
+ }
1
11
  type TOIMDevMcpOptions = {
2
12
  /** Port for the WebSocket bridge that browser tabs connect to. Default: 7432 */
3
13
  wsPort?: number;
14
+ /**
15
+ * An in-process registry to read directly (your `OIMDevRegistry`). When
16
+ * provided, the tools answer from it without a browser/WebSocket bridge
17
+ * (no `wsPort` server started).
18
+ */
19
+ registry?: IOIMDevRegistryLike;
20
+ /**
21
+ * Path to a module that builds the model in this process for offline
22
+ * introspection. The module must export a `registry` (or a default export,
23
+ * or a zero-arg factory) exposing `inspect()` and `dumpString()`.
24
+ * Also settable via the `OIMDB_MODEL_MODULE` env var for the CLI.
25
+ */
26
+ modelModulePath?: string;
4
27
  };
5
28
 
6
29
  declare class OIMDevMcpServer {
@@ -9,7 +32,19 @@ declare class OIMDevMcpServer {
9
32
  private wss;
10
33
  private latestBrowser;
11
34
  private readonly pending;
35
+ private inProcessRegistry;
36
+ private readonly modelModulePath?;
37
+ private modelModuleLoaded;
12
38
  constructor(options?: TOIMDevMcpOptions);
39
+ /**
40
+ * Resolves the in-process registry, loading the model module on first use.
41
+ * Returns null when no in-process source is configured (browser-bridge mode).
42
+ */
43
+ private getInProcessRegistry;
44
+ /** Reads the inspect snapshot from the in-process model or the browser. */
45
+ private readInspect;
46
+ /** Reads the human-readable dump from the in-process model or the browser. */
47
+ private readDump;
13
48
  private registerTools;
14
49
  private queryBrowser;
15
50
  private setupWebSocketServer;
@@ -41,4 +76,4 @@ declare class OIMDevUIServer {
41
76
  declare function createOIMDevMcpServer(options?: TOIMDevMcpOptions): OIMDevMcpServer;
42
77
  declare function createOIMDevUIServer(options: TOIMDevUIServerOptions): OIMDevUIServer;
43
78
 
44
- export { OIMDevMcpServer, OIMDevUIServer, type TOIMDevMcpOptions, type TOIMDevUIServerOptions, createOIMDevMcpServer, createOIMDevUIServer };
79
+ export { type IOIMDevRegistryLike, OIMDevMcpServer, OIMDevUIServer, type TOIMDevMcpOptions, type TOIMDevUIServerOptions, createOIMDevMcpServer, createOIMDevUIServer };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,29 @@
1
+ /**
2
+ * The readable shape the MCP tools need from a registry — `OIMDevRegistry` from
3
+ * `@oimdb/devtools` already satisfies it. Declared structurally (following the
4
+ * existing `IOIMDev*Like` convention) so `@oimdb/mcp` need not depend on
5
+ * `@oimdb/devtools`; it is not a separate registry implementation.
6
+ */
7
+ interface IOIMDevRegistryLike {
8
+ inspect(): unknown;
9
+ dumpString(): string;
10
+ }
1
11
  type TOIMDevMcpOptions = {
2
12
  /** Port for the WebSocket bridge that browser tabs connect to. Default: 7432 */
3
13
  wsPort?: number;
14
+ /**
15
+ * An in-process registry to read directly (your `OIMDevRegistry`). When
16
+ * provided, the tools answer from it without a browser/WebSocket bridge
17
+ * (no `wsPort` server started).
18
+ */
19
+ registry?: IOIMDevRegistryLike;
20
+ /**
21
+ * Path to a module that builds the model in this process for offline
22
+ * introspection. The module must export a `registry` (or a default export,
23
+ * or a zero-arg factory) exposing `inspect()` and `dumpString()`.
24
+ * Also settable via the `OIMDB_MODEL_MODULE` env var for the CLI.
25
+ */
26
+ modelModulePath?: string;
4
27
  };
5
28
 
6
29
  declare class OIMDevMcpServer {
@@ -9,7 +32,19 @@ declare class OIMDevMcpServer {
9
32
  private wss;
10
33
  private latestBrowser;
11
34
  private readonly pending;
35
+ private inProcessRegistry;
36
+ private readonly modelModulePath?;
37
+ private modelModuleLoaded;
12
38
  constructor(options?: TOIMDevMcpOptions);
39
+ /**
40
+ * Resolves the in-process registry, loading the model module on first use.
41
+ * Returns null when no in-process source is configured (browser-bridge mode).
42
+ */
43
+ private getInProcessRegistry;
44
+ /** Reads the inspect snapshot from the in-process model or the browser. */
45
+ private readInspect;
46
+ /** Reads the human-readable dump from the in-process model or the browser. */
47
+ private readDump;
13
48
  private registerTools;
14
49
  private queryBrowser;
15
50
  private setupWebSocketServer;
@@ -41,4 +76,4 @@ declare class OIMDevUIServer {
41
76
  declare function createOIMDevMcpServer(options?: TOIMDevMcpOptions): OIMDevMcpServer;
42
77
  declare function createOIMDevUIServer(options: TOIMDevUIServerOptions): OIMDevUIServer;
43
78
 
44
- export { OIMDevMcpServer, OIMDevUIServer, type TOIMDevMcpOptions, type TOIMDevUIServerOptions, createOIMDevMcpServer, createOIMDevUIServer };
79
+ export { type IOIMDevRegistryLike, OIMDevMcpServer, OIMDevUIServer, type TOIMDevMcpOptions, type TOIMDevUIServerOptions, createOIMDevMcpServer, createOIMDevUIServer };
package/dist/index.js CHANGED
@@ -2,36 +2,86 @@
2
2
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
3
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
4
  import { WebSocketServer } from "ws";
5
+ import { pathToFileURL } from "url";
6
+ import { resolve as resolvePath } from "path";
5
7
  import { z } from "zod";
8
+ async function loadRegistryFromModule(path) {
9
+ const url = pathToFileURL(resolvePath(path)).href;
10
+ const mod = await import(url);
11
+ let candidate = mod.registry ?? mod.default ?? mod;
12
+ if (typeof candidate === "function") {
13
+ candidate = await candidate();
14
+ }
15
+ if (!candidate || typeof candidate.inspect !== "function" || typeof candidate.dumpString !== "function") {
16
+ throw new Error(
17
+ `[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).`
18
+ );
19
+ }
20
+ return candidate;
21
+ }
6
22
  var OIMDevMcpServer = class {
7
23
  constructor(options = {}) {
8
24
  this.wss = null;
9
25
  this.latestBrowser = null;
10
26
  this.pending = /* @__PURE__ */ new Map();
27
+ this.modelModuleLoaded = false;
11
28
  this.wsPort = options.wsPort ?? 7432;
12
- this.mcpServer = new McpServer({ name: "oimdb", version: "1.0.0" });
29
+ this.inProcessRegistry = options.registry ?? null;
30
+ this.modelModulePath = options.modelModulePath;
31
+ this.mcpServer = new McpServer(
32
+ { name: "oimdb", version: "1.0.0" },
33
+ {
34
+ instructions: "Before using these tools, read the OIMDB API reference so you know how to write correct code: node_modules/@oimdb/core/llms.txt. Then use oimdb_inspect to see the data model (collections, indexes, computeds). The source is either a live browser tab (WebSocket bridge) or an in-process model (when a registry / model module is provided) for offline/static introspection."
35
+ }
36
+ );
13
37
  this.registerTools();
14
38
  }
39
+ /**
40
+ * Resolves the in-process registry, loading the model module on first use.
41
+ * Returns null when no in-process source is configured (browser-bridge mode).
42
+ */
43
+ async getInProcessRegistry() {
44
+ if (this.inProcessRegistry) return this.inProcessRegistry;
45
+ if (this.modelModulePath && !this.modelModuleLoaded) {
46
+ this.modelModuleLoaded = true;
47
+ this.inProcessRegistry = await loadRegistryFromModule(
48
+ this.modelModulePath
49
+ );
50
+ }
51
+ return this.inProcessRegistry;
52
+ }
53
+ /** Reads the inspect snapshot from the in-process model or the browser. */
54
+ async readInspect() {
55
+ const registry = await this.getInProcessRegistry();
56
+ if (registry) return registry.inspect();
57
+ return this.queryBrowser("inspect");
58
+ }
59
+ /** Reads the human-readable dump from the in-process model or the browser. */
60
+ async readDump() {
61
+ const registry = await this.getInProcessRegistry();
62
+ if (registry) return registry.dumpString();
63
+ return await this.queryBrowser("dump");
64
+ }
15
65
  registerTools() {
16
66
  const server = this.mcpServer;
17
67
  const nameSchema = { name: z.string().describe("Registered name") };
18
68
  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."
69
+ 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)."
20
70
  }, async () => {
21
- const data = await this.queryBrowser("inspect");
71
+ const data = await this.readInspect();
22
72
  return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
23
73
  });
24
74
  server.registerTool("oimdb_dump", {
25
- description: "Get a human-readable text summary of OIMDB state. Same output as registry.dump() in the browser."
75
+ description: "Get a human-readable text summary of OIMDB state. Same output as registry.dump(). Reads the connected browser tab or an in-process model when configured."
26
76
  }, async () => {
27
- const text = await this.queryBrowser("dump");
77
+ const text = await this.readDump();
28
78
  return { content: [{ type: "text", text }] };
29
79
  });
30
80
  server.registerTool("oimdb_collection", {
31
81
  description: "Get details of a specific registered collection: entity count, field names, index keys, and relations.",
32
82
  inputSchema: nameSchema
33
83
  }, async ({ name }) => {
34
- const data = await this.queryBrowser("inspect");
84
+ const data = await this.readInspect();
35
85
  const info = data.collections[name];
36
86
  if (!info) {
37
87
  const available = Object.keys(data.collections).join(", ") || "(none registered)";
@@ -43,7 +93,7 @@ var OIMDevMcpServer = class {
43
93
  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
94
  inputSchema: nameSchema
45
95
  }, async ({ name }) => {
46
- const data = await this.queryBrowser("inspect");
96
+ const data = await this.readInspect();
47
97
  const info = data.computeds[name];
48
98
  if (!info) {
49
99
  const available = Object.keys(data.computeds).join(", ") || "(none registered)";
@@ -57,11 +107,14 @@ var OIMDevMcpServer = class {
57
107
  throw new Error(
58
108
  `No browser tab connected to the OIMDB MCP bridge.
59
109
 
60
- Add to your debug entry point:
110
+ Live mode \u2014 add to your debug entry point:
61
111
  import { registry } from '@oimdb/devtools';
62
112
  registry.connect(); // connects to ws://localhost:${this.wsPort}
113
+ Then reload the page.
63
114
 
64
- Then reload the page.`
115
+ Offline mode \u2014 point the server at a model module instead:
116
+ OIMDB_MODEL_MODULE=./path/to/model.js npx @oimdb/mcp
117
+ (the module exports a "registry" with inspect()/dumpString()).`
65
118
  );
66
119
  }
67
120
  return new Promise((resolve, reject) => {
@@ -121,7 +174,14 @@ Set a different port: OIMDB_WS_PORT=7433 npx @oimdb/mcp
121
174
  `);
122
175
  }
123
176
  async start() {
124
- this.setupWebSocketServer();
177
+ const registry = await this.getInProcessRegistry();
178
+ if (registry) {
179
+ process.stderr.write(
180
+ "[OIMDB MCP] In-process model loaded \u2014 serving offline (no browser bridge).\n"
181
+ );
182
+ } else {
183
+ this.setupWebSocketServer();
184
+ }
125
185
  const transport = new StdioServerTransport();
126
186
  await this.mcpServer.connect(transport);
127
187
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oimdb/mcp",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "MCP server and CDP inspector for OIMDB — connects AI assistants to live browser state",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",