@mokoconsulting/mokomysql-mcp 0.0.0-stage → 2.5.4

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,70 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ *
3
+ * This file is part of a Moko Consulting project.
4
+ *
5
+ * SPDX-License-Identifier: GPL-3.0-or-later
6
+ *
7
+ * FILE INFORMATION
8
+ * DEFGROUP: mokomysql-mcp.Secrets
9
+ * INGROUP: mokomysql-mcp
10
+ * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokomysql-mcp
11
+ * PATH: /source/secrets.ts
12
+ * BRIEF: Resolve file-backed secret references in a parsed config object.
13
+ */
14
+
15
+ import { readFile } from 'node:fs/promises';
16
+ import { homedir } from 'node:os';
17
+ import { resolve } from 'node:path';
18
+
19
+ /** Expand a leading "~" to the user's home directory, then resolve to absolute. */
20
+ function expandHome(p: string): string {
21
+ if (p === '~') return homedir();
22
+ if (p.startsWith('~/') || p.startsWith('~\\')) {
23
+ return resolve(homedir(), p.slice(2));
24
+ }
25
+ return resolve(p);
26
+ }
27
+
28
+ /**
29
+ * Resolve a single config string.
30
+ * - "file:<path>" -> the trimmed contents of <path> (a leading "~" expands to $HOME).
31
+ * - anything else -> returned unchanged, so inline literals keep working.
32
+ *
33
+ * A missing/unreadable file throws (fail loud) — a secret must never silently
34
+ * degrade to the literal "file:..." string.
35
+ */
36
+ async function resolveRef(value: string): Promise<string> {
37
+ const m = /^file:(.+)$/s.exec(value.trim());
38
+ if (!m) return value;
39
+ const path = expandHome(m[1].trim());
40
+ try {
41
+ const raw = await readFile(path, 'utf-8');
42
+ return raw.replace(/\r?\n+$/, '');
43
+ } catch (err) {
44
+ const msg = err instanceof Error ? err.message : String(err);
45
+ throw new Error(`Failed to read secret file "${path}" (referenced as "${value}"): ${msg}`);
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Recursively resolve every "file:" secret reference in a parsed config object,
51
+ * in place. Lets secrets (tokens, passwords, API keys) live in files such as
52
+ * ~/.ssh/tokens/* instead of inline in the config JSON. Plain values are left
53
+ * untouched, so pre-existing inline configs remain valid.
54
+ */
55
+ export async function resolveSecretRefs(obj: unknown): Promise<void> {
56
+ if (Array.isArray(obj)) {
57
+ for (let i = 0; i < obj.length; i++) {
58
+ const v = obj[i];
59
+ if (typeof v === 'string') obj[i] = await resolveRef(v);
60
+ else if (v && typeof v === 'object') await resolveSecretRefs(v);
61
+ }
62
+ } else if (obj && typeof obj === 'object') {
63
+ const rec = obj as Record<string, unknown>;
64
+ for (const k of Object.keys(rec)) {
65
+ const v = rec[k];
66
+ if (typeof v === 'string') rec[k] = await resolveRef(v);
67
+ else if (v && typeof v === 'object') await resolveSecretRefs(v);
68
+ }
69
+ }
70
+ }
@@ -0,0 +1,62 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ *
3
+ * This file is part of a Moko Consulting project.
4
+ *
5
+ * SPDX-License-Identifier: GPL-3.0-or-later
6
+ *
7
+ * FILE INFORMATION
8
+ * DEFGROUP: mokomysql.State
9
+ * INGROUP: mokomysql
10
+ * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokomysql-mcp
11
+ * PATH: /source/state.ts
12
+ * BRIEF: Sidecar state store for learned Joomla markers (per host:port/database).
13
+ */
14
+
15
+ import { readFile, writeFile } from 'node:fs/promises';
16
+ import { getStatePath } from './config.js';
17
+ import type { JoomlaMarker, MySqlState } from './types.js';
18
+
19
+ let cache: MySqlState | null = null;
20
+ /** Serializes write-through so concurrent setMarker calls can't clobber the file. */
21
+ let writeChain: Promise<void> = Promise.resolve();
22
+
23
+ /** Stable key for a learned verdict: identifies the physical database target. */
24
+ export function markerKey(host: string, port: number, database: string): string {
25
+ return `${host}:${port}/${database}`;
26
+ }
27
+
28
+ /** Load the sidecar once into memory; a missing/corrupt file is an empty store. */
29
+ export async function loadState(): Promise<MySqlState> {
30
+ if (cache) return cache;
31
+ try {
32
+ const parsed = JSON.parse(await readFile(getStatePath(), 'utf-8')) as Partial<MySqlState>;
33
+ cache = parsed && typeof parsed.markers === 'object' && parsed.markers !== null
34
+ ? { markers: parsed.markers }
35
+ : { markers: {} };
36
+ } catch {
37
+ cache = { markers: {} };
38
+ }
39
+ return cache;
40
+ }
41
+
42
+ export async function getMarker(key: string): Promise<JoomlaMarker | undefined> {
43
+ return (await loadState()).markers[key];
44
+ }
45
+
46
+ /**
47
+ * Record a learned verdict and persist it. The write is best-effort: an unwritable
48
+ * sidecar (read-only home, permissions) must not break queries — the in-memory
49
+ * cache still carries the verdict for this process, we just can't remember it next time.
50
+ */
51
+ export async function setMarker(key: string, marker: JoomlaMarker): Promise<void> {
52
+ const state = await loadState();
53
+ state.markers[key] = marker;
54
+ writeChain = writeChain.then(async () => {
55
+ try {
56
+ await writeFile(getStatePath(), JSON.stringify(state, null, '\t') + '\n', 'utf-8');
57
+ } catch (e) {
58
+ process.stderr.write(`mokomysql: could not write state file ${getStatePath()}: ${e}\n`);
59
+ }
60
+ });
61
+ await writeChain;
62
+ }
@@ -0,0 +1,91 @@
1
+ /* Copyright (C) 2026 Moko Consulting <hello@mokoconsulting.tech>
2
+ *
3
+ * This file is part of a Moko Consulting project.
4
+ *
5
+ * SPDX-License-Identifier: GPL-3.0-or-later
6
+ *
7
+ * FILE INFORMATION
8
+ * DEFGROUP: mokomysql.Types
9
+ * INGROUP: mokomysql
10
+ * REPO: https://git.mokoconsulting.tech/MokoConsulting/mokomysql-mcp
11
+ * PATH: /src/types.ts
12
+ * VERSION: 01.00.00
13
+ * BRIEF: TypeScript type definitions for MySQL MCP server
14
+ */
15
+
16
+ export interface MySqlSslConfig {
17
+ /** Verify the server certificate against a CA. Defaults to true. */
18
+ rejectUnauthorized?: boolean;
19
+ /** PEM CA certificate(s) to trust. */
20
+ ca?: string;
21
+ /** PEM client certificate. */
22
+ cert?: string;
23
+ /** PEM client private key. */
24
+ key?: string;
25
+ }
26
+
27
+ export interface MySqlConnection {
28
+ host: string;
29
+ port?: number;
30
+ user: string;
31
+ password: string;
32
+ database?: string;
33
+ /**
34
+ * TLS for the connection. `true` enables TLS with certificate verification
35
+ * (secure default). Pass an object to customize — e.g. `{ ca: '...' }` for a
36
+ * private CA, or `{ rejectUnauthorized: false }` to explicitly opt out of
37
+ * verification (insecure; only for trusted internal hosts with self-signed
38
+ * certs). A bare `true` is never silently downgraded.
39
+ */
40
+ ssl?: boolean | MySqlSslConfig;
41
+ timezone?: string;
42
+ /**
43
+ * Extra keyword shortcuts that resolve to this connection.
44
+ * Matched case-insensitively with all punctuation stripped, so
45
+ * "moko.live", "moko-live", "Moko Live" and "mokolive" are equivalent.
46
+ */
47
+ tags?: string[];
48
+ /**
49
+ * Marks this connection as pointing at a Joomla database. Writes to a Joomla DB
50
+ * require the two-step `confirm_token` handshake (first call returns a token and
51
+ * does not execute; the same call repeated with the token runs it) on top of the
52
+ * usual `confirm: true` — reads stay unrestricted. Leave unset to auto-detect from
53
+ * the schema signature (presence of `<prefix>extensions` + `<prefix>assets`);
54
+ * set `true` to force the gate on, or `false` to force it off and skip detection.
55
+ */
56
+ joomla?: boolean;
57
+ /**
58
+ * Joomla table prefix for this connection (e.g. "jos_", "vkw4n_"). Only used when
59
+ * `joomla: true` is set explicitly — it lets the nested-set guard recognize
60
+ * `#__assets`/`#__categories`/`#__menu` without a schema probe. When omitted, the
61
+ * prefix is derived from the detected signature tables.
62
+ */
63
+ joomlaPrefix?: string;
64
+ }
65
+
66
+ export interface MySqlConfig {
67
+ connections: Record<string, MySqlConnection>;
68
+ defaultConnection: string;
69
+ }
70
+
71
+ /**
72
+ * A learned Joomla verdict for one `host:port/database` target, persisted to the
73
+ * sidecar state file so the server knows the target is Joomla on future connects
74
+ * without re-probing the schema.
75
+ */
76
+ export interface JoomlaMarker {
77
+ joomla: boolean;
78
+ /** Live table prefix (e.g. "jos_", "vkw4n_") when known. */
79
+ prefix: string | null;
80
+ /** ISO timestamp of when the verdict was learned. */
81
+ detectedAt: string;
82
+ }
83
+
84
+ /**
85
+ * Sidecar state the MCP manages itself (distinct from the user-authored config).
86
+ * Lives next to the config as `<config>.state.json`. Pure cache: delete it and
87
+ * the server re-detects; an explicit per-connection `joomla` flag overrides it.
88
+ */
89
+ export interface MySqlState {
90
+ markers: Record<string, JoomlaMarker>;
91
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "./dist",
7
+ "rootDir": "./source",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "resolveJsonModule": true,
13
+ "declaration": true,
14
+ "declarationMap": true,
15
+ "sourceMap": true
16
+ },
17
+ "include": ["source/**/*"],
18
+ "exclude": ["node_modules", "dist"]
19
+ }