inshow-ai-query-mcp
Version:
MCP服务器,用于连接Supabase数据库并提供表结构查询功能
2,367 lines • 85.4 kB
JavaScript
#!/usr/bin/env node
import {ResourceTemplate,McpServer}from'@modelcontextprotocol/sdk/server/mcp.js';import dotenv from'dotenv';import {Command}from'commander';import fs from'fs';import path from'path';import {createBrowserClient}from'@supabase/ssr';import {z}from'zod';import {StdioServerTransport}from'@modelcontextprotocol/sdk/server/stdio.js';import express from'express';import {randomUUID}from'node:crypto';import {StreamableHTTPServerTransport}from'@modelcontextprotocol/sdk/server/streamableHttp.js';import {isInitializeRequest}from'@modelcontextprotocol/sdk/types.js';// 判断是否在Inspector模式下运行
const isInspectorMode = process.env.MCP_STDIO === 'true';
/**
* 安全的日志输出函数,在Inspector模式下通过stderr输出
* @param {string|Object} message 要输出的消息
* @param {boolean} isError 是否错误级别日志
* @param {string} tag 日志标签,默认为'MCP'
*/
function safeLog(message, isError = false, tag = 'MCP') {
// 处理对象类型消息
const formattedMessage = typeof message === 'object' ? JSON.stringify(message) : message;
// 添加标签
const taggedMessage = tag ? `[${tag}] ${formattedMessage}` : formattedMessage;
// 在Inspector模式下只通过stderr输出日志
if (isInspectorMode || isError) {
console.error(taggedMessage);
} else {
console.log(taggedMessage);
}
}
/**
* 信息级别日志
* @param {string|Object} message 日志消息
* @param {string} tag 日志标签
*/
function logInfo(message, tag = 'INFO') {
safeLog(message, false, tag);
}
/**
* 警告级别日志
* @param {string|Object} message 日志消息
* @param {string} tag 日志标签
*/
function logWarn(message, tag = 'WARN') {
safeLog(message, false, tag);
}
/**
* 错误级别日志
* @param {string|Object} message 日志消息
* @param {string} tag 日志标签
*/
function logError(message, tag = 'ERROR') {
safeLog(message, true, tag);
}
/**
* 调试级别日志
* @param {string|Object} message 日志消息
* @param {string} tag 日志标签
*/
function logDebug(message, tag = 'DEBUG') {
// 只在非生产环境输出调试日志
if (process.env.NODE_ENV !== 'production') {
safeLog(message, false, tag);
}
}/**
* 初始化Supabase客户端并提供数据库访问方法
* @returns {Object} Supabase服务对象
*/
function setupSupabaseService() {
// 从环境变量获取Supabase配置
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_KEY;
if (!supabaseUrl || !supabaseKey) {
logError('缺少Supabase配置!');
throw new Error('缺少Supabase配置,请确保设置了SUPABASE_URL和SUPABASE_KEY环境变量');
}
logInfo(`正在初始化Supabase客户端,URL: ${supabaseUrl.substring(0, 20)}...`);
// 创建Supabase客户端
const supabase = createBrowserClient(supabaseUrl, supabaseKey, {
auth: {
persistSession: false, // 服务器端模式不需要持久化会话
},
});
return {
/**
* 获取数据库中所有表及其定义
* @returns {Promise<Array>} 表结构信息数组
*/
async getTableDefinitions() {
logInfo('调用 get_table_definitions RPC...');
try {
const { data, error } = await supabase.rpc('get_table_definitions');
logDebug('get_table_definitions RPC 返回数据: ' + JSON.stringify(data));
if (error) {
logError('获取表定义失败: ' + error);
// 尝试确定错误原因
if (
error.message &&
error.message.includes('function get_table_definitions() does not exist')
) {
logError('错误: get_table_definitions 函数不存在,请确保已在 Supabase 中运行安装脚本');
throw new Error(
`获取表定义失败: Supabase 数据库缺少必要的函数。请确保已运行 setup-supabase.sql 脚本`
);
} else if (error.message && error.message.includes('permission denied')) {
logError('错误: 权限被拒绝。请检查 Supabase 权限设置');
throw new Error(
`获取表定义失败: 权限被拒绝。请确保 RPC 函数已授权给 authenticated 角色`
);
}
throw new Error(`获取表定义失败: ${error.message}`);
}
// 确保返回的数据是数组
if (!data) {
logWarn('获取表定义返回的数据为空');
return [];
}
if (!Array.isArray(data)) {
logError(`获取表定义返回的不是数组,而是 ${typeof data}: ` + JSON.stringify(data));
// 尝试转换数据为数组格式
if (typeof data === 'object') {
try {
const convertedData = Object.keys(data).map(key => ({
table_name: key,
description: data[key]?.description || '',
}));
logInfo(`已将对象转换为数组,共 ${convertedData.length} 项`);
return convertedData;
} catch (conversionError) {
logError('无法转换对象到数组: ' + conversionError);
}
}
// 如果转换失败,返回空数组
return [];
}
// 验证并过滤数据
const validatedData = data.filter(item => {
if (!item || typeof item !== 'object') {
logWarn('表定义中存在无效项: ' + JSON.stringify(item));
return false;
}
if (!item.table_name) {
logWarn('表定义缺少表名: ' + JSON.stringify(item));
return false;
}
return true;
});
if (validatedData.length !== data.length) {
logWarn(`过滤掉了 ${data.length - validatedData.length} 个无效项`);
}
return validatedData;
} catch (err) {
logError('获取表定义时出现异常: ' + err);
throw err;
}
},
/**
* 执行只读SQL查询
* @param {string} query SQL查询语句
* @returns {Promise<Object>} 查询结果
*/
async executeReadQuery(query) {
query = query.trim();
// 记录原始查询
logInfo(`原始SQL查询: ${query.substring(0, 100)}${query.length > 100 ? '...' : ''}`);
// 简化前端验证,避免与后端验证冲突
// 只进行基本检查以防止明显的危险查询
const upperQuery = query.toUpperCase();
const dangerousKeywords = ['DROP DATABASE', 'TRUNCATE ALL', 'DELETE FROM'];
const containsDangerousKeyword = dangerousKeywords.some(keyword =>
upperQuery.includes(keyword)
);
if (containsDangerousKeyword) {
logError('危险查询被前端拒绝: ' + query);
throw new Error('查询包含危险操作,已被拒绝');
}
try {
logDebug(`发送到Supabase RPC的SQL: ${query}`);
const { data, error } = await supabase.rpc('execute_read_query', { query_text: query });
if (error) {
logError('查询执行失败: ' + JSON.stringify(error));
throw new Error(`查询执行失败: ${error.message}`);
}
logInfo(
`查询执行成功, 结果数量: ${data ? (Array.isArray(data) ? data.length : '非数组') : 0}`
);
logDebug(`查询结果: ${JSON.stringify(data).substring(0, 200)}...`);
return data || [];
} catch (err) {
logError(`查询执行时出现异常: ${err.message}`);
logError(`详细错误信息: ${JSON.stringify(err)}`);
throw err;
}
},
/**
* 获取表的列信息
* @param {string} tableName 表名
* @returns {Promise<Array>} 列信息数组
*/
async getTableColumns(tableName) {
logInfo(`获取表列信息: ${tableName}`);
try {
const { data, error } = await supabase.rpc('get_table_columns', { table_name: tableName });
if (error) {
logError(`获取表 ${tableName} 列信息失败: ` + JSON.stringify(error));
throw new Error(`获取表列信息失败: ${error.message}`);
}
return data || [];
} catch (err) {
logError(`获取表 ${tableName} 列信息时出现异常: ` + err);
throw err;
}
},
/**
* 获取表的前N行数据
* @param {string} tableName 表名
* @param {number} limit 限制行数
* @returns {Promise<Array>} 表数据数组
*/
async getTableSample(tableName, limit = 10) {
logInfo(`获取表样本数据: ${tableName}, 限制: ${limit} 行`);
try {
const { data, error } = await supabase.from(tableName).select('*').limit(limit);
if (error) {
logError(`获取表 ${tableName} 样本数据失败: ` + error);
throw new Error(`获取表样本数据失败: ${error.message}`);
}
return data || [];
} catch (err) {
logError(`获取表 ${tableName} 样本数据时出现异常: ` + err);
throw err;
}
},
};
}/**
* 数据库资源描述文件
* 为MCP资源提供清晰的描述,帮助LLM更准确地理解和使用资源
*/
// 资源描述对象
const resourceDescriptions = {
// 数据库概览资源
'database-overview': {
name: '数据库概览',
description:
'提供数据库中所有表的高级概述,包括表名和描述。当需要了解数据库整体结构时,应首先查看此资源。',
uri_pattern: 'db://overview',
when_to_use:
'在开始任何数据库探索任务时,或需要快速了解数据库包含哪些表时使用。这应该是探索未知数据库的第一步。特别适用于:1)初次接触该数据库时;2)需要理解数据库整体功能和用途时;3)寻找特定功能相关表时;4)用户请求数据库概览或表列表时;5)在执行查询前需要了解可用表。',
when_not_to_use:
'当已经知道需要操作的具体表,或需要详细的表结构信息时,应直接使用更具体的资源或工具。不适用于:1)需要查看表内数据时;2)需要了解表间关系时;3)需要获取表的详细结构时;4)需要执行数据查询时;5)用户已明确指定目标表。',
versus_tools:
'比起getDatabaseStats工具,此资源更关注表的功能描述而非数据量统计。比起getTableStructure,此资源提供更高层次的概览而非详细列信息。比起executeQuery查询information_schema.tables,此资源提供更友好的格式化表描述。当需要了解数据库结构和表功能时,应优先使用此资源而非工具。',
examples: [
'数据库中有哪些表',
'数据库结构是什么样的',
'给我一个数据库的总体视图',
'我需要了解这个数据库包含什么内容',
],
},
// 表列表资源
'table-list': {
name: '数据库表列表',
description:
'列出数据库中所有可用的表,以资源列表形式返回。每个表都包含URI和名称,可用于访问详细信息。',
uri_pattern: 'db://tables',
when_to_use:
'当需要以程序化方式获取所有表资源的URI时使用,尤其是需要后续导航到特定表详情时。适用场景:1)准备多表连续探索时;2)需要构建表资源URI时;3)需要以结构化格式获取表列表时;4)为编程处理表列表做准备时;5)需要在回答中包含指向特定表的链接时。',
when_not_to_use:
'如果只需要可读的表列表概览,应使用db://overview。如果已知目标表名,可直接访问特定表详情。不适用于:1)需要了解表功能和描述时;2)只关注单个特定表时;3)需要表结构或数据内容时;4)需要了解表间关系时;5)用户更倾向于叙述性描述而非技术性列表时。',
versus_tools:
'比起db://overview,此资源返回更结构化的表列表,适合进一步通过URI导航。比起getDatabaseStats,不包含行数统计信息。比起使用executeQuery查询表信息,这提供更标准化的格式和更简洁的访问方式。当需要获取完整表列表作为进一步探索的起点时,应使用此资源。',
examples: ['列出所有表', '数据库中有哪些可用的表', '显示表资源列表', '获取所有表的URI'],
},
// 表详情资源
'table-details': {
name: '表详情',
description:
'提供特定表的详细信息,包括列定义(名称、类型、可空性、描述)和样本数据。当需要深入了解表结构时使用。',
uri_pattern: 'db://tables/{tableName}',
when_to_use:
'当需要同时了解表的结构和数据内容时使用,这是获取表完整信息的最全面方法。特别适用于:1)初次了解特定表时;2)需要同时查看结构和数据示例时;3)准备编写查询前全面了解表时;4)需要人类可读的表信息时;5)用户请求特定表的完整信息时。',
when_not_to_use:
'如果只需要表结构,应使用getTableStructure工具;如果只需要数据样本,应使用getTableSample工具。对于复杂查询,应使用executeQuery。不适用于:1)只关注列定义而不需要示例数据时;2)只需查看数据而不关心结构时;3)需要复杂条件过滤时;4)需要处理大量数据时;5)需要进行数据聚合或分析时。',
versus_tools:
'此资源结合了getTableStructure和getTableSample的功能,提供更全面的表视图。返回格式化的文本而非JSON结构,更适合人类阅读。相比executeQuery,这是一种无需编写SQL就能获取表基本信息的简便方法。当用户需要了解表的整体情况而不需要特定查询时,应优先使用此资源。',
parameters: {
tableName: '要查看详情的表名',
},
examples: [
'获取users表的详细信息',
'显示products表的结构和数据',
'查看orders表的列定义',
'我想了解customers表的完整信息',
],
},
// 数据库关系图资源
'database-relationships': {
name: '数据库关系图',
description:
'展示数据库表之间的关系,包括外键约束和引用。提供表格和PlantUML格式的ER图,帮助理解数据模型。',
uri_pattern: 'db://relationships',
when_to_use:
'当需要了解表之间的关联关系、外键引用或整体数据模型时使用。对于理解复杂数据库的结构非常有用。适用于:1)准备多表联接查询前;2)理解数据模型和实体关系;3)追踪引用完整性和依赖关系;4)分析数据流和业务逻辑;5)用户询问表间关系或引用时。',
when_not_to_use:
'如果已知表之间的关系或只关心单一表的结构时,不需要使用此资源。对于数据查询操作,应使用查询工具而非此资源。不适用于:1)只需了解单表结构时;2)需要查看实际数据内容时;3)需要执行数据查询或分析时;4)已经熟悉数据模型时;5)用户明确只关注特定表而非关系时。',
versus_tools:
'没有工具可以提供类似的表关系概览。这是理解数据库模型的唯一专用资源。比起executeQuery查询约束信息,此资源提供更直观的关系图和更全面的关系描述。比起单独查询每个表的结构,此资源更有效地展示全局关系。当需要理解数据模型或准备复杂联接查询时,应优先使用此资源。',
examples: [
'表之间的关系是什么',
'数据库ER图',
'显示数据库的外键约束',
'如何理解数据库中的表是如何关联的',
],
},
};/**
* 注册数据库相关资源
* @param {Object} server MCP服务器实例
* @param {Object} supabaseService Supabase服务实例
*/
function registerDatabaseResources(server, supabaseService) {
// 数据库概览资源
server.resource(
'database-overview',
'db://overview',
async uri => {
try {
const tables = await supabaseService.getTableDefinitions();
let overviewText = '# 数据库结构概览\n\n';
if (!tables || !Array.isArray(tables) || tables.length === 0) {
overviewText += '数据库中没有可用的表,或无法获取表定义。\n';
} else {
overviewText += tables
.map(table => {
return `## ${table.table_name}\n${table.description || '无描述'}\n`;
})
.join('\n');
}
return {
contents: [
{
uri: uri.href,
text: overviewText,
},
],
};
} catch (error) {
return {
contents: [
{
uri: uri.href,
text: `获取数据库概览失败: ${error.message}`,
},
],
};
}
},
{
description: resourceDescriptions['database-overview'].description,
name: resourceDescriptions['database-overview'].name,
usage_examples: resourceDescriptions['database-overview'].examples,
}
);
// 定义表列表资源处理函数
async function listTablesHandler() {
try {
logInfo('正在获取可用表列表...');
const tables = await supabaseService.getTableDefinitions();
if (!tables || !Array.isArray(tables)) {
logError('获取表列表失败: 返回的不是数组');
return { resources: [] }; // 返回正确格式的空资源列表
}
// 转换为标准资源列表格式,每项包含uri和name属性
const resources = tables
.filter(table => table && table.table_name)
.map(table => ({
uri: `db://tables/${table.table_name}`,
name: table.table_name,
description: table.description || `${table.table_name} 表`,
}));
logDebug(`获取到 ${resources.length} 个表资源: ${JSON.stringify(resources)}`);
return { resources }; // 返回正确格式的资源列表
} catch (error) {
logError(`获取表列表失败: ${error}`);
return { resources: [] }; // 出错时返回正确格式的空资源列表
}
}
// 注册表列表资源
server.resource(
'table-list',
'db://tables',
async uri => {
try {
const resources = await listTablesHandler();
return {
contents: [
{
uri: uri.href,
text: JSON.stringify(resources, null, 2),
mimeType: 'application/json',
},
],
};
} catch (error) {
logError(`获取表列表内容失败: ${error}`);
return {
contents: [
{
uri: uri.href,
text: `获取表列表失败: ${error.message}`,
mimeType: 'text/plain',
},
],
};
}
},
{
description: resourceDescriptions['table-list'].description,
name: resourceDescriptions['table-list'].name,
usage_examples: resourceDescriptions['table-list'].examples,
}
);
// 表详情资源模板 - 修复资源列表处理
server.resource(
'table-details',
new ResourceTemplate('db://tables/{tableName}', {
list: listTablesHandler, // 直接使用表列表处理函数
}),
async (uri, { tableName }) => {
try {
if (!tableName) {
return {
contents: [
{
uri: uri.href,
text: `错误: 未提供表名`,
},
],
};
}
// 获取表的列信息
const columns = await supabaseService.getTableColumns(tableName);
// 验证列信息
if (!columns || !Array.isArray(columns)) {
return {
contents: [
{
uri: uri.href,
text: `表 "${tableName}" 没有列信息或无法获取列信息`,
},
],
};
}
// 生成格式化的表详情内容
let tableDetails = `# ${tableName} 表详情\n\n`;
tableDetails += '## 列定义\n\n';
if (columns.length === 0) {
tableDetails += '该表没有列信息。\n';
} else {
tableDetails += '| 列名 | 数据类型 | 可为空 | 描述 |\n';
tableDetails += '|------|----------|--------|------|\n';
tableDetails += columns
.map(col => {
return `| ${col.column_name || '未知'} | ${col.data_type || '未知'} | ${col.is_nullable ? '是' : '否'} | ${col.description || '-'} |`;
})
.join('\n');
}
try {
// 获取样本数据
const sampleData = await supabaseService.getTableSample(tableName, 5);
if (sampleData && Array.isArray(sampleData) && sampleData.length > 0) {
tableDetails += '\n\n## 样本数据\n\n```json\n';
tableDetails += JSON.stringify(sampleData, null, 2);
tableDetails += '\n```';
} else {
tableDetails += '\n\n## 样本数据\n\n无样本数据或表为空。';
}
} catch (sampleError) {
logError(`获取表 "${tableName}" 样本数据失败: ` + sampleError);
tableDetails += `\n\n## 样本数据\n\n获取样本数据失败: ${sampleError.message}`;
}
return {
contents: [
{
uri: uri.href,
text: tableDetails,
},
],
};
} catch (error) {
logError(`获取表 "${tableName}" 详情失败: ` + error);
return {
contents: [
{
uri: uri.href,
text: `获取表 "${tableName}" 详情失败: ${error.message}`,
},
],
};
}
},
{
description: resourceDescriptions['table-details'].description,
name: resourceDescriptions['table-details'].name,
usage_examples: resourceDescriptions['table-details'].examples,
parameters_description: resourceDescriptions['table-details'].parameters,
}
);
// 数据库关系图资源
server.resource(
'database-relationships',
'db://relationships',
async uri => {
try {
// 获取表之间的关系
logInfo('正在获取数据库关系图...');
const query = `
SELECT
tc.table_schema,
tc.constraint_name,
tc.table_name,
kcu.column_name,
ccu.table_schema AS foreign_table_schema,
ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
FROM
information_schema.table_constraints AS tc
JOIN information_schema.key_column_usage AS kcu
ON tc.constraint_name = kcu.constraint_name
AND tc.table_schema = kcu.table_schema
JOIN information_schema.constraint_column_usage AS ccu
ON ccu.constraint_name = tc.constraint_name
AND ccu.table_schema = tc.table_schema
WHERE tc.constraint_type = 'FOREIGN KEY'
`;
logDebug(`执行关系查询: ${query.replace(/\s+/g, ' ')}`);
try {
const relationships = await supabaseService.executeReadQuery(query);
logInfo(
`获取到 ${relationships ? (Array.isArray(relationships) ? relationships.length : '非数组') : 0} 条关系数据`
);
let relationshipText = '# 数据库关系图\n\n';
relationshipText += '## 表之间的关系\n\n';
// 确保关系数据是数组
if (!relationships || !Array.isArray(relationships) || relationships.length === 0) {
relationshipText += '数据库中没有定义外键关系或无法获取关系数据。\n';
return {
contents: [
{
uri: uri.href,
text: relationshipText,
},
],
};
}
relationshipText += '| 源表 | 源列 | 引用表 | 引用列 |\n';
relationshipText += '|------|------|--------|--------|\n';
relationshipText += relationships
.map(rel => {
return `| ${rel.table_name} | ${rel.column_name} | ${rel.foreign_table_name} | ${rel.foreign_column_name} |`;
})
.join('\n');
// 添加简单的PlantUML格式的ER图
relationshipText += '\n\n## ER图 (PlantUML格式)\n\n```plantuml\n';
relationshipText += '@startuml\n\n';
// 添加表
const uniqueTables = new Set();
relationships.forEach(rel => {
if (rel && rel.table_name) uniqueTables.add(rel.table_name);
if (rel && rel.foreign_table_name) uniqueTables.add(rel.foreign_table_name);
});
[...uniqueTables].forEach(tableName => {
relationshipText += `entity "${tableName}" {\n}\n\n`;
});
// 添加关系
relationships.forEach(rel => {
if (rel && rel.table_name && rel.foreign_table_name && rel.column_name) {
relationshipText += `"${rel.table_name}" }|--|| "${rel.foreign_table_name}" : ${rel.column_name}\n`;
}
});
relationshipText += '\n@enduml\n```';
return {
contents: [
{
uri: uri.href,
text: relationshipText,
},
],
};
} catch (queryError) {
logError(`执行关系查询失败: ${queryError.message}`);
// 尝试使用替代方案查询
logInfo('尝试使用替代查询方法...');
// 返回错误信息但不抛出异常
return {
contents: [
{
uri: uri.href,
text: `# 数据库关系图\n\n获取数据库关系失败: ${queryError.message}\n\n可能原因:\n1. 数据库权限不足\n2. 查询语法与数据库版本不兼容\n3. Supabase函数未正确配置`,
},
],
};
}
} catch (error) {
logError('获取数据库关系失败: ' + error);
return {
contents: [
{
uri: uri.href,
text: `获取数据库关系失败: ${error.message}`,
},
],
};
}
},
{
description: resourceDescriptions['database-relationships'].description,
name: resourceDescriptions['database-relationships'].name,
usage_examples: resourceDescriptions['database-relationships'].examples,
}
);
}/**
* 数据库工具描述文件
* 为MCP工具提供清晰的描述,帮助LLM更准确地选择工具
*/
// 工具描述对象
const toolDescriptions = {
// 数据库连接检查工具
checkDatabaseConnection: {
name: '检查数据库连接',
description:
'检测Supabase数据库连接是否正常工作。当遇到数据库查询错误或需要诊断连接问题时使用此工具。该工具会验证环境配置并测试基本数据库连接功能。',
when_to_use:
"仅在怀疑数据库连接有问题时使用,例如查询失败、无法获取表数据或返回错误。作为诊断工具,不应用于正常的数据查询操作。当用户提到'连接错误'、'无法访问数据库'、'查询超时'等问题时,应首先使用此工具。",
when_not_to_use:
'不要用于获取数据库结构或表数据,这些操作应使用专门的资源或查询工具。当数据库正常工作时,避免使用此工具作为常规探索数据库的方法。此工具不返回业务数据,仅返回连接诊断结果。',
examples: [
'数据库连接似乎有问题',
'我无法获取任何表的数据',
'数据库查询返回连接错误',
'为什么我的查询总是超时或失败',
],
returnsExample: `{
"配置状态": {
"SUPABASE_URL": "已设置",
"SUPABASE_KEY": "已设置"
},
"表定义查询": {
"成功获取": "是",
"表数量": 5,
"表名列表": "users, products, orders, categories, inventory"
}
}`,
},
// 环境检查工具
checkEnvironment: {
name: '检查环境配置',
description:
'检查应用程序运行环境和配置参数。当怀疑配置问题或需要了解应用环境信息时使用此工具。该工具会安全地显示环境变量和运行时信息,不会暴露敏感信息。',
when_to_use:
'在需要了解系统环境或验证配置设置时使用,特别是在怀疑环境变量配置错误或缺失时。当用户询问关于应用程序如何配置、运行环境或系统状态时,这是首选工具。也适用于排查非数据库特定的应用问题。',
when_not_to_use:
'不要用于数据库操作或查询,这仅是一个诊断工具。当用户明确要求数据库相关内容或查询时,应优先考虑数据库特定工具。此工具不会修复任何问题,仅提供诊断信息。',
examples: [
'应用程序环境是如何配置的',
'检查当前环境变量',
'应用运行在什么环境中',
'确认Supabase配置是否正确设置',
],
returnsExample: `{
"运行环境": {
"NODE_VERSION": "v18.12.1",
"PLATFORM": "darwin",
"NODE_ENV": "development",
"运行时间": "35 秒"
},
"配置参数": {
"SUPABASE_URL": "已设置 (https://xxxxx...)",
"SUPABASE_KEY": "已设置 (长度: 43)",
"APP_PORT": "9000"
}
}`,
},
// SQL查询执行工具
executeQuery: {
name: '执行SQL查询',
description:
'执行只读SQL查询并返回结果。当需要运行自定义SQL来查询、聚合或分析数据时使用此工具。查询必须是只读类型,不支持修改数据的操作。支持复杂的SELECT查询、JOIN操作和子查询。',
when_to_use:
'需要执行复杂查询、多表联接、条件过滤、聚合或自定义数据分析时使用。这是最灵活但也最复杂的查询工具。应该在以下情况使用:1)需要特定条件过滤数据;2)需要多表关联查询;3)需要聚合计算如COUNT、SUM、AVG等;4)需要复杂排序或分组;5)简单工具无法满足查询需求时。',
when_not_to_use:
'如果只是用于测试样本数据,使用更简单的专用工具如getTableStructure或getTableSample。不要用于数据修改操作。避免在以下情况使用:1)只需表的基本信息时;2)只查看前N行数据时;3)只需了解表关系时;4)有更简单的专用工具可用时。避免使用此工具查询information_schema以获取元数据,应使用专用资源代替。',
examples: [
'SELECT * FROM users WHERE age > 30 LIMIT 5',
'SELECT p.name, c.category_name FROM products p JOIN categories c ON p.category_id = c.id',
'SELECT count(*), avg(price) FROM products GROUP BY category_id',
'SELECT u.name, COUNT(o.id) FROM users u LEFT JOIN orders o ON u.id = o.user_id GROUP BY u.id LIMIT 10',
],
parameters: {
query: 'SQL查询语句(必须是只读查询)',
},
returnsExample: `[
{ "id": 1, "name": "John Doe", "email": "john@example.com" },
{ "id": 2, "name": "Jane Smith", "email": "jane@example.com" }
]`,
},
// 获取表样本数据工具
getTableSample: {
name: '获取表样本数据',
description:
'获取指定表的样本数据。当需要了解表中的实际数据内容、格式或示例时使用此工具。返回表中的前N行记录(默认10行)。仅用于获取测试数据,不适用于直接获取SQL数据,如需要通过SQL查询获取数据,请使用executeQuery。',
when_to_use:
'需要快速查看表中数据示例、了解数据格式或验证表是否有数据时使用。这是获取表数据最简单的方法。适用于:1)初步探索表数据;2)了解实际数据格式;3)查看数据示例而不关心特定条件;4)验证表是否包含数据;5)用户直接要求查看表内容而未指定复杂条件时。',
when_not_to_use:
'不适合复杂条件过滤、聚合计算或多表查询,这些情况应使用executeQuery。也不适合只想了解表结构而不关心数据内容的场景。不应用于:1)需要特定条件筛选数据时;2)需要排序或分组时;3)需要多表联接时;4)表数据量很大且只关注统计信息时;5)用户明确要求的查询超出了简单取样的范围。',
examples: [
'获取users表的样本数据',
'我想看看products表中有什么数据',
'显示orders表的前5行数据',
'查看customer表的数据格式和内容示例',
],
parameters: {
tableName: '要查询的表名(必需)',
limit: '要返回的行数(可选,默认10)',
},
returnsExample: `[
{ "id": 1, "product_name": "Laptop", "price": 1200 },
{ "id": 2, "product_name": "Smartphone", "price": 800 }
]`,
},
// 获取表结构工具
getTableStructure: {
name: '获取表结构',
description:
'获取指定表的列定义和结构信息。当需要了解表的模式、字段类型或列属性时使用此工具。该工具返回表中所有列的详细信息,包括列名、数据类型、是否可为空及描述。',
when_to_use:
'需要详细了解表结构、列定义、数据类型或准备执行查询前检查列名时使用。特别适用于:1)查看表的完整列定义;2)了解列的数据类型和约束;3)准备编写SQL查询前确认列名;4)了解主键和外键列;5)用户明确询问表结构或表定义时。',
when_not_to_use:
'不适合查看表数据内容或执行查询,这些操作应分别使用getTableSample或executeQuery。若需同时了解表结构和示例数据,可考虑使用db://tables/{tableName}资源。不应用于:1)只关心数据内容不关心结构时;2)需要了解表间关系时(应使用db://relationships);3)需要执行数据筛选或聚合时;4)用户没有明确要求表结构细节时。',
examples: [
'users表有哪些列',
'获取products表的结构',
'显示orders表的字段定义',
'customer表的主键和数据类型是什么',
],
parameters: {
tableName: '要查询结构的表名(必需)',
},
returnsExample: `[
{ "column_name": "id", "data_type": "integer", "is_nullable": false, "description": "主键" },
{ "column_name": "name", "data_type": "character varying", "is_nullable": false, "description": "用户名" }
]`,
},
// 获取数据库统计信息工具
getDatabaseStats: {
name: '获取数据库统计信息',
description:
'获取数据库中所有表的行数统计信息。当需要了解数据库整体情况、表大小或数据分布时使用此工具。该工具返回所有公共模式中表的名称和行数,帮助识别大表和空表。',
when_to_use:
'需要了解数据库整体规模、表大小对比或识别最大/空表时使用。这有助于数据库性能分析和查询规划。适合于:1)评估表大小和数据分布;2)找出数据量最大的表;3)检测空表;4)为查询优化提供数据量参考;5)用户询问数据量或表大小相关问题时。',
when_not_to_use:
'不适合查询特定表数据或表结构,这些操作应分别使用getTableSample/executeQuery或getTableStructure。不适用于:1)需要了解表结构时;2)需要查看实际数据内容时;3)需要了解表间关系时;4)需要执行具体业务查询时;5)用户对数据量没有明确关注点时。此工具仅提供数量统计,不提供数据内容或结构信息。',
examples: [
'数据库中有多少数据',
'各个表的行数是多少',
'哪些表包含最多的数据',
'数据库中有哪些空表',
],
returnsExample: `[
{ "table_schema": "public", "table_name": "users", "row_count": 1250 },
{ "table_schema": "public", "table_name": "products", "row_count": 3764 },
{ "table_schema": "public", "table_name": "orders", "row_count": 8927 }
]`,
},
};/**
* 注册数据库查询相关工具
* @param {Object} server MCP服务器实例
* @param {Object} supabaseService Supabase服务实例
*/
function registerQueryTools(server, supabaseService) {
// 添加诊断工具
server.tool(
'checkDatabaseConnection',
{},
async () => {
try {
logInfo('开始数据库连接诊断...');
// 获取Supabase环境配置状态
const supabaseUrl = process.env.SUPABASE_URL;
const supabaseKey = process.env.SUPABASE_KEY;
const configStatus = {
hasUrl: !!supabaseUrl,
hasKey: !!supabaseKey,
urlStart: supabaseUrl ? supabaseUrl.substring(0, 10) + '...' : 'undefined',
keyLength: supabaseKey ? supabaseKey.length : 0,
};
// 尝试获取表定义
logInfo('正在尝试获取表定义...');
const tables = await supabaseService.getTableDefinitions();
const tablesInfo = Array.isArray(tables)
? {
count: tables.length,
names: tables.map(t => t.table_name || '未知表名'),
}
: {
count: 0,
error: '返回的表定义不是数组',
actualType: typeof tables,
actualValue: JSON.stringify(tables).substring(0, 100),
};
return {
content: [
{
type: 'text',
text: `数据库连接诊断结果:
1. 配置状态:
- SUPABASE_URL 配置: ${configStatus.hasUrl ? '已设置' : '未设置'} (${configStatus.urlStart})
- SUPABASE_KEY 配置: ${configStatus.hasKey ? '已设置' : '未设置'} (长度: ${configStatus.keyLength})
2. 表定义查询:
- 成功获取: ${Array.isArray(tables) ? '是' : '否'}
- 表数量: ${tablesInfo.count}
- ${
Array.isArray(tables)
? `表名列表: ${tablesInfo.names.join(', ')}`
: `错误信息: ${tablesInfo.error}, 实际类型: ${tablesInfo.actualType}, 值: ${tablesInfo.actualValue}`
}
诊断完成,数据库连接${Array.isArray(tables) ? '正常' : '异常'}。`,
},
],
};
} catch (error) {
logError('数据库连接诊断失败: ' + error);
return {
content: [
{
type: 'text',
text: `数据库连接诊断错误:
错误信息: ${error.message}
错误类型: ${error.name}
调用栈: ${error.stack}
请检查:
1. Supabase配置是否正确设置 (SUPABASE_URL, SUPABASE_KEY)
2. Supabase服务是否在线并可访问
3. Supabase数据库是否已配置必要的函数 (get_table_definitions 等)
4. 网络连接是否正常`,
},
],
isError: true,
};
}
},
{
description: toolDescriptions.checkDatabaseConnection.description,
name: toolDescriptions.checkDatabaseConnection.name,
usage_examples: toolDescriptions.checkDatabaseConnection.examples,
returns_example: toolDescriptions.checkDatabaseConnection.returnsExample,
}
);
// 添加环境变量诊断工具
server.tool(
'checkEnvironment',
{},
async () => {
try {
// 安全地获取环境变量信息,不暴露完整密钥
const envInfo = {
NODE_ENV: process.env.NODE_ENV || '未设置',
SUPABASE_URL: process.env.SUPABASE_URL
? `已设置 (${process.env.SUPABASE_URL.substring(0, 15)}...)`
: '未设置',
SUPABASE_KEY: process.env.SUPABASE_KEY
? `已设置 (长度: ${process.env.SUPABASE_KEY.length})`
: '未设置',
MCP_STDIO: process.env.MCP_STDIO || '未设置',
APP_PORT: process.env.APP_PORT || '默认 (9000)',
NODE_VERSION: process.version,
PLATFORM: process.platform,
UPTIME: `${Math.floor(process.uptime())} 秒`,
};
return {
content: [
{
type: 'text',
text: `环境变量诊断结果:
运行环境:
- NODE_VERSION: ${envInfo.NODE_VERSION}
- PLATFORM: ${envInfo.PLATFORM}
- NODE_ENV: ${envInfo.NODE_ENV}
- 运行时间: ${envInfo.UPTIME}
配置参数:
- SUPABASE_URL: ${envInfo.SUPABASE_URL}
- SUPABASE_KEY: ${envInfo.SUPABASE_KEY}
- MCP_STDIO: ${envInfo.MCP_STDIO}
- APP_PORT: ${envInfo.APP_PORT}
如果您看到"未设置"的关键变量,请确保正确配置 .env 文件。`,
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `环境变量诊断错误: ${error.message}`,
},
],
isError: true,
};
}
},
{
description: toolDescriptions.checkEnvironment.description,
name: toolDescriptions.checkEnvironment.name,
usage_examples: toolDescriptions.checkEnvironment.examples,
returns_example: toolDescriptions.checkEnvironment.returnsExample,
}
);
// 执行SQL查询工具
server.tool(
'executeQuery',
{
query: z.string().min(1, 'SQL查询不能为空'),
},
async ({ query }) => {
try {
const result = await supabaseService.executeReadQuery(query);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `查询执行错误: ${error.message}`,
},
],
isError: true,
};
}
},
{
description: toolDescriptions.executeQuery.description,
name: toolDescriptions.executeQuery.name,
usage_examples: toolDescriptions.executeQuery.examples,
returns_example: toolDescriptions.executeQuery.returnsExample,
parameters_description: toolDescriptions.executeQuery.parameters,
}
);
// 获取表样本数据工具
server.tool(
'getTableSample',
{
tableName: z.string().min(1, '表名不能为空'),
limit: z.number().optional().default(10),
},
async ({ tableName, limit }) => {
try {
const result = await supabaseService.getTableSample(tableName, limit);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `获取表样本数据错误: ${error.message}`,
},
],
isError: true,
};
}
},
{
description: toolDescriptions.getTableSample.description,
name: toolDescriptions.getTableSample.name,
usage_examples: toolDescriptions.getTableSample.examples,
returns_example: toolDescriptions.getTableSample.returnsExample,
parameters_description: toolDescriptions.getTableSample.parameters,
}
);
// 获取表结构工具
server.tool(
'getTableStructure',
{
tableName: z.string().min(1, '表名不能为空'),
},
async ({ tableName }) => {
try {
const columns = await supabaseService.getTableColumns(tableName);
return {
content: [
{
type: 'text',
text: JSON.stringify(columns, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `获取表结构错误: ${error.message}`,
},
],
isError: true,
};
}
},
{
description: toolDescriptions.getTableStructure.description,
name: toolDescriptions.getTableStructure.name,
usage_examples: toolDescriptions.getTableStructure.examples,
returns_example: toolDescriptions.getTableStructure.returnsExample,
parameters_description: toolDescriptions.getTableStructure.parameters,
}
);
// 获取数据库统计信息工具
server.tool(
'getDatabaseStats',
{},
async () => {
try {
const query = `
SELECT
table_schema,
table_name,
(xpath('/row/cnt/text()', query_to_xml('select count(*) as cnt from "' || table_schema || '"."' || table_name || '"', true, false, '')))[1]::text::int as row_count
FROM
information_schema.tables
WHERE
table_schema NOT IN ('pg_catalog', 'information_schema')
AND table_type = 'BASE TABLE'
ORDER BY
table_schema, table_name
`;
const result = await supabaseService.executeReadQuery(query);
return {
content: [
{
type: 'text',
text: JSON.stringify(result, null, 2),
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `获取数据库统计信息错误: ${error.message}`,
},
],
isError: true,
};
}
},
{
description: toolDescriptions.getDatabaseStats.description,
name: toolDescriptions.getDatabaseStats.name,
usage_examples: toolDescriptions.getDatabaseStats.examples,
returns_example: toolDescriptions.getDatabaseStats.returnsExample,
}
);
}/**
* 数据库工具使用指南
* 为LLM提供工具使用场景和决策指导
*/
const toolUsageGuide = `
# Inshow AI Query MCP 工具使用指南
## 工具选择决策树
根据用户意图选择最合适的工具或资源:
### 1. 诊断问题和故障排除
如果用户询问为什么查询失败或报错:
- **首选**: \`checkDatabaseConnection\` - 验证数据库连接是否正常
- **其次**: \`checkEnvironment\` - 检查环境配置是否正确
- **然后**: 根据诊断结果提供具体修复建议
### 2. 数据库结构探索
如果用户想了解数据库结构:
- **首选**: \`db://overview\` - 获取所有表的概览和描述
- **然后**: \`db://relationships\` - 了解表之间的关系
- **最后**: \`db://tables/{tableName}\` 或 \`getTableStructure\` - 查看特定表结构
### 3. 数据查询和浏览
如果用户想查看或分析表数据:
- **简单查看**: \`getTableSample\` - 获取表的示例数据
- **结构化数据**: \`getTableStructure\` - 了解表结构
- **复杂查询**: \`executeQuery\` - 执行自定义SQL查询
- **统计信息**: \`getDatabaseStats\` - 获取表行数统计
### 4. 查询路径决策图
查询路径决策流程:
1. 首先检查是否有连接/环境问题?
- 如果有: 使用checkDatabaseConnection或checkEnvironment
- 如果没有: 继续下一步
2. 需要了解数据库整体结构?
- 如果是: 访问db://overview
- 如果否: 继续下一步
3. 需要了解表之间的关系?
- 如果是: 访问db://relationships
- 如果否: 继续下一步
4. 是否只关注特定表?
- 如果否: 返回使用db://overview
- 如果是: 继续下一步
5. 是否同时需要表结构和数据示例?
- 如果是: 访问db://tables/{tableName}
- 如果否: 继续下一步
6. 是否只需要查看表结构?
- 如果是: 使用getTableStructure
- 如果否: 继续下一步
7. 是否只需要表数据示例?
- 如果是: 使用getTableSample
- 如果否: 继续下一步
8. 是否需要复杂查询或数据分析?
- 如果是: 使用executeQuery
## 工具使用场景对比表
| 用户需求 | 最佳工具/资源 | 理由 |
|---------|-------------|------|
| 初次了解数据库 | \`db://overview\` | 提供所有表的概览和描述,最适合初步探索 |
| 理解数据模型 | \`db://relationships\` | 展示表之间的关系和外键约束 |
| 了解特定表结构 | \`getTableStructure\` | 返回详细的列定义和数据类型 |
| 快速查看表数据 | \`getTableSample\` | 返回表的前N行数据,简单直接 |
| 复杂数据查询 | \`executeQuery\` | 支持自定义SQL查询,最灵活 |
| 表大小对比 | \`getDatabaseStats\` | 返回所有表的行数统计 |
| 连接问题排查 | \`checkDatabaseConnection\` | 诊断数据库连接和配置问题 |
| 环境配置检查 | \`checkEnvironment\` | 显示应用环境和配置参数 |
### 特定工具详细对比
#### 1. \`getTableStructure\` vs \`db://tables/{tableName}\`
- **getTableStructure**:
- 返回JSON格式的列定义
- 只包含表结构,不包含数据
- 适合需要程序处理的场景
- 例如: "获取users表的列定义"
- **db://tables/{tableName}**:
- 返回格式化的Markdown文本
- 同时包含表结构和样本数据
- 提供更全面的表信息
- 例如: "我想全面了解products表"
#### 2. \`getTableSample\` vs \`executeQuery\`
- **getTableSample**:
- 简单API,只需表名参数
- 返回前N行数据,无需编写SQL
- 无法过滤或排序数据
- 例如: "显示customers表的样本数据"
- **executeQuery**:
- 需要编写SQL查询
- 支持复杂条件、联接、排序、分组
- 可以自定义返回的列和行
- 例如: "查询近30天的高价值订单"
## SQL查询最佳实践
使用 \`executeQuery\` 工具时的建议:
### 1. 性能与安全
- **限制结果集大小**
- 总是使用LIMIT子句限制返回行数
- 对于大表,考虑先使用COUNT查询了解数据量
- **选择性查询**
- 避免SELECT *,只选择需要的列
- 使用WHERE子句过滤不必要的数据
- **优化联接操作**
- 联接前先评估表大小(getDatabaseStats)
- 小表放在联接右侧,大表放在左侧
- 对多表联接使用适当的JOINs类型
### 2. 常见查询模式模板
**基本聚合查询**:
\`\`\`sql
SELECT
category,
COUNT(*) as count,
AVG(price) as avg_price,
SUM(quantity) as total_quantity
FROM
products
WHERE
created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY
category
ORDER BY
count DESC
LIMIT 10
\`\`\`
**多表联接查询**:
\`\`\`sql
SELECT
u.username,
COUNT(o.id) AS order_count,
SUM(o.total) AS total_spent
FROM
users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE
u.status = 'active'
AND o.created_at >= CURRENT_DATE - INTERVAL '90 days'
GROUP BY
u.id, u.username
ORDER BY
total_spent DESC
LIMIT 15
\`\`\`
**子查询与CTE示例**:
\`\`\`sql
-- 使用CTE提高可读性
WITH recent_orders AS (
SELECT
user_id,
COUNT(*) as order_count,
SUM(total) as total_spent
FROM
orders
WHERE
created_at >= CURRENT_DATE - INTERVAL '30 days'
GROUP BY
user_id
)
SELECT
u.username,
u.email,
COALESCE(ro.order_count, 0) as order_count,
COALESCE(ro.total_spent, 0) as total_spent
FROM
users u
LEFT JOIN recent_orders ro ON u.id = ro.user_id
ORDER BY
total_spent DESC
LIMIT 20
\`\`\`
### 3. 查询错误防范
- **表不存在**: 先使用db://overview确认表名
- **列不存在**: 先使用getTableStructure获取正确列名
- **数据类型不匹配**: 注意日期、数字和文本类型的正确使用
- **NULL值处理**: 使用COALESCE或IS NULL/IS NOT NULL正确处理
### 4. 简单vs复杂工具选择
- **只需查看少量数据时,避免使用executeQuery**
- 不要写: \`executeQuery("SELECT * FROM users LIMIT 10")\`
- 应该用: \`getTableSample({ tableName: "users", limit: 10 })\`
- **只查看表结构时,避免复杂SQL**
- 不要写: \`executeQuery("SELECT column_name, data_type FROM information_schema.columns WHERE table_name='users'")\`
- 应该用: \`getTableStructure({ tableName: "users" })\`
- **多表关系查看时,优先使用资源而非查询**
- 不要写: \`executeQuery("SELECT * FROM information_schema.table_constraints WHERE constraint_type='FOREIGN KEY'")\`
- 应该用: \`访问db://relationships资源\`
`;
const resourceUsageGuide = `
# Inshow AI Query MCP 资源使用指南
## 资源类型与用途
MCP提供四种主要数据库资源类型,每种都有特定用途:
### 1. 数据库概览 (\`db://overview\`)
- **定义**: 数据库中所有表的高级概述
- **内容**: 表名和表描述列表
- **格式**: Markdown文本,适合人类阅读
- **使用时机**:
- 初次了解数据库时
- 需要快速了解可用表时
- 探索未知数据库的第一步
### 2. 表列表 (\`db://tables\`)
- **定义**: 所有表的结构化资源列表
- **内容**: 包含URI和表名的列表
- **格式**: JSON结构,适合程序处理
- **使用时机**:
- 需要以程序方式获取表列表时
- 计划后续访问多个特定表时
- 需要根据表名构建URI时
### 3. 表详情 (\`db://tables/{tableName}\`)
- **定义**: 特定表的详细信息
- **内容**: 列定义和样本数据
- **格式**: 格式化Markdown,包含表格和代码区
- **使用时机**:
- 需要同时了解表结构和数据时
- 希望获得人类可读的表完整信息时
- 需要列定义和数据示例的组合视图时
### 4. 数据库关系图 (\`db://relationships\`)
- **定义**: 表之间的关系和引用
- **内容**: 外键约束和PlantUML格式ER图
- **格式**: Markdown文本与表格
- **使用时机**:
- 需要理解数据模型时
- 准备编写多表联接查询前
- 分析表之间的依赖关系时
## 资源选择决策流程
### 探索未知数据库
1. 获取数据库概览: \`db://overview\`
2. 查看表之间的关系: \`db://relationships\`
3. 深入了解关键表: \`db://tables/{重要表名}\`
### 特定表的深入分析
1. 获取表详情: \`db://tables/{tableName}\`
2. 检查表与其他表的关系: \`db://relationships\` (找到相关表)
3. 获取关联表详情: \`db://tables/{关联表名}\`
### 为复杂查询做准备
1. 了解表结构: \`db://tables/{tableName}\` 或 \`getTableStructure\`
2. 检查表关系: \`db://relationships\`
3. 获取样本数据: \`getTableSample\` 或通过表详情资源
4. 设计并执行查询: \`executeQuery\`
## 资源与工具联合使用模式
最有效的策略是结合使用资源和工具,形成完整的工作流:
1. **探索阶段**
- 使用资源了解数据库结构: \`db://overview\`, \`db://relationships\`
- 检查数据库规模: \`getDatabaseStats\`
2. **分析阶段**
- 查看特定表的详细信息: \`db://tables/{tableName}\`
- 获取更多样本数据: \`getTableSample\`
3. **查询阶段**
- 执行特定查询: \`executeQuery\`
- 分析查询结果,提供见解
每个阶段使用的工具或资源应该基于当前任务的具体需求选择,避免使用过于复杂的工具来完成简单任务。
## 常见错误模式和防范
### 1. 工具过度选择
**错误模式**: 使用过于复杂的工具完成简单任务。
**示例**:
\`\`\`
用户: "显示所有表名称"
错误: 使用executeQuery("SELECT table_name FROM information_schema.tables WHERE table_schema='public'")
正确: 访问db://overview获取表概览
\`\`\`
### 2. 工具不足选择
**错误模式**: 使用能力不足的工具尝试完成复杂任务。
**示例**:
\`\`\`
用户: "查找去年消费超过1000元的VIP客户"
错误: 尝试使用getTableSample然后手动筛选
正确: 使用executeQuery执行JOIN查询和复杂条件筛选
\`\`\`
### 3. 忽略资源直接查询
**错误模式**: 跳过资源直接编写SQL查询,缺少必要上下文。
**示例**:
\`\`\`
用户: "如何查询订单和用户关联数据"
错误: 直接构建JOIN查询但不了解表关系
正确: 先查看db://relationships了解关系,再构建查询
\`\`\`
`;/**
* MCP系统提示配置
* 为LLM提供关于如何正确使用工具和资源的指导
*/
// 完整的系统提示,指导LLM如何使用工具
const toolSelectionSystemPrompt = `
# Inshow AI Query MCP 系统
你是一个专门处理数据库查询和分析的助手,可以访问Supabase数据库并执行SQL查询。
你的主要任务是帮助用户探索数据库、执行查询、分析数据以及解决可能出现的问题。
## 核心工具选择原则
1. **最小复杂度原则**: 总是选择能完成任务的最简单工具。仅当简单工具无法满足需求时,才使用更复杂的工具。
2. **工具专用性原则**: 优先选择为特定任务设计的专用工具,而非通用工具。
3. **渐进探索原则**: 在未知数据库上,先获取概览,再深入具体表,最后执行查询。
## 可用工具
你可以使用以下工具和资源来完成任务:
### 诊断工具
- checkDatabaseConnection - 诊断数据库连接问题
- checkEnvironment - 检查环境配置
### 数据查询工具
- executeQuery - 执行SQL查询 (仅当其他工具无法满足需求时使用)
- getTableSample - 获取表样本数据 (优先用于简单数据查看)
- getTableStructure - 获取表结构 (优先用于结构查询)
- getDatabaseStats - 获取数据库统计信息 (优先用于表大小分析)
### 数据库资源
- db://overview - 数据库概览 (探索的第一步)
- db://tables - 表列表 (用于获取表资源)
- db://tables/{tableName} - 特定表详情 (同时查看结构和数据)
- db://relationships - 数据库关系图 (了解表间关系)
${toolUsageGuide}
${resourceUsageGuide}
## 处理用户请求的方法
1. **意图识别**:
- 首先确定用户是需要探索、查询、分析还是诊断
- 识别用户是否知道具体表名或只是泛泛而谈
2. **工具选择原则**:
- 总是优先使用专用工具,避免过度依赖executeQuery
- 对于结构性探索,使用资源而不是查询
- 对于简单数据查看,使用getTableSample而非executeQuery
- 只有在需要条件过滤、多表联接、聚合计算时才使用executeQuery
3. **渐进式探索路径**:
- 未知数据库: db://overview -> db://relationships -> 特定表详情
- 已知表: getTableStructure -> getTableSample -> 根据需要executeQuery
- 错误情况: checkDatabaseConnection -> checkEnvironment -> 排查具体问题
4. **常见错误防范**:
- 避免对简单查询使用复杂工具
- 避免在不了解表结构的情况下直接编写查询
- 避免跳过资源直接使用工具
- 遇到错误时,优先使用诊断工具而非重试
始终优先选择最简单、最专用的方法来满足用户需求,遵循"最小复杂度原则"。
如果用户请求的操作可以通过多种工具完成,优先选择专为该任务设计的工具。
`;
/**
* 注册系统提示和指南
* @param {Object} server MCP服务器实例
*/
function registerSystemPrompts(server) {
// 为所有会话添加系统提示
server.prompt('system-prompt', {}, () => ({
messages: [
{
role: 'system',
content: {
type: 'text',
text: toolSelectionSystemPrompt,
},
},
],
}));
// 注册特定任务的提示
// 表探索提示
server.prompt('explore-tables', {}, () => ({
messages: [
{
role: 'system',
content: {
type: 'text',
text: `作为数据库助手,请遵循工具选择最佳实践,首先使用资源和工具来探索数据库结构:
1. 获取数据库概览 (db://overview) - 了解所有可用表和它们的用途
2. 获取表之间的关系 (db://relationships) - 理解数据模型和引用关系
3. 获取各表的行数统计 (getDatabaseStats) - 了解数据分布情况
根据这些信息,为用户提供一个结构化的数据库总结,包括:
- 主要实体表及其用途
- 关键关系和数据流程
- 推荐的数据探索途径
记住:避免直接使用executeQuery来获取schema信息,而应使用专门的资源和工具。`,
},
},
],
}));
// SQL建议提示
server.prompt('sql-assistant', {}, () => ({
messages: [
{
role: 'system',
content: {
type: 'text',
text: `作为SQL助手,请先了解数据库结构再构建查询:
1. 数据准备工作:
- 使用getTableStructure获取相关表的列定义
- 使用db://relationships了解表之间的关系
- 使用getDatabaseStats评估表大小,优化查询设计
2. 遵循查询构建最佳实践:
- 总是使用LIMIT限制结果数量
- 只选择必要的列,避免SELECT *
- 对大表使用适当的WHERE条件
- 小表放在JOIN右侧,大表放在左侧
- 使用CTE提高复杂查询的可读性
3. 查询验证:
- 确认表名和列名是正确的
- 验证数据类型匹配,特别是日期和数字类型
- 检查是否处理了NULL值
记住:只有在需要复杂条件、多表联接或聚合计算时才使用executeQuery,对于简单的数据获取优先使用getTableSample。`,
},
},
],
}));
// 错误排查提示
server.prompt('troubleshoot-database', {}, () => ({
messages: [
{
role: 'system',
content: {
type: 'text',
text: `作为数据库故障排除专家,请系统地帮助用户解决问题:
1. 诊断阶段:
- 首先使用checkDatabaseConnection验证数据库连接状态
- 使用checkEnvironment检查环境配置
- 根据错误消息确定问题类别(连接、权限、查询语法、功能)
2. 分析阶段:
- 对于结构问题,使用db://overview和db://relationships验证表和关系
- 对于数据问题,使用getTableSample获取示例数据
- 对于查询问题,通过getTableStructure确认正确的列名和类型
3. 解决阶段:
- 提供清晰的故障原因解释
- 给出具体的修复步骤
- 提供防止问题再次发生的建议
记住:采用渐进式排查,从基础设施开始,再到数据结构,最后到查询细节。避免直接重试失败的操作,优先理解并解决根本问题。`,
},
},
],
}));
// 数据分析提示
server.prompt('data-analysis', {}, () => ({
messages: [
{
role: 'system',
content: {
type: 'text',
text: `作为数据分析助手,请遵循结构化的分析流程:
1. 准备阶段:
- 使用getDatabaseStats了解表大小和数据分布
- 使用db://relationships理解数据模型和实体关系
- 使用getTableStructure确认分析所需的列和数据类型
2. 分析阶段:
- 对于简单分析,优先使用专用工具而非复杂查询
- 对于复杂分析,使用executeQuery执行聚合和统计
- 总是添加LIMIT子句,避免返回过大结果集
- 优先使用CTE或子查询提高复杂分析的可读性
3. 解释阶段:
- 提供清晰的分析结果解释
- 突出关键发现和见解
- 提供可能的进一步分析建议
记住:有效的数据分析依赖于对数据结构的正确理解,始终先了解数据模型再进行分析。复杂分析应分解为多个步骤,而不是一个巨大的查询。`,
},
},
],
}));
}/**
* 注册数据分析相关提示
* @param {Object} server MCP服务器实例
*/
function registerDataAnalysisPrompts(server) {
// 首先注册系统提示和指南
registerSystemPrompts(server);
// 数据探索提示
server.prompt(
'data-exploration',
{
tableName: z.string().min(1, '表名不能为空'),
},
({ tableName }) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `请帮我探索 ${tableName} 表的数据。我想了解这个表的基本结构、列的数据类型以及可能存在的数据模式或趋势。请使用适当的SQL查询来分析数据,并以易于理解的方式呈现结果。`,
},
},
],
}),
{
description: '探索指定表的数据结构和内容,生成分析报告',
arguments_description: {
tableName: '要探索的表名(必需)',
},
usage_examples: ['探索用户表的基本结构和数据模式', '分析订单表中可能存在的趋势'],
}
);
// 数据分析提示
server.prompt(
'data-analysis-prompt',
{
query: z.string().min(1, '查询不能为空'),
},
({ query }) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `请帮我分析以下SQL查询的结果:\n\n\`\`\`sql\n${query}\n\`\`\`\n\n执行这个查询,并提供详细的数据分析,包括:\n1. 结果的总体概述\n2. 发现的关键趋势或模式\n3. 数据分布情况\n4. 异常值或有趣的观察\n5. 可能的业务洞见\n\n如果可能,请考虑使用统计方法来支持你的分析。`,
},
},
],
}),
{
description: '分析SQL查询结果,提供数据洞察和统计分析',
arguments_description: {
query: '要执行和分析的SQL查询(必需)',
},
usage_examples: [
'分析销售数据的查询结果,查找销售趋势',
'分析用户行为查询结果,发现用户模式',
],
}
);
// 数据可视化建议提示
server.prompt(
'visualization-suggestion',
{
query: z.string().min(1, '查询不能为空'),
},
({ query }) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `我执行了以下SQL查询:\n\n\`\`\`sql\n${query}\n\`\`\`\n\n请帮我确定最适合可视化这些数据的图表类型。请考虑以下因素:\n1. 查询返回的数据类型(分类、连续、时间序列等)\n2. 变量之间的关系\n3. 想要强调的见解类型\n\n对于你推荐的每种图表类型,请解释为什么它适合这些数据,以及如何构建这种可视化以最好地传达数据中的信息。`,
},
},
],
}),
{
description: '为SQL查询结果推荐最佳可视化图表类型',
arguments_description: {
query: '要可视化结果的SQL查询(必需)',
},
usage_examples: ['为销售报表数据推荐合适的图表类型', '针对时间序列数据建议最佳可视化方式'],
}
);
// SQL查询生成提示
server.prompt(
'generate-sql',
{
description: z.string().min(1, '描述不能为空'),
},
({ description }) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `请根据以下描述生成SQL查询:\n\n${description}\n\n请首先分析我的需求,然后生成符合PostgreSQL语法的SQL查询。确保查询是高效的,并包含适当的注释来解释复杂的部分。如果有多种方法可以实现,请提供最优的解决方案并解释你的选择理由。`,
},
},
],
}),
{
description: '根据自然语言描述生成SQL查询',
arguments_description: {
description: '查询需求的自然语言描述(必需)',
},
usage_examples: ['生成查询过去30天内最活跃用户的SQL', '创建按类别统计产品销量的查询'],
}
);
// 数据质量评估提示
server.prompt(
'data-quality-assessment',
{
tableName: z.string().min(1, '表名不能为空'),
},
({ tableName }) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `请帮我评估 ${tableName} 表的数据质量。我需要了解:\n\n1. 完整性:是否存在缺失值?各列的缺失率如何?\n2. 准确性:数据是否在合理范围内?是否存在异常值?\n3. 一致性:数据格式是否一致?是否存在重复记录?\n4. 及时性:数据是否最新?\n\n请使用适当的SQL查询来检查这些问题,并提供改进数据质量的建议。`,
},
},
],
}),
{
description: '评估表数据质量,包括完整性、准确性、一致性和及时性',
arguments_description: {
tableName: '要评估质量的表名(必需)',
},
usage_examples: ['检查客户表的数据质量问题', '评估产品数据的完整性和一致性'],
}
);
// 数据关系探索提示
server.prompt(
'relationship-exploration',
{
tableA: z.string().min(1, '第一个表名不能为空'),
tableB: z.string().min(1, '第二个表名不能为空'),
},
({ tableA, tableB }) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `请帮我探索 ${tableA} 和 ${tableB} 这两个表之间的关系。我想了解:\n\n1. 这两个表是如何关联的(通过哪些字段)\n2. 它们之间的基数关系(一对一、一对多、多对多)\n3. 两个表中数据的相关模式或趋势\n\n请使用适当的SQL查询(如JOIN操作)来分析这些关系,并以清晰的方式解释发现。如果可能,请提供优化这些关系的建议。`,
},
},
],
}),
{
description: '探索两个表之间的关系和数据模式',
arguments_description: {
tableA: '第一个表名(必需)',
tableB: '第二个表名(必需)',
},
usage_examples: ['分析用户表和订单表之间的关系', '探索产品表和类别表之间的数据关联'],
}
);
// 表格数据摘要提示
server.prompt(
'table-summary',
{
tableName: z.string().min(1, '表名不能为空'),
limit: z.number().optional().default(1000),
},
({ tableName, limit }) => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `请为 ${tableName} 表提供一个简洁的数据摘要。分析表中最多 ${limit} 行数据,包括:
1. 数值列的基本统计信息(最小值、最大值、平均值、中位数等)
2. 分类列的值分布和频率
3. 日期列的时间范围和分布
4. 识别的主要数据特征和模式
使用适当的SQL查询来生成这些统计信息,并以清晰、结构化的方式呈现结果。`,
},
},
],
}),
{
description: '生成表数据的统计摘要和关键特征',
arguments_description: {
tableName: '要分析的表名(必需)',
limit: '分析的最大行数(可选,默认1000)',
},
usage_examples: ['生成客户表的数据统计摘要', '分析销售表的前500行数据特征'],
}
);
// 数据库健康检查提示
server.prompt(
'database-health-check',
{},
() => ({
messages: [
{
role: 'user',
content: {
type: 'text',
text: `请对数据库进行全面的健康检查,分析以下方面:
1. 表大小和行数统计
2. 查询性能指标
3. 数据完整性检查
4. 可能的优化机会
使用适当的诊断工具和查询收集这些信息,并提供一份简明的报告,包括任何发现的问题和改进建议。`,
},
},
],
}),
{
description: '执行数据库健康检查,评估性能和完整性',
usage_examples: ['进行数据库全面健康检查', '检查数据库性能和完整性'],
}
);
}/**
* 启动基于stdio的MCP服务器
* @param {Object} server - MCP服务器实例
* @param {Object} options - 配置选项
* @returns {Promise<StdioServerTransport>} 已连接的STDIO传输实例
*/
async function startStdioServer(server, options = {}) {
logInfo('MCP服务器运行于STDIO模式');
// 创建并连接STDIO传输
const stdioTransport = new StdioServerTransport();
try {
// 连接到MCP服务器
await server.connect(stdioTransport);
logInfo('STDIO传输已连接');
return stdioTransport;
} catch (error) {
logError(`STDIO传输连接失败: ${error.message}`);
throw error;
}
}/**
* 启动基于Streamable HTTP的MCP服务器
* 遵循MCP协议的Streamable HTTP规范,支持会话恢复、无状态服务和流式响应
*
* @param {Object} server - MCP服务器实例
* @param {Object} supabaseService - Supabase服务实例
* @param {Object} options - 配置选项
* @returns {Object} Express应用实例
*/
function startHttpServer(server, supabaseService, options = {}) {
const app = express();
app.use(express.json({ limit: '10mb' }));
// 启用CORS支持以便跨域调用
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', options.corsOrigin || '*');
res.header('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept, mcp-session-id'
);
res.header('Access-Control-Expose-Headers', 'mcp-session-id');
if (req.method === 'OPTIONS') {
return res.status(200).end();
}
next();
});
// 会话管理 - 存储传输实例
const transports = {};
// 会话保活与清理 - 自动清理长时间未活动的会话
const SESSION_TIMEOUT = options.sessionTimeout || 30 * 60 * 1000; // 默认30分钟
if (!options.disableSessionCleanup) {
setInterval(() => {
const now = Date.now();
Object.entries(transports).forEach(([sessionId, transport]) => {
if (transport.lastActivity && now - transport.lastActivity > SESSION_TIMEOUT) {
logInfo(`会话 ${sessionId} 超时,正在清理`);
delete transports[sessionId];
if (typeof transport.close === 'function') {
transport.close();
}
}
});
}, 60000); // 每分钟检查一次
}
// 错误处理中间件
app.use((err, req, res, next) => {
logError(`Express错误: ${err.message}`);
console.error(err.stack);
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: `服务器内部错误: ${err.message}`,
},
id: null,
});
});
// 统一的MCP端点,处理初始化和后续请求
app.post('/mcp', async (req, res) => {
// 检查现有会话ID
const sessionId = req.headers['mcp-session-id'];
let transport;
if (sessionId && transports[sessionId]) {
// 复用现有传输
transport = transports[sessionId];
transport.lastActivity = Date.now(); // 更新会话活动时间
} else if (!sessionId && isInitializeRequest(req.body)) {
// 创建新的初始化请求 (无状态或有状态都支持)
transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: newSessionId => {
// 按会话ID存储传输
transports[newSessionId] = transport;
transport.lastActivity = Date.now();
// 记录会话创建信息
if (options.debug) {
logInfo(`新会话已创建: ${newSessionId}`);
}
},
});
// 传输关闭时清理
transport.onclose = () => {
if (transport.sessionId) {
if (options.debug) {
logInfo(`会话已关闭: ${transport.sessionId}`);
}
delete transports[transport.sessionId];
}
};
// 连接到MCP服务器
await server.connect(transport);
} else {
// 无效请求
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Bad Request: No valid session ID provided',
},
id: null,
});
return;
}
// 处理请求 - 对于响应可以是普通HTTP或SSE流
await transport.handleRequest(req, res, req.body);
});
// 处理GET请求,用于建立SSE流或获取服务端通知
app.get('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'];
// 特殊情况:没有会话ID但允许创建的场景
if (!sessionId && options.allowAnonymousSSE) {
// 创建匿名会话并建立SSE
const transport = new StreamableHTTPServerTransport({
sessionIdGenerator: () => randomUUID(),
onsessioninitialized: newSessionId => {
transports[newSessionId] = transport;
transport.lastActivity = Date.now();
if (options.debug) {
logInfo(`匿名SSE会话已创建: ${newSessionId}`);
}
},
});
transport.onclose = () => {
if (transport.sessionId) {
delete transports[transport.sessionId];
}
};
await server.connect(transport);
await transport.handleRequest(req, res);
return;
}
// 常规情况:需要有效会话ID
if (!sessionId || !transports[sessionId]) {
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Invalid or missing session ID',
},
id: null,
});
return;
}
const transport = transports[sessionId];
transport.lastActivity = Date.now(); // 更新会话活动时间
// 处理SSE流请求
await transport.handleRequest(req, res);
});
// 处理DELETE请求,用于会话终止
app.delete('/mcp', async (req, res) => {
const sessionId = req.headers['mcp-session-id'];
if (!sessionId || !transports[sessionId]) {
res.status(400).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: 'Invalid or missing session ID',
},
id: null,
});
return;
}
const transport = transports[sessionId];
// 优雅关闭会话
try {
if (typeof transport.close === 'function') {
await transport.close();
}
delete transports[sessionId];
res.status(200).json({ success: true, message: 'Session terminated' });
if (options.debug) {
logInfo(`会话已主动终止: ${sessionId}`);
}
} catch (error) {
logError(`终止会话 ${sessionId} 时发生错误: ${error.message}`);
res.status(500).json({
jsonrpc: '2.0',
error: {
code: -32000,
message: `Failed to terminate session: ${error.message}`,
},
id: null,
});
}
});
// 健康检查端点
app.get('/health', async (req, res) => {
try {
// 尝试获取表定义以验证数据库连接
const tables = await supabaseService.getTableDefinitions();
res.json({
status: 'ok',
uptime: process.uptime(),
timestamp: new Date().toISOString(),
version: '1.0.0',
protocol: 'Streamable HTTP',
activeSessions: Object.keys(transports).length,
database: {
connected: true,
tables: tables.length,
},
});
} catch (error) {
logError(`健康检查失败: ${error.message}`);
res.status(500).json({
status: 'error',
uptime: process.uptime(),
timestamp: new Date().toISOString(),
error: error.message,
database: {
connected: false,
},
});
}
});
// 启动服务器
const APP_PORT = options.port || 9000;
const appServer = app.listen(APP_PORT, () => {
logInfo(`MCP Streamable HTTP 服务器已启动,端口: ${APP_PORT}`);
if (options.debug) {
logInfo(`服务地址: http://localhost:${APP_PORT}/mcp`);
logInfo(`健康检查: http://localhost:${APP_PORT}/health`);
}
});
// 优雅关闭
process.on('SIGTERM', () => {
logInfo('收到SIGTERM信号,正在优雅关闭服务...');
appServer.close(() => {
logInfo('服务器已关闭');
process.exit(0);
});
});
return app;
}// 设置命令行参数
const program = new Command();
program
.name('inshow-ai-query-mcp')
.description('Inshow AI Query MCP Service')
.version('1.0.0')
.option('-p, --port <number>', '服务端口号', '9000')
.option('-m, --mode <string>', '运行模式 (stdio|http)', 'stdio')
.option('-d, --debug', '启用调试模式', false)
.option('-e, --env <path>', '环境变量文件路径', '.env')
.option('--supabase-url <url>', 'Supabase项目URL')
.option('--supabase-key <key>', 'Supabase项目API密钥')
.option('--supabase-config <path>', 'Supabase配置文件路径(JSON格式)')
.option('--allow-anonymous', '允许匿名SSE连接(仅HTTP模式)', false)
.option('--session-timeout <minutes>', 'HTTP会话超时时间(分钟,默认30分钟)', '30')
.option('--disable-session-cleanup', '禁用HTTP会话自动清理', false)
.option('--cors-origin <origin>', '允许的CORS来源', '*');
program.parse(process.argv);
const options = program.opts();
logInfo(options);
// 加载环境变量
dotenv.config({ path: options.env });
dotenv.config({ path: '.env.local' });
// 如果提供了Supabase配置文件,加载配置
if (options.supabaseConfig) {
try {
const configPath = options.supabaseConfig;
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
process.env.SUPABASE_URL = config.url;
process.env.SUPABASE_KEY = config.key;
logInfo(`已从配置文件加载Supabase配置: ${configPath}`);
} catch (error) {
logError(`加载Supabase配置文件失败: ${error.message}`);
process.exit(1);
}
}
// 命令行参数优先级高于配置文件和环境变量
if (options.supabaseUrl) {
process.env.SUPABASE_URL = options.supabaseUrl;
}
if (options.supabaseKey) {
process.env.SUPABASE_KEY = options.supabaseKey;
}
// 验证必需的环境变量
function validateEnvVariables() {
const requiredVars = ['SUPABASE_KEY'];
const missing = requiredVars.filter(varName => !process.env[varName]);
if (missing.length > 0) {
logError('缺少必需的环境变量: ' + missing.join(', '));
logError('请通过以下方式提供Supabase配置:');
logError('1. 环境变量文件 (.env)');
logError('2. 命令行参数 (--supabase-key)');
logError('3. 配置文件 (--supabase-config)');
process.exit(1);
}
if (options.debug) {
logInfo('Supabase配置验证通过');
logInfo(`Supabase URL: ${process.env.SUPABASE_URL}`);
logInfo(`Supabase Key: ${process.env.SUPABASE_KEY.substring(0, 8)}...`);
}
}
// 在启动前验证环境变量
validateEnvVariables();
// 检测运行模式
const isStdioMode = options.mode === 'stdio';
if (options.debug) {
logInfo('调试模式已启用');
logInfo(`运行配置: ${JSON.stringify(options, null, 2)}`);
}
// 创建MCP服务器
const server = new McpServer({
name: 'Inshow AI Query',
version: '1.0.0',
});
// 添加全局错误处理
server.onError = (error, request) => {
if (request) {
logError(`处理请求 ${request.method} 时发生错误: ${error.message}`);
logError(`请求详情: ${JSON.stringify(request, null, 2)}`);
} else {
logError(`MCP服务器错误: ${error.message}`);
}
logError(`错误调用栈: ${error.stack}`);
// 返回带有详细信息的错误
return {
code: -32603, // 内部错误
message: `MCP 错误: ${error.message}`,
data: {
stack: error.stack,
},
};
};
// 加载MCP调试规范
try {
const currentDir = process.cwd();
const rulesPath = path.join(currentDir, 'docs', 'mcp-rules.md');
if (fs.existsSync(rulesPath)) {
const rulesContent = fs.readFileSync(rulesPath, 'utf8');
server.rule('mcp-rules', 'MCP应用的调试规范', rulesContent);
logInfo('已加载MCP应用调试规范');
} else {
logWarn('未找到MCP应用调试规范文件: ' + rulesPath);
}
} catch (error) {
logError('加载MCP应用调试规范失败: ' + error.message);
}
// 初始化Supabase服务
const supabaseService = setupSupabaseService();
// 注册资源、工具和提示
logInfo('正在注册数据库资源...');
registerDatabaseResources(server, supabaseService);
logInfo('正在注册数据库查询工具...');
registerQueryTools(server, supabaseService);
logInfo('正在注册数据分析提示...');
registerDataAnalysisPrompts(server);
// 根据运行模式启动相应的服务
if (isStdioMode) {
// 启动STDIO模式服务器
startStdioServer(server, { debug: options.debug });
} else {
// 配置HTTP服务器选项
const httpOptions = {
port: options.port,
debug: options.debug,
allowAnonymousSSE: options.allowAnonymous,
disableSessionCleanup: options.disableSessionCleanup,
sessionTimeout: parseInt(options.sessionTimeout) * 60 * 1000, // 转换为毫秒
corsOrigin: options.corsOrigin,
};
// 输出HTTP服务器配置信息
if (options.debug) {
logInfo(`HTTP服务器配置: ${JSON.stringify(httpOptions, null, 2)}`);
logInfo('正在启动Streamable HTTP服务器...');
}
// 启动HTTP服务器模式
startHttpServer(server, supabaseService, httpOptions);
}//# sourceMappingURL=index.js.map