inshow-ai-content-mcp
Version:
MCP服务器,用于连接Supabase数据库并提供表结构查询功能
2,404 lines • 83.2 kB
JavaScript
#!/usr/bin/env node
import {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;
}
},
};
}/**
* 应用配置文件
* 集中管理应用的配置项
*/
// 数据库配置
const dbConfig = {
// 默认操作的表名
defaultTableName: 'content_items',
// 限制查询数量
limit: 30,
};/**
* 数据库资源描述文件
* 为MCP资源提供清晰的描述,帮助LLM更准确地理解和使用资源
*/
// 资源描述对象
const resourceDescriptions = {
// 表详情资源
'table-details': {
name: '表详情',
description: `提供${dbConfig.defaultTableName}表的详细信息,包括列定义(名称、类型、可空性、描述)和样本数据。当需要深入了解表结构和数据样例时使用。`,
uri_pattern: `db://tables/${dbConfig.defaultTableName}`,
when_to_use: `当需要同时了解${dbConfig.defaultTableName}表的结构和数据内容时使用,这是获取表完整信息的最全面方法。特别适用于初次了解表时、需要同时查看结构和数据示例时、准备编写查询前全面了解表时。`,
when_not_to_use:
'如果只需要表结构,应使用getTableStructure工具;如果只需要数据样本,应使用getTableSample工具;如果需要特定查询结果,应使用executeQuery工具。',
examples: [
`获取${dbConfig.defaultTableName}表的详细信息`,
`显示${dbConfig.defaultTableName}表的结构和数据`,
`查看${dbConfig.defaultTableName}表的列定义`,
`我想了解${dbConfig.defaultTableName}表的完整信息`,
],
},
};/**
* 注册数据库相关资源
* @param {Object} server MCP服务器实例
* @param {Object} supabaseService Supabase服务实例
*/
function registerDatabaseResources(server, supabaseService) {
// 表详情资源 - 使用配置中的表名
server.resource(
'table-details',
`db://tables/${dbConfig.defaultTableName}`,
async uri => {
try {
const tableName = dbConfig.defaultTableName;
logInfo(`获取 ${tableName} 表详情`);
// 获取表的列信息
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(`获取表 ${dbConfig.defaultTableName} 详情失败: ` + error);
return {
contents: [
{
uri: uri.href,
text: `获取表 ${dbConfig.defaultTableName} 详情失败: ${error.message}`,
},
],
};
}
},
{
description: resourceDescriptions['table-details'].description,
name: resourceDescriptions['table-details'].name,
usage_examples: resourceDescriptions['table-details'].examples,
}
);
}/**
* 数据库工具描述文件
* 为MCP工具提供清晰的描述,帮助LLM更准确地选择工具
*/
// 工具描述对象
const toolDescriptions = {
// 数据库连接检查工具
checkDatabaseConnection: {
name: '检查数据库连接',
description: `检测Supabase数据库连接是否正常工作。当遇到数据库查询错误或需要诊断连接问题时使用此工具。该工具会验证环境配置并测试对${dbConfig.defaultTableName}表的基本连接功能。`,
when_to_use:
"仅在怀疑数据库连接有问题时使用,例如查询失败、无法获取表数据或返回错误。作为诊断工具,不应用于正常的数据查询操作。当用户提到'连接错误'、'无法访问数据库'、'查询超时'等问题时,应首先使用此工具。",
when_not_to_use:
'不要用于获取数据库结构或表数据,这些操作应使用专门的资源或查询工具。当数据库正常工作时,避免使用此工具作为常规探索数据库的方法。此工具不返回业务数据,仅返回连接诊断结果。',
examples: [
'数据库连接似乎有问题',
'我无法获取任何表的数据',
'数据库查询返回连接错误',
'为什么我的查询总是超时或失败',
],
returnsExample: `{
"配置状态": {
"SUPABASE_URL": "已设置",
"SUPABASE_KEY": "已设置"
},
"表[${dbConfig.defaultTableName}]结构查询": {
"成功获取": "是",
"列数量": 10,
"部分列名": "id, title, url, content, status"
}
}`,
},
// 环境检查工具
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 * FROM 表名 LIMIT 10"。主要用于查询${dbConfig.defaultTableName}表,但也支持其他表查询。`,
when_to_use: `需要执行针对${dbConfig.defaultTableName}表的查询,或需要特定条件过滤、分组、排序等操作时使用。当需要获取实际数据内容而不只是统计数量时使用此工具。适用于探索数据格式、查看特定条件的数据、获取详细数据记录等场景。`,
when_not_to_use:
'如果只需查看表结构,应使用getTableStructure工具。如果只需查询结果数量,应使用getQueryResultCount工具。此工具仅支持只读查询,不支持数据修改操作。',
examples: [
`SELECT * FROM ${dbConfig.defaultTableName} WHERE status = 'active' LIMIT 5`,
`SELECT id, title, url FROM ${dbConfig.defaultTableName} WHERE created_at > '2023-01-01'`,
`SELECT COUNT(*), status FROM ${dbConfig.defaultTableName} GROUP BY status`,
`${dbConfig.defaultTableName}`,
],
parameters: {
query: 'SQL查询语句(必须是只读查询)或表名',
},
returnsExample: `[
{ "id": 1, "title": "示例文章", "url": "https://example.com/1", "status": "active" },
{ "id": 2, "title": "另一个示例", "url": "https://example.com/2", "status": "pending" }
]`,
},
// 获取表样本数据工具
getTableSample: {
name: '获取表样本数据',
description: `获取指定表的样本数据。当需要了解表中的实际数据内容、格式或示例时使用此工具。返回表中的前N行记录(默认10行)。默认获取${dbConfig.defaultTableName}表的样本数据,但也可以指定其他表。`,
when_to_use:
'需要快速查看表中数据示例、了解数据格式或验证表是否有数据时使用。这是获取表数据最简单的方法。适用于初步探索表数据、了解实际数据格式、查看数据示例等场景。',
when_not_to_use:
'不适合复杂条件过滤、聚合计算或多表查询,这些情况应使用executeQuery。也不适合只想了解表结构而不关心数据内容的场景。当需要获取大量数据或需要特定条件筛选时,应使用executeQuery工具。',
examples: [
`获取${dbConfig.defaultTableName}表的样本数据`,
`我想看看${dbConfig.defaultTableName}表中有什么数据`,
`显示${dbConfig.defaultTableName}表的前5行数据`,
`查看${dbConfig.defaultTableName}表的数据格式和内容示例`,
],
parameters: {
tableName: `要查询的表名(可选,默认为${dbConfig.defaultTableName})`,
limit: '要返回的行数(可选,默认10)',
},
returnsExample: `[
{ "id": 1, "title": "示例文章", "url": "https://example.com/1", "status": "active" },
{ "id": 2, "title": "另一个示例", "url": "https://example.com/2", "status": "pending" }
]`,
},
// 获取表结构工具
getTableStructure: {
name: '获取表结构',
description: `获取指定表的列定义和结构信息。当需要了解表的模式、字段类型或列属性时使用此工具。默认返回${dbConfig.defaultTableName}表的结构,但也可以指定其他表。该工具返回表中所有列的详细信息,包括列名、数据类型、是否可为空及描述。`,
when_to_use:
'需要详细了解表结构、列定义、数据类型或准备执行查询前检查列名时使用。特别适用于查看表的完整列定义、了解列的数据类型和约束、准备编写SQL查询前确认列名等场景。',
when_not_to_use:
'不适合查看表数据内容或执行查询,这些操作应分别使用getTableSample或executeQuery。如果已经熟悉表结构,或只需查看数据内容,应使用其他工具。',
examples: [
`${dbConfig.defaultTableName}表有哪些列`,
`获取${dbConfig.defaultTableName}表的结构`,
`显示${dbConfig.defaultTableName}表的字段定义`,
`${dbConfig.defaultTableName}表的主键和数据类型是什么`,
],
parameters: {
tableName: `要查询结构的表名(可选,默认为${dbConfig.defaultTableName})`,
},
returnsExample: `[
{ "column_name": "id", "data_type": "integer", "is_nullable": false, "description": "主键" },
{ "column_name": "title", "data_type": "character varying", "is_nullable": false, "description": "文章标题" }
]`,
},
// 获取查询结果数量工具
getQueryResultCount: {
name: '获取查询结果数量',
description:
'获取SQL查询结果的记录数量。可以直接提供表名或完整SQL查询,工具会自动转换为计数查询。当只需了解结果数量而不需要实际数据内容时使用此工具。',
when_to_use:
'需要快速了解查询结果的记录数量、检查数据量、验证筛选条件是否有效,或进行简单统计分析时使用此工具。特别适用于大数据量情况下,只需知道结果数量而不需获取全部数据的场景。',
when_not_to_use:
'当需要查看实际数据内容、需要详细记录信息,或执行复杂聚合分析时,应使用executeQuery工具。此工具只返回记录数量,不返回实际数据。',
examples: [
`${dbConfig.defaultTableName}`,
`SELECT * FROM ${dbConfig.defaultTableName} WHERE status = 'active'`,
`SELECT id, title FROM ${dbConfig.defaultTableName} WHERE created_at > '2023-01-01'`,
'查询有多少条记录符合条件',
],
parameters: {
query: 'SQL查询语句或表名',
},
returnsExample: `{
"查询结果数量": 42,
"计数查询": "SELECT COUNT(*) as total_count FROM ${dbConfig.defaultTableName}"
}`,
},
};/**
* 注册数据库查询相关工具
* @param {Object} server MCP服务器实例
* @param {Object} supabaseService Supabase服务实例
*/
function registerQueryTools(server, supabaseService) {
// 添加环境变量诊断工具
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,
}
);
// 添加数据库连接诊断工具
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 tableName = dbConfig.defaultTableName;
const columns = await supabaseService.getTableColumns(tableName);
const columnsInfo = Array.isArray(columns)
? {
count: columns.length,
names: columns.map(c => c.column_name || '未知列名').slice(0, 5),
}
: {
count: 0,
error: '返回的列定义不是数组',
actualType: typeof columns,
actualValue: JSON.stringify(columns).substring(0, 100),
};
return {
content: [
{
type: 'text',
text: `数据库连接诊断结果:
1. 配置状态:
- SUPABASE_URL 配置: ${configStatus.hasUrl ? '已设置' : '未设置'} (${configStatus.urlStart})
- SUPABASE_KEY 配置: ${configStatus.hasKey ? '已设置' : '未设置'} (长度: ${configStatus.keyLength})
2. 表[${tableName}]结构查询:
- 成功获取: ${Array.isArray(columns) ? '是' : '否'}
- 列数量: ${columnsInfo.count}
- ${
Array.isArray(columns)
? `部分列名: ${columnsInfo.names.join(', ')}...`
: `错误信息: ${columnsInfo.error}, 实际类型: ${columnsInfo.actualType}, 值: ${columnsInfo.actualValue}`
}
诊断完成,数据库连接${Array.isArray(columns) ? '正常' : '异常'}。`,
},
],
};
} 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数据库是否已配置必要的函数
4. 网络连接是否正常`,
},
],
isError: true,
};
}
},
{
description: toolDescriptions.checkDatabaseConnection.description,
name: toolDescriptions.checkDatabaseConnection.name,
usage_examples: toolDescriptions.checkDatabaseConnection.examples,
returns_example: toolDescriptions.checkDatabaseConnection.returnsExample,
}
);
// 执行SQL查询工具
server.tool(
'executeQuery',
{
query: z.string().min(1, 'SQL查询不能为空'),
},
async ({ query }) => {
try {
// 自动处理仅包含表名的情况,转换为完整SQL
if (!query.includes('SELECT') && !query.includes('select')) {
// 假设输入只是表名的情况,转换为默认查询
if (!query.includes(' ') && !query.includes(';')) {
query = `SELECT * FROM ${query} LIMIT 10`;
}
}
// 确保查询是针对指定表的
let targetTable = dbConfig.defaultTableName;
if (!query.toLowerCase().includes(targetTable.toLowerCase())) {
// 记录警告但仍执行查询
logInfo(`警告: 查询未包含目标表 [${targetTable}]`);
}
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(
'getTableStructure',
{
tableName: z.string().min(1, '表名不能为空').default(dbConfig.defaultTableName),
},
async ({ tableName }) => {
try {
// 如果未提供表名,使用默认表名
if (!tableName) {
tableName = dbConfig.defaultTableName;
logInfo(`使用默认表名: ${tableName}`);
}
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(
'getTableSample',
{
tableName: z.string().min(1, '表名不能为空').default(dbConfig.defaultTableName),
limit: z.number().optional().default(10),
},
async ({ tableName, limit }) => {
try {
// 如果未提供表名,使用默认表名
if (!tableName) {
tableName = dbConfig.defaultTableName;
logInfo(`使用默认表名: ${tableName}`);
}
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,
}
);
// 新增:获取SQL查询结果长度
server.tool(
'getQueryResultCount',
{
query: z.string().min(1, 'SQL查询不能为空'),
},
async ({ query }) => {
try {
// 规范化SQL查询字符串,替换所有换行符和多余的空白为单个空格
const normalizedQuery = query.replace(/\s+/g, ' ').trim();
// 处理JSONB和数组操作符问题
// 修复常见的数组操作符语法问题: ARRAY[] -> '[]'::jsonb 或适当的数组类型转换
let preprocessedQuery = normalizedQuery;
// 检测并修复 @> ARRAY['值'] 模式
const arrayPattern = /@>\s*ARRAY\s*\[\s*['"]([^'"]+)['"]\s*\]/gi;
preprocessedQuery = preprocessedQuery.replace(arrayPattern, (match, value) => {
// 替换为JSONB兼容语法
logInfo(`修复数组操作符: ${match} -> @> '["${value}"]'::jsonb`);
return `@> '["${value}"]'::jsonb`;
});
// 自动转换为COUNT查询
let countQuery = preprocessedQuery;
// 如果只输入了表名,转换为默认计数查询
if (!preprocessedQuery.toLowerCase().includes('select')) {
if (!preprocessedQuery.includes(' ') && !preprocessedQuery.includes(';')) {
countQuery = `SELECT COUNT(*) as total_count FROM ${preprocessedQuery}`;
}
}
// 如果是SELECT查询但不是COUNT查询,转换为COUNT查询
else if (!preprocessedQuery.toLowerCase().includes('count(*)')) {
try {
// 判断是否为有效的SELECT查询
const isSelectQuery = /^\s*SELECT\s+.+\s+FROM\s+/i.test(preprocessedQuery);
if (!isSelectQuery) {
throw new Error('无法识别为有效的SELECT查询');
}
// 检查是否包含LIMIT, ORDER BY, GROUP BY 等子句
const hasLimit = /\s+LIMIT\s+\d+/i.test(preprocessedQuery);
const hasOrderBy = /\s+ORDER\s+BY\s+/i.test(preprocessedQuery);
const hasGroupBy = /\s+GROUP\s+BY\s+/i.test(preprocessedQuery);
const hasHaving = /\s+HAVING\s+/i.test(preprocessedQuery);
// 如果包含GROUP BY或HAVING,使用子查询方式
if (hasGroupBy || hasHaving) {
countQuery = `SELECT COUNT(*) as total_count FROM (${preprocessedQuery}) as subquery`;
logInfo(`检测到GROUP BY或HAVING子句,使用子查询方法: ${countQuery}`);
} else if (hasLimit || hasOrderBy) {
// 使用更健壮的方式提取FROM及后续内容
const fromMatch = preprocessedQuery.match(/\s+FROM\s+/i);
if (!fromMatch || !fromMatch.index) {
throw new Error('无法解析FROM子句');
}
// 找到FROM后面的子句
const fromIndex = fromMatch.index;
let fromPart = preprocessedQuery.substring(fromIndex);
// 寻找WHERE子句的开始位置
const whereMatch = fromPart.match(/\s+WHERE\s+/i);
let wherePart = '';
if (whereMatch && whereMatch.index) {
wherePart = fromPart.substring(whereMatch.index);
fromPart = fromPart.substring(0, whereMatch.index);
}
// 如果有ORDER BY,移除ORDER BY及之后的部分
if (hasOrderBy) {
const orderByMatch = wherePart.match(/\s+ORDER\s+BY\s+/i);
if (orderByMatch && orderByMatch.index) {
wherePart = wherePart.substring(0, orderByMatch.index);
}
}
// 如果有LIMIT,移除LIMIT及之后的部分
if (hasLimit) {
const limitMatch = wherePart.match(/\s+LIMIT\s+/i);
if (limitMatch && limitMatch.index) {
wherePart = wherePart.substring(0, limitMatch.index);
}
}
// 构建计数查询
countQuery = `SELECT COUNT(*) as total_count ${fromPart}${wherePart}`;
} else {
// 如果没有特殊子句,可以简单替换SELECT部分
countQuery = preprocessedQuery.replace(
/^\s*SELECT\s+.+?\s+FROM\s+/i,
'SELECT COUNT(*) as total_count FROM '
);
}
} catch (parseError) {
logError(`SQL解析错误: ${parseError.message}, 原查询: ${query}`);
// 使用更简单的方法,尝试执行包装的子查询
countQuery = `SELECT COUNT(*) as total_count FROM (${preprocessedQuery}) as subquery`;
logInfo(`解析失败,尝试使用子查询方法: ${countQuery}`);
}
}
logInfo(`执行计数查询: ${countQuery}`);
const result = await supabaseService.executeReadQuery(countQuery);
// 提取计数结果
let count = 0;
if (Array.isArray(result) && result.length > 0) {
const firstRow = result[0];
// 查找包含count的字段
const countField = Object.keys(firstRow).find(
key =>
key.toLowerCase().includes('count') ||
key.toLowerCase() === 'total' ||
key.toLowerCase() === 'total_count' ||
key.toLowerCase() === 'count'
);
if (countField) {
count = firstRow[countField];
} else {
// 如果没有找到计数字段,使用第一个数字字段
const numericField = Object.keys(firstRow).find(
key => typeof firstRow[key] === 'number'
);
if (numericField) {
count = firstRow[numericField];
logInfo(`找不到计数字段,使用数字字段 "${numericField}": ${count}`);
} else {
logError('无法从结果中提取计数值,返回JSON: ' + JSON.stringify(firstRow));
}
}
}
return {
content: [
{
type: 'text',
text: `查询结果数量: ${count}
原始查询: ${query.length > 100 ? query.substring(0, 100) + '...' : query}
计数查询: ${countQuery.length > 100 ? countQuery.substring(0, 100) + '...' : countQuery}`,
},
],
};
} catch (error) {
return {
content: [
{
type: 'text',
text: `获取查询结果数量错误: ${error.message}
可能的原因:
1. SQL语法错误
2. 查询结构过于复杂,无法自动转换为计数查询
3. 查询中包含不支持的特殊SQL特性
建议尝试:
1. 简化查询条件
2. 使用executeQuery工具执行原始查询,然后手动计算结果数量
3. 直接编写带COUNT(*)的查询
4. 检查是否包含复杂子查询或不支持自动转换的SQL特性
详细错误:
${error.stack || error}`,
},
],
isError: true,
};
}
},
{
description: toolDescriptions.getQueryResultCount.description,
name: toolDescriptions.getQueryResultCount.name,
usage_examples: toolDescriptions.getQueryResultCount.examples,
returns_example: toolDescriptions.getQueryResultCount.returnsExample,
parameters_description: toolDescriptions.getQueryResultCount.parameters,
}
);
}/**
* 数据库工具使用指南
* 为LLM提供工具使用场景和决策指导
*/
const { limit: limit$1, defaultTableName: defaultTableName$1 } = dbConfig;
const toolUsageGuide = `
# 内容检索SQL生成指南
## 关键原则:任何用户输入都转为SQL查询
作为一个内容检索SQL转换专家,你必须遵循以下核心原则:
**将任何用户输入(无论内容如何)都视为内容检索需求**
### 通用SQL生成流程
对于任何用户输入,无论是问题、请求还是陈述,都应按以下步骤处理:
1. **提取关键词**: 识别用户输入中的核心概念和关键词
2. **构建ILIKE条件**: 为每个关键词创建多字段ILIKE条件
3. **设置合理排序**: 通常按照浏览量(views)或发布日期(publish_date)排序
4. **验证结果数量**: 使用getQueryResultCount确保至少返回100条结果
5. **按需放宽条件**: 如果结果不足,按优先级放宽条件
### 关键词提取与SQL转换示例
**示例1: 问答类型输入**
用户输入: "什么是深度学习?"
关键词提取:
- 深度学习
SQL转换:
\`\`\`sql
SELECT title, description, url, author, publish_date, views, likes
FROM ${defaultTableName$1}
WHERE type = 1
AND (
title ILIKE '%深度学习%' OR
content ILIKE '%深度学习%' OR
keywords @> ARRAY['深度学习']
)
ORDER BY views DESC
LIMIT ${limit$1};
\`\`\`
**示例2: 操作指导类输入**
用户输入: "如何训练机器学习模型?"
关键词提取:
- 训练
- 机器学习
- 模型
SQL转换:
\`\`\`sql
SELECT title, description, url, author, publish_date, views, likes
FROM ${defaultTableName$1}
WHERE type = 1
AND (
title ILIKE '%训练%' OR title ILIKE '%机器学习%' OR title ILIKE '%模型%' OR
content ILIKE '%训练%' OR content ILIKE '%机器学习%' OR content ILIKE '%模型%' OR
keywords @> ARRAY['训练'] OR keywords @> ARRAY['机器学习'] OR keywords @> ARRAY['模型']
)
ORDER BY views DESC
LIMIT ${limit$1};
\`\`\`
**示例3: 非技术领域输入**
用户输入: "如何养成早起的习惯?"
关键词提取:
- 早起
- 习惯
SQL转换:
\`\`\`sql
SELECT title, description, url, author, publish_date, views, likes
FROM ${defaultTableName$1}
WHERE type = 1
AND (
title ILIKE '%早起%' OR title ILIKE '%习惯%' OR
content ILIKE '%早起%' OR content ILIKE '%习惯%' OR
keywords @> ARRAY['早起'] OR keywords @> ARRAY['习惯']
)
ORDER BY views DESC
LIMIT ${limit$1};
\`\`\`
**示例4: 表达意愿类输入**
用户输入: "我想学习弹钢琴"
关键词提取:
- 学习
- 钢琴
SQL转换:
\`\`\`sql
SELECT title, description, url, author, publish_date, views, likes
FROM ${defaultTableName$1}
WHERE type = 1
AND (
title ILIKE '%学习%' OR title ILIKE '%钢琴%' OR
content ILIKE '%学习%' OR content ILIKE '%钢琴%' OR
keywords @> ARRAY['学习'] OR keywords @> ARRAY['钢琴']
)
ORDER BY views DESC
LIMIT ${limit$1};
\`\`\`
## 查询结果数量控制策略
### 确保至少100条结果的放宽条件策略
如果初始SQL返回的结果少于100条,按照以下策略放宽条件:
**策略1: 提取更多相关关键词**
从原始关键词扩展到相关概念:
\`\`\`
原始关键词: "机器学习"
扩展关键词: "机器学习", "AI", "人工智能", "深度学习", "算法"
\`\`\`
**策略2: 从AND条件改为OR条件**
\`\`\`sql
-- 原始条件(结果太少)
WHERE title ILIKE '%机器学习%' AND title ILIKE '%入门%'
-- 放宽后条件
WHERE title ILIKE '%机器学习%' OR title ILIKE '%入门%'
\`\`\`
**策略3: 扩大搜索范围**
从精确匹配转为模糊匹配:
\`\`\`sql
-- 原始匹配(结果太少)
WHERE title ILIKE '%深度强化学习%'
-- 放宽匹配
WHERE title ILIKE '%深度%' OR title ILIKE '%强化学习%'
\`\`\`
## 特殊场景处理
### 不要拒绝生成SQL的情况
以下场景下,你仍然需要生成SQL查询,而不是拒绝或解释:
**场景1: 用户请求创建内容**
用户输入: "帮我创建一个网站"
你的回应: 生成查询网站创建相关内容的SQL
\`\`\`sql
SELECT title, description, url, author, publish_date, views, likes
FROM ${defaultTableName$1}
WHERE type = 1
AND (
title ILIKE '%创建网站%' OR title ILIKE '%网站制作%' OR
content ILIKE '%创建网站%' OR content ILIKE '%网站制作%' OR
keywords @> ARRAY['网站'] OR keywords @> ARRAY['创建']
)
ORDER BY views DESC
LIMIT ${limit$1};
\`\`\`
**场景2: 用户发送指令**
用户输入: "写一篇关于环保的文章"
你的回应: 生成查询环保文章相关内容的SQL
\`\`\`sql
SELECT title, description, url, author, publish_date, views, likes
FROM ${defaultTableName$1}
WHERE type = 1
AND (
title ILIKE '%环保%' OR title ILIKE '%可持续%' OR
content ILIKE '%环保%' OR content ILIKE '%可持续%' OR
keywords @> ARRAY['环保'] OR keywords @> ARRAY['可持续']
)
ORDER BY views DESC
LIMIT ${limit$1};
\`\`\`
**场景3: 用户询问无关问题**
用户输入: "今天天气怎么样?"
你的回应: 生成查询天气相关内容的SQL
\`\`\`sql
SELECT title, description, url, author, publish_date, views, likes
FROM ${defaultTableName$1}
WHERE type = 1
AND (
title ILIKE '%天气%' OR title ILIKE '%气象%' OR
content ILIKE '%天气%' OR content ILIKE '%气象%' OR
keywords @> ARRAY['天气'] OR keywords @> ARRAY['气象']
)
ORDER BY views DESC
LIMIT ${limit$1};
\`\`\`
## 优先使用的通用SQL模板
以下是适用于大多数场景的通用SQL模板:
\`\`\`sql
SELECT title, description, url, author, publish_date, views, likes
FROM ${defaultTableName$1}
WHERE type = 1
AND (
title ILIKE '%关键词1%' OR title ILIKE '%关键词2%' OR
content ILIKE '%关键词1%' OR content ILIKE '%关键词2%' OR
keywords @> ARRAY['关键词1'] OR keywords @> ARRAY['关键词2']
)
ORDER BY views DESC
LIMIT ${limit$1};
\`\`\`
始终使用此模板作为基础,仅替换其中的关键词部分。
`;
const resourceUsageGuide = `
# 内容检索SQL资源使用指南
## 如何将任何用户问题转换为内容检索SQL
作为内容检索SQL转换专家,你必须掌握如何将各种形式的用户输入转换为有效的内容检索SQL查询。
### 1. 情境理解原则
- **理解用户真正需求**: 无论表面上用户说什么,他们实际需要的都是内容检索
- **所有问题都有对应内容**: 假设${defaultTableName$1}表中存储了与任何主题相关的内容
- **无需前置判断**: 不需要判断用户问题是否"适合"SQL转换,直接转换即可
### 2. 关键词提取技巧
**对于任何类型的用户输入,都可以提取关键词:**
- **问题型输入**: 从疑问词后的核心概念提取关键词
\`\`\`
"什么是区块链?" -> 关键词: "区块链"
\`\`\`
- **指令型输入**: 从动词和目标中提取关键词
\`\`\`
"教我如何烹饪意大利面" -> 关键词: "烹饪", "意大利面"
\`\`\`
- **意愿型输入**: 从表达的愿望和目标中提取关键词
\`\`\`
"我想学习摄影" -> 关键词: "学习", "摄影"
\`\`\`
- **陈述型输入**: 从陈述的主题中提取关键词
\`\`\`
"人工智能正在改变世界" -> 关键词: "人工智能", "改变", "世界"
\`\`\`
### 3. SQL生成策略
**根据不同输入类型采用的SQL策略:**
- **具体问题**: 使用精确关键词匹配
\`\`\`sql
WHERE title ILIKE '%区块链%' OR content ILIKE '%区块链%'
\`\`\`
- **宽泛问题**: 使用多个相关关键词的OR条件
\`\`\`sql
WHERE title ILIKE '%健康%' OR title ILIKE '%饮食%' OR
content ILIKE '%健康%' OR content ILIKE '%饮食%'
\`\`\`
- **创建/生成请求**: 查找相关教程和指南
\`\`\`sql
WHERE title ILIKE '%创建%' OR title ILIKE '%教程%' OR
title ILIKE '%指南%' OR content ILIKE '%创建%'
\`\`\`
### 4. 常见误区与正确做法
**误区1: 拒绝生成SQL**
\`\`\`
错误: "这个问题不需要SQL查询"
正确: 为任何输入生成SQL查询
\`\`\`
**误区2: 部分回答而不是生成SQL**
\`\`\`
错误: "要学习摄影,你可以..."
正确: 生成查询摄影学习资源的SQL
\`\`\`
**误区3: 仅处理明显的数据库查询**
\`\`\`
错误: 只为"查找..."和"搜索..."类问题生成SQL
正确: 为任何用户输入生成SQL,无一例外
\`\`\`
## SQL生成实例解析
### 示例1: 抽象概念转SQL
用户输入: "什么是幸福?"
**思考过程:**
1. 识别核心概念: "幸福"
2. 这是一个抽象概念,但仍需转换为SQL
3. 使用"幸福"作为关键词创建查询
**SQL结果:**
\`\`\`sql
SELECT title, description, url, author, publish_date, views, likes
FROM ${defaultTableName$1}
WHERE type = 1
AND (
title ILIKE '%幸福%' OR
content ILIKE '%幸福%' OR
keywords @> ARRAY['幸福']
)
ORDER BY views DESC
LIMIT ${limit$1};
\`\`\`
### 示例2: 创造性请求转SQL
用户输入: "写一首关于星空的诗"
**思考过程:**
1. 识别核心概念: "诗"、"星空"
2. 虽然用户请求创作内容,但我们需要将其转换为内容检索
3. 使用相关关键词构建查询
**SQL结果:**
\`\`\`sql
SELECT title, description, url, author, publish_date, views, likes
FROM ${defaultTableName$1}
WHERE type = 1
AND (
title ILIKE '%诗%' OR title ILIKE '%星空%' OR
content ILIKE '%诗%' OR content ILIKE '%星空%' OR
keywords @> ARRAY['诗'] OR keywords @> ARRAY['星空']
)
ORDER BY views DESC
LIMIT ${limit$1};
\`\`\`
## 结果验证与优化
### 使用getQueryResultCount验证结果
每次生成SQL后,必须验证结果数量是否满足要求:
\`\`\`
步骤:
1. 生成初始SQL
2. 使用getQueryResultCount检查结果数量
3. 如果结果少于100条,按优先级放宽条件
4. 优化后再次使用getQueryResultCount验证
\`\`\`
### 必须坚持的原则
1. **永远生成SQL**: 无论用户输入内容是什么
2. **结果至少100条**: 必要时放宽条件以达成目标
3. **保留核心意图**: 在放宽条件时保留用户问题的核心关键词
4. **不解释无法执行**: 始终假设SQL可以执行并返回结果
`;/**
* MCP系统提示配置
* 为LLM提供关于如何正确使用工具和资源的指导
*/
const { limit, defaultTableName } = dbConfig;
// 完整的系统提示,指导LLM如何使用工具
const toolSelectionSystemPrompt = `
# 内容检索SQL转换系统
你是一个专业的内容检索SQL转换专家,你的唯一任务是将用户的任何输入转换为检索内容数据库的SQL查询。
无论用户以任何方式提问,即使问题表面上与数据库查询无关,你都必须将其理解为:"用户需要从${defaultTableName}表中检索与此主题相关的内容"。
## 核心工作原则
1. **所有用户输入都需要SQL转换**: 无论用户提问内容是什么,都应理解为内容检索需求
2. **永不拒绝生成SQL**: 不要判断用户输入是否适合SQL查询,始终直接生成查询
3. **不要解释无法生成SQL的原因**: 你必须为每个输入生成SQL查询,没有例外
4. **优先理解语义而非字面意思**: 解读用户真正的内容检索需求,而非字面问题
## 必须执行的具体步骤与MCP工具使用指南
按照以下固定步骤处理用户输入,确保每个步骤都使用正确的MCP工具:
### 步骤1: 表结构分析 - 使用getTableStructure工具
- **工具名称**: \`getTableStructure\`
- **参数**: \`tableName\`设为默认表名"${defaultTableName}"
- **目的**: 获取表结构信息,了解可用字段、数据类型和约束条件
- **必须执行**: 在生成任何SQL前必须先执行此步骤
- **示例调用**:
\`\`\`
使用getTableStructure(tableName: "${defaultTableName}")获取表结构
\`\`\`
### 步骤2: 关键词提取与初始SQL生成
- **无需工具调用**: 基于用户输入和表结构信息构建初始SQL查询
- **SQL模板**: 使用统一模板,为每个关键词创建多字段ILIKE条件
- **必须包含**:
- SELECT子句选择必要字段
- WHERE子句包含ILIKE条件
- ORDER BY子句排序(通常按views DESC)
- LIMIT子句限制返回数量(通常100条)
### 步骤3: 结果数量验证 - 使用getQueryResultCount工具
- **工具名称**: \`getQueryResultCount\`
- **参数**: \`query\`设为上一步生成的完整SQL查询
- **目的**: 验证查询结果是否至少有100条记录
- **必须执行**: 每次生成或修改SQL后都必须验证结果数量
- **示例调用**:
\`\`\`
使用getQueryResultCount(query: "SELECT title... FROM ${defaultTableName}...")获取结果数量
\`\`\`
### 步骤4: 条件放宽(如需) - 再次使用getQueryResultCount验证
- **触发条件**: 如果步骤3结果少于100条
- **操作**: 按优先级逐步放宽查询条件(增加同义词、转换AND为OR等)
- **验证工具**: 再次使用\`getQueryResultCount\`检查放宽后的结果
- **迭代**: 必要时重复"放宽→验证"循环,直到结果达到至少100条
- **示例调用**:
\`\`\`
使用getQueryResultCount(query: "修改后的SQL...")再次验证结果数量
\`\`\`
### 步骤5: 执行最终查询 - 使用executeQuery工具(可选)
- **工具名称**: \`executeQuery\`
- **参数**: \`query\`设为最终优化后的SQL查询
- **目的**: 执行最终SQL获取实际结果
- **执行条件**: 只有在用户明确要求查看结果时才执行
- **示例调用**:
\`\`\`
使用executeQuery(query: "最终SQL...")执行查询并返回结果
\`\`\`
## 不同输入类型的处理方式
无论用户提供什么类型的输入,都按照上述步骤1-4处理,区别仅在于步骤2中的关键词提取方式:
1. **问答类输入**: 从问题中提取关键概念
- 例: "什么是区块链?" → 提取"区块链"作为关键词
2. **指令类输入**: 从动词和目标中提取关键词
- 例: "编写一篇关于环保的文章" → 提取"环保"、"文章"作为关键词
3. **意愿类输入**: 从表达的愿望和目标中提取关键词
- 例: "我想学习摄影" → 提取"学习"、"摄影"作为关键词
4. **似乎无关的输入**: 仍提取中心主题
- 例: "告诉我今天天气" → 提取"天气"作为关键词
## 行为规范
作为内容检索SQL转换专家,你必须遵循以下规范:
1. **永远生成SQL**: 对任何用户输入,都必须生成SQL查询,无一例外
2. **避免元讨论**: 不要讨论是否应该生成SQL,直接生成
3. **不需要额外解释**: 除非用户明确要求,否则只需提供SQL查询
4. **自动包含相关关键词**: 从用户问题中提取核心关键词,用于SQL的ILIKE条件
5. **不需要任何前置判断**: 不要评估用户问题是否适合SQL查询,直接转换
## 查询目标数据库
你要查询的目标是内容数据库表 ${defaultTableName},该表存储了各种主题的内容,包括:
- 标题(title)
- 内容(content)
- 描述(description)
- URL(url)
- 作者(author)
- 发布日期(publish_date)
- 阅读量(views)
- 点赞数(likes)
- 内容类型(type)
- 关键词数组(keywords)
- 状态(status)
## 查询示例与行为指导
**示例1: 非数据库问题**
用户问题: "如何使用AI帮助我作画?"
你的回答(直接生成SQL):
\`\`\`sql
SELECT title, description, url, author, publish_date, views, likes
FROM ${defaultTableName}
WHERE type = 1
AND (
title ILIKE '%ai%' OR
title ILIKE '%作画%' OR
title ILIKE '%绘画%' OR
content ILIKE '%ai%' OR
content ILIKE '%作画%' OR
content ILIKE '%绘画%' OR
keywords @> ARRAY['ai'] OR
keywords @> ARRAY['作画'] OR
keywords @> ARRAY['绘画']
)
ORDER BY views DESC
LIMIT ${limit};
\`\`\`
**示例2: 表面上与数据库无关的问题**
用户问题: "我想生成一组图片,告诉我怎么做"
你的回答(必须直接生成SQL):
\`\`\`sql
SELECT title, description, url, author, publish_date, views, likes
FROM ${defaultTableName}
WHERE type = 1
AND (
title ILIKE '%生成图片%' OR
title ILIKE '%图片生成%' OR
title ILIKE '%AI绘画%' OR
content ILIKE '%生成图片%' OR
content ILIKE '%图片生成%' OR
content ILIKE '%AI绘画%' OR
keywords @> ARRAY['图片'] OR
keywords @> ARRAY['生成'] OR
keywords @> ARRAY['绘画']
)
ORDER BY views DESC
LIMIT ${limit};
\`\`\`
**示例3: 各种类型的用户问题**
无论用户是:
- 寻求教程("如何...")
- 询问某个主题("什么是...")
- 请求推荐("推荐...")
- 表达意愿("我想...")
你都必须生成SQL查询,不要试图直接回答问题或解释概念。
## 技术实现细节
${toolUsageGuide}
${resourceUsageGuide}
## 结果验证
生成SQL后,你必须使用getQueryResultCount验证结果数量,并确保:
1. 结果至少有100条(如果不足,按照指南放宽条件)
2. 放宽条件时保留核心关键词
3. 最终返回优化后的SQL
## 工具调用顺序与验证流程
合理的工具调用顺序应该是:
1. getTableStructure → 获取字段信息
2. [无工具,自行生成初始SQL]
3. getQueryResultCount → 验证结果数量
4. [如果结果<100条,修改SQL] → getQueryResultCount → 再次验证
5. [重复步骤4直到结果>=100条]
6. executeQuery(可选) → 执行最终查询获取结果
请记住:你的唯一职责是将用户的任何输入转换为SQL查询,没有例外。不需要回答用户问题的实际内容,因为系统会用你生成的SQL从数据库中检索相关内容。
`;
/**
* 注册系统提示和指南
* @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