@reddb-io/ui-mcp 0.1.1

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/dist/index.js ADDED
@@ -0,0 +1,599 @@
1
+ #!/usr/bin/env node
2
+ import { registerAppResource, registerAppTool, RESOURCE_MIME_TYPE, } from "@modelcontextprotocol/ext-apps/server";
3
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+ import { readFileSync } from "node:fs";
6
+ import { dirname, join } from "node:path";
7
+ import { fileURLToPath } from "node:url";
8
+ import { z } from "zod/v4";
9
+ import { classifyTarget } from "./target-mode.js";
10
+ import { resolveLocalFilePath, spawnLocalServer, stopAllLocalServers, } from "./local-server.js";
11
+ import { startEmbedServer, stopAllEmbedServers, } from "./embed-server.js";
12
+ import { registerDataTools } from "./data-tools.js";
13
+ // Single source of truth for "what version am I": the package.json version
14
+ // (changeset-managed, version-locked with @reddb-io/ui + ui-kit), overridable by
15
+ // the RED_BUILD_VERSION env the release CI sets from the git tag. From the
16
+ // compiled dist/index.js, ../package.json is the package root manifest (shipped
17
+ // because `files` always includes it). Mirrors the ../red-skills build-info
18
+ // pattern; replaces the previously hardcoded "0.1.0".
19
+ function resolveVersion() {
20
+ const fromEnv = process.env.RED_BUILD_VERSION;
21
+ if (fromEnv)
22
+ return fromEnv.replace(/^v/, "");
23
+ try {
24
+ const here = dirname(fileURLToPath(import.meta.url));
25
+ const pkg = JSON.parse(readFileSync(join(here, "..", "package.json"), "utf8"));
26
+ if (pkg.version)
27
+ return pkg.version;
28
+ }
29
+ catch {
30
+ // fall through
31
+ }
32
+ return "0.0.0-dev";
33
+ }
34
+ const RED_UI_MCP_VERSION = resolveVersion();
35
+ const APP_RESOURCE_URI = "ui://red-ui/app.html";
36
+ const MCP_APPS_CDN = "https://esm.sh/@modelcontextprotocol/ext-apps@1.7.2/app-with-deps";
37
+ const DEFAULT_APP_URL = "https://ui.reddb.io";
38
+ function parseList(value) {
39
+ if (!value)
40
+ return [];
41
+ return value
42
+ .split(",")
43
+ .map((item) => item.trim())
44
+ .filter(Boolean);
45
+ }
46
+ function normalizeHttpUrl(value, fallback) {
47
+ const raw = value?.trim() || fallback;
48
+ const url = new URL(raw);
49
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
50
+ throw new Error(`RED_UI_APP_URL must be an http(s) URL, got ${raw}`);
51
+ }
52
+ url.hash = "";
53
+ return url.toString().replace(/\/$/, "");
54
+ }
55
+ function getOrigin(url) {
56
+ return new URL(url).origin;
57
+ }
58
+ // Local mode (#51, ADR-0006) serves the embeddable bundle from an ephemeral
59
+ // `http://127.0.0.1:<port>` the MCP picks at runtime, so the CSP cannot name the
60
+ // exact origin ahead of time. Allow loopback on any port for framing, scripts,
61
+ // and connections; the hosted origin stays explicit. Port-wildcard host-sources
62
+ // are valid CSP (`http://127.0.0.1:*`).
63
+ const LOCALHOST_CSP = ["http://localhost:*", "http://127.0.0.1:*"];
64
+ function appCsp(config) {
65
+ const appOrigin = getOrigin(config.appUrl);
66
+ return {
67
+ connectDomains: [...config.connectDomains, ...LOCALHOST_CSP],
68
+ frameDomains: [appOrigin, ...LOCALHOST_CSP],
69
+ resourceDomains: [appOrigin, "https://esm.sh", ...LOCALHOST_CSP],
70
+ };
71
+ }
72
+ function loadConfig() {
73
+ const appUrl = normalizeHttpUrl(process.env.RED_UI_APP_URL, DEFAULT_APP_URL);
74
+ const defaultConnectDomains = [
75
+ getOrigin(appUrl),
76
+ "http://localhost:5055",
77
+ "http://127.0.0.1:5055",
78
+ "http://localhost:15055",
79
+ "http://127.0.0.1:15055",
80
+ "http://localhost:25055",
81
+ "http://127.0.0.1:25055",
82
+ ];
83
+ // Auth for the server-side data tools: a bearer token (RED_UI_CONNECTION_TOKEN)
84
+ // or a raw Authorization header (RED_UI_CONNECTION_AUTH). Read from env only —
85
+ // never echoed into tool output or logs.
86
+ const token = process.env.RED_UI_CONNECTION_TOKEN?.trim();
87
+ const rawAuth = process.env.RED_UI_CONNECTION_AUTH?.trim();
88
+ const connectionAuthHeader = rawAuth || (token ? `Bearer ${token}` : undefined);
89
+ return {
90
+ appUrl,
91
+ connectDomains: [
92
+ ...new Set([
93
+ ...defaultConnectDomains,
94
+ ...parseList(process.env.RED_UI_CONNECT_DOMAINS),
95
+ ]),
96
+ ],
97
+ connectionUrl: process.env.RED_UI_CONNECTION_URL?.trim() || undefined,
98
+ connectionAuthHeader,
99
+ };
100
+ }
101
+ function escapeHtml(value) {
102
+ return value
103
+ .replaceAll("&", "&amp;")
104
+ .replaceAll("<", "&lt;")
105
+ .replaceAll(">", "&gt;")
106
+ .replaceAll('"', "&quot;")
107
+ .replaceAll("'", "&#39;");
108
+ }
109
+ function jsonForInlineScript(value) {
110
+ return JSON.stringify(value).replaceAll("<", "\\u003c");
111
+ }
112
+ function renderRedUiAppHtml(config) {
113
+ return `<!doctype html>
114
+ <html lang="en">
115
+ <head>
116
+ <meta charset="utf-8" />
117
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
118
+ <title>red-ui</title>
119
+ <style>
120
+ :root {
121
+ color-scheme: light dark;
122
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
123
+ background: #101214;
124
+ color: #f5f7f8;
125
+ }
126
+ * { box-sizing: border-box; }
127
+ body {
128
+ margin: 0;
129
+ min-height: 100vh;
130
+ background: #101214;
131
+ }
132
+ .shell {
133
+ display: grid;
134
+ grid-template-rows: auto 1fr;
135
+ min-height: 100vh;
136
+ }
137
+ header {
138
+ display: flex;
139
+ align-items: center;
140
+ justify-content: space-between;
141
+ gap: 16px;
142
+ padding: 10px 12px;
143
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
144
+ background: #171a1d;
145
+ }
146
+ .brand {
147
+ display: flex;
148
+ align-items: center;
149
+ gap: 10px;
150
+ min-width: 0;
151
+ }
152
+ .mark {
153
+ display: grid;
154
+ place-items: center;
155
+ width: 28px;
156
+ height: 28px;
157
+ border: 1px solid rgba(255, 255, 255, 0.18);
158
+ border-radius: 6px;
159
+ color: #ff5a4d;
160
+ font-weight: 720;
161
+ }
162
+ .title {
163
+ display: grid;
164
+ gap: 1px;
165
+ min-width: 0;
166
+ }
167
+ .title strong {
168
+ font-size: 14px;
169
+ font-weight: 680;
170
+ }
171
+ .title span {
172
+ overflow: hidden;
173
+ max-width: min(60vw, 720px);
174
+ color: #a9b0b6;
175
+ font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
176
+ font-size: 11px;
177
+ text-overflow: ellipsis;
178
+ white-space: nowrap;
179
+ }
180
+ .actions {
181
+ display: flex;
182
+ align-items: center;
183
+ gap: 8px;
184
+ flex: 0 0 auto;
185
+ }
186
+ button, a.button {
187
+ appearance: none;
188
+ display: inline-flex;
189
+ align-items: center;
190
+ justify-content: center;
191
+ min-height: 30px;
192
+ padding: 0 10px;
193
+ border: 1px solid rgba(255, 255, 255, 0.14);
194
+ border-radius: 6px;
195
+ background: #22272b;
196
+ color: #f5f7f8;
197
+ font: inherit;
198
+ font-size: 12px;
199
+ text-decoration: none;
200
+ cursor: pointer;
201
+ }
202
+ button:hover, a.button:hover {
203
+ background: #2a3035;
204
+ }
205
+ main {
206
+ min-height: 0;
207
+ background: #0d0f11;
208
+ }
209
+ iframe {
210
+ display: block;
211
+ width: 100%;
212
+ height: 100%;
213
+ min-height: 620px;
214
+ border: 0;
215
+ background: white;
216
+ }
217
+ .empty {
218
+ display: none;
219
+ padding: 28px;
220
+ color: #c7cdd2;
221
+ }
222
+ .empty code {
223
+ padding: 2px 5px;
224
+ border-radius: 4px;
225
+ background: rgba(255, 255, 255, 0.08);
226
+ color: #fff;
227
+ }
228
+ body[data-error="true"] iframe { display: none; }
229
+ body[data-error="true"] .empty { display: block; }
230
+ @media (max-width: 720px) {
231
+ header {
232
+ align-items: stretch;
233
+ flex-direction: column;
234
+ }
235
+ .actions {
236
+ justify-content: flex-end;
237
+ }
238
+ .title span {
239
+ max-width: 100%;
240
+ }
241
+ }
242
+ </style>
243
+ </head>
244
+ <body>
245
+ <div class="shell">
246
+ <header>
247
+ <div class="brand">
248
+ <div class="mark">r</div>
249
+ <div class="title">
250
+ <strong>red-ui MCP App</strong>
251
+ <span id="target">${escapeHtml(config.appUrl)}</span>
252
+ </div>
253
+ </div>
254
+ <div class="actions">
255
+ <button id="fullscreen" type="button">Fullscreen</button>
256
+ <button id="open" type="button">Open</button>
257
+ </div>
258
+ </header>
259
+ <main>
260
+ <iframe id="red-ui-frame" title="red-ui"></iframe>
261
+ <section class="empty">
262
+ <h1>red-ui is not reachable</h1>
263
+ <p>Start the web app with <code>pnpm dev</code>, or set <code>RED_UI_APP_URL</code> before starting this MCP server.</p>
264
+ </section>
265
+ </main>
266
+ </div>
267
+ <script type="module">
268
+ const DEFAULT_APP_URL = ${jsonForInlineScript(config.appUrl)};
269
+ const MCP_APPS_CDN = ${jsonForInlineScript(MCP_APPS_CDN)};
270
+ const frame = document.getElementById('red-ui-frame');
271
+ const target = document.getElementById('target');
272
+ const openButton = document.getElementById('open');
273
+ const fullscreenButton = document.getElementById('fullscreen');
274
+ const state = {
275
+ appUrl: DEFAULT_APP_URL,
276
+ view: 'query',
277
+ connectionUrl: ''
278
+ };
279
+
280
+ let app;
281
+
282
+ function applyPatch(patch) {
283
+ if (!patch || typeof patch !== 'object') return;
284
+ if (typeof patch.appUrl === 'string' && patch.appUrl) state.appUrl = patch.appUrl;
285
+ if (typeof patch.view === 'string') state.view = patch.view;
286
+ if (typeof patch.connectionUrl === 'string') state.connectionUrl = patch.connectionUrl;
287
+ render();
288
+ }
289
+
290
+ function buildFrameUrl() {
291
+ const url = new URL(state.appUrl || DEFAULT_APP_URL);
292
+ url.searchParams.set('mcp', '1');
293
+ // Open Contract pre-configuration: seed non-secret endpoint + route.
294
+ // Never seed a token here — secrets are reserved for a later
295
+ // postMessage channel.
296
+ if (state.connectionUrl) url.searchParams.set('cs', state.connectionUrl);
297
+ if (state.view) url.searchParams.set('to', '/' + state.view.replace(/^\\//, ''));
298
+ return url.toString();
299
+ }
300
+
301
+ function render() {
302
+ const nextUrl = buildFrameUrl();
303
+ target.textContent = nextUrl;
304
+ openButton.dataset.url = nextUrl;
305
+ if (frame.src !== nextUrl) frame.src = nextUrl;
306
+ }
307
+
308
+ openButton.addEventListener('click', async () => {
309
+ const url = openButton.dataset.url || buildFrameUrl();
310
+ if (app?.openLink) {
311
+ await app.openLink({ url }).catch(() => window.open(url, '_blank', 'noopener,noreferrer'));
312
+ return;
313
+ }
314
+ window.open(url, '_blank', 'noopener,noreferrer');
315
+ });
316
+
317
+ fullscreenButton.addEventListener('click', async () => {
318
+ if (app?.requestDisplayMode) {
319
+ await app.requestDisplayMode({ mode: 'fullscreen' }).catch(() => {});
320
+ }
321
+ });
322
+
323
+ frame.addEventListener('error', () => {
324
+ document.body.dataset.error = 'true';
325
+ });
326
+
327
+ render();
328
+
329
+ try {
330
+ const { App } = await import(MCP_APPS_CDN);
331
+ app = new App(
332
+ { name: 'red-ui', version: ${JSON.stringify(RED_UI_MCP_VERSION)} },
333
+ {},
334
+ { autoResize: true }
335
+ );
336
+ app.ontoolinput = (params) => applyPatch(params.arguments);
337
+ app.ontoolresult = (params) => applyPatch(params.structuredContent);
338
+ await app.connect();
339
+ } catch (error) {
340
+ console.warn('MCP Apps bridge unavailable; rendering red-ui with static defaults.', error);
341
+ }
342
+ </script>
343
+ </body>
344
+ </html>`;
345
+ }
346
+ function createServer(config) {
347
+ const server = new McpServer({
348
+ name: "red-ui",
349
+ version: RED_UI_MCP_VERSION,
350
+ });
351
+ // One `red server` per local file (its flock is single-writer — ADR-0006).
352
+ // Keyed by the resolved absolute path so a repeated open reuses the child.
353
+ const localServers = new Map();
354
+ async function openLocalServer(target) {
355
+ const key = resolveLocalFilePath(target);
356
+ const existing = localServers.get(key);
357
+ if (existing) {
358
+ try {
359
+ const handle = await existing;
360
+ if (handle.child.exitCode === null &&
361
+ handle.child.signalCode === null) {
362
+ return handle;
363
+ }
364
+ }
365
+ catch {
366
+ // Previous attempt failed; fall through and respawn.
367
+ }
368
+ }
369
+ const pending = spawnLocalServer({ target });
370
+ localServers.set(key, pending);
371
+ try {
372
+ return await pending;
373
+ }
374
+ catch (error) {
375
+ localServers.delete(key);
376
+ throw error;
377
+ }
378
+ }
379
+ // One embed server per MCP process serves the Embeddable Lib bundle (#37) over
380
+ // `http://127.0.0.1:<port>` for local mode (#51, ADR-0006). The bundle is the
381
+ // same regardless of which file is open, so it is a singleton; it is torn down
382
+ // with the local servers on disconnect/exit.
383
+ let embedServer = null;
384
+ async function openEmbedServer() {
385
+ if (embedServer) {
386
+ try {
387
+ const handle = await embedServer;
388
+ if (handle.server.listening)
389
+ return handle;
390
+ }
391
+ catch {
392
+ // Previous attempt failed; fall through and respawn.
393
+ }
394
+ }
395
+ const pending = startEmbedServer();
396
+ embedServer = pending;
397
+ try {
398
+ return await pending;
399
+ }
400
+ catch (error) {
401
+ embedServer = null;
402
+ throw error;
403
+ }
404
+ }
405
+ registerAppResource(server, "red-ui app", APP_RESOURCE_URI, {
406
+ description: "Interactive red-ui database workspace embedded as an MCP App.",
407
+ _meta: {
408
+ ui: {
409
+ csp: appCsp(config),
410
+ prefersBorder: false,
411
+ },
412
+ },
413
+ }, async () => ({
414
+ contents: [
415
+ {
416
+ uri: APP_RESOURCE_URI,
417
+ mimeType: RESOURCE_MIME_TYPE,
418
+ text: renderRedUiAppHtml(config),
419
+ _meta: {
420
+ ui: {
421
+ csp: appCsp(config),
422
+ prefersBorder: false,
423
+ },
424
+ },
425
+ },
426
+ ],
427
+ }));
428
+ registerAppTool(server, "open_red_ui", {
429
+ title: "Open red-ui",
430
+ description: "Display the red-ui database workspace as an embedded MCP App.",
431
+ inputSchema: {
432
+ connectionUrl: z
433
+ .string()
434
+ .optional()
435
+ .describe("Optional red/http connection URL to preselect in red-ui, for example http://localhost:5055."),
436
+ view: z
437
+ .enum(["home", "query", "collections", "cluster", "security"])
438
+ .optional()
439
+ .describe("Initial red-ui view to display. Defaults to query."),
440
+ },
441
+ _meta: {
442
+ ui: {
443
+ resourceUri: APP_RESOURCE_URI,
444
+ visibility: ["model", "app"],
445
+ },
446
+ },
447
+ }, async ({ connectionUrl, view }) => {
448
+ const selectedView = view ?? "query";
449
+ const mode = classifyTarget(connectionUrl);
450
+ // Local-file targets (#48, ADR-0006): the browser Surface cannot reach a
451
+ // filesystem path, so the MCP process spawns one `red server` that owns
452
+ // the file, health-checks it via `/stats`, and points the UI at the
453
+ // ephemeral `http://127.0.0.1:<port>` it exposes. The child is torn down
454
+ // on MCP exit/disconnect.
455
+ if (mode === "local") {
456
+ const target = connectionUrl ?? "";
457
+ try {
458
+ const local = await openLocalServer(target);
459
+ const readOnlyNote = local.readOnly
460
+ ? " (read-only: the file is already held by another writer)"
461
+ : "";
462
+ // Serve the UI same-origin over http://localhost so the iframe and
463
+ // the spawned red server share an http:// scheme — no HTTPS→localhost
464
+ // mixed-content block (Firefox/Safari), fully offline (#51, ADR-0006).
465
+ // If the embeddable bundle can't be found we still hand back a working
466
+ // local API, falling back to the hosted UI with a note.
467
+ let uiUrl = config.appUrl;
468
+ let uiNote = "";
469
+ try {
470
+ const embed = await openEmbedServer();
471
+ uiUrl = embed.baseUrl;
472
+ }
473
+ catch (embedError) {
474
+ const detail = embedError instanceof Error
475
+ ? embedError.message
476
+ : String(embedError);
477
+ uiNote = ` (local UI bundle unavailable, falling back to ${config.appUrl}: ${detail})`;
478
+ }
479
+ return {
480
+ content: [
481
+ {
482
+ type: "text",
483
+ text: `Serving local file ${local.filePath} via a red server at ${local.baseUrl}${readOnlyNote}; opening red-ui ${selectedView} view from ${uiUrl}.${uiNote}`,
484
+ },
485
+ ],
486
+ structuredContent: {
487
+ appUrl: uiUrl,
488
+ connectionUrl: local.baseUrl,
489
+ view: selectedView,
490
+ mode,
491
+ localServer: {
492
+ filePath: local.filePath,
493
+ port: local.port,
494
+ readOnly: local.readOnly,
495
+ },
496
+ },
497
+ };
498
+ }
499
+ catch (error) {
500
+ const message = error instanceof Error ? error.message : String(error);
501
+ return {
502
+ content: [
503
+ {
504
+ type: "text",
505
+ text: `Could not open the local file "${target}": ${message}`,
506
+ },
507
+ ],
508
+ isError: true,
509
+ structuredContent: {
510
+ appUrl: config.appUrl,
511
+ connectionUrl: "",
512
+ view: selectedView,
513
+ mode,
514
+ },
515
+ };
516
+ }
517
+ }
518
+ // Remote target: hand the URL to the browser via the Open Contract; no
519
+ // process is spawned.
520
+ return {
521
+ content: [
522
+ {
523
+ type: "text",
524
+ text: `Opening red-ui ${selectedView} view from ${config.appUrl}.`,
525
+ },
526
+ ],
527
+ structuredContent: {
528
+ appUrl: config.appUrl,
529
+ connectionUrl: connectionUrl ?? "",
530
+ view: selectedView,
531
+ mode,
532
+ },
533
+ };
534
+ });
535
+ // Server-side data tools (#49, ADR-0006): run a RedClient in this process and
536
+ // return reddb reads to the model as structuredContent, without driving the
537
+ // visual iframe. Honour the configured default connection + auth (env-only).
538
+ registerDataTools(server, {
539
+ defaultUrl: config.connectionUrl,
540
+ authHeader: config.connectionAuthHeader,
541
+ });
542
+ return server;
543
+ }
544
+ function printHelp() {
545
+ process.stdout.write(`red-ui MCP server
546
+
547
+ Usage:
548
+ ui-mcp --stdio
549
+
550
+ Environment:
551
+ RED_UI_APP_URL URL of the hosted or running red-ui web app. Default: ${DEFAULT_APP_URL}
552
+ RED_UI_CONNECT_DOMAINS Comma-separated extra connect-src domains for the embedded app.
553
+ RED_BINARY Path to the reddb \`red\` binary used to serve local .rdb files.
554
+ Defaults to the @reddb-io/sdk binary, else \`red\` on PATH.
555
+ RED_UI_EMBED_DIR Directory holding the embeddable bundle (\`index.js\`) served over
556
+ http://localhost in local mode. Defaults to the installed @reddb-io/ui.
557
+ RED_UI_CONNECTION_URL Default reddb endpoint for the data tools (query/list/get), e.g. http://localhost:5055.
558
+ RED_UI_CONNECTION_TOKEN Bearer token for the data tools' RedClient (sent as Authorization: Bearer …; never logged).
559
+ RED_UI_CONNECTION_AUTH Raw Authorization header for the data tools (overrides RED_UI_CONNECTION_TOKEN).
560
+
561
+ Example client config:
562
+ {
563
+ "mcpServers": {
564
+ "red-ui": {
565
+ "command": "npx",
566
+ "args": ["-y", "@reddb-io/ui@latest", "mcp", "--stdio"],
567
+ "env": { "RED_UI_APP_URL": "${DEFAULT_APP_URL}" }
568
+ }
569
+ }
570
+ }
571
+ `);
572
+ }
573
+ async function main() {
574
+ const args = new Set(process.argv.slice(2));
575
+ if (args.has("--help") || args.has("-h")) {
576
+ printHelp();
577
+ return;
578
+ }
579
+ if (!args.has("--stdio")) {
580
+ throw new Error("Only --stdio transport is supported for now.");
581
+ }
582
+ const server = createServer(loadConfig());
583
+ const transport = new StdioServerTransport();
584
+ // Tear down any spawned local `red server` children when the MCP client
585
+ // disconnects (stdin closes) — no orphan servers (ADR-0006). The process
586
+ // exit hooks in local-server.ts are the synchronous backstop.
587
+ const previousOnClose = transport.onclose?.bind(transport);
588
+ transport.onclose = () => {
589
+ previousOnClose?.();
590
+ void stopAllLocalServers();
591
+ void stopAllEmbedServers();
592
+ };
593
+ await server.connect(transport);
594
+ }
595
+ main().catch((error) => {
596
+ process.stderr.write(`${error instanceof Error ? error.stack : String(error)}\n`);
597
+ process.exit(1);
598
+ });
599
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,62 @@
1
+ import { type ChildProcess } from "node:child_process";
2
+ export interface RedBinary {
3
+ binaryPath: string;
4
+ /** Where we found it: an SDK package name, "path", or "env". */
5
+ source: string;
6
+ }
7
+ export interface LocalServerHandle {
8
+ baseUrl: string;
9
+ port: number;
10
+ filePath: string;
11
+ readOnly: boolean;
12
+ child: ChildProcess;
13
+ /** Idempotent teardown — SIGTERM then SIGKILL, resolves once the child exits. */
14
+ stop(): Promise<void>;
15
+ }
16
+ export interface SpawnLocalServerOptions {
17
+ /** Connection target as received by the tool (path / file:// / ~/… / *.rdb). */
18
+ target: string;
19
+ readOnly?: boolean;
20
+ /** Override binary resolution (tests, explicit config). */
21
+ binaryPath?: string;
22
+ /** Override the ephemeral port (tests). */
23
+ port?: number;
24
+ /** Max time to wait for `/stats` to report healthy. Default 10s. */
25
+ healthTimeoutMs?: number;
26
+ signal?: AbortSignal;
27
+ }
28
+ /**
29
+ * Resolve a fuzzy local target into an absolute filesystem path the `red`
30
+ * binary can `--path`. Handles `file://` URLs, `~`/`~/…` home expansion, and
31
+ * relative paths (resolved against the MCP process cwd).
32
+ */
33
+ export declare function resolveLocalFilePath(target: string): string;
34
+ /**
35
+ * Resolve the `red` binary: explicit `RED_BINARY` env → `@reddb-io/sdk`'s
36
+ * subprocess binary (or `reddb-cli`'s) → a PATH `red`. Throws an actionable
37
+ * error when none is found.
38
+ */
39
+ export declare function resolveRedBinary(): Promise<RedBinary>;
40
+ /**
41
+ * Raised when `red server` dies trying to acquire the single-writer `flock` on
42
+ * the target — the signal to retry the open with `--read-only` (#50).
43
+ */
44
+ export declare class LockContentionError extends Error {
45
+ readonly detail: string;
46
+ constructor(detail: string);
47
+ }
48
+ /** True when `text` (a `red server` stderr) looks like single-writer lock
49
+ * contention — used to decide whether to retry the open as `--read-only`. */
50
+ export declare function looksLikeLockContention(text: string): boolean;
51
+ /**
52
+ * Spawn one `red server` that owns `target`, wait for `/stats` to report
53
+ * healthy, and return a handle whose `stop()` tears the child down.
54
+ *
55
+ * Open mode (#50): an explicit `readOnly` is honoured verbatim. When it is
56
+ * unspecified, the file opens read-write but transparently falls back to
57
+ * `--read-only` if reddb dies acquiring the single-writer `flock` — the trigger
58
+ * case being a second viewer of a live agent memory `.rdb` (ADR-0006).
59
+ */
60
+ export declare function spawnLocalServer(options: SpawnLocalServerOptions): Promise<LocalServerHandle>;
61
+ /** Stop every tracked local server (MCP transport close / shutdown). */
62
+ export declare function stopAllLocalServers(): Promise<void>;