@shibbirweb/mcp-db-read-only 0.2.0 → 1.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.
@@ -0,0 +1,117 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { FolderLogStore } from "../logging/store/FolderLogStore.js";
4
+ import { LiveLogViewer } from "../logging/viewer/LiveLogViewer.js";
5
+ /**
6
+ * `mcp-db-read-only viewer --dir <folder> [--port 4800] [--host 0.0.0.0]`
7
+ *
8
+ * The live log viewer on its own, with no MCP server, no database and no
9
+ * credentials: it only reads the log folder that MCP servers configured with
10
+ * DB_LOG_DIR write into, and serves the same page as DB_LOG_PORT does.
11
+ *
12
+ * Run separately, in a terminal, so the servers an MCP client starts stay as
13
+ * light as they can be and never compete for a port. It shows the calls of
14
+ * every copy of the server writing to the folder, across restarts.
15
+ *
16
+ * Unlike the in-server viewer, it binds at once and exits if it cannot: here
17
+ * a person is watching the terminal, so a clear error and a non-zero exit is
18
+ * the useful answer to a taken port, where inside an MCP server it would have
19
+ * cost the chat its tools.
20
+ */
21
+ export class ViewerCommand {
22
+ output;
23
+ env;
24
+ static DEFAULT_PORT = 4800;
25
+ static DEFAULT_HOST = "0.0.0.0";
26
+ static USAGE = [
27
+ "Usage: mcp-db-read-only viewer --dir <folder> [--port <port>] [--host <address>]",
28
+ "",
29
+ "Serves the live log viewer for a log folder written by MCP servers run with DB_LOG_DIR.",
30
+ "",
31
+ ` --dir, -d The log folder (required; DB_LOG_DIR is used when omitted)`,
32
+ ` --port, -p Port to listen on (default ${ViewerCommand.DEFAULT_PORT})`,
33
+ ` --host Address to listen on (default ${ViewerCommand.DEFAULT_HOST}, every interface)`,
34
+ " --help, -h Show this help",
35
+ "",
36
+ "The viewer has no access control: anyone who can reach the port can read the log.",
37
+ ].join("\n");
38
+ constructor(output = {
39
+ out: (message) => process.stdout.write(`${message}\n`),
40
+ error: (message) => process.stderr.write(`${message}\n`),
41
+ }, env = process.env) {
42
+ this.output = output;
43
+ this.env = env;
44
+ }
45
+ /** Pure: argument text in, options or an exit out. */
46
+ parse(args) {
47
+ let directory = this.env.DB_LOG_DIR ?? "";
48
+ let port = ViewerCommand.DEFAULT_PORT;
49
+ let host = ViewerCommand.DEFAULT_HOST;
50
+ for (let index = 0; index < args.length; index += 1) {
51
+ const [flag, inline] = args[index].split(/=(.*)/s, 2);
52
+ const value = () => inline ?? args[++index];
53
+ switch (flag) {
54
+ case "--help":
55
+ case "-h":
56
+ return { kind: "exit", code: 0, message: ViewerCommand.USAGE };
57
+ case "--dir":
58
+ case "-d":
59
+ directory = value() ?? "";
60
+ break;
61
+ case "--port":
62
+ case "-p": {
63
+ const text = value() ?? "";
64
+ const parsed = Number(text);
65
+ if (!/^\d+$/.test(text) || parsed < 1 || parsed > 65535) {
66
+ return { kind: "exit", code: 2, message: `--port must be a number from 1 to 65535, got "${text}".\n\n${ViewerCommand.USAGE}` };
67
+ }
68
+ port = parsed;
69
+ break;
70
+ }
71
+ case "--host":
72
+ host = value() ?? "";
73
+ break;
74
+ default:
75
+ return { kind: "exit", code: 2, message: `Unknown option "${args[index]}".\n\n${ViewerCommand.USAGE}` };
76
+ }
77
+ }
78
+ if (!directory) {
79
+ return { kind: "exit", code: 2, message: `--dir is required: the folder your MCP servers write with DB_LOG_DIR.\n\n${ViewerCommand.USAGE}` };
80
+ }
81
+ if (!host) {
82
+ return { kind: "exit", code: 2, message: `--host needs an address.\n\n${ViewerCommand.USAGE}` };
83
+ }
84
+ return { kind: "run", options: { directory: resolve(directory), port, host } };
85
+ }
86
+ /** @returns the exit code: 0 after a clean shutdown, non-zero when it could not start. */
87
+ async run(args) {
88
+ const parsed = this.parse(args);
89
+ if (parsed.kind === "exit") {
90
+ (parsed.code === 0 ? this.output.out : this.output.error)(parsed.message);
91
+ return parsed.code;
92
+ }
93
+ const { directory, port, host } = parsed.options;
94
+ // Created if missing, owner-only, so the viewer can be started before the
95
+ // first call is ever logged.
96
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
97
+ const viewer = new LiveLogViewer(host, port, new FolderLogStore(directory), (message) => this.output.out(message));
98
+ const status = await viewer.ensureRunning();
99
+ if (status.state !== "running") {
100
+ const reason = status.state === "unavailable" ? status.reason : "it could not start";
101
+ this.output.error(`Could not start the viewer: ${reason}. Free that port, or choose another with --port.`);
102
+ await viewer.stop();
103
+ return 1;
104
+ }
105
+ // The in-server viewer never holds its process open; here it is the
106
+ // whole point of the process.
107
+ viewer.holdProcessOpen();
108
+ this.output.out(`Reading ${directory}. Press Ctrl+C to stop.`);
109
+ return new Promise((done) => {
110
+ const stop = () => {
111
+ void viewer.stop().then(() => done(0));
112
+ };
113
+ process.once("SIGINT", stop);
114
+ process.once("SIGTERM", stop);
115
+ });
116
+ }
117
+ }
package/dist/index.js CHANGED
@@ -3,13 +3,22 @@ import { ApplicationFactory } from "./ApplicationFactory.js";
3
3
  /**
4
4
  * Entry point. Nothing but construction and start.
5
5
  *
6
- * There is no configuration check and no exit path here on purpose. A server
7
- * with no usable connection still starts, still answers tools/list, and
8
- * reports the problem through tool results. An MCP client cannot show a stderr
9
- * message from a process that exited during handshake; it reports "server
10
- * failed to start", which is indistinguishable from a broken image or a wrong
11
- * path. A running server that says "call connect" is diagnosable, and usually
12
- * fixable in the same conversation.
6
+ * With no arguments, which is how every MCP client runs it, this is the MCP
7
+ * server. `viewer` runs the standalone live log viewer instead; it is loaded
8
+ * only then, so it costs the MCP server's startup nothing.
9
+ *
10
+ * For the MCP server there is no configuration check and no exit path here,
11
+ * on purpose. A server with no usable connection still starts, still answers
12
+ * tools/list, and reports the problem through tool results. An MCP client
13
+ * cannot show a stderr message from a process that exited during handshake;
14
+ * it reports "server failed to start", which is indistinguishable from a
15
+ * broken image or a wrong path. A running server that says "call connect" is
16
+ * diagnosable, and usually fixable in the same conversation.
13
17
  */
