prompt-discovery-mcp
Version:
基于Nacos的动态提示词MCP服务器,支持智能推荐和实时热更新
274 lines (273 loc) • 10.7 kB
JavaScript
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { z } from "zod";
import { NacosService } from "./services/NacosService.js";
import { readFileSync } from "fs";
import { join } from "path";
// 不再需要扩展 McpServer 类型,使用标准的 tools.register 方法
// 获取包版本
let version = '1.0.2'; // 默认版本
try {
// 尝试从当前工作目录读取 package.json
const packageJson = JSON.parse(readFileSync(join(process.cwd(), 'package.json'), 'utf-8'));
version = packageJson.version || version;
}
catch (error) {
// 如果无法读取 package.json,使用默认版本
console.error('警告:无法读取 package.json,使用默认版本', version);
}
// 创建服务器实例
const server = new McpServer({
name: "PromptDiscovery",
version,
});
if (process.env.NACOS_USERNAME && !process.env.NACOS_PASSWORD) {
console.error('错误:缺少必要的环境变量 NACOS_PASSWORD');
process.exit(1);
}
if (!process.env.NACOS_DATA_ID) {
console.error('错误:缺少必要的环境变量 NACOS_DATA_ID');
process.exit(1);
}
// 从环境变量获取Nacos配置
const nacosService = new NacosService({
serverAddr: process.env.NACOS_ADDR || '127.0.0.1:8848',
username: process.env.NACOS_USERNAME || 'nacos',
password: process.env.NACOS_PASSWORD,
group: process.env.NACOS_GROUP || 'DEFAULT_GROUP',
configId: process.env.NACOS_DATA_ID,
refreshInterval: parseInt(process.env.NACOS_REFRESH_INTERVAL || '30000') // 默认30秒刷新一次
});
// 注册工具:获取所有提示词
server.tool("getAllPrompts", "获取所有可用的提示词", {}, async () => {
const prompts = nacosService.getAllPrompts();
return {
content: [{ type: "text", text: JSON.stringify({ prompts, count: prompts.length }) }]
};
});
// 注册工具:按标签获取提示词
server.tool("getPromptsByTags", "根据标签获取提示词", {
tags: z.array(z.string()).describe("标签列表")
}, async (params) => {
const prompts = nacosService.getPromptsByTags(params.tags);
return {
content: [{ type: "text", text: JSON.stringify({ prompts }) }]
};
});
// 注册工具:按ID获取提示词
server.tool("getPromptById", "根据ID获取特定提示词", {
id: z.string().describe("提示词ID"),
trackUsage: z.boolean().optional().describe("是否记录使用统计")
}, async (params) => {
const prompt = nacosService.getPromptById(params.id);
// 如果找到提示词并且需要记录使用统计
if (prompt && params.trackUsage) {
nacosService.trackPromptUsage(params.id);
}
return {
content: [{ type: "text", text: JSON.stringify({ prompt: prompt || null }) }]
};
});
// 注册工具:搜索提示词
server.tool("searchPrompts", "根据关键词搜索提示词", {
keyword: z.string().describe("搜索关键词"),
limit: z.number().optional().describe("返回结果数量限制"),
trackUsage: z.boolean().optional().describe("是否记录使用统计")
}, async (params) => {
let results = nacosService.searchPrompts(params.keyword);
if (params.limit && params.limit > 0) {
results = results.slice(0, params.limit);
}
// 如果需要记录使用统计并且有结果
if (params.trackUsage && results.length > 0) {
// 记录第一个结果的使用统计
nacosService.trackPromptUsage(results[0].id);
}
return {
content: [{ type: "text", text: JSON.stringify({
prompts: results,
totalMatches: results.length
}) }]
};
});
// 注册工具:根据当前上下文推荐相关提示词
server.tool("suggestPrompts", "根据当前上下文推荐相关提示词", {
context: z.string().describe("当前编码上下文"),
limit: z.number().optional().describe("返回结果数量限制")
}, async (params) => {
// 改进的上下文匹配逻辑
// 1. 提取关键词
const context = params.context.toLowerCase();
// 按不同分隔符拆分上下文
const contextWords = new Set();
// 按空格分词
context.split(/\s+/).forEach(word => {
if (word.length > 2)
contextWords.add(word);
});
// 按常见编程符号分词
context.split(/[\.,\(\)\{\}\[\]\:\;\-\_\=\+\/\*]/)
.forEach(word => {
word = word.trim();
if (word.length > 2)
contextWords.add(word);
});
// 提取驼峰命名法单词
const camelCaseWords = context.match(/[a-z]+(?=[A-Z])|[A-Z][a-z]+/g) || [];
camelCaseWords.forEach(word => {
if (word.length > 2)
contextWords.add(word.toLowerCase());
});
// 提取常见编程关键词
const programmingKeywords = [
'function', 'class', 'method', 'test', 'api', 'component',
'service', 'model', 'view', 'controller', 'database', 'query',
'error', 'exception', 'debug', 'performance', 'optimization',
'security', 'authentication', 'authorization', 'validation'
];
programmingKeywords.forEach(keyword => {
if (context.includes(keyword)) {
contextWords.add(keyword);
}
});
const allPrompts = nacosService.getAllPrompts();
// 为每个提示词计算相关性分数
const scoredPrompts = allPrompts.map(prompt => {
let score = 0;
const keywordMatches = [];
// 标签匹配 (权重最高)
for (const tag of prompt.tags) {
const tagLower = tag.toLowerCase();
for (const word of contextWords) {
if (tagLower.includes(word) || word.includes(tagLower)) {
score += 3;
keywordMatches.push(tag);
break;
}
}
}
// 标题匹配 (权重次之)
const titleLower = prompt.title.toLowerCase();
for (const word of contextWords) {
if (titleLower.includes(word)) {
score += 2;
keywordMatches.push(word);
}
}
// 描述匹配
const descLower = prompt.description.toLowerCase();
for (const word of contextWords) {
if (descLower.includes(word)) {
score += 1;
if (!keywordMatches.includes(word)) {
keywordMatches.push(word);
}
}
}
// 内容匹配 (权重最低但仍然重要)
const contentLower = prompt.content.toLowerCase();
for (const word of contextWords) {
if (contentLower.includes(word)) {
score += 0.5;
}
}
// 考虑使用频率因素 (如果有使用统计)
if (prompt.usageCount && prompt.usageCount > 0) {
// 使用过的提示词得到轻微加分
score += Math.min(prompt.usageCount / 10, 1); // 最多加1分
}
// 如果有最近使用时间,给最近使用的提示词一些优先级
if (prompt.lastUsed) {
const lastUsedDate = new Date(prompt.lastUsed);
const now = new Date();
const daysDiff = (now.getTime() - lastUsedDate.getTime()) / (1000 * 3600 * 24);
if (daysDiff < 7) { // 一周内使用过
score += Math.max(0, (7 - daysDiff) / 7); // 最近使用的得分更高
}
}
return { prompt, score, keywordMatches };
});
// 按分数排序并限制结果数量
const sortedPrompts = scoredPrompts
.filter(item => item.score > 0)
.sort((a, b) => b.score - a.score);
// 转换为结果格式,包含匹配原因
const results = sortedPrompts.slice(0, params.limit || 5).map(item => {
return {
...item.prompt,
matchScore: item.score,
matchReasons: item.keywordMatches
};
});
// 如果有结果,记录使用统计
if (results.length > 0) {
// 记录第一个推荐提示词的使用统计(假设它是最相关的)
nacosService.trackPromptUsage(results[0].id);
}
return {
content: [{ type: "text", text: JSON.stringify({
prompts: results,
totalMatches: sortedPrompts.length
}) }]
};
});
// 启动服务器
async function main() {
try {
// 初始化 NacosService
await nacosService.init();
// 监听事件
nacosService.on('error', (error) => {
console.error('Nacos 错误:', error);
});
nacosService.on('configChanged', (config) => {
console.error(`提示词配置已更新,当前共有 ${config.length} 个提示词`);
});
nacosService.on('usingCache', (config) => {
console.error(`使用本地缓存的提示词配置,共有 ${config.length} 个提示词`);
});
nacosService.on('connected', () => {
console.error('已成功连接到 Nacos 服务器');
});
nacosService.on('disconnected', (error) => {
console.error('与 Nacos 服务器的连接已断开:', error.message);
});
// 注册工具:记录提示词使用情况
server.tool("trackPromptUsage", "记录提示词使用情况", {
promptId: z.string().describe("提示词ID")
}, async (params) => {
const updatedPrompt = nacosService.trackPromptUsage(params.promptId);
return {
content: [{ type: "text", text: JSON.stringify({
success: !!updatedPrompt,
prompt: updatedPrompt || null
}) }]
};
});
// 启动 MCP 服务器
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("PromptDiscovery MCP 服务已启动");
// console.log(`已注册 ${Object.keys(server['_registeredTools'] || {}).length} 个工具`);
}
catch (error) {
console.error("初始化失败:", error instanceof Error ? error.message : error);
process.exit(1);
}
}
// 处理进程退出
process.on('SIGINT', async () => {
console.log('关闭服务...');
await nacosService.close();
process.exit(0);
});
process.on('SIGTERM', async () => {
console.log('关闭服务...');
await nacosService.close();
process.exit(0);
});
main().catch((error) => {
console.error("Fatal error in main():", error instanceof Error ? error.message : error);
process.exit(1);
});