db-read 0.1.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/.db-read.example.yml +28 -0
- package/LICENSE +21 -0
- package/README.md +81 -0
- package/dist/adapters/base.d.ts +10 -0
- package/dist/adapters/base.js +2 -0
- package/dist/adapters/base.js.map +1 -0
- package/dist/adapters/createAdapter.d.ts +3 -0
- package/dist/adapters/createAdapter.js +14 -0
- package/dist/adapters/createAdapter.js.map +1 -0
- package/dist/adapters/mongoAdapter.d.ts +12 -0
- package/dist/adapters/mongoAdapter.js +79 -0
- package/dist/adapters/mongoAdapter.js.map +1 -0
- package/dist/adapters/mysqlAdapter.d.ts +11 -0
- package/dist/adapters/mysqlAdapter.js +68 -0
- package/dist/adapters/mysqlAdapter.js.map +1 -0
- package/dist/adapters/postgresAdapter.d.ts +11 -0
- package/dist/adapters/postgresAdapter.js +64 -0
- package/dist/adapters/postgresAdapter.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +81 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/loadConfig.d.ts +20 -0
- package/dist/config/loadConfig.js +158 -0
- package/dist/config/loadConfig.js.map +1 -0
- package/dist/lib/errors.d.ts +9 -0
- package/dist/lib/errors.js +19 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/mongoGuard.d.ts +2 -0
- package/dist/lib/mongoGuard.js +20 -0
- package/dist/lib/mongoGuard.js.map +1 -0
- package/dist/lib/sqlGuard.d.ts +1 -0
- package/dist/lib/sqlGuard.js +41 -0
- package/dist/lib/sqlGuard.js.map +1 -0
- package/dist/lib/types.d.ts +64 -0
- package/dist/lib/types.js +2 -0
- package/dist/lib/types.js.map +1 -0
- package/dist/mcp/server.d.ts +2 -0
- package/dist/mcp/server.js +172 -0
- package/dist/mcp/server.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
version: 1
|
|
2
|
+
defaultEnvironment: dev
|
|
3
|
+
defaults:
|
|
4
|
+
timeoutMs: 10000
|
|
5
|
+
maxRows: 1000
|
|
6
|
+
maxDocuments: 1000
|
|
7
|
+
environments:
|
|
8
|
+
dev:
|
|
9
|
+
connections:
|
|
10
|
+
local_pg:
|
|
11
|
+
kind: postgres
|
|
12
|
+
uri: postgresql://readonly:readonly@localhost:5432/app
|
|
13
|
+
hosted_mysql:
|
|
14
|
+
kind: mysql
|
|
15
|
+
host: mysql.example.com
|
|
16
|
+
port: 3306
|
|
17
|
+
user: readonly
|
|
18
|
+
password: change-me
|
|
19
|
+
database: billing
|
|
20
|
+
ssl: true
|
|
21
|
+
local_mongo:
|
|
22
|
+
kind: mongodb
|
|
23
|
+
uri: mongodb://readonly:readonly@localhost:27017
|
|
24
|
+
database: analytics
|
|
25
|
+
staging:
|
|
26
|
+
connections: {}
|
|
27
|
+
prod:
|
|
28
|
+
connections: {}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# db-read
|
|
2
|
+
|
|
3
|
+
`db-read` is a read-only MCP server for coding agents that need safe access to PostgreSQL, MySQL, and MongoDB. It supports both hosted and local databases through a git-ignored `.db-read.yml`.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- One MCP server for PostgreSQL, MySQL, and MongoDB
|
|
8
|
+
- Environment-based config: `dev`, `staging`, `prod`
|
|
9
|
+
- Local and hosted connection support via `uri` or explicit host settings
|
|
10
|
+
- Read-only guards for SQL and MongoDB aggregation
|
|
11
|
+
- MCP tools for listing connections, describing them, inspecting schema, and running read queries
|
|
12
|
+
|
|
13
|
+
## Config
|
|
14
|
+
|
|
15
|
+
Copy `.db-read.example.yml` to `.db-read.yml` and fill in your credentials.
|
|
16
|
+
|
|
17
|
+
```yaml
|
|
18
|
+
version: 1
|
|
19
|
+
defaultEnvironment: dev
|
|
20
|
+
defaults:
|
|
21
|
+
timeoutMs: 10000
|
|
22
|
+
maxRows: 1000
|
|
23
|
+
maxDocuments: 1000
|
|
24
|
+
environments:
|
|
25
|
+
dev:
|
|
26
|
+
connections:
|
|
27
|
+
app_pg:
|
|
28
|
+
kind: postgres
|
|
29
|
+
uri: postgresql://readonly:secret@localhost:5432/app
|
|
30
|
+
billing_mysql:
|
|
31
|
+
kind: mysql
|
|
32
|
+
host: db.example.com
|
|
33
|
+
port: 3306
|
|
34
|
+
user: readonly
|
|
35
|
+
password: secret
|
|
36
|
+
database: billing
|
|
37
|
+
ssl: true
|
|
38
|
+
analytics_mongo:
|
|
39
|
+
kind: mongodb
|
|
40
|
+
uri: mongodb+srv://readonly:secret@cluster.example.mongodb.net
|
|
41
|
+
database: analytics
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## CLI
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npm run build
|
|
48
|
+
node dist/cli.js validate-config --config .db-read.yml
|
|
49
|
+
node dist/cli.js serve --env dev --config .db-read.yml
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## MCP client examples
|
|
53
|
+
|
|
54
|
+
Codex-style stdio config:
|
|
55
|
+
|
|
56
|
+
```json
|
|
57
|
+
{
|
|
58
|
+
"mcpServers": {
|
|
59
|
+
"db-read": {
|
|
60
|
+
"command": "node",
|
|
61
|
+
"args": [
|
|
62
|
+
"/absolute/path/to/db-read/dist/cli.js",
|
|
63
|
+
"serve",
|
|
64
|
+
"--env",
|
|
65
|
+
"dev",
|
|
66
|
+
"--config",
|
|
67
|
+
"/absolute/path/to/project/.db-read.yml"
|
|
68
|
+
]
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Cursor and Claude Code can use the same `stdio` command/args pattern.
|
|
75
|
+
|
|
76
|
+
## Safety model
|
|
77
|
+
|
|
78
|
+
- Use least-privileged read-only DB credentials.
|
|
79
|
+
- SQL tools reject multi-statement and non-read queries.
|
|
80
|
+
- MongoDB tools expose only `find` and aggregation, and block `$out` and `$merge`.
|
|
81
|
+
- Timeouts and result limits are enforced per request.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { LoadedConnection, MongoQueryResult, SchemaOverview, SqlQueryResult } from "../lib/types.js";
|
|
2
|
+
export interface DatabaseAdapter {
|
|
3
|
+
readonly connection: LoadedConnection;
|
|
4
|
+
describe(): Promise<Record<string, unknown>>;
|
|
5
|
+
getSchemaOverview(): Promise<SchemaOverview>;
|
|
6
|
+
runSqlQuery?(sql: string): Promise<SqlQueryResult>;
|
|
7
|
+
runMongoFind?(collection: string, filter: Record<string, unknown>, limit?: number): Promise<MongoQueryResult>;
|
|
8
|
+
runMongoAggregate?(collection: string, pipeline: unknown[], limit?: number): Promise<MongoQueryResult>;
|
|
9
|
+
close(): Promise<void>;
|
|
10
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"base.js","sourceRoot":"","sources":["../../src/adapters/base.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { MongoAdapter } from "./mongoAdapter.js";
|
|
2
|
+
import { MysqlAdapter } from "./mysqlAdapter.js";
|
|
3
|
+
import { PostgresAdapter } from "./postgresAdapter.js";
|
|
4
|
+
export function createAdapter(connection) {
|
|
5
|
+
switch (connection.kind) {
|
|
6
|
+
case "postgres":
|
|
7
|
+
return new PostgresAdapter(connection);
|
|
8
|
+
case "mysql":
|
|
9
|
+
return new MysqlAdapter(connection);
|
|
10
|
+
case "mongodb":
|
|
11
|
+
return new MongoAdapter(connection);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=createAdapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"createAdapter.js","sourceRoot":"","sources":["../../src/adapters/createAdapter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAEvD,MAAM,UAAU,aAAa,CAAC,UAA4B;IACxD,QAAQ,UAAU,CAAC,IAAI,EAAE,CAAC;QACxB,KAAK,UAAU;YACb,OAAO,IAAI,eAAe,CAAC,UAAU,CAAC,CAAC;QACzC,KAAK,OAAO;YACV,OAAO,IAAI,YAAY,CAAC,UAAU,CAAC,CAAC;QACtC,KAAK,SAAS;YACZ,OAAO,IAAI,YAAY,CAAC,UAAU,CAAC,CAAC;IACxC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LoadedConnection, MongoQueryResult, SchemaOverview } from "../lib/types.js";
|
|
2
|
+
import type { DatabaseAdapter } from "./base.js";
|
|
3
|
+
export declare class MongoAdapter implements DatabaseAdapter {
|
|
4
|
+
#private;
|
|
5
|
+
readonly connection: LoadedConnection;
|
|
6
|
+
constructor(connection: LoadedConnection);
|
|
7
|
+
describe(): Promise<Record<string, unknown>>;
|
|
8
|
+
getSchemaOverview(): Promise<SchemaOverview>;
|
|
9
|
+
runMongoFind(collection: string, filter: Record<string, unknown>, limit?: number): Promise<MongoQueryResult>;
|
|
10
|
+
runMongoAggregate(collection: string, pipeline: unknown[], limit?: number): Promise<MongoQueryResult>;
|
|
11
|
+
close(): Promise<void>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { MongoClient } from "mongodb";
|
|
2
|
+
import { buildMongoConnectionOptions } from "../config/loadConfig.js";
|
|
3
|
+
import { ExecutionError } from "../lib/errors.js";
|
|
4
|
+
import { assertSafeMongoCollection, assertSafeMongoPipeline } from "../lib/mongoGuard.js";
|
|
5
|
+
export class MongoAdapter {
|
|
6
|
+
connection;
|
|
7
|
+
#client;
|
|
8
|
+
#databaseName;
|
|
9
|
+
constructor(connection) {
|
|
10
|
+
this.connection = connection;
|
|
11
|
+
const options = buildMongoConnectionOptions(connection);
|
|
12
|
+
this.#databaseName = options.database;
|
|
13
|
+
this.#client = new MongoClient(options.uri, options.options);
|
|
14
|
+
}
|
|
15
|
+
async describe() {
|
|
16
|
+
return {
|
|
17
|
+
connection: this.connection.name,
|
|
18
|
+
kind: this.connection.kind,
|
|
19
|
+
database: this.#databaseName,
|
|
20
|
+
host: this.connection.config.host ?? null
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
async getSchemaOverview() {
|
|
24
|
+
try {
|
|
25
|
+
const database = this.#client.db(this.#databaseName);
|
|
26
|
+
const collections = await database.listCollections({}, { nameOnly: true }).toArray();
|
|
27
|
+
return {
|
|
28
|
+
connection: this.connection.name,
|
|
29
|
+
kind: this.connection.kind,
|
|
30
|
+
summary: {
|
|
31
|
+
database: this.#databaseName,
|
|
32
|
+
collections: collections.map((collection) => collection.name)
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new ExecutionError(`MongoDB schema lookup failed: ${String(error)}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async runMongoFind(collection, filter, limit) {
|
|
41
|
+
assertSafeMongoCollection(collection);
|
|
42
|
+
try {
|
|
43
|
+
const database = this.#client.db(this.#databaseName);
|
|
44
|
+
const resolvedLimit = Math.min(limit ?? this.connection.defaults.maxDocuments, this.connection.defaults.maxDocuments);
|
|
45
|
+
const cursor = database.collection(collection).find(filter).limit(resolvedLimit + 1);
|
|
46
|
+
const documents = await cursor.toArray();
|
|
47
|
+
return {
|
|
48
|
+
documents: documents.slice(0, resolvedLimit),
|
|
49
|
+
count: Math.min(documents.length, resolvedLimit),
|
|
50
|
+
truncated: documents.length > resolvedLimit
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
throw new ExecutionError(`MongoDB find failed: ${String(error)}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async runMongoAggregate(collection, pipeline, limit) {
|
|
58
|
+
assertSafeMongoCollection(collection);
|
|
59
|
+
assertSafeMongoPipeline(pipeline);
|
|
60
|
+
try {
|
|
61
|
+
const database = this.#client.db(this.#databaseName);
|
|
62
|
+
const resolvedLimit = Math.min(limit ?? this.connection.defaults.maxDocuments, this.connection.defaults.maxDocuments);
|
|
63
|
+
const stages = [...pipeline, { $limit: resolvedLimit + 1 }];
|
|
64
|
+
const documents = await database.collection(collection).aggregate(stages).toArray();
|
|
65
|
+
return {
|
|
66
|
+
documents: documents.slice(0, resolvedLimit),
|
|
67
|
+
count: Math.min(documents.length, resolvedLimit),
|
|
68
|
+
truncated: documents.length > resolvedLimit
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
throw new ExecutionError(`MongoDB aggregate failed: ${String(error)}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async close() {
|
|
76
|
+
await this.#client.close();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=mongoAdapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mongoAdapter.js","sourceRoot":"","sources":["../../src/adapters/mongoAdapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LoadedConnection, SchemaOverview, SqlQueryResult } from "../lib/types.js";
|
|
2
|
+
import type { DatabaseAdapter } from "./base.js";
|
|
3
|
+
export declare class MysqlAdapter implements DatabaseAdapter {
|
|
4
|
+
#private;
|
|
5
|
+
readonly connection: LoadedConnection;
|
|
6
|
+
constructor(connection: LoadedConnection);
|
|
7
|
+
describe(): Promise<Record<string, unknown>>;
|
|
8
|
+
getSchemaOverview(): Promise<SchemaOverview>;
|
|
9
|
+
runSqlQuery(sql: string): Promise<SqlQueryResult>;
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import mysql from "mysql2/promise";
|
|
2
|
+
import { buildSqlConnectionOptions } from "../config/loadConfig.js";
|
|
3
|
+
import { ExecutionError } from "../lib/errors.js";
|
|
4
|
+
import { assertReadOnlySql } from "../lib/sqlGuard.js";
|
|
5
|
+
export class MysqlAdapter {
|
|
6
|
+
connection;
|
|
7
|
+
#pool;
|
|
8
|
+
constructor(connection) {
|
|
9
|
+
this.connection = connection;
|
|
10
|
+
this.#pool = mysql.createPool({
|
|
11
|
+
...buildSqlConnectionOptions(connection),
|
|
12
|
+
waitForConnections: true,
|
|
13
|
+
connectionLimit: 5,
|
|
14
|
+
queueLimit: 0
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
async describe() {
|
|
18
|
+
return {
|
|
19
|
+
connection: this.connection.name,
|
|
20
|
+
kind: this.connection.kind,
|
|
21
|
+
database: this.connection.config.database ?? null,
|
|
22
|
+
host: this.connection.config.host ?? null
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
async getSchemaOverview() {
|
|
26
|
+
const [rows] = await this.#pool.query(`
|
|
27
|
+
select table_schema, table_name
|
|
28
|
+
from information_schema.tables
|
|
29
|
+
where table_schema not in ('information_schema', 'mysql', 'performance_schema', 'sys')
|
|
30
|
+
order by table_schema, table_name
|
|
31
|
+
limit ?
|
|
32
|
+
`, [this.connection.defaults.maxRows]);
|
|
33
|
+
const grouped = new Map();
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
const schema = String(row.table_schema);
|
|
36
|
+
const table = String(row.table_name);
|
|
37
|
+
const tables = grouped.get(schema) ?? [];
|
|
38
|
+
tables.push(table);
|
|
39
|
+
grouped.set(schema, tables);
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
connection: this.connection.name,
|
|
43
|
+
kind: this.connection.kind,
|
|
44
|
+
summary: Object.fromEntries(grouped)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
async runSqlQuery(sql) {
|
|
48
|
+
assertReadOnlySql(sql);
|
|
49
|
+
const limitedSql = `select * from (${sql}) as db_read_subquery limit ${this.connection.defaults.maxRows + 1}`;
|
|
50
|
+
try {
|
|
51
|
+
const [rows, fields] = await this.#pool.query(limitedSql);
|
|
52
|
+
const limitedRows = rows.slice(0, this.connection.defaults.maxRows);
|
|
53
|
+
return {
|
|
54
|
+
columns: (fields ?? []).map((field) => field.name),
|
|
55
|
+
rows: limitedRows,
|
|
56
|
+
rowCount: limitedRows.length,
|
|
57
|
+
truncated: rows.length > this.connection.defaults.maxRows
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw new ExecutionError(`MySQL query failed: ${String(error)}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
async close() {
|
|
65
|
+
await this.#pool.end();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=mysqlAdapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mysqlAdapter.js","sourceRoot":"","sources":["../../src/adapters/mysqlAdapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LoadedConnection, SchemaOverview, SqlQueryResult } from "../lib/types.js";
|
|
2
|
+
import type { DatabaseAdapter } from "./base.js";
|
|
3
|
+
export declare class PostgresAdapter implements DatabaseAdapter {
|
|
4
|
+
#private;
|
|
5
|
+
readonly connection: LoadedConnection;
|
|
6
|
+
constructor(connection: LoadedConnection);
|
|
7
|
+
describe(): Promise<Record<string, unknown>>;
|
|
8
|
+
getSchemaOverview(): Promise<SchemaOverview>;
|
|
9
|
+
runSqlQuery(sql: string): Promise<SqlQueryResult>;
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import pg from "pg";
|
|
2
|
+
import { buildSqlConnectionOptions } from "../config/loadConfig.js";
|
|
3
|
+
import { ExecutionError } from "../lib/errors.js";
|
|
4
|
+
import { assertReadOnlySql } from "../lib/sqlGuard.js";
|
|
5
|
+
export class PostgresAdapter {
|
|
6
|
+
connection;
|
|
7
|
+
#pool;
|
|
8
|
+
constructor(connection) {
|
|
9
|
+
this.connection = connection;
|
|
10
|
+
this.#pool = new pg.Pool(buildSqlConnectionOptions(connection));
|
|
11
|
+
}
|
|
12
|
+
async describe() {
|
|
13
|
+
return {
|
|
14
|
+
connection: this.connection.name,
|
|
15
|
+
kind: this.connection.kind,
|
|
16
|
+
database: this.connection.config.database ?? null,
|
|
17
|
+
host: this.connection.config.host ?? null
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async getSchemaOverview() {
|
|
21
|
+
const sql = `
|
|
22
|
+
select table_schema, table_name
|
|
23
|
+
from information_schema.tables
|
|
24
|
+
where table_schema not in ('pg_catalog', 'information_schema')
|
|
25
|
+
order by table_schema, table_name
|
|
26
|
+
limit $1
|
|
27
|
+
`;
|
|
28
|
+
const result = await this.#pool.query(sql, [this.connection.defaults.maxRows]);
|
|
29
|
+
const grouped = new Map();
|
|
30
|
+
for (const row of result.rows) {
|
|
31
|
+
const schema = String(row.table_schema);
|
|
32
|
+
const table = String(row.table_name);
|
|
33
|
+
const tables = grouped.get(schema) ?? [];
|
|
34
|
+
tables.push(table);
|
|
35
|
+
grouped.set(schema, tables);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
connection: this.connection.name,
|
|
39
|
+
kind: this.connection.kind,
|
|
40
|
+
summary: Object.fromEntries(grouped)
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
async runSqlQuery(sql) {
|
|
44
|
+
assertReadOnlySql(sql);
|
|
45
|
+
const limitedSql = `select * from (${sql}) as db_read_subquery limit ${this.connection.defaults.maxRows + 1}`;
|
|
46
|
+
try {
|
|
47
|
+
const result = await this.#pool.query(limitedSql);
|
|
48
|
+
const rows = result.rows.slice(0, this.connection.defaults.maxRows);
|
|
49
|
+
return {
|
|
50
|
+
columns: result.fields.map((field) => field.name),
|
|
51
|
+
rows,
|
|
52
|
+
rowCount: rows.length,
|
|
53
|
+
truncated: result.rows.length > this.connection.defaults.maxRows
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
throw new ExecutionError(`PostgreSQL query failed: ${String(error)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async close() {
|
|
61
|
+
await this.#pool.end();
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=postgresAdapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgresAdapter.js","sourceRoot":"","sources":["../../src/adapters/postgresAdapter.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { loadConfig } from "./config/loadConfig.js";
|
|
4
|
+
import { ConfigError, ExecutionError, ValidationError } from "./lib/errors.js";
|
|
5
|
+
import { startMcpServer } from "./mcp/server.js";
|
|
6
|
+
async function main() {
|
|
7
|
+
const [command, ...args] = process.argv.slice(2);
|
|
8
|
+
switch (command) {
|
|
9
|
+
case "serve":
|
|
10
|
+
await serveCommand(args);
|
|
11
|
+
return;
|
|
12
|
+
case "validate-config":
|
|
13
|
+
await validateConfigCommand(args);
|
|
14
|
+
return;
|
|
15
|
+
default:
|
|
16
|
+
printUsage();
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
async function serveCommand(args) {
|
|
21
|
+
const options = parseArgs(args);
|
|
22
|
+
const config = await loadConfig({
|
|
23
|
+
configPath: options.configPath,
|
|
24
|
+
environment: options.environment,
|
|
25
|
+
connectionNames: options.connections
|
|
26
|
+
});
|
|
27
|
+
await startMcpServer(config);
|
|
28
|
+
}
|
|
29
|
+
async function validateConfigCommand(args) {
|
|
30
|
+
const options = parseArgs(args);
|
|
31
|
+
const config = await loadConfig({
|
|
32
|
+
configPath: options.configPath,
|
|
33
|
+
environment: options.environment,
|
|
34
|
+
connectionNames: options.connections
|
|
35
|
+
});
|
|
36
|
+
process.stdout.write(`${JSON.stringify({
|
|
37
|
+
ok: true,
|
|
38
|
+
environment: config.activeEnvironment.name,
|
|
39
|
+
connectionCount: config.activeEnvironment.connections.size
|
|
40
|
+
}, null, 2)}\n`);
|
|
41
|
+
}
|
|
42
|
+
function parseArgs(args) {
|
|
43
|
+
let configPath = ".db-read.yml";
|
|
44
|
+
let environment;
|
|
45
|
+
let connections;
|
|
46
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
47
|
+
const arg = args[index];
|
|
48
|
+
const next = args[index + 1];
|
|
49
|
+
if (arg === "--config" && next) {
|
|
50
|
+
configPath = next;
|
|
51
|
+
index += 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (arg === "--env" && next) {
|
|
55
|
+
environment = next;
|
|
56
|
+
index += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (arg === "--connections" && next) {
|
|
60
|
+
connections = next.split(",").map((value) => value.trim()).filter(Boolean);
|
|
61
|
+
index += 1;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return { configPath, environment, connections };
|
|
66
|
+
}
|
|
67
|
+
function printUsage() {
|
|
68
|
+
process.stderr.write([
|
|
69
|
+
"Usage:",
|
|
70
|
+
" db-read serve --env <environment> [--config .db-read.yml] [--connections a,b,c]",
|
|
71
|
+
" db-read validate-config [--env <environment>] [--config .db-read.yml] [--connections a,b,c]"
|
|
72
|
+
].join("\n") + "\n");
|
|
73
|
+
}
|
|
74
|
+
main().catch((error) => {
|
|
75
|
+
const message = error instanceof ConfigError || error instanceof ValidationError || error instanceof ExecutionError
|
|
76
|
+
? error.message
|
|
77
|
+
: String(error);
|
|
78
|
+
process.stderr.write(`${message}\n`);
|
|
79
|
+
process.exitCode = 1;
|
|
80
|
+
});
|
|
81
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ConfigDefaults, LoadedConnection, LoadedEnvironment } from "../lib/types.js";
|
|
2
|
+
export interface LoadConfigOptions {
|
|
3
|
+
configPath: string;
|
|
4
|
+
environment?: string;
|
|
5
|
+
connectionNames?: string[];
|
|
6
|
+
}
|
|
7
|
+
export interface LoadedConfig {
|
|
8
|
+
configPath: string;
|
|
9
|
+
defaults: ConfigDefaults;
|
|
10
|
+
activeEnvironment: LoadedEnvironment;
|
|
11
|
+
environments: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function loadConfig(options: LoadConfigOptions): Promise<LoadedConfig>;
|
|
14
|
+
export declare function maskConnection(connection: LoadedConnection): Record<string, unknown>;
|
|
15
|
+
export declare function buildSqlConnectionOptions(connection: LoadedConnection): Record<string, unknown>;
|
|
16
|
+
export declare function buildMongoConnectionOptions(connection: LoadedConnection): {
|
|
17
|
+
uri: string;
|
|
18
|
+
database: string;
|
|
19
|
+
options: Record<string, unknown>;
|
|
20
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parse } from "yaml";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { ConfigError } from "../lib/errors.js";
|
|
6
|
+
const DEFAULTS = {
|
|
7
|
+
timeoutMs: 10_000,
|
|
8
|
+
maxRows: 1_000,
|
|
9
|
+
maxDocuments: 1_000
|
|
10
|
+
};
|
|
11
|
+
const baseConnectionSchema = z.object({
|
|
12
|
+
kind: z.enum(["postgres", "mysql", "mongodb"]),
|
|
13
|
+
uri: z.string().min(1).optional(),
|
|
14
|
+
host: z.string().min(1).optional(),
|
|
15
|
+
port: z.number().int().positive().optional(),
|
|
16
|
+
user: z.string().min(1).optional(),
|
|
17
|
+
password: z.string().min(1).optional(),
|
|
18
|
+
database: z.string().min(1).optional(),
|
|
19
|
+
ssl: z.boolean().optional(),
|
|
20
|
+
options: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])).optional()
|
|
21
|
+
});
|
|
22
|
+
const configSchema = z.object({
|
|
23
|
+
version: z.literal(1),
|
|
24
|
+
defaultEnvironment: z.string().min(1).optional(),
|
|
25
|
+
defaults: z.object({
|
|
26
|
+
timeoutMs: z.number().int().positive().optional(),
|
|
27
|
+
maxRows: z.number().int().positive().optional(),
|
|
28
|
+
maxDocuments: z.number().int().positive().optional()
|
|
29
|
+
}).optional(),
|
|
30
|
+
environments: z.record(z.string(), z.object({
|
|
31
|
+
connections: z.record(z.string(), baseConnectionSchema)
|
|
32
|
+
}))
|
|
33
|
+
});
|
|
34
|
+
export async function loadConfig(options) {
|
|
35
|
+
const resolvedPath = path.resolve(options.configPath);
|
|
36
|
+
const raw = await readFile(resolvedPath, "utf8").catch((error) => {
|
|
37
|
+
throw new ConfigError(`Unable to read config file at ${resolvedPath}: ${String(error)}`);
|
|
38
|
+
});
|
|
39
|
+
const parsed = parse(raw);
|
|
40
|
+
const config = configSchema.safeParse(parsed);
|
|
41
|
+
if (!config.success) {
|
|
42
|
+
throw new ConfigError(`Invalid config file: ${config.error.message}`);
|
|
43
|
+
}
|
|
44
|
+
const defaults = {
|
|
45
|
+
...DEFAULTS,
|
|
46
|
+
...config.data.defaults
|
|
47
|
+
};
|
|
48
|
+
const environmentName = options.environment ?? config.data.defaultEnvironment ?? firstEnvironmentName(config.data);
|
|
49
|
+
const envConfig = config.data.environments[environmentName];
|
|
50
|
+
if (!envConfig) {
|
|
51
|
+
throw new ConfigError(`Environment '${environmentName}' was not found in ${resolvedPath}.`);
|
|
52
|
+
}
|
|
53
|
+
const requestedNames = options.connectionNames?.length ? new Set(options.connectionNames) : null;
|
|
54
|
+
const connections = new Map();
|
|
55
|
+
for (const [name, connection] of Object.entries(envConfig.connections)) {
|
|
56
|
+
if (requestedNames && !requestedNames.has(name)) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
validateConnectionShape(name, connection);
|
|
60
|
+
connections.set(name, {
|
|
61
|
+
name,
|
|
62
|
+
environment: environmentName,
|
|
63
|
+
kind: connection.kind,
|
|
64
|
+
config: connection,
|
|
65
|
+
defaults
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (requestedNames) {
|
|
69
|
+
for (const name of requestedNames) {
|
|
70
|
+
if (!connections.has(name)) {
|
|
71
|
+
throw new ConfigError(`Connection '${name}' was not found in environment '${environmentName}'.`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
configPath: resolvedPath,
|
|
77
|
+
defaults,
|
|
78
|
+
activeEnvironment: {
|
|
79
|
+
name: environmentName,
|
|
80
|
+
connections
|
|
81
|
+
},
|
|
82
|
+
environments: Object.keys(config.data.environments)
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function firstEnvironmentName(config) {
|
|
86
|
+
const name = Object.keys(config.environments)[0];
|
|
87
|
+
if (!name) {
|
|
88
|
+
throw new ConfigError("At least one environment is required.");
|
|
89
|
+
}
|
|
90
|
+
return name;
|
|
91
|
+
}
|
|
92
|
+
function validateConnectionShape(name, connection) {
|
|
93
|
+
const hasUri = Boolean(connection.uri);
|
|
94
|
+
const hasHostShape = Boolean(connection.host && connection.user && connection.database);
|
|
95
|
+
if (!hasUri && !hasHostShape) {
|
|
96
|
+
throw new ConfigError(`Connection '${name}' must define either 'uri' or host/user/database fields.`);
|
|
97
|
+
}
|
|
98
|
+
if (connection.kind === "mongodb" && !connection.database) {
|
|
99
|
+
throw new ConfigError(`MongoDB connection '${name}' requires 'database'.`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function maskConnection(connection) {
|
|
103
|
+
return {
|
|
104
|
+
name: connection.name,
|
|
105
|
+
environment: connection.environment,
|
|
106
|
+
kind: connection.kind,
|
|
107
|
+
database: connection.config.database ?? null,
|
|
108
|
+
host: connection.config.host ?? null,
|
|
109
|
+
hasUri: Boolean(connection.config.uri),
|
|
110
|
+
ssl: connection.config.ssl ?? false
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
export function buildSqlConnectionOptions(connection) {
|
|
114
|
+
const options = { ...(connection.config.options ?? {}) };
|
|
115
|
+
if (connection.config.uri) {
|
|
116
|
+
return {
|
|
117
|
+
connectionString: connection.config.uri,
|
|
118
|
+
ssl: connection.config.ssl ? { rejectUnauthorized: false } : undefined,
|
|
119
|
+
...options
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
host: connection.config.host,
|
|
124
|
+
port: connection.config.port,
|
|
125
|
+
user: connection.config.user,
|
|
126
|
+
password: connection.config.password,
|
|
127
|
+
database: connection.config.database,
|
|
128
|
+
ssl: connection.config.ssl ? { rejectUnauthorized: false } : undefined,
|
|
129
|
+
...options
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
export function buildMongoConnectionOptions(connection) {
|
|
133
|
+
const database = connection.config.database;
|
|
134
|
+
if (!database) {
|
|
135
|
+
throw new ConfigError(`MongoDB connection '${connection.name}' is missing 'database'.`);
|
|
136
|
+
}
|
|
137
|
+
if (connection.config.uri) {
|
|
138
|
+
return {
|
|
139
|
+
uri: connection.config.uri,
|
|
140
|
+
database,
|
|
141
|
+
options: { ...(connection.config.options ?? {}) }
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const auth = connection.config.user
|
|
145
|
+
? `${encodeURIComponent(connection.config.user)}:${encodeURIComponent(connection.config.password ?? "")}@`
|
|
146
|
+
: "";
|
|
147
|
+
const host = connection.config.host ?? "localhost";
|
|
148
|
+
const port = connection.config.port ?? 27017;
|
|
149
|
+
return {
|
|
150
|
+
uri: `${connection.config.uri ?? `mongodb://${auth}${host}:${port}`}`,
|
|
151
|
+
database,
|
|
152
|
+
options: {
|
|
153
|
+
...(connection.config.ssl ? { tls: true } : {}),
|
|
154
|
+
...(connection.config.options ?? {})
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
//# sourceMappingURL=loadConfig.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"loadConfig.js","sourceRoot":"","sources":["../../src/config/loadConfig.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare class ConfigError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
export declare class ValidationError extends Error {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
export declare class ExecutionError extends Error {
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export class ConfigError extends Error {
|
|
2
|
+
constructor(message) {
|
|
3
|
+
super(message);
|
|
4
|
+
this.name = "ConfigError";
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
export class ValidationError extends Error {
|
|
8
|
+
constructor(message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "ValidationError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class ExecutionError extends Error {
|
|
14
|
+
constructor(message) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.name = "ExecutionError";
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/lib/errors.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,WAAY,SAAQ,KAAK;IACpC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;IAC5B,CAAC;CACF;AAED,MAAM,OAAO,eAAgB,SAAQ,KAAK;IACxC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAC;IAChC,CAAC;CACF;AAED,MAAM,OAAO,cAAe,SAAQ,KAAK;IACvC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAC;IAC/B,CAAC;CACF"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { ValidationError } from "./errors.js";
|
|
2
|
+
const FORBIDDEN_STAGES = new Set(["$out", "$merge"]);
|
|
3
|
+
export function assertSafeMongoCollection(name) {
|
|
4
|
+
if (!name.trim()) {
|
|
5
|
+
throw new ValidationError("Collection name is required.");
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export function assertSafeMongoPipeline(pipeline) {
|
|
9
|
+
for (const stage of pipeline) {
|
|
10
|
+
if (!stage || typeof stage !== "object" || Array.isArray(stage)) {
|
|
11
|
+
throw new ValidationError("Each pipeline stage must be an object.");
|
|
12
|
+
}
|
|
13
|
+
for (const key of Object.keys(stage)) {
|
|
14
|
+
if (FORBIDDEN_STAGES.has(key)) {
|
|
15
|
+
throw new ValidationError(`Aggregation stage ${key} is not allowed.`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=mongoGuard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mongoGuard.js","sourceRoot":"","sources":["../../src/lib/mongoGuard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,gBAAgB,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC,CAAC;AAErD,MAAM,UAAU,yBAAyB,CAAC,IAAY;IACpD,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;QACjB,MAAM,IAAI,eAAe,CAAC,8BAA8B,CAAC,CAAC;IAC5D,CAAC;AACH,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,QAAmB;IACzD,KAAK,MAAM,KAAK,IAAI,QAAQ,EAAE,CAAC;QAC7B,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YAChE,MAAM,IAAI,eAAe,CAAC,wCAAwC,CAAC,CAAC;QACtE,CAAC;QAED,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,KAAgC,CAAC,EAAE,CAAC;YAChE,IAAI,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;gBAC9B,MAAM,IAAI,eAAe,CAAC,qBAAqB,GAAG,kBAAkB,CAAC,CAAC;YACxE,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function assertReadOnlySql(sql: string): void;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { ValidationError } from "./errors.js";
|
|
2
|
+
const READ_ONLY_KEYWORDS = new Set([
|
|
3
|
+
"select",
|
|
4
|
+
"with"
|
|
5
|
+
]);
|
|
6
|
+
const FORBIDDEN_SQL_PATTERNS = [
|
|
7
|
+
/\binsert\b/i,
|
|
8
|
+
/\bupdate\b/i,
|
|
9
|
+
/\bdelete\b/i,
|
|
10
|
+
/\bdrop\b/i,
|
|
11
|
+
/\btruncate\b/i,
|
|
12
|
+
/\balter\b/i,
|
|
13
|
+
/\bcreate\b/i,
|
|
14
|
+
/\bgrant\b/i,
|
|
15
|
+
/\brevoke\b/i,
|
|
16
|
+
/\bcopy\b/i,
|
|
17
|
+
/\bmerge\b/i,
|
|
18
|
+
/\bcall\b/i,
|
|
19
|
+
/\bdo\b/i,
|
|
20
|
+
/\bset\b/i,
|
|
21
|
+
/\block\b/i
|
|
22
|
+
];
|
|
23
|
+
export function assertReadOnlySql(sql) {
|
|
24
|
+
const trimmed = sql.trim();
|
|
25
|
+
if (!trimmed) {
|
|
26
|
+
throw new ValidationError("SQL query is required.");
|
|
27
|
+
}
|
|
28
|
+
if (trimmed.includes(";")) {
|
|
29
|
+
throw new ValidationError("Only a single SQL statement is allowed.");
|
|
30
|
+
}
|
|
31
|
+
const firstToken = trimmed.split(/\s+/u, 1)[0]?.toLowerCase();
|
|
32
|
+
if (!firstToken || !READ_ONLY_KEYWORDS.has(firstToken)) {
|
|
33
|
+
throw new ValidationError(`Only read-only SQL statements are allowed. Received: ${firstToken ?? "unknown"}`);
|
|
34
|
+
}
|
|
35
|
+
for (const pattern of FORBIDDEN_SQL_PATTERNS) {
|
|
36
|
+
if (pattern.test(trimmed)) {
|
|
37
|
+
throw new ValidationError("Write or session-modifying SQL is not allowed.");
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=sqlGuard.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sqlGuard.js","sourceRoot":"","sources":["../../src/lib/sqlGuard.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,MAAM,kBAAkB,GAAG,IAAI,GAAG,CAAC;IACjC,QAAQ;IACR,MAAM;CACP,CAAC,CAAC;AAEH,MAAM,sBAAsB,GAAG;IAC7B,aAAa;IACb,aAAa;IACb,aAAa;IACb,WAAW;IACX,eAAe;IACf,YAAY;IACZ,aAAa;IACb,YAAY;IACZ,aAAa;IACb,WAAW;IACX,YAAY;IACZ,WAAW;IACX,SAAS;IACT,UAAU;IACV,WAAW;CACZ,CAAC;AAEF,MAAM,UAAU,iBAAiB,CAAC,GAAW;IAC3C,MAAM,OAAO,GAAG,GAAG,CAAC,IAAI,EAAE,CAAC;IAE3B,IAAI,CAAC,OAAO,EAAE,CAAC;QACb,MAAM,IAAI,eAAe,CAAC,wBAAwB,CAAC,CAAC;IACtD,CAAC;IAED,IAAI,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,EAAE,CAAC;QAC1B,MAAM,IAAI,eAAe,CAAC,yCAAyC,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;IAC9D,IAAI,CAAC,UAAU,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,UAAU,CAAC,EAAE,CAAC;QACvD,MAAM,IAAI,eAAe,CAAC,wDAAwD,UAAU,IAAI,SAAS,EAAE,CAAC,CAAC;IAC/G,CAAC;IAED,KAAK,MAAM,OAAO,IAAI,sBAAsB,EAAE,CAAC;QAC7C,IAAI,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YAC1B,MAAM,IAAI,eAAe,CAAC,gDAAgD,CAAC,CAAC;QAC9E,CAAC;IACH,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export type ConnectionKind = "postgres" | "mysql" | "mongodb";
|
|
2
|
+
export interface ConfigDefaults {
|
|
3
|
+
timeoutMs: number;
|
|
4
|
+
maxRows: number;
|
|
5
|
+
maxDocuments: number;
|
|
6
|
+
}
|
|
7
|
+
interface BaseConnectionConfig {
|
|
8
|
+
kind: ConnectionKind;
|
|
9
|
+
uri?: string;
|
|
10
|
+
host?: string;
|
|
11
|
+
port?: number;
|
|
12
|
+
user?: string;
|
|
13
|
+
password?: string;
|
|
14
|
+
database?: string;
|
|
15
|
+
ssl?: boolean;
|
|
16
|
+
options?: Record<string, string | number | boolean>;
|
|
17
|
+
}
|
|
18
|
+
export interface PostgresConnectionConfig extends BaseConnectionConfig {
|
|
19
|
+
kind: "postgres";
|
|
20
|
+
}
|
|
21
|
+
export interface MysqlConnectionConfig extends BaseConnectionConfig {
|
|
22
|
+
kind: "mysql";
|
|
23
|
+
}
|
|
24
|
+
export interface MongoConnectionConfig extends BaseConnectionConfig {
|
|
25
|
+
kind: "mongodb";
|
|
26
|
+
}
|
|
27
|
+
export type ConnectionConfig = PostgresConnectionConfig | MysqlConnectionConfig | MongoConnectionConfig;
|
|
28
|
+
export interface EnvironmentConfig {
|
|
29
|
+
connections: Record<string, ConnectionConfig>;
|
|
30
|
+
}
|
|
31
|
+
export interface DbReadConfig {
|
|
32
|
+
version: 1;
|
|
33
|
+
defaultEnvironment?: string;
|
|
34
|
+
defaults?: Partial<ConfigDefaults>;
|
|
35
|
+
environments: Record<string, EnvironmentConfig>;
|
|
36
|
+
}
|
|
37
|
+
export interface LoadedConnection {
|
|
38
|
+
name: string;
|
|
39
|
+
environment: string;
|
|
40
|
+
kind: ConnectionKind;
|
|
41
|
+
config: ConnectionConfig;
|
|
42
|
+
defaults: ConfigDefaults;
|
|
43
|
+
}
|
|
44
|
+
export interface LoadedEnvironment {
|
|
45
|
+
name: string;
|
|
46
|
+
connections: Map<string, LoadedConnection>;
|
|
47
|
+
}
|
|
48
|
+
export interface SqlQueryResult {
|
|
49
|
+
columns: string[];
|
|
50
|
+
rows: Array<Record<string, unknown>>;
|
|
51
|
+
rowCount: number;
|
|
52
|
+
truncated: boolean;
|
|
53
|
+
}
|
|
54
|
+
export interface MongoQueryResult {
|
|
55
|
+
documents: Array<Record<string, unknown>>;
|
|
56
|
+
count: number;
|
|
57
|
+
truncated: boolean;
|
|
58
|
+
}
|
|
59
|
+
export interface SchemaOverview {
|
|
60
|
+
connection: string;
|
|
61
|
+
kind: ConnectionKind;
|
|
62
|
+
summary: Record<string, unknown>;
|
|
63
|
+
}
|
|
64
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { createAdapter } from "../adapters/createAdapter.js";
|
|
5
|
+
import { maskConnection } from "../config/loadConfig.js";
|
|
6
|
+
import { ExecutionError, ValidationError } from "../lib/errors.js";
|
|
7
|
+
export async function startMcpServer(config) {
|
|
8
|
+
const server = new McpServer({
|
|
9
|
+
name: "db-read",
|
|
10
|
+
version: "0.1.0"
|
|
11
|
+
});
|
|
12
|
+
const adapters = new Map(Array.from(config.activeEnvironment.connections.values()).map((connection) => [connection.name, createAdapter(connection)]));
|
|
13
|
+
server.registerResource("connections", "dbread://connections", {
|
|
14
|
+
title: "Configured DB connections",
|
|
15
|
+
description: "Configured database connections for the active environment.",
|
|
16
|
+
mimeType: "application/json"
|
|
17
|
+
}, async () => ({
|
|
18
|
+
contents: [{
|
|
19
|
+
uri: "dbread://connections",
|
|
20
|
+
mimeType: "application/json",
|
|
21
|
+
text: JSON.stringify(Array.from(config.activeEnvironment.connections.values()).map(maskConnection), null, 2)
|
|
22
|
+
}]
|
|
23
|
+
}));
|
|
24
|
+
for (const [name, adapter] of adapters.entries()) {
|
|
25
|
+
server.registerResource(`${name}-schema`, `dbread://connections/${name}/schema`, {
|
|
26
|
+
title: `${name} schema overview`,
|
|
27
|
+
description: `High-level schema overview for ${name}.`,
|
|
28
|
+
mimeType: "application/json"
|
|
29
|
+
}, async () => {
|
|
30
|
+
const overview = await withAdapter(adapter, () => adapter.getSchemaOverview());
|
|
31
|
+
return {
|
|
32
|
+
contents: [{
|
|
33
|
+
uri: `dbread://connections/${name}/schema`,
|
|
34
|
+
mimeType: "application/json",
|
|
35
|
+
text: JSON.stringify(overview, null, 2)
|
|
36
|
+
}]
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
server.registerTool("list_connections", {
|
|
41
|
+
title: "List connections",
|
|
42
|
+
description: "List the configured database connections for the active environment."
|
|
43
|
+
}, async () => ({
|
|
44
|
+
content: [{
|
|
45
|
+
type: "text",
|
|
46
|
+
text: JSON.stringify(Array.from(config.activeEnvironment.connections.values()).map(maskConnection), null, 2)
|
|
47
|
+
}]
|
|
48
|
+
}));
|
|
49
|
+
server.registerTool("describe_connection", {
|
|
50
|
+
title: "Describe connection",
|
|
51
|
+
description: "Describe one configured database connection.",
|
|
52
|
+
inputSchema: {
|
|
53
|
+
name: z.string().min(1)
|
|
54
|
+
}
|
|
55
|
+
}, async ({ name }) => {
|
|
56
|
+
const adapter = requireAdapter(adapters, name);
|
|
57
|
+
const description = await withAdapter(adapter, () => adapter.describe());
|
|
58
|
+
return {
|
|
59
|
+
content: [{
|
|
60
|
+
type: "text",
|
|
61
|
+
text: JSON.stringify(description, null, 2)
|
|
62
|
+
}]
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
server.registerTool("get_schema_overview", {
|
|
66
|
+
title: "Get schema overview",
|
|
67
|
+
description: "Inspect the high-level schema for one configured connection.",
|
|
68
|
+
inputSchema: {
|
|
69
|
+
name: z.string().min(1)
|
|
70
|
+
}
|
|
71
|
+
}, async ({ name }) => {
|
|
72
|
+
const adapter = requireAdapter(adapters, name);
|
|
73
|
+
const overview = await withAdapter(adapter, () => adapter.getSchemaOverview());
|
|
74
|
+
return {
|
|
75
|
+
content: [{
|
|
76
|
+
type: "text",
|
|
77
|
+
text: JSON.stringify(overview, null, 2)
|
|
78
|
+
}]
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
server.registerTool("sql_query", {
|
|
82
|
+
title: "Run SQL query",
|
|
83
|
+
description: "Run a single read-only SQL query against a PostgreSQL or MySQL connection.",
|
|
84
|
+
inputSchema: {
|
|
85
|
+
name: z.string().min(1),
|
|
86
|
+
sql: z.string().min(1)
|
|
87
|
+
}
|
|
88
|
+
}, async ({ name, sql }) => {
|
|
89
|
+
const adapter = requireAdapter(adapters, name);
|
|
90
|
+
if (!adapter.runSqlQuery) {
|
|
91
|
+
throw new ValidationError(`Connection '${name}' does not support SQL queries.`);
|
|
92
|
+
}
|
|
93
|
+
const result = await withAdapter(adapter, () => adapter.runSqlQuery(sql));
|
|
94
|
+
return {
|
|
95
|
+
content: [{
|
|
96
|
+
type: "text",
|
|
97
|
+
text: JSON.stringify(result, null, 2)
|
|
98
|
+
}]
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
server.registerTool("mongo_find", {
|
|
102
|
+
title: "Run Mongo find",
|
|
103
|
+
description: "Run a read-only MongoDB find query against one collection.",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
name: z.string().min(1),
|
|
106
|
+
collection: z.string().min(1),
|
|
107
|
+
filter: z.record(z.string(), z.unknown()).default({}),
|
|
108
|
+
limit: z.number().int().positive().optional()
|
|
109
|
+
}
|
|
110
|
+
}, async ({ name, collection, filter, limit }) => {
|
|
111
|
+
const adapter = requireAdapter(adapters, name);
|
|
112
|
+
if (!adapter.runMongoFind) {
|
|
113
|
+
throw new ValidationError(`Connection '${name}' does not support MongoDB find queries.`);
|
|
114
|
+
}
|
|
115
|
+
const result = await withAdapter(adapter, () => adapter.runMongoFind(collection, filter, limit));
|
|
116
|
+
return {
|
|
117
|
+
content: [{
|
|
118
|
+
type: "text",
|
|
119
|
+
text: JSON.stringify(result, null, 2)
|
|
120
|
+
}]
|
|
121
|
+
};
|
|
122
|
+
});
|
|
123
|
+
server.registerTool("mongo_aggregate", {
|
|
124
|
+
title: "Run Mongo aggregate",
|
|
125
|
+
description: "Run a read-only MongoDB aggregation pipeline.",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
name: z.string().min(1),
|
|
128
|
+
collection: z.string().min(1),
|
|
129
|
+
pipeline: z.array(z.unknown()),
|
|
130
|
+
limit: z.number().int().positive().optional()
|
|
131
|
+
}
|
|
132
|
+
}, async ({ name, collection, pipeline, limit }) => {
|
|
133
|
+
const adapter = requireAdapter(adapters, name);
|
|
134
|
+
if (!adapter.runMongoAggregate) {
|
|
135
|
+
throw new ValidationError(`Connection '${name}' does not support MongoDB aggregation.`);
|
|
136
|
+
}
|
|
137
|
+
const result = await withAdapter(adapter, () => adapter.runMongoAggregate(collection, pipeline, limit));
|
|
138
|
+
return {
|
|
139
|
+
content: [{
|
|
140
|
+
type: "text",
|
|
141
|
+
text: JSON.stringify(result, null, 2)
|
|
142
|
+
}]
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
const transport = new StdioServerTransport();
|
|
146
|
+
await server.connect(transport);
|
|
147
|
+
}
|
|
148
|
+
function requireAdapter(adapters, name) {
|
|
149
|
+
const adapter = adapters.get(name);
|
|
150
|
+
if (!adapter) {
|
|
151
|
+
throw new ValidationError(`Unknown connection '${name}'.`);
|
|
152
|
+
}
|
|
153
|
+
return adapter;
|
|
154
|
+
}
|
|
155
|
+
async function withAdapter(adapter, fn) {
|
|
156
|
+
const timeoutMs = adapter.connection.defaults.timeoutMs;
|
|
157
|
+
let timeout;
|
|
158
|
+
try {
|
|
159
|
+
return await Promise.race([
|
|
160
|
+
fn(),
|
|
161
|
+
new Promise((_, reject) => {
|
|
162
|
+
timeout = setTimeout(() => reject(new ExecutionError(`Operation timed out after ${timeoutMs}ms.`)), timeoutMs);
|
|
163
|
+
})
|
|
164
|
+
]);
|
|
165
|
+
}
|
|
166
|
+
finally {
|
|
167
|
+
if (timeout) {
|
|
168
|
+
clearTimeout(timeout);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../../src/mcp/server.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "db-read",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Read-only MCP server for PostgreSQL, MySQL, and MongoDB.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": "dist/cli.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
".db-read.example.yml",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc -p tsconfig.json",
|
|
14
|
+
"dev": "tsx src/cli.ts serve --env dev --config .db-read.yml",
|
|
15
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
16
|
+
"test": "vitest run",
|
|
17
|
+
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
18
|
+
},
|
|
19
|
+
"keywords": [
|
|
20
|
+
"mcp",
|
|
21
|
+
"database",
|
|
22
|
+
"postgres",
|
|
23
|
+
"mysql",
|
|
24
|
+
"mongodb",
|
|
25
|
+
"read-only"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
33
|
+
"mongodb": "^6.21.0",
|
|
34
|
+
"mysql2": "^3.22.3",
|
|
35
|
+
"pg": "^8.21.0",
|
|
36
|
+
"yaml": "^2.9.0",
|
|
37
|
+
"zod": "^4.4.3"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@types/node": "^25.9.1",
|
|
41
|
+
"@types/pg": "^8.20.0",
|
|
42
|
+
"tsx": "^4.22.3",
|
|
43
|
+
"typescript": "^6.0.3",
|
|
44
|
+
"vitest": "^4.1.7"
|
|
45
|
+
}
|
|
46
|
+
}
|