mcp-standalone.ts•13 kB
#!/usr/bin/env node
/**
* MCP Server 独立启动入口
* 通过 stdio 通信,可被 Kiro、Claude Desktop 等 MCP 客户端调用
*/
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
CallToolRequestSchema,
ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { createConnection, Connection } from 'typeorm';
import * as dotenv from 'dotenv';
import * as fs from 'fs';
import * as path from 'path';
// 加载环境变量
dotenv.config();
// 权限配置
interface PermissionConfig {
permissions: {
allowedTables: string[];
readOnlyTables: string[];
allowedOperations: Record<string, string[]>;
forbiddenOperations: string[];
maxQueryLimit: number;
};
audit: {
enabled: boolean;
logPath: string;
trackChanges: boolean;
};
}
let permissionConfig: PermissionConfig;
let dbConnection: Connection;
// 加载权限配置
function loadPermissions(): PermissionConfig {
const configPath = path.join(process.cwd(), 'database-permissions.json');
try {
const configData = fs.readFileSync(configPath, 'utf8');
return JSON.parse(configData);
} catch (error) {
console.error('Failed to load permissions config, using defaults');
return {
permissions: {
allowedTables: ['users'],
readOnlyTables: [],
allowedOperations: {
users: ['SELECT', 'INSERT', 'UPDATE', 'DELETE'],
},
forbiddenOperations: ['DROP TABLE', 'DROP DATABASE', 'TRUNCATE', 'ALTER TABLE'],
maxQueryLimit: 1000,
},
audit: {
enabled: true,
logPath: './logs/audit.log',
trackChanges: true,
},
};
}
}
// 审计日志
function logAudit(log: any) {
if (!permissionConfig.audit.enabled) return;
const logDir = path.join(process.cwd(), 'logs');
if (!fs.existsSync(logDir)) {
fs.mkdirSync(logDir, { recursive: true });
}
const logPath = path.join(process.cwd(), permissionConfig.audit.logPath);
const logEntry = {
...log,
timestamp: new Date().toISOString(),
};
fs.appendFileSync(logPath, JSON.stringify(logEntry) + '\n', 'utf8');
}
// 权限验证
function canAccessTable(tableName: string): boolean {
const allTables = [
...permissionConfig.permissions.allowedTables,
...permissionConfig.permissions.readOnlyTables,
];
return allTables.includes(tableName);
}
function canPerformOperation(tableName: string, operation: string): boolean {
if (!canAccessTable(tableName)) return false;
const allowedOps = permissionConfig.permissions.allowedOperations[tableName] || [];
return allowedOps.includes(operation);
}
function validateSql(sql: string): { valid: boolean; error?: string } {
const upperSql = sql.toUpperCase();
for (const forbidden of permissionConfig.permissions.forbiddenOperations) {
if (upperSql.includes(forbidden.toUpperCase())) {
return { valid: false, error: `禁止的操作: ${forbidden}` };
}
}
return { valid: true };
}
// 数据库操作
async function listTables(): Promise<string[]> {
const result = await dbConnection.query('SHOW TABLES');
const dbName = process.env.DB_NAME || 'mcp-test';
return result.map((row: any) => row[`Tables_in_${dbName}`]);
}
async function getTableSchema(tableName: string): Promise<any> {
if (!canAccessTable(tableName)) {
throw new Error(`无权访问表 '${tableName}'`);
}
const columns = await dbConnection.query(`DESCRIBE ${tableName}`);
return {
tableName,
columns: columns.map((col: any) => ({
field: col.Field,
type: col.Type,
null: col.Null,
key: col.Key,
default: col.Default,
extra: col.Extra,
})),
};
}
async function queryDatabase(sql: string, params: any[] = []): Promise<any> {
const validation = validateSql(sql);
if (!validation.valid) {
throw new Error(validation.error);
}
const maxLimit = permissionConfig.permissions.maxQueryLimit;
if (sql.toUpperCase().includes('SELECT') && !sql.toUpperCase().includes('LIMIT')) {
sql += ` LIMIT ${maxLimit}`;
}
const result = await dbConnection.query(sql, params);
logAudit({
operation: 'SELECT',
sql,
params,
rowCount: result.length,
success: true,
});
return result;
}
async function insertRecord(tableName: string, data: Record<string, any>): Promise<any> {
if (!canAccessTable(tableName)) {
throw new Error(`无权访问表 '${tableName}'`);
}
if (!canPerformOperation(tableName, 'INSERT')) {
throw new Error(`表 '${tableName}' 不允许 INSERT 操作`);
}
const keys = Object.keys(data);
const values = Object.values(data);
const placeholders = keys.map(() => '?').join(', ');
const sql = `INSERT INTO ${tableName} (${keys.join(', ')}) VALUES (${placeholders})`;
const result = await dbConnection.query(sql, values);
logAudit({
operation: 'INSERT',
tableName,
data,
insertId: result.insertId,
success: true,
});
return {
insertId: result.insertId,
affectedRows: result.affectedRows,
insertedData: data,
};
}
async function updateRecord(
tableName: string,
data: Record<string, any>,
where: Record<string, any>
): Promise<any> {
if (!canAccessTable(tableName)) {
throw new Error(`无权访问表 '${tableName}'`);
}
if (!canPerformOperation(tableName, 'UPDATE')) {
throw new Error(`表 '${tableName}' 不允许 UPDATE 操作`);
}
// 获取修改前的数据
let beforeData = null;
if (permissionConfig.audit.trackChanges) {
const whereClause = Object.keys(where)
.map((key) => `${key} = ?`)
.join(' AND ');
const selectSql = `SELECT * FROM ${tableName} WHERE ${whereClause}`;
beforeData = await dbConnection.query(selectSql, Object.values(where));
}
const setClause = Object.keys(data)
.map((key) => `${key} = ?`)
.join(', ');
const whereClause = Object.keys(where)
.map((key) => `${key} = ?`)
.join(' AND ');
const sql = `UPDATE ${tableName} SET ${setClause} WHERE ${whereClause}`;
const params = [...Object.values(data), ...Object.values(where)];
const result = await dbConnection.query(sql, params);
logAudit({
operation: 'UPDATE',
tableName,
beforeData,
afterData: data,
affectedRows: result.affectedRows,
success: true,
});
return {
affectedRows: result.affectedRows,
changedRows: result.changedRows,
beforeData,
afterData: data,
};
}
async function deleteRecord(tableName: string, where: Record<string, any>): Promise<any> {
if (!canAccessTable(tableName)) {
throw new Error(`无权访问表 '${tableName}'`);
}
if (!canPerformOperation(tableName, 'DELETE')) {
throw new Error(`表 '${tableName}' 不允许 DELETE 操作`);
}
// 获取删除前的数据
let beforeData = null;
if (permissionConfig.audit.trackChanges) {
const whereClause = Object.keys(where)
.map((key) => `${key} = ?`)
.join(' AND ');
const selectSql = `SELECT * FROM ${tableName} WHERE ${whereClause}`;
beforeData = await dbConnection.query(selectSql, Object.values(where));
}
const whereClause = Object.keys(where)
.map((key) => `${key} = ?`)
.join(' AND ');
const sql = `DELETE FROM ${tableName} WHERE ${whereClause}`;
const params = Object.values(where);
const result = await dbConnection.query(sql, params);
logAudit({
operation: 'DELETE',
tableName,
beforeData,
affectedRows: result.affectedRows,
success: true,
});
return {
affectedRows: result.affectedRows,
deletedData: beforeData,
};
}
// 主函数
async function main() {
// 加载配置
permissionConfig = loadPermissions();
// 连接数据库
dbConnection = await createConnection({
type: 'mysql',
host: process.env.DB_HOST || 'localhost',
port: parseInt(process.env.DB_PORT || '3306'),
username: process.env.DB_USER || 'root',
password: process.env.DB_PASSWORD || '',
database: process.env.DB_NAME || 'mcp-test',
logging: false,
});
console.error('✅ Database connected');
// 创建 MCP Server
const server = new Server(
{
name: 'mysql-mcp-server',
version: '1.0.0',
},
{
capabilities: {
tools: {},
},
}
);
// 注册工具列表
server.setRequestHandler(ListToolsRequestSchema, async () => ({
tools: [
{
name: 'list_tables',
description: '列出数据库中的所有表',
inputSchema: {
type: 'object',
properties: {},
},
},
{
name: 'get_schema',
description: '获取指定表的结构信息',
inputSchema: {
type: 'object',
properties: {
tableName: { type: 'string', description: '表名' },
},
required: ['tableName'],
},
},
{
name: 'query_database',
description: '执行 SELECT 查询',
inputSchema: {
type: 'object',
properties: {
sql: { type: 'string', description: 'SQL 查询语句' },
params: {
type: 'array',
description: '查询参数(可选)',
items: { type: 'string' },
},
},
required: ['sql'],
},
},
{
name: 'insert_record',
description: '向表中插入一条记录',
inputSchema: {
type: 'object',
properties: {
tableName: { type: 'string', description: '表名' },
data: { type: 'object', description: '要插入的数据(键值对)' },
},
required: ['tableName', 'data'],
},
},
{
name: 'update_record',
description: '更新表中的记录',
inputSchema: {
type: 'object',
properties: {
tableName: { type: 'string', description: '表名' },
data: { type: 'object', description: '要更新的数据(键值对)' },
where: { type: 'object', description: '更新条件(键值对)' },
},
required: ['tableName', 'data', 'where'],
},
},
{
name: 'delete_record',
description: '删除表中的记录',
inputSchema: {
type: 'object',
properties: {
tableName: { type: 'string', description: '表名' },
where: { type: 'object', description: '删除条件(键值对)' },
},
required: ['tableName', 'where'],
},
},
],
}));
// 处理工具调用
server.setRequestHandler(CallToolRequestSchema, async (request) => {
try {
const { name, arguments: args } = request.params;
let result: any;
switch (name) {
case 'list_tables':
result = await listTables();
return {
content: [{ type: 'text', text: JSON.stringify({ tables: result }, null, 2) }],
};
case 'get_schema':
result = await getTableSchema((args as any).tableName);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
};
case 'query_database':
result = await queryDatabase((args as any).sql, (args as any).params || []);
return {
content: [
{
type: 'text',
text: JSON.stringify({ rows: result, count: result.length }, null, 2),
},
],
};
case 'insert_record':
result = await insertRecord((args as any).tableName, (args as any).data);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
};
case 'update_record':
result = await updateRecord(
(args as any).tableName,
(args as any).data,
(args as any).where
);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
};
case 'delete_record':
result = await deleteRecord((args as any).tableName, (args as any).where);
return {
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
};
default:
throw new Error(`Unknown tool: ${name}`);
}
} catch (error: any) {
logAudit({
operation: request.params.name,
error: error.message,
success: false,
});
return {
content: [{ type: 'text', text: JSON.stringify({ error: error.message }, null, 2) }],
isError: true,
};
}
});
// 启动 stdio transport
const transport = new StdioServerTransport();
await server.connect(transport);
console.error('🔧 MCP Server started via stdio');
}
main().catch((error) => {
console.error('Failed to start MCP server:', error);
process.exit(1);
});