@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.
- package/CHANGELOG.md +8 -0
- package/README.dockerhub.md +215 -293
- package/README.md +205 -330
- package/dist/cli/ViewerCommand.js +117 -0
- package/dist/index.js +16 -7
- package/dist/logging/viewer/LiveLogViewer.js +8 -0
- package/package.json +1 -1
|
@@ -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
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
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.
|
|
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>",
|