@oimdb/mcp 2.0.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 +17 -221
- package/bin/oimdb-mcp.cjs +2 -14
- package/dist/index.cjs +32 -447
- package/dist/index.d.cts +5 -71
- package/dist/index.d.ts +5 -71
- package/dist/index.js +37 -444
- package/package.json +9 -12
- package/bin/cdp.cjs +0 -98
- package/bin/devtools.cjs +0 -26
- package/ui/app.css +0 -323
- package/ui/app.js +0 -409
- package/ui/index.html +0 -63
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
inspect(): unknown;
|
|
3
|
-
dumpString(): string;
|
|
4
|
-
}
|
|
1
|
+
import { IOIMDevRegistry } from '@oimdb/cli';
|
|
5
2
|
|
|
6
3
|
type TOIMDevMcpOptions = {
|
|
7
4
|
/** Default 7432; unused when `registry` is given — no bridge is started. */
|
|
@@ -17,77 +14,14 @@ type TOIMDevMcpOptions = {
|
|
|
17
14
|
|
|
18
15
|
declare class OIMDevMcpServer {
|
|
19
16
|
private readonly mcpServer;
|
|
20
|
-
private readonly
|
|
21
|
-
private readonly
|
|
22
|
-
private
|
|
23
|
-
private readonly modelModulePath?;
|
|
24
|
-
private modelModuleLoaded;
|
|
17
|
+
private readonly devBridge;
|
|
18
|
+
private readonly devModuleStateSource;
|
|
19
|
+
private readonly devStateReader;
|
|
25
20
|
constructor(options?: TOIMDevMcpOptions);
|
|
26
|
-
/**
|
|
27
|
-
* Resolves the in-process registry, loading the model module on first use.
|
|
28
|
-
* Returns null when no in-process source is configured (browser-bridge mode).
|
|
29
|
-
*/
|
|
30
|
-
private getInProcessRegistry;
|
|
31
|
-
/** Reads the inspect snapshot from the in-process model or the browser. */
|
|
32
|
-
private readInspect;
|
|
33
|
-
/** Reads the human-readable dump from the in-process model or the browser. */
|
|
34
|
-
private readDump;
|
|
35
21
|
private registerTools;
|
|
36
|
-
private requestBrowser;
|
|
37
22
|
start(): Promise<void>;
|
|
38
23
|
}
|
|
39
24
|
|
|
40
|
-
type TOIMDevUIServerOptions = {
|
|
41
|
-
wsPort?: number;
|
|
42
|
-
uiPort?: number;
|
|
43
|
-
uiDir: string;
|
|
44
|
-
open?: boolean;
|
|
45
|
-
/**
|
|
46
|
-
* The interface both ports bind to. LOOPBACK BY DEFAULT: a tool that hands
|
|
47
|
-
* out the application's entire data model has no business being reachable
|
|
48
|
-
* from the café Wi-Fi, and `listen` without a host binds every interface.
|
|
49
|
-
*
|
|
50
|
-
* Set it — `'0.0.0.0'` — when the browser genuinely runs somewhere else: a
|
|
51
|
-
* container, a VM, a phone. That is an opt-in to everyone who can route
|
|
52
|
-
* there reading the model.
|
|
53
|
-
*/
|
|
54
|
-
host?: string;
|
|
55
|
-
/**
|
|
56
|
-
* Page origins allowed onto the bridge and allowed to read `/api/inspect`,
|
|
57
|
-
* on top of the loopback origins allowed by default.
|
|
58
|
-
*
|
|
59
|
-
* Exact origins (`'http://app.test:3000'`). Never `'*'`: with that on every
|
|
60
|
-
* response, any page in any tab the developer has open can read every entity
|
|
61
|
-
* of every registered collection.
|
|
62
|
-
*/
|
|
63
|
-
allowedOrigins?: readonly string[];
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
declare class OIMDevUIServer {
|
|
67
|
-
private readonly uiPort;
|
|
68
|
-
private readonly shouldOpen;
|
|
69
|
-
private readonly host;
|
|
70
|
-
private readonly allowedOrigins;
|
|
71
|
-
private readonly devUIAssetCatalog;
|
|
72
|
-
private readonly bridge;
|
|
73
|
-
constructor(options: TOIMDevUIServerOptions);
|
|
74
|
-
/**
|
|
75
|
-
* While bound to loopback, answer only what was addressed to a loopback
|
|
76
|
-
* name.
|
|
77
|
-
*
|
|
78
|
-
* Without this, a page on any site can point a hostname it controls at
|
|
79
|
-
* 127.0.0.1 and become same-origin with this server. The `Host` header is
|
|
80
|
-
* the part of that a browser cannot forge. Binding elsewhere is an explicit
|
|
81
|
-
* decision, so the check stands down there.
|
|
82
|
-
*/
|
|
83
|
-
private isRequestAddressedHere;
|
|
84
|
-
private requestBrowser;
|
|
85
|
-
private setupHTTP;
|
|
86
|
-
private openURL;
|
|
87
|
-
start(): void;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
25
|
declare function createOIMDevMcpServer(options?: TOIMDevMcpOptions): OIMDevMcpServer;
|
|
91
|
-
declare function createOIMDevUIServer(options: TOIMDevUIServerOptions): OIMDevUIServer;
|
|
92
26
|
|
|
93
|
-
export {
|
|
27
|
+
export { OIMDevMcpServer, type TOIMDevMcpOptions, createOIMDevMcpServer };
|
package/dist/index.js
CHANGED
|
@@ -1,227 +1,35 @@
|
|
|
1
1
|
// src/core/OIMDevMcpServer.ts
|
|
2
2
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
3
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4
|
-
import { pathToFileURL } from "url";
|
|
5
|
-
import { resolve as resolvePath } from "path";
|
|
6
|
-
import { z as z2 } from "zod";
|
|
7
|
-
|
|
8
|
-
// src/core/OIMDevBridge.ts
|
|
9
|
-
import { WebSocketServer } from "ws";
|
|
10
|
-
|
|
11
|
-
// src/core/OIMDevBridgeMessageReader.ts
|
|
12
4
|
import { z } from "zod";
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
const result = _OIMDevBridgeMessageReader.SCHEMA.safeParse(parsed);
|
|
22
|
-
if (!result.success) return null;
|
|
23
|
-
return result.data;
|
|
24
|
-
}
|
|
25
|
-
};
|
|
26
|
-
_OIMDevBridgeMessageReader.SCHEMA = z.discriminatedUnion("type", [
|
|
27
|
-
z.object({
|
|
28
|
-
type: z.literal("hello"),
|
|
29
|
-
url: z.string(),
|
|
30
|
-
title: z.string(),
|
|
31
|
-
timestamp: z.number()
|
|
32
|
-
}),
|
|
33
|
-
z.object({
|
|
34
|
-
type: z.literal("response"),
|
|
35
|
-
id: z.string(),
|
|
36
|
-
data: z.unknown(),
|
|
37
|
-
error: z.string().optional()
|
|
38
|
-
})
|
|
39
|
-
]);
|
|
40
|
-
var OIMDevBridgeMessageReader = _OIMDevBridgeMessageReader;
|
|
41
|
-
|
|
42
|
-
// src/core/OIMDevLoopbackPolicy.ts
|
|
43
|
-
var _OIMDevLoopbackPolicy = class _OIMDevLoopbackPolicy {
|
|
44
|
-
static isLoopbackHost(host) {
|
|
45
|
-
return _OIMDevLoopbackPolicy.HOSTS.has(host) || host.endsWith(".localhost");
|
|
46
|
-
}
|
|
47
|
-
static isLoopbackAuthority(authority) {
|
|
48
|
-
const host = authority.startsWith("[") ? authority.slice(0, authority.indexOf("]") + 1) : authority.split(":", 1)[0];
|
|
49
|
-
return _OIMDevLoopbackPolicy.isLoopbackHost(host);
|
|
50
|
-
}
|
|
51
|
-
// The literal string `null` is NOT loopback: a sandboxed iframe on any site
|
|
52
|
-
// in the world sends exactly that.
|
|
53
|
-
static isLoopbackOrigin(origin) {
|
|
54
|
-
try {
|
|
55
|
-
return _OIMDevLoopbackPolicy.isLoopbackHost(new URL(origin).hostname);
|
|
56
|
-
} catch {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
_OIMDevLoopbackPolicy.HOSTS = /* @__PURE__ */ new Set([
|
|
62
|
-
"localhost",
|
|
63
|
-
"127.0.0.1",
|
|
64
|
-
"::1",
|
|
65
|
-
"[::1]"
|
|
66
|
-
]);
|
|
67
|
-
var OIMDevLoopbackPolicy = _OIMDevLoopbackPolicy;
|
|
68
|
-
|
|
69
|
-
// src/core/OIMDevBridge.ts
|
|
70
|
-
var OIM_REQUEST_TIMEOUT_MS = 5e3;
|
|
71
|
-
var OIMDevBridge = class {
|
|
72
|
-
constructor(bridgeOptions) {
|
|
73
|
-
this.bridgeOptions = bridgeOptions;
|
|
74
|
-
this.messageReader = new OIMDevBridgeMessageReader();
|
|
75
|
-
this.pending = /* @__PURE__ */ new Map();
|
|
76
|
-
this.webSocketServer = null;
|
|
77
|
-
this.devConnectedBrowser = null;
|
|
78
|
-
}
|
|
79
|
-
get connectedBrowser() {
|
|
80
|
-
return this.devConnectedBrowser;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* May a page use this bridge?
|
|
84
|
-
*
|
|
85
|
-
* From this machine, or named by whoever started the server. An ABSENT
|
|
86
|
-
* origin is a non-browser client — a CLI, a test — and is allowed; the
|
|
87
|
-
* literal `'null'` is not, because a sandboxed iframe on any site can send
|
|
88
|
-
* exactly that.
|
|
89
|
-
*/
|
|
90
|
-
isOriginAllowed(origin) {
|
|
91
|
-
if (origin === void 0 || origin === "") return true;
|
|
92
|
-
if (this.bridgeOptions.allowedOrigins.has(origin)) return true;
|
|
93
|
-
return OIMDevLoopbackPolicy.isLoopbackOrigin(origin);
|
|
94
|
-
}
|
|
95
|
-
request(action) {
|
|
96
|
-
const browser = this.devConnectedBrowser;
|
|
97
|
-
if (browser === null) {
|
|
98
|
-
return Promise.reject(
|
|
99
|
-
new Error(this.bridgeOptions.requestTimeoutMessage)
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
return new Promise((resolve, reject) => {
|
|
103
|
-
const id = Math.random().toString(36).slice(2);
|
|
104
|
-
const timer = setTimeout(() => {
|
|
105
|
-
this.pending.delete(id);
|
|
106
|
-
reject(new Error(this.bridgeOptions.requestTimeoutMessage));
|
|
107
|
-
}, OIM_REQUEST_TIMEOUT_MS);
|
|
108
|
-
this.pending.set(id, { resolve, reject, timer });
|
|
109
|
-
browser.ws.send(JSON.stringify({ type: "request", id, action }));
|
|
110
|
-
});
|
|
111
|
-
}
|
|
112
|
-
start() {
|
|
113
|
-
this.webSocketServer = new WebSocketServer({
|
|
114
|
-
port: this.bridgeOptions.wsPort,
|
|
115
|
-
host: this.bridgeOptions.host,
|
|
116
|
-
verifyClient: (info) => this.isOriginAllowed(info.origin)
|
|
117
|
-
});
|
|
118
|
-
this.webSocketServer.on("error", (err) => {
|
|
119
|
-
if (err.code === "EADDRINUSE") {
|
|
120
|
-
this.write(
|
|
121
|
-
`Port ${this.bridgeOptions.wsPort} is already in use.
|
|
122
|
-
`
|
|
123
|
-
);
|
|
124
|
-
process.stderr.write(
|
|
125
|
-
`Set a different port: OIMDB_WS_PORT=7433 ${this.bridgeOptions.portInUseHint}
|
|
126
|
-
`
|
|
127
|
-
);
|
|
128
|
-
process.exit(1);
|
|
129
|
-
}
|
|
130
|
-
this.write(`Bridge error: ${err.message}
|
|
131
|
-
`);
|
|
132
|
-
process.exit(1);
|
|
133
|
-
});
|
|
134
|
-
this.webSocketServer.on("connection", (ws) => {
|
|
135
|
-
ws.on("error", (err) => {
|
|
136
|
-
this.write(`Bridge socket error: ${err.message}
|
|
137
|
-
`);
|
|
138
|
-
});
|
|
139
|
-
ws.on("message", (raw) => this.readFrame(ws, raw.toString()));
|
|
140
|
-
ws.on("close", () => {
|
|
141
|
-
if (this.devConnectedBrowser?.ws === ws) {
|
|
142
|
-
this.devConnectedBrowser = null;
|
|
143
|
-
this.write("Browser disconnected\n");
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
this.write(
|
|
148
|
-
`Bridge ready on ws://localhost:${this.bridgeOptions.wsPort} (bound to ${this.bridgeOptions.host})
|
|
149
|
-
`
|
|
150
|
-
);
|
|
151
|
-
}
|
|
152
|
-
readFrame(ws, raw) {
|
|
153
|
-
const devBridgeMessage = this.messageReader.read(raw);
|
|
154
|
-
if (devBridgeMessage === null) {
|
|
155
|
-
this.write("Dropping a client that sent a malformed frame\n");
|
|
156
|
-
ws.close(1007, "Malformed bridge message");
|
|
157
|
-
return;
|
|
158
|
-
}
|
|
159
|
-
if (devBridgeMessage.type === "hello") {
|
|
160
|
-
this.adoptBrowser(ws, devBridgeMessage.url, devBridgeMessage.title);
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
if (this.devConnectedBrowser?.ws !== ws) return;
|
|
164
|
-
const devPendingRequest = this.pending.get(devBridgeMessage.id);
|
|
165
|
-
if (!devPendingRequest) return;
|
|
166
|
-
this.pending.delete(devBridgeMessage.id);
|
|
167
|
-
clearTimeout(devPendingRequest.timer);
|
|
168
|
-
if (devBridgeMessage.error) devPendingRequest.reject(new Error(devBridgeMessage.error));
|
|
169
|
-
else devPendingRequest.resolve(devBridgeMessage.data);
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* Newest tab wins — a reload is the ordinary reason for a second `hello` —
|
|
173
|
-
* but the tab it displaces is CLOSED and the swap is printed. Overwriting the
|
|
174
|
-
* field in place leaves the displaced socket open for ever and lets a second
|
|
175
|
-
* connection take the bridge over in silence.
|
|
176
|
-
*/
|
|
177
|
-
adoptBrowser(ws, url, title) {
|
|
178
|
-
const previousDevConnectedBrowser = this.devConnectedBrowser;
|
|
179
|
-
this.devConnectedBrowser = { ws, url, title };
|
|
180
|
-
if (previousDevConnectedBrowser !== null && previousDevConnectedBrowser.ws !== ws) {
|
|
181
|
-
this.write(
|
|
182
|
-
`Bridge taken over by "${title}" (${url}); dropping "${previousDevConnectedBrowser.title}"
|
|
183
|
-
`
|
|
184
|
-
);
|
|
185
|
-
previousDevConnectedBrowser.ws.close(1e3, "Replaced by a newer OIMDB DevTools tab");
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
this.write(`Browser connected: ${title} (${url})
|
|
189
|
-
`);
|
|
190
|
-
}
|
|
191
|
-
write(line) {
|
|
192
|
-
process.stderr.write(`${this.bridgeOptions.logPrefix} ${line}`);
|
|
193
|
-
}
|
|
194
|
-
};
|
|
195
|
-
|
|
196
|
-
// src/core/OIMDevMcpServer.ts
|
|
197
|
-
async function loadRegistryFromModule(path) {
|
|
198
|
-
const url = pathToFileURL(resolvePath(path)).href;
|
|
199
|
-
const mod = await import(url);
|
|
200
|
-
let candidate = mod.oimDevRegistry ?? mod.default ?? mod;
|
|
201
|
-
if (typeof candidate === "function") {
|
|
202
|
-
candidate = await candidate();
|
|
203
|
-
}
|
|
204
|
-
if (!candidate || typeof candidate.inspect !== "function" || typeof candidate.dumpString !== "function") {
|
|
205
|
-
throw new Error(
|
|
206
|
-
`[OIMDB MCP] Module "${path}" must export a registry exposing inspect() and dumpString() (as an "oimDevRegistry" or default export, or a zero-arg factory returning one).`
|
|
207
|
-
);
|
|
208
|
-
}
|
|
209
|
-
return candidate;
|
|
210
|
-
}
|
|
5
|
+
import {
|
|
6
|
+
OIMDevBridge,
|
|
7
|
+
OIMDevBridgeStateSource,
|
|
8
|
+
OIMDevModuleStateSource,
|
|
9
|
+
OIMDevRegistryStateSource,
|
|
10
|
+
OIMDevStateReader
|
|
11
|
+
} from "@oimdb/cli";
|
|
211
12
|
var OIMDevMcpServer = class {
|
|
212
13
|
constructor(options = {}) {
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
14
|
+
const wsPort = options.wsPort ?? 7432;
|
|
15
|
+
let devStateSource;
|
|
16
|
+
if (options.registry !== void 0) {
|
|
17
|
+
devStateSource = new OIMDevRegistryStateSource(options.registry);
|
|
18
|
+
} else if (options.modelModulePath !== void 0) {
|
|
19
|
+
this.devModuleStateSource = new OIMDevModuleStateSource(options.modelModulePath);
|
|
20
|
+
devStateSource = this.devModuleStateSource;
|
|
21
|
+
} else {
|
|
22
|
+
this.devBridge = new OIMDevBridge({
|
|
23
|
+
wsPort,
|
|
24
|
+
host: options.host ?? "127.0.0.1",
|
|
25
|
+
allowedOrigins: new Set(options.allowedOrigins ?? []),
|
|
26
|
+
logPrefix: "[OIMDB MCP]",
|
|
27
|
+
portInUseHint: "npx @oimdb/mcp",
|
|
28
|
+
requestTimeoutMessage: "Browser response timeout (5s). Is the page active?"
|
|
29
|
+
});
|
|
30
|
+
devStateSource = new OIMDevBridgeStateSource(this.devBridge, wsPort);
|
|
31
|
+
}
|
|
32
|
+
this.devStateReader = new OIMDevStateReader(devStateSource);
|
|
225
33
|
this.mcpServer = new McpServer(
|
|
226
34
|
{ name: "oimdb", version: "1.0.0" },
|
|
227
35
|
{
|
|
@@ -230,240 +38,30 @@ var OIMDevMcpServer = class {
|
|
|
230
38
|
);
|
|
231
39
|
this.registerTools();
|
|
232
40
|
}
|
|
233
|
-
/**
|
|
234
|
-
* Resolves the in-process registry, loading the model module on first use.
|
|
235
|
-
* Returns null when no in-process source is configured (browser-bridge mode).
|
|
236
|
-
*/
|
|
237
|
-
async getInProcessRegistry() {
|
|
238
|
-
if (this.inProcessRegistry) return this.inProcessRegistry;
|
|
239
|
-
if (this.modelModulePath && !this.modelModuleLoaded) {
|
|
240
|
-
this.modelModuleLoaded = true;
|
|
241
|
-
this.inProcessRegistry = await loadRegistryFromModule(
|
|
242
|
-
this.modelModulePath
|
|
243
|
-
);
|
|
244
|
-
}
|
|
245
|
-
return this.inProcessRegistry;
|
|
246
|
-
}
|
|
247
|
-
/** Reads the inspect snapshot from the in-process model or the browser. */
|
|
248
|
-
async readInspect() {
|
|
249
|
-
const registry = await this.getInProcessRegistry();
|
|
250
|
-
if (registry) return registry.inspect();
|
|
251
|
-
return this.requestBrowser("inspect");
|
|
252
|
-
}
|
|
253
|
-
/** Reads the human-readable dump from the in-process model or the browser. */
|
|
254
|
-
async readDump() {
|
|
255
|
-
const registry = await this.getInProcessRegistry();
|
|
256
|
-
if (registry) return registry.dumpString();
|
|
257
|
-
return await this.requestBrowser("dump");
|
|
258
|
-
}
|
|
259
41
|
registerTools() {
|
|
260
42
|
const server = this.mcpServer;
|
|
261
|
-
const nameSchema = { name:
|
|
43
|
+
const nameSchema = { name: z.string().describe("Registered name") };
|
|
44
|
+
const asText = (text) => ({ content: [{ type: "text", text }] });
|
|
262
45
|
server.registerTool("oimdb_inspect", {
|
|
263
46
|
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)."
|
|
264
|
-
}, async () =>
|
|
265
|
-
const data = await this.readInspect();
|
|
266
|
-
return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
|
|
267
|
-
});
|
|
47
|
+
}, async () => asText(await this.devStateReader.readInspectText()));
|
|
268
48
|
server.registerTool("oimdb_dump", {
|
|
269
49
|
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."
|
|
270
|
-
}, async () =>
|
|
271
|
-
const text = await this.readDump();
|
|
272
|
-
return { content: [{ type: "text", text }] };
|
|
273
|
-
});
|
|
50
|
+
}, async () => asText(await this.devStateReader.readDumpText()));
|
|
274
51
|
server.registerTool("oimdb_collection", {
|
|
275
52
|
description: "Get details of a specific registered collection: entity count, field names, index keys, and relations.",
|
|
276
53
|
inputSchema: nameSchema
|
|
277
|
-
}, async ({ name }) =>
|
|
278
|
-
const data = await this.readInspect();
|
|
279
|
-
const info = data.collections[name];
|
|
280
|
-
if (!info) {
|
|
281
|
-
const available = Object.keys(data.collections).join(", ") || "(none registered)";
|
|
282
|
-
return { content: [{ type: "text", text: `Collection '${name}' not found. Registered: ${available}` }] };
|
|
283
|
-
}
|
|
284
|
-
return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
|
|
285
|
-
});
|
|
54
|
+
}, async ({ name }) => asText(await this.devStateReader.readCollectionText(name)));
|
|
286
55
|
server.registerTool("oimdb_computed", {
|
|
287
56
|
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.",
|
|
288
57
|
inputSchema: nameSchema
|
|
289
|
-
}, async ({ name }) =>
|
|
290
|
-
const data = await this.readInspect();
|
|
291
|
-
const info = data.computeds[name];
|
|
292
|
-
if (!info) {
|
|
293
|
-
const available = Object.keys(data.computeds).join(", ") || "(none registered)";
|
|
294
|
-
return { content: [{ type: "text", text: `Computed '${name}' not found. Registered: ${available}` }] };
|
|
295
|
-
}
|
|
296
|
-
return { content: [{ type: "text", text: JSON.stringify(info, null, 2) }] };
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
requestBrowser(action) {
|
|
300
|
-
if (this.bridge.connectedBrowser === null) {
|
|
301
|
-
throw new Error(
|
|
302
|
-
`No browser tab connected to the OIMDB MCP bridge.
|
|
303
|
-
|
|
304
|
-
Live mode \u2014 add to your debug entry point:
|
|
305
|
-
import { oimDevRegistry } from '@oimdb/devtools';
|
|
306
|
-
oimDevRegistry.connect(); // connects to ws://localhost:${this.wsPort}
|
|
307
|
-
Then reload the page.
|
|
308
|
-
|
|
309
|
-
Offline mode \u2014 point the server at a model module instead:
|
|
310
|
-
OIMDB_MODEL_MODULE=./path/to/model.js npx @oimdb/mcp
|
|
311
|
-
(the module exports an "oimDevRegistry" with inspect()/dumpString()).`
|
|
312
|
-
);
|
|
313
|
-
}
|
|
314
|
-
return this.bridge.request(action);
|
|
58
|
+
}, async ({ name }) => asText(await this.devStateReader.readComputedText(name)));
|
|
315
59
|
}
|
|
316
60
|
async start() {
|
|
317
|
-
|
|
318
|
-
if (
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
);
|
|
322
|
-
} else {
|
|
323
|
-
this.bridge.start();
|
|
324
|
-
}
|
|
325
|
-
const transport = new StdioServerTransport();
|
|
326
|
-
await this.mcpServer.connect(transport);
|
|
327
|
-
}
|
|
328
|
-
};
|
|
329
|
-
|
|
330
|
-
// src/core/OIMDevUIServer.ts
|
|
331
|
-
import { createServer } from "http";
|
|
332
|
-
import { exec } from "child_process";
|
|
333
|
-
|
|
334
|
-
// src/core/OIMDevUIAssetCatalog.ts
|
|
335
|
-
import { readFileSync } from "fs";
|
|
336
|
-
import { join } from "path";
|
|
337
|
-
var _OIMDevUIAssetCatalog = class _OIMDevUIAssetCatalog {
|
|
338
|
-
constructor(uiDir) {
|
|
339
|
-
this.uiDir = uiDir;
|
|
340
|
-
}
|
|
341
|
-
resolve(requestUrl) {
|
|
342
|
-
const path = requestUrl.split("?", 1)[0].split("#", 1)[0];
|
|
343
|
-
return _OIMDevUIAssetCatalog.ASSETS.get(path) ?? null;
|
|
344
|
-
}
|
|
345
|
-
// Per request, so the UI stays editable; the constant file name is what
|
|
346
|
-
// makes that safe, not the timing.
|
|
347
|
-
read(asset) {
|
|
348
|
-
return readFileSync(join(this.uiDir, asset.fileName));
|
|
349
|
-
}
|
|
350
|
-
};
|
|
351
|
-
_OIMDevUIAssetCatalog.ASSETS = /* @__PURE__ */ new Map([
|
|
352
|
-
["/", { fileName: "index.html", contentType: "text/html; charset=utf-8" }],
|
|
353
|
-
["/index.html", { fileName: "index.html", contentType: "text/html; charset=utf-8" }],
|
|
354
|
-
["/app.js", { fileName: "app.js", contentType: "text/javascript; charset=utf-8" }],
|
|
355
|
-
["/app.css", { fileName: "app.css", contentType: "text/css; charset=utf-8" }]
|
|
356
|
-
]);
|
|
357
|
-
var OIMDevUIAssetCatalog = _OIMDevUIAssetCatalog;
|
|
358
|
-
|
|
359
|
-
// src/core/OIMDevUIServer.ts
|
|
360
|
-
var OIMDevUIServer = class {
|
|
361
|
-
constructor(options) {
|
|
362
|
-
this.uiPort = options.uiPort ?? 7433;
|
|
363
|
-
this.shouldOpen = options.open ?? true;
|
|
364
|
-
this.host = options.host ?? "127.0.0.1";
|
|
365
|
-
this.allowedOrigins = new Set(options.allowedOrigins ?? []);
|
|
366
|
-
this.devUIAssetCatalog = new OIMDevUIAssetCatalog(options.uiDir);
|
|
367
|
-
this.bridge = new OIMDevBridge({
|
|
368
|
-
wsPort: options.wsPort ?? 7432,
|
|
369
|
-
host: this.host,
|
|
370
|
-
allowedOrigins: this.allowedOrigins,
|
|
371
|
-
logPrefix: "[OIMDB DevTools]",
|
|
372
|
-
portInUseHint: "npx @oimdb/mcp devtools",
|
|
373
|
-
requestTimeoutMessage: "timeout"
|
|
374
|
-
});
|
|
375
|
-
}
|
|
376
|
-
/**
|
|
377
|
-
* While bound to loopback, answer only what was addressed to a loopback
|
|
378
|
-
* name.
|
|
379
|
-
*
|
|
380
|
-
* Without this, a page on any site can point a hostname it controls at
|
|
381
|
-
* 127.0.0.1 and become same-origin with this server. The `Host` header is
|
|
382
|
-
* the part of that a browser cannot forge. Binding elsewhere is an explicit
|
|
383
|
-
* decision, so the check stands down there.
|
|
384
|
-
*/
|
|
385
|
-
isRequestAddressedHere(req) {
|
|
386
|
-
if (!OIMDevLoopbackPolicy.isLoopbackHost(this.host)) return true;
|
|
387
|
-
const host = req.headers.host;
|
|
388
|
-
return host !== void 0 && OIMDevLoopbackPolicy.isLoopbackAuthority(host);
|
|
389
|
-
}
|
|
390
|
-
requestBrowser(action) {
|
|
391
|
-
if (this.bridge.connectedBrowser === null) return Promise.resolve(null);
|
|
392
|
-
return this.bridge.request(action);
|
|
393
|
-
}
|
|
394
|
-
setupHTTP() {
|
|
395
|
-
const server = createServer((req, res) => {
|
|
396
|
-
if (!this.isRequestAddressedHere(req)) {
|
|
397
|
-
res.writeHead(421);
|
|
398
|
-
res.end("Misdirected request");
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
const origin = req.headers.origin;
|
|
402
|
-
if (origin !== void 0 && this.allowedOrigins.has(origin)) {
|
|
403
|
-
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
404
|
-
res.setHeader("Vary", "Origin");
|
|
405
|
-
}
|
|
406
|
-
const url = req.url ?? "/";
|
|
407
|
-
if (url.split("?", 1)[0] === "/api/inspect") {
|
|
408
|
-
this.requestBrowser("inspect").then((data) => {
|
|
409
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
410
|
-
const connectedBrowser = this.bridge.connectedBrowser;
|
|
411
|
-
res.end(JSON.stringify({
|
|
412
|
-
connected: connectedBrowser !== null,
|
|
413
|
-
tab: connectedBrowser === null ? null : { url: connectedBrowser.url, title: connectedBrowser.title },
|
|
414
|
-
data
|
|
415
|
-
}));
|
|
416
|
-
}).catch(() => {
|
|
417
|
-
res.writeHead(200, { "Content-Type": "application/json" });
|
|
418
|
-
res.end(JSON.stringify({ connected: false, tab: null, data: null }));
|
|
419
|
-
});
|
|
420
|
-
return;
|
|
421
|
-
}
|
|
422
|
-
const asset = this.devUIAssetCatalog.resolve(url);
|
|
423
|
-
if (asset === null) {
|
|
424
|
-
res.writeHead(404);
|
|
425
|
-
res.end("Not found");
|
|
426
|
-
return;
|
|
427
|
-
}
|
|
428
|
-
try {
|
|
429
|
-
res.writeHead(200, { "Content-Type": asset.contentType });
|
|
430
|
-
res.end(this.devUIAssetCatalog.read(asset));
|
|
431
|
-
} catch {
|
|
432
|
-
res.writeHead(404);
|
|
433
|
-
res.end("Not found");
|
|
434
|
-
}
|
|
435
|
-
});
|
|
436
|
-
server.on("error", (err) => {
|
|
437
|
-
if (err.code === "EADDRINUSE") {
|
|
438
|
-
process.stderr.write(
|
|
439
|
-
`[OIMDB DevTools] UI port ${this.uiPort} is already in use.
|
|
440
|
-
Set a different port: OIMDB_UI_PORT=7434 npx @oimdb/mcp devtools
|
|
441
|
-
`
|
|
442
|
-
);
|
|
443
|
-
process.exit(1);
|
|
444
|
-
}
|
|
445
|
-
});
|
|
446
|
-
server.listen(this.uiPort, this.host, () => {
|
|
447
|
-
const uiUrl = `http://localhost:${this.uiPort}`;
|
|
448
|
-
process.stderr.write(`[OIMDB DevTools] UI ready \u2192 ${uiUrl} (bound to ${this.host})
|
|
449
|
-
`);
|
|
450
|
-
if (this.shouldOpen) this.openURL(uiUrl);
|
|
451
|
-
});
|
|
452
|
-
}
|
|
453
|
-
openURL(url) {
|
|
454
|
-
const cmd = process.platform === "win32" ? `start "" "${url}"` : process.platform === "darwin" ? `open "${url}"` : `xdg-open "${url}"`;
|
|
455
|
-
exec(cmd, (err) => {
|
|
456
|
-
if (err) process.stderr.write(`[OIMDB DevTools] Could not open browser: ${err.message}
|
|
457
|
-
`);
|
|
458
|
-
});
|
|
459
|
-
}
|
|
460
|
-
start() {
|
|
461
|
-
this.bridge.start();
|
|
462
|
-
this.setupHTTP();
|
|
463
|
-
process.stderr.write(
|
|
464
|
-
`[OIMDB DevTools] Add to debug.ts: oimDevRegistry.connect();
|
|
465
|
-
`
|
|
466
|
-
);
|
|
61
|
+
if (this.devModuleStateSource !== void 0) await this.devModuleStateSource.loadDevRegistry();
|
|
62
|
+
if (this.devBridge !== void 0) this.devBridge.start();
|
|
63
|
+
else process.stderr.write("[OIMDB MCP] In-process model loaded \u2014 serving offline (no browser bridge).\n");
|
|
64
|
+
await this.mcpServer.connect(new StdioServerTransport());
|
|
467
65
|
}
|
|
468
66
|
};
|
|
469
67
|
|
|
@@ -471,12 +69,7 @@ Set a different port: OIMDB_UI_PORT=7434 npx @oimdb/mcp devtools
|
|
|
471
69
|
function createOIMDevMcpServer(options) {
|
|
472
70
|
return new OIMDevMcpServer(options);
|
|
473
71
|
}
|
|
474
|
-
function createOIMDevUIServer(options) {
|
|
475
|
-
return new OIMDevUIServer(options);
|
|
476
|
-
}
|
|
477
72
|
export {
|
|
478
73
|
OIMDevMcpServer,
|
|
479
|
-
|
|
480
|
-
createOIMDevMcpServer,
|
|
481
|
-
createOIMDevUIServer
|
|
74
|
+
createOIMDevMcpServer
|
|
482
75
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oimdb/mcp",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "MCP server
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "MCP server for OIMDB — gives AI assistants the oimdb_inspect, oimdb_dump, oimdb_collection and oimdb_computed tools over @oimdb/cli",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -28,28 +28,25 @@
|
|
|
28
28
|
},
|
|
29
29
|
"files": [
|
|
30
30
|
"dist",
|
|
31
|
-
"bin"
|
|
32
|
-
"ui"
|
|
31
|
+
"bin"
|
|
33
32
|
],
|
|
34
33
|
"scripts": {
|
|
35
34
|
"build": "tsup --config tsup.config.ts --tsconfig tsconfig.build.json",
|
|
36
35
|
"dev": "tsup --config tsup.config.ts --tsconfig tsconfig.build.json --watch",
|
|
37
36
|
"type-check": "tsc --noEmit -p tsconfig.tests.json",
|
|
38
|
-
"prepublishOnly": "npm run type-check && npm run build"
|
|
39
|
-
"test": "jest"
|
|
37
|
+
"prepublishOnly": "npm run type-check && npm run build"
|
|
40
38
|
},
|
|
41
39
|
"dependencies": {
|
|
42
40
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
43
|
-
"ws": "^8.18.0",
|
|
44
41
|
"zod": "^3.22.0"
|
|
45
42
|
},
|
|
43
|
+
"peerDependencies": {
|
|
44
|
+
"@oimdb/cli": "^1.0.0"
|
|
45
|
+
},
|
|
46
46
|
"devDependencies": {
|
|
47
|
-
"@
|
|
47
|
+
"@oimdb/cli": "*",
|
|
48
48
|
"tsup": "^8.0.0",
|
|
49
|
-
"typescript": "^5.2.2"
|
|
50
|
-
"@types/jest": "^29.5.5",
|
|
51
|
-
"jest": "^29.7.0",
|
|
52
|
-
"ts-jest": "^29.1.1"
|
|
49
|
+
"typescript": "^5.2.2"
|
|
53
50
|
},
|
|
54
51
|
"keywords": [
|
|
55
52
|
"oimdb",
|