18
+ const [command, ...rest] = process.argv.slice(2);
19
+ if (command === "viewer") {
20
+ const { ViewerCommand } = await import("./cli/ViewerCommand.js");
21
+ process.exit(await new ViewerCommand().run(rest));
22
+ }
14
23
  const application = new ApplicationFactory();
15
24
  await application.create().start();
@@ -108,6 +108,14 @@ export class LiveLogViewer {
108
108
  }
109
109
  return this.binding;
110
110
  }
111
+ /**
112
+ * Keep the process alive for as long as the viewer listens. Only the
113
+ * standalone `viewer` command wants this; inside an MCP server the viewer
114
+ * must never be what keeps the process running.
115
+ */
116
+ holdProcessOpen() {
117
+ this.server?.ref();
118
+ }
111
119
  /** Never throws: shutdown calls it. */
112
120
  async stop() {
113
121
  this.stopped = true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shibbirweb/mcp-db-read-only",
3
- "version": "0.2.0",
3
+ "version": "1.0.0",
4
4
  "description": "A read-only MCP server for MySQL, MariaDB, PostgreSQL, SQLite, SQL Server, ClickHouse, MongoDB, Redis and Elasticsearch, with connections switchable at runtime.",
5
5
  "license": "MIT",
6
6
  "author": "Md. Shibbir Ahmed <shibbirweb@gmail.com>",