@harneon-ai/db 0.0.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.
@@ -0,0 +1,83 @@
1
+ /**
2
+ * MCP Tool: query_metadata
3
+ *
4
+ * 允许 AI 执行自定义的元数据查询,提供灵活性的同时通过严格的安全过滤确保安全。
5
+ *
6
+ * 安全验证规则(全部通过才允许执行):
7
+ * 1. SQL 必须以 SELECT 开头
8
+ * 2. SQL 不能包含分号(防止多语句攻击)
9
+ * 3. SQL 不能包含写操作关键字(INSERT/UPDATE/DELETE/DROP/ALTER/CREATE/TRUNCATE/GRANT/REVOKE)
10
+ * 4. SQL 的 FROM 子句必须直接引用 information_schema 或 pg_catalog(防止通过构造字符串绕过)
11
+ *
12
+ * 注意: 这是 SQL 级别的安全层,连接级别还有 default_transaction_read_only=on 作为兜底
13
+ */
14
+ import { z } from 'zod';
15
+ /** 禁止的写操作关键字 */
16
+ const FORBIDDEN_KEYWORDS = /\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE|GRANT|REVOKE)\b/i;
17
+ /**
18
+ * FROM 子句必须直接引用 information_schema 或 pg_catalog
19
+ * 这比简单的子字符串检查更安全: 防止通过 WHERE 'information_schema'='information_schema' 绕过
20
+ */
21
+ const ALLOWED_FROM_SCHEMAS = /\bFROM\s+(?:ONLY\s+)?(?:information_schema|pg_catalog)\b/i;
22
+ /**
23
+ * 验证 SQL 是否为合法的元数据查询
24
+ * @returns 错误信息(字符串)或 null(验证通过)
25
+ */
26
+ function validateMetadataQuery(sql) {
27
+ const trimmed = sql.trim();
28
+ if (!/^SELECT\b/i.test(trimmed)) {
29
+ return '仅允许 SELECT 语句';
30
+ }
31
+ // 拒绝分号: 防止通过 SELECT 1; DELETE FROM ... 形式的多语句攻击
32
+ if (trimmed.includes(';')) {
33
+ return '不允许包含分号(禁止多语句查询)';
34
+ }
35
+ if (FORBIDDEN_KEYWORDS.test(trimmed)) {
36
+ return '检测到禁止的关键字(INSERT/UPDATE/DELETE/DROP/ALTER/CREATE/TRUNCATE/GRANT/REVOKE)';
37
+ }
38
+ // 要求 FROM 子句直接引用系统 schema,而非仅在 SQL 中出现该字符串
39
+ if (!ALLOWED_FROM_SCHEMAS.test(trimmed)) {
40
+ return '仅允许查询 information_schema 或 pg_catalog 的 SELECT 语句(FROM 子句必须引用这些 schema)';
41
+ }
42
+ return null;
43
+ }
44
+ export function registerQueryMetadata(server, _config, connectionManager) {
45
+ server.tool('query_metadata', '执行自定义的元数据查询(仅允许查询 information_schema 或 pg_catalog 的 SELECT 语句)', {
46
+ datasource: z.string().describe('数据源名称'),
47
+ sql: z.string().describe('SQL 查询语句(仅允许 SELECT information_schema/pg_catalog)'),
48
+ }, async ({ datasource, sql }) => {
49
+ // 先验证 SQL 安全性,不通过则直接返回错误
50
+ const error = validateMetadataQuery(sql);
51
+ if (error) {
52
+ return {
53
+ content: [{
54
+ type: 'text',
55
+ text: JSON.stringify({ error }),
56
+ }],
57
+ isError: true,
58
+ };
59
+ }
60
+ try {
61
+ const pool = await connectionManager.getPool(datasource);
62
+ const result = await pool.query(sql);
63
+ return {
64
+ content: [{
65
+ type: 'text',
66
+ text: JSON.stringify({
67
+ rowCount: result.rowCount,
68
+ rows: result.rows,
69
+ }, null, 2),
70
+ }],
71
+ };
72
+ }
73
+ catch (err) {
74
+ return {
75
+ content: [{
76
+ type: 'text',
77
+ text: JSON.stringify({ error: err instanceof Error ? err.message : String(err) }),
78
+ }],
79
+ isError: true,
80
+ };
81
+ }
82
+ });
83
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * MCP Tool: save_datasource_config
3
+ *
4
+ * 将数据源配置持久化到 YAML 文件并更新内存状态。
5
+ *
6
+ * 设计要点:
7
+ * - 原地修改 config 对象: 所有现有 tool 闭包捕获了同一个 config 引用,
8
+ * 原地修改避免引入间接层,确保所有 tool 立即看到新配置
9
+ * - 写入失败时不修改内存: 先写入磁盘,成功后再更新内存状态,保证一致性
10
+ * - 调用 connectionManager.reloadConfig() 清空旧连接池缓存
11
+ */
12
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
13
+ import type { ParsedConfig } from '../config/types.js';
14
+ import { ConnectionManager } from '../db/connection-manager.js';
15
+ export declare function registerSaveDatasourceConfig(server: McpServer, config: ParsedConfig, connectionManager: ConnectionManager, configPath: string): void;
@@ -0,0 +1,112 @@
1
+ /**
2
+ * MCP Tool: save_datasource_config
3
+ *
4
+ * 将数据源配置持久化到 YAML 文件并更新内存状态。
5
+ *
6
+ * 设计要点:
7
+ * - 原地修改 config 对象: 所有现有 tool 闭包捕获了同一个 config 引用,
8
+ * 原地修改避免引入间接层,确保所有 tool 立即看到新配置
9
+ * - 写入失败时不修改内存: 先写入磁盘,成功后再更新内存状态,保证一致性
10
+ * - 调用 connectionManager.reloadConfig() 清空旧连接池缓存
11
+ */
12
+ import { z } from 'zod';
13
+ import { mergeConfigs, writeConfigFile } from '../config/writer.js';
14
+ export function registerSaveDatasourceConfig(server, config, connectionManager, configPath) {
15
+ server.tool('save_datasource_config', '保存数据源配置到 YAML 文件。支持添加一个或多个数据源,可选配置分片规则。', {
16
+ dataSources: z.array(z.object({
17
+ name: z.string().describe('数据源名称,如 ds_0'),
18
+ jdbcUrl: z.string().describe('JDBC URL,如 jdbc:postgresql://host:port/database'),
19
+ username: z.string().describe('数据库用户名'),
20
+ password: z.string().describe('数据库密码'),
21
+ })).min(1).describe('数据源列表,至少 1 个'),
22
+ shardingRule: z.object({
23
+ tables: z.record(z.string(), z.object({
24
+ actualDataNodes: z.string(),
25
+ tableStrategy: z.object({
26
+ none: z.union([z.null(), z.object({})]).optional(),
27
+ standard: z.object({
28
+ shardingColumn: z.string(),
29
+ shardingAlgorithmName: z.string(),
30
+ }).optional(),
31
+ complex: z.object({
32
+ shardingColumns: z.string(),
33
+ shardingAlgorithmName: z.string(),
34
+ }).optional(),
35
+ }).optional(),
36
+ })).optional(),
37
+ defaultShardingColumn: z.string().optional(),
38
+ defaultDatabaseStrategy: z.object({
39
+ standard: z.object({
40
+ shardingColumn: z.string(),
41
+ shardingAlgorithmName: z.string(),
42
+ }).optional(),
43
+ none: z.union([z.null(), z.object({})]).optional(),
44
+ }).optional(),
45
+ defaultTableStrategy: z.object({
46
+ standard: z.object({
47
+ shardingColumn: z.string(),
48
+ shardingAlgorithmName: z.string(),
49
+ }).optional(),
50
+ none: z.union([z.null(), z.object({})]).optional(),
51
+ }).optional(),
52
+ shardingAlgorithms: z.record(z.string(), z.object({
53
+ type: z.string(),
54
+ props: z.record(z.string(), z.union([z.string(), z.number()])).optional(),
55
+ })).optional(),
56
+ }).optional().describe('可选的分片规则配置'),
57
+ }, async (params) => {
58
+ // 1. 将 dataSources 数组转换为 Record<string, DataSourceConfig> 格式
59
+ const incomingDataSources = {};
60
+ for (const ds of params.dataSources) {
61
+ incomingDataSources[ds.name] = {
62
+ jdbcUrl: ds.jdbcUrl,
63
+ username: ds.username,
64
+ password: ds.password,
65
+ };
66
+ }
67
+ const incomingConfig = {
68
+ dataSources: incomingDataSources,
69
+ shardingRule: params.shardingRule,
70
+ };
71
+ // 2. 合并到现有配置
72
+ const merged = mergeConfigs(config, incomingConfig);
73
+ // 3. 写入磁盘(失败时抛出异常,不会执行后续内存修改)
74
+ try {
75
+ writeConfigFile(configPath, merged);
76
+ }
77
+ catch (err) {
78
+ const errorMessage = err instanceof Error ? err.message : String(err);
79
+ return {
80
+ content: [{
81
+ type: 'text',
82
+ text: JSON.stringify({ success: false, error: `写入配置文件失败: ${errorMessage}` }),
83
+ }],
84
+ };
85
+ }
86
+ // 4. 原地修改 config 对象,确保所有 tool 闭包立即看到新配置
87
+ // 清空现有 dataSources 再赋值,保持引用不变
88
+ for (const key of Object.keys(config.dataSources)) {
89
+ delete config.dataSources[key];
90
+ }
91
+ Object.assign(config.dataSources, merged.dataSources);
92
+ config.shardingRule = merged.shardingRule;
93
+ config.props = merged.props;
94
+ // 5. 清空旧连接池缓存,下次使用时会基于新配置创建
95
+ await connectionManager.reloadConfig();
96
+ // 6. 返回成功摘要
97
+ const savedNames = params.dataSources.map(ds => ds.name);
98
+ const hasSharding = !!params.shardingRule;
99
+ return {
100
+ content: [{
101
+ type: 'text',
102
+ text: JSON.stringify({
103
+ success: true,
104
+ savedDataSources: savedNames,
105
+ shardingConfigured: hasSharding,
106
+ configFile: configPath,
107
+ totalDataSources: Object.keys(config.dataSources).length,
108
+ }),
109
+ }],
110
+ };
111
+ });
112
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * MCP Tool: sharding_topology
3
+ *
4
+ * 展示 ShardingSphere 分片拓扑信息。
5
+ * 这是一个纯配置解析操作,不连接任何数据库。
6
+ *
7
+ * 解析逻辑:
8
+ * 1. 从配置中读取分片规则(tables + shardingAlgorithms)
9
+ * 2. 使用 expandDataNodes() 展开 actualDataNodes 表达式为具体节点列表
10
+ * 3. 解析表级分片策略(standard/complex),回退到默认策略
11
+ * 4. 查找对应的分片算法类型和表达式
12
+ * 5. 组装完整的拓扑信息返回
13
+ *
14
+ * 策略解析优先级:
15
+ * 表级 tableStrategy > 全局 defaultTableStrategy > defaultShardingColumn
16
+ */
17
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
18
+ import { type ParsedConfig } from '../config/types.js';
19
+ import { type ConnectionManager } from '../db/connection-manager.js';
20
+ export declare function registerShardingTopology(server: McpServer, config: ParsedConfig, _connectionManager: ConnectionManager): void;
@@ -0,0 +1,111 @@
1
+ /**
2
+ * MCP Tool: sharding_topology
3
+ *
4
+ * 展示 ShardingSphere 分片拓扑信息。
5
+ * 这是一个纯配置解析操作,不连接任何数据库。
6
+ *
7
+ * 解析逻辑:
8
+ * 1. 从配置中读取分片规则(tables + shardingAlgorithms)
9
+ * 2. 使用 expandDataNodes() 展开 actualDataNodes 表达式为具体节点列表
10
+ * 3. 解析表级分片策略(standard/complex),回退到默认策略
11
+ * 4. 查找对应的分片算法类型和表达式
12
+ * 5. 组装完整的拓扑信息返回
13
+ *
14
+ * 策略解析优先级:
15
+ * 表级 tableStrategy > 全局 defaultTableStrategy > defaultShardingColumn
16
+ */
17
+ import { z } from 'zod';
18
+ import { expandDataNodes } from '../config/parser.js';
19
+ export function registerShardingTopology(server, config, _connectionManager) {
20
+ server.tool('sharding_topology', '查看分片拓扑信息:逻辑表名、分片列、分片算法、实际数据节点', {
21
+ table: z.string().optional().describe('逻辑表名(不传则返回所有表的拓扑)'),
22
+ }, async ({ table }) => {
23
+ const shardingRule = config.shardingRule;
24
+ if (!shardingRule || !shardingRule.tables) {
25
+ return {
26
+ content: [{
27
+ type: 'text',
28
+ text: JSON.stringify({ message: '未配置分片规则' }),
29
+ }],
30
+ };
31
+ }
32
+ const tables = shardingRule.tables;
33
+ const algorithms = shardingRule.shardingAlgorithms || {};
34
+ // 支持查询单表或全部表的拓扑
35
+ const filteredEntries = table
36
+ ? Object.entries(tables).filter(([name]) => name === table)
37
+ : Object.entries(tables);
38
+ if (filteredEntries.length === 0) {
39
+ return {
40
+ content: [{
41
+ type: 'text',
42
+ text: JSON.stringify({ message: table ? `逻辑表 "${table}" 未找到分片规则` : '无分片表配置' }),
43
+ }],
44
+ };
45
+ }
46
+ const topology = filteredEntries.map(([logicalTable, tableConfig]) => {
47
+ // 展开 actualDataNodes 表达式为具体节点列表
48
+ const actualDataNodes = expandDataNodes(tableConfig.actualDataNodes);
49
+ // 解析表级分片策略
50
+ let shardingColumn;
51
+ let shardingAlgorithmName;
52
+ let strategyType = 'none';
53
+ const strategy = tableConfig.tableStrategy;
54
+ if (strategy?.standard) {
55
+ strategyType = 'standard';
56
+ shardingColumn = strategy.standard.shardingColumn;
57
+ shardingAlgorithmName = strategy.standard.shardingAlgorithmName;
58
+ }
59
+ else if (strategy?.complex) {
60
+ strategyType = 'complex';
61
+ shardingColumn = strategy.complex.shardingColumns;
62
+ shardingAlgorithmName = strategy.complex.shardingAlgorithmName;
63
+ }
64
+ // 表级策略为 none 时,回退到全局默认表策略
65
+ if (!strategy || strategy.none !== undefined) {
66
+ if (shardingRule.defaultTableStrategy?.standard) {
67
+ strategyType = 'standard (default)';
68
+ shardingColumn = shardingRule.defaultTableStrategy.standard.shardingColumn;
69
+ shardingAlgorithmName = shardingRule.defaultTableStrategy.standard.shardingAlgorithmName;
70
+ }
71
+ }
72
+ // 最终兜底: 使用全局默认分片列
73
+ if (!shardingColumn && shardingRule.defaultShardingColumn) {
74
+ shardingColumn = shardingRule.defaultShardingColumn;
75
+ }
76
+ // 根据算法名称查找算法详情
77
+ let algorithmType;
78
+ let algorithmExpression;
79
+ if (shardingAlgorithmName && algorithms[shardingAlgorithmName]) {
80
+ const algo = algorithms[shardingAlgorithmName];
81
+ algorithmType = algo.type; // 如 INLINE, MOD
82
+ algorithmExpression = algo.props?.['algorithm-expression']?.toString();
83
+ }
84
+ // 数据库级分片策略(通常按 corporation_id 分库)
85
+ let databaseStrategy = 'none';
86
+ let dbShardingColumn;
87
+ if (shardingRule.defaultDatabaseStrategy?.standard) {
88
+ databaseStrategy = 'standard';
89
+ dbShardingColumn = shardingRule.defaultDatabaseStrategy.standard.shardingColumn;
90
+ }
91
+ return {
92
+ logicalTable,
93
+ actualDataNodes,
94
+ nodeCount: actualDataNodes.length,
95
+ tableStrategy: strategyType,
96
+ shardingColumn,
97
+ algorithmName: shardingAlgorithmName,
98
+ algorithmType,
99
+ algorithmExpression,
100
+ databaseStrategy,
101
+ databaseShardingColumn: dbShardingColumn,
102
+ };
103
+ });
104
+ return {
105
+ content: [{
106
+ type: 'text',
107
+ text: JSON.stringify(topology, null, 2),
108
+ }],
109
+ };
110
+ });
111
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * MCP Tool: test_connection
3
+ *
4
+ * 测试 PostgreSQL 数据库连接是否可用。
5
+ * 完全独立于现有配置和连接池,使用临时连接执行 SELECT version() 验证。
6
+ * 支持两种输入方式: 分字段输入或 JDBC URL 输入。
7
+ */
8
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
9
+ export declare function registerTestConnection(server: McpServer): void;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * MCP Tool: test_connection
3
+ *
4
+ * 测试 PostgreSQL 数据库连接是否可用。
5
+ * 完全独立于现有配置和连接池,使用临时连接执行 SELECT version() 验证。
6
+ * 支持两种输入方式: 分字段输入或 JDBC URL 输入。
7
+ */
8
+ import { z } from 'zod';
9
+ import pg from 'pg';
10
+ export function registerTestConnection(server) {
11
+ server.tool('test_connection', '测试 PostgreSQL 数据库连接。支持分字段输入(host/port/database)或 jdbcUrl 输入。', {
12
+ host: z.string().optional().describe('数据库主机地址(与 jdbcUrl 二选一)'),
13
+ port: z.number().default(5432).describe('数据库端口,默认 5432'),
14
+ database: z.string().optional().describe('数据库名称(与 jdbcUrl 二选一)'),
15
+ username: z.string().describe('数据库用户名'),
16
+ password: z.string().describe('数据库密码'),
17
+ jdbcUrl: z.string().optional().describe('JDBC URL,如 jdbc:postgresql://host:port/database(与 host/database 二选一)'),
18
+ }, async (params) => {
19
+ let host;
20
+ let port;
21
+ let database;
22
+ // 从 jdbcUrl 或分字段中解析连接参数
23
+ if (params.jdbcUrl) {
24
+ const parsed = parseTestJdbcUrl(params.jdbcUrl);
25
+ if (!parsed) {
26
+ return {
27
+ content: [{
28
+ type: 'text',
29
+ text: JSON.stringify({ connected: false, error: `无法解析 JDBC URL: ${params.jdbcUrl}` }),
30
+ }],
31
+ };
32
+ }
33
+ host = parsed.host;
34
+ port = parsed.port;
35
+ database = parsed.database;
36
+ }
37
+ else if (params.host && params.database) {
38
+ host = params.host;
39
+ port = params.port;
40
+ database = params.database;
41
+ }
42
+ else {
43
+ return {
44
+ content: [{
45
+ type: 'text',
46
+ text: JSON.stringify({ connected: false, error: '请提供 jdbcUrl 或者 host + database 参数' }),
47
+ }],
48
+ };
49
+ }
50
+ // 创建临时连接池进行测试
51
+ const pool = new pg.Pool({
52
+ host,
53
+ port,
54
+ database,
55
+ user: params.username,
56
+ password: params.password,
57
+ max: 1,
58
+ connectionTimeoutMillis: 10000,
59
+ // 测试连接也使用只读模式,避免任何意外写入
60
+ options: '-c default_transaction_read_only=on',
61
+ });
62
+ try {
63
+ const result = await pool.query('SELECT version()');
64
+ const serverVersion = result.rows[0]?.version ?? 'unknown';
65
+ return {
66
+ content: [{
67
+ type: 'text',
68
+ text: JSON.stringify({ connected: true, serverVersion }),
69
+ }],
70
+ };
71
+ }
72
+ catch (err) {
73
+ const errorMessage = err instanceof Error ? err.message : String(err);
74
+ return {
75
+ content: [{
76
+ type: 'text',
77
+ text: JSON.stringify({ connected: false, error: errorMessage }),
78
+ }],
79
+ };
80
+ }
81
+ finally {
82
+ await pool.end();
83
+ }
84
+ });
85
+ }
86
+ /**
87
+ * 从 JDBC URL 中提取连接参数(工具内部使用,与 parser.ts 的 parseJdbcUrl 独立)
88
+ * 不抛出异常,解析失败返回 null
89
+ */
90
+ function parseTestJdbcUrl(jdbcUrl) {
91
+ const url = jdbcUrl.replace(/^jdbc:/, '');
92
+ const match = url.match(/postgresql:\/\/([^:/]+)(?::(\d+))?\/([^?]+)/);
93
+ if (!match)
94
+ return null;
95
+ return {
96
+ host: match[1],
97
+ port: match[2] ? parseInt(match[2], 10) : 5432,
98
+ database: match[3],
99
+ };
100
+ }
package/package.json ADDED
@@ -0,0 +1,42 @@
1
+ {
2
+ "name": "@harneon-ai/db",
3
+ "version": "0.0.1",
4
+ "description": "MCP Server for database metadata sync with ShardingSphere topology support",
5
+ "type": "module",
6
+ "bin": {
7
+ "harneon-db": "dist/index.js"
8
+ },
9
+ "files": [
10
+ "dist/",
11
+ "config/datasource.example.yaml",
12
+ "README.md"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "dev": "tsx src/index.ts",
17
+ "start": "node dist/index.js",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "keywords": ["mcp", "postgresql", "database", "metadata", "shardingsphere", "harneon"],
21
+ "author": "lyuzb <lyuzb@lyuzb.com>",
22
+ "license": "MIT",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://gitee.com/harneon/harneon-db.git"
26
+ },
27
+ "engines": {
28
+ "node": ">=18"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.12.1",
32
+ "pg": "^8.13.1",
33
+ "yaml": "^2.7.1",
34
+ "zod": "^3.24.4"
35
+ },
36
+ "devDependencies": {
37
+ "@types/node": "^20.17.14",
38
+ "@types/pg": "^8.11.11",
39
+ "tsx": "^4.19.2",
40
+ "typescript": "^5.7.3"
41
+ }
42
+ }