freature: 支持mcp 协议
This commit is contained in:
@ -6,6 +6,8 @@ const packageJson = require('../../package.json')
|
||||
|
||||
// 导入锦囊框架
|
||||
const { cli } = require('../lib/core/pouch')
|
||||
// 导入MCP Server命令
|
||||
const { MCPServerCommand } = require('../lib/commands/MCPServerCommand')
|
||||
|
||||
// 创建主程序
|
||||
const program = new Command()
|
||||
@ -60,6 +62,21 @@ program
|
||||
await cli.execute('remember', args)
|
||||
})
|
||||
|
||||
// MCP Server命令
|
||||
program
|
||||
.command('mcp-server')
|
||||
.description('🔌 启动MCP Server,支持Claude Desktop等AI应用接入')
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const mcpServer = new MCPServerCommand();
|
||||
await mcpServer.execute();
|
||||
} catch (error) {
|
||||
// 输出到stderr,不污染MCP的stdout通信
|
||||
console.error(chalk.red(`❌ MCP Server 启动失败: ${error.message}`));
|
||||
process.exit(1);
|
||||
}
|
||||
})
|
||||
|
||||
// 全局错误处理
|
||||
program.configureHelp({
|
||||
helpWidth: 100,
|
||||
@ -71,13 +88,14 @@ program.addHelpText('after', `
|
||||
|
||||
${chalk.cyan('💡 PromptX 锦囊框架 - AI use CLI get prompt for AI')}
|
||||
|
||||
${chalk.cyan('🎒 五大锦囊命令:')}
|
||||
${chalk.cyan('🎒 六大核心命令:')}
|
||||
🏗️ ${chalk.cyan('init')} → 初始化环境,传达系统协议
|
||||
👋 ${chalk.yellow('hello')} → 发现可用角色和领域专家
|
||||
⚡ ${chalk.red('action')} → 激活特定角色,获取专业能力
|
||||
📚 ${chalk.blue('learn')} → 深入学习领域知识体系
|
||||
🔍 ${chalk.green('recall')} → AI主动检索应用记忆
|
||||
🧠 ${chalk.magenta('remember')} → AI主动内化知识增强记忆
|
||||
🔌 ${chalk.blue('mcp-server')} → 启动MCP Server,连接AI应用
|
||||
|
||||
${chalk.cyan('示例:')}
|
||||
${chalk.gray('# 1️⃣ 初始化锦囊系统')}
|
||||
@ -102,6 +120,9 @@ ${chalk.cyan('示例:')}
|
||||
promptx remember "每日站会控制在15分钟内"
|
||||
promptx remember "测试→预发布→生产"
|
||||
|
||||
${chalk.gray('# 7️⃣ 启动MCP服务')}
|
||||
promptx mcp-server
|
||||
|
||||
${chalk.cyan('🔄 PATEOAS状态机:')}
|
||||
每个锦囊输出都包含 PATEOAS 导航,引导 AI 发现下一步操作
|
||||
即使 AI 忘记上文,仍可通过锦囊独立执行
|
||||
@ -112,6 +133,11 @@ ${chalk.cyan('💭 核心理念:')}
|
||||
• 分阶段专注:每个锦囊专注单一任务
|
||||
• Prompt驱动:输出引导AI发现下一步
|
||||
|
||||
${chalk.cyan('🔌 MCP集成:')}
|
||||
• AI应用连接:通过MCP协议连接Claude Desktop等AI应用
|
||||
• 标准化接口:遵循Model Context Protocol标准
|
||||
• 无环境依赖:解决CLI环境配置问题
|
||||
|
||||
${chalk.cyan('更多信息:')}
|
||||
GitHub: ${chalk.underline('https://github.com/Deepractice/PromptX')}
|
||||
组织: ${chalk.underline('https://github.com/Deepractice')}
|
||||
|
||||
141
src/lib/adapters/MCPOutputAdapter.js
Normal file
141
src/lib/adapters/MCPOutputAdapter.js
Normal file
@ -0,0 +1,141 @@
|
||||
/**
|
||||
* MCP输出适配器
|
||||
* 负责将PromptX CLI的富文本输出转换为MCP标准JSON格式
|
||||
*
|
||||
* 设计原则:
|
||||
* - 保留所有emoji、markdown、中文字符
|
||||
* - 转换为MCP标准的content数组格式
|
||||
* - 提供统一的错误处理机制
|
||||
*/
|
||||
class MCPOutputAdapter {
|
||||
constructor() {
|
||||
this.version = '1.0.0';
|
||||
}
|
||||
|
||||
/**
|
||||
* 将CLI输出转换为MCP标准格式
|
||||
* @param {any} input - CLI输出(可能是字符串、对象、PouchOutput等)
|
||||
* @returns {object} MCP标准格式的响应
|
||||
*/
|
||||
convertToMCPFormat(input) {
|
||||
try {
|
||||
const text = this.normalizeInput(input);
|
||||
const sanitizedText = this.sanitizeText(text);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: sanitizedText
|
||||
}
|
||||
]
|
||||
};
|
||||
} catch (error) {
|
||||
return this.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标准化输入,将各种类型转换为字符串
|
||||
* @param {any} input - 输入数据
|
||||
* @returns {string} 标准化后的字符串
|
||||
*/
|
||||
normalizeInput(input) {
|
||||
// 处理null和undefined
|
||||
if (input === null) return 'null';
|
||||
if (input === undefined) return 'undefined';
|
||||
|
||||
// 处理字符串
|
||||
if (typeof input === 'string') {
|
||||
return input;
|
||||
}
|
||||
|
||||
// 处理有toString方法的对象(如PouchOutput)
|
||||
if (input && typeof input.toString === 'function' && input.toString !== Object.prototype.toString) {
|
||||
return input.toString();
|
||||
}
|
||||
|
||||
// 处理数组和普通对象
|
||||
if (typeof input === 'object') {
|
||||
return JSON.stringify(input, null, 2);
|
||||
}
|
||||
|
||||
// 其他类型直接转换
|
||||
return String(input);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理文本,确保JSON兼容性但保留所有格式
|
||||
* @param {string} text - 输入文本
|
||||
* @returns {string} 清理后的文本
|
||||
*/
|
||||
sanitizeText(text) {
|
||||
// 对于MCP协议,我们实际上不需要做任何转义
|
||||
// emoji、中文字符、markdown都应该保留
|
||||
// MCP的content格式本身就支持UTF-8字符
|
||||
return text;
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的错误处理
|
||||
* @param {Error|string} error - 错误对象或错误信息
|
||||
* @returns {object} MCP格式的错误响应
|
||||
*/
|
||||
handleError(error) {
|
||||
const errorMessage = error instanceof Error
|
||||
? error.message
|
||||
: String(error);
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: `❌ 执行失败: ${errorMessage}`
|
||||
}
|
||||
],
|
||||
isError: true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证输出格式是否符合MCP标准
|
||||
* @param {object} output - 要验证的输出
|
||||
* @returns {boolean} 是否符合标准
|
||||
*/
|
||||
validateMCPFormat(output) {
|
||||
if (!output || typeof output !== 'object') {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Array.isArray(output.content)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return output.content.every(item =>
|
||||
item &&
|
||||
typeof item === 'object' &&
|
||||
item.type === 'text' &&
|
||||
typeof item.text === 'string'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成功响应的快捷方法
|
||||
* @param {string} text - 响应文本
|
||||
* @returns {object} MCP格式响应
|
||||
*/
|
||||
createSuccessResponse(text) {
|
||||
return this.convertToMCPFormat(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误响应的快捷方法
|
||||
* @param {string} message - 错误消息
|
||||
* @returns {object} MCP格式错误响应
|
||||
*/
|
||||
createErrorResponse(message) {
|
||||
return this.handleError(message);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { MCPOutputAdapter };
|
||||
281
src/lib/commands/MCPServerCommand.js
Normal file
281
src/lib/commands/MCPServerCommand.js
Normal file
@ -0,0 +1,281 @@
|
||||
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
|
||||
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
|
||||
const { cli } = require('../core/pouch');
|
||||
const { MCPOutputAdapter } = require('../adapters/MCPOutputAdapter');
|
||||
const { getExecutionContext, getDebugInfo } = require('../utils/executionContext');
|
||||
|
||||
/**
|
||||
* MCP Server 适配器 - 函数调用架构
|
||||
* 将MCP协议请求转换为PromptX函数调用,实现零开销适配
|
||||
* 支持智能工作目录检测,确保MCP和CLI模式下的一致性
|
||||
*/
|
||||
class MCPServerCommand {
|
||||
constructor() {
|
||||
this.name = 'promptx-mcp-server';
|
||||
this.version = '1.0.0';
|
||||
this.debug = process.env.MCP_DEBUG === 'true';
|
||||
|
||||
// 智能检测执行上下文
|
||||
this.executionContext = getExecutionContext();
|
||||
|
||||
// 调试信息输出
|
||||
this.log(`🎯 检测到执行模式: ${this.executionContext.mode}`);
|
||||
this.log(`📍 原始工作目录: ${this.executionContext.originalCwd}`);
|
||||
this.log(`📁 目标工作目录: ${this.executionContext.workingDirectory}`);
|
||||
|
||||
// 如果需要切换工作目录
|
||||
if (this.executionContext.workingDirectory !== this.executionContext.originalCwd) {
|
||||
this.log(`🔄 切换工作目录: ${this.executionContext.originalCwd} -> ${this.executionContext.workingDirectory}`);
|
||||
try {
|
||||
process.chdir(this.executionContext.workingDirectory);
|
||||
this.log(`✅ 工作目录切换成功`);
|
||||
} catch (error) {
|
||||
this.log(`❌ 工作目录切换失败: ${error.message}`);
|
||||
this.log(`🔄 继续使用原始目录: ${this.executionContext.originalCwd}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.log(`📂 最终工作目录: ${process.cwd()}`);
|
||||
this.log(`📋 预期记忆文件路径: ${require('path').join(process.cwd(), '.promptx/memory/declarative.md')}`);
|
||||
|
||||
// 输出完整调试信息
|
||||
if (this.debug) {
|
||||
this.log(`🔍 完整调试信息: ${JSON.stringify(getDebugInfo(), null, 2)}`);
|
||||
}
|
||||
|
||||
// 创建输出适配器
|
||||
this.outputAdapter = new MCPOutputAdapter();
|
||||
|
||||
// 创建MCP服务器实例 - 使用正确的API
|
||||
this.server = new Server(
|
||||
{
|
||||
name: this.name,
|
||||
version: this.version
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
tools: {}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
this.setupHandlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试日志 - 输出到stderr,不影响MCP协议
|
||||
*/
|
||||
log(message) {
|
||||
if (this.debug) {
|
||||
console.error(`[MCP DEBUG] ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动MCP Server
|
||||
*/
|
||||
async execute() {
|
||||
try {
|
||||
this.log('🚀 启动MCP Server...');
|
||||
const transport = new StdioServerTransport();
|
||||
await this.server.connect(transport);
|
||||
this.log('✅ MCP Server 已启动,等待连接...');
|
||||
|
||||
// 保持进程运行
|
||||
return new Promise((resolve) => {
|
||||
// MCP服务器现在正在运行,监听stdin输入
|
||||
process.on('SIGINT', () => {
|
||||
this.log('🛑 收到终止信号,关闭MCP Server');
|
||||
resolve();
|
||||
});
|
||||
|
||||
process.on('SIGTERM', () => {
|
||||
this.log('🛑 收到终止信号,关闭MCP Server');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
// 输出到stderr
|
||||
console.error(`❌ MCP Server 启动失败: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置MCP工具处理程序 - 使用正确的MCP SDK API
|
||||
*/
|
||||
setupHandlers() {
|
||||
// 使用Schema常量进行注册
|
||||
const {
|
||||
ListToolsRequestSchema,
|
||||
CallToolRequestSchema
|
||||
} = require('@modelcontextprotocol/sdk/types.js');
|
||||
|
||||
// 注册工具列表处理程序
|
||||
this.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
||||
this.log('📋 收到工具列表请求');
|
||||
return {
|
||||
tools: this.getToolDefinitions()
|
||||
};
|
||||
});
|
||||
|
||||
// 注册工具调用处理程序
|
||||
this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
||||
const { name, arguments: args } = request.params;
|
||||
this.log(`🔧 调用工具: ${name} 参数: ${JSON.stringify(args)}`);
|
||||
return await this.callTool(name, args || {});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取工具定义
|
||||
*/
|
||||
getToolDefinitions() {
|
||||
return [
|
||||
{
|
||||
name: 'promptx_init',
|
||||
description: '🏗️ [流程启动锦囊] 启动PromptX专业能力增强流程,创建工作环境标识,自动引导到角色发现阶段',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'promptx_hello',
|
||||
description: '👋 [角色发现锦囊] 让AI浏览专业角色库(产品经理、Java开发者、设计师等),当需要专业能力时使用,引导角色激活',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {}
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'promptx_action',
|
||||
description: '⚡ [专家变身锦囊] 让AI获得指定专业角色的思维模式和核心能力,即时变身领域专家,开始提供专业服务',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
role: {
|
||||
type: 'string',
|
||||
description: '要激活的角色ID,如:copywriter, product-manager, java-backend-developer'
|
||||
}
|
||||
},
|
||||
required: ['role']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'promptx_learn',
|
||||
description: '📚 [专业深化锦囊] 让AI学习特定领域的思维模式和执行模式(如敏捷开发、产品设计),强化当前专家角色能力',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
resource: {
|
||||
type: 'string',
|
||||
description: '资源URL,支持格式:thought://creativity, execution://best-practice, knowledge://scrum'
|
||||
}
|
||||
},
|
||||
required: ['resource']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'promptx_recall',
|
||||
description: '🔍 [经验检索锦囊] 让AI从专业记忆库中检索相关经验和最佳实践,当需要基于历史经验工作时使用',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
random_string: {
|
||||
type: 'string',
|
||||
description: 'Dummy parameter for no-parameter tools'
|
||||
},
|
||||
query: {
|
||||
type: 'string',
|
||||
description: '检索关键词或描述,可选参数,不提供则返回所有记忆'
|
||||
}
|
||||
},
|
||||
required: ['random_string']
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'promptx_remember',
|
||||
description: '💾 [知识积累锦囊] 让AI将重要经验和专业知识保存到记忆库,构建可复用的专业知识体系,供未来检索应用',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
content: {
|
||||
type: 'string',
|
||||
description: '要保存的重要信息或经验'
|
||||
},
|
||||
tags: {
|
||||
type: 'string',
|
||||
description: '自定义标签,用空格分隔,可选'
|
||||
}
|
||||
},
|
||||
required: ['content']
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行工具调用
|
||||
*/
|
||||
async callTool(toolName, args) {
|
||||
try {
|
||||
// 将MCP参数转换为CLI函数调用参数
|
||||
const cliArgs = this.convertMCPToCliParams(toolName, args);
|
||||
this.log(`🎯 CLI调用: ${toolName} -> ${JSON.stringify(cliArgs)}`);
|
||||
this.log(`🗂️ 当前工作目录: ${process.cwd()}`);
|
||||
|
||||
// 直接调用PromptX CLI函数 - 启用静默模式避免console.log干扰MCP协议
|
||||
const result = await cli.execute(toolName.replace('promptx_', ''), cliArgs, true);
|
||||
this.log(`✅ CLI执行完成: ${toolName}`);
|
||||
|
||||
// 使用输出适配器转换为MCP响应格式
|
||||
return this.outputAdapter.convertToMCPFormat(result);
|
||||
|
||||
} catch (error) {
|
||||
this.log(`❌ 工具调用失败: ${toolName} - ${error.message}`);
|
||||
return this.outputAdapter.handleError(error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换MCP参数为CLI函数调用参数
|
||||
*/
|
||||
convertMCPToCliParams(toolName, mcpArgs) {
|
||||
const paramMapping = {
|
||||
'promptx_init': () => [],
|
||||
|
||||
'promptx_hello': () => [],
|
||||
|
||||
'promptx_action': (args) => [args.role],
|
||||
|
||||
'promptx_learn': (args) => args.resource ? [args.resource] : [],
|
||||
|
||||
'promptx_recall': (args) => {
|
||||
// 忽略random_string dummy参数,只处理query
|
||||
// 处理各种空值情况:undefined、null、空对象、空字符串
|
||||
if (!args || !args.query || typeof args.query !== 'string' || args.query.trim() === '') {
|
||||
return [];
|
||||
}
|
||||
return [args.query];
|
||||
},
|
||||
|
||||
'promptx_remember': (args) => {
|
||||
const result = [args.content];
|
||||
if (args.tags) {
|
||||
result.push('--tags', args.tags);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
const mapper = paramMapping[toolName];
|
||||
if (!mapper) {
|
||||
throw new Error(`未知工具: ${toolName}`);
|
||||
}
|
||||
|
||||
return mapper(mcpArgs);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { MCPServerCommand };
|
||||
@ -48,9 +48,10 @@ class PouchCLI {
|
||||
* 执行命令
|
||||
* @param {string} commandName - 命令名称
|
||||
* @param {Array} args - 命令参数
|
||||
* @param {boolean} silent - 静默模式,不输出到console(用于MCP)
|
||||
* @returns {Promise<PouchOutput>} 执行结果
|
||||
*/
|
||||
async execute (commandName, args = []) {
|
||||
async execute (commandName, args = [], silent = false) {
|
||||
// 确保已初始化
|
||||
if (!this.initialized) {
|
||||
await this.initialize()
|
||||
@ -65,16 +66,22 @@ class PouchCLI {
|
||||
// 通过状态机执行命令
|
||||
const result = await this.stateMachine.transition(commandName, args)
|
||||
|
||||
// 如果结果有 toString 方法,打印人类可读格式
|
||||
if (result && result.toString && typeof result.toString === 'function') {
|
||||
console.log(result.toString())
|
||||
} else {
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
// 只在非静默模式下输出(避免干扰MCP协议)
|
||||
if (!silent) {
|
||||
// 如果结果有 toString 方法,打印人类可读格式
|
||||
if (result && result.toString && typeof result.toString === 'function') {
|
||||
console.log(result.toString())
|
||||
} else {
|
||||
console.log(JSON.stringify(result, null, 2))
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error(`执行命令出错: ${error.message}`)
|
||||
// 错误输出始终使用stderr,不干扰MCP协议
|
||||
if (!silent) {
|
||||
console.error(`执行命令出错: ${error.message}`)
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
157
src/lib/utils/executionContext.js
Normal file
157
src/lib/utils/executionContext.js
Normal file
@ -0,0 +1,157 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
/**
|
||||
* 执行上下文检测工具
|
||||
* 根据命令入口自动判断执行模式(CLI vs MCP)并获取正确的工作目录
|
||||
* 基于MCP社区标准实践,通过环境变量解决cwd获取问题
|
||||
*/
|
||||
|
||||
/**
|
||||
* 获取执行上下文信息
|
||||
* @returns {Object} 包含模式和工作目录的上下文对象
|
||||
*/
|
||||
function getExecutionContext() {
|
||||
const args = process.argv;
|
||||
const command = args[2]; // 第一个命令参数
|
||||
|
||||
const isMCPMode = command === 'mcp-server';
|
||||
|
||||
return {
|
||||
mode: isMCPMode ? 'MCP' : 'CLI',
|
||||
command: command,
|
||||
workingDirectory: isMCPMode ? getMCPWorkingDirectory() : process.cwd(),
|
||||
originalCwd: process.cwd()
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* MCP模式下获取工作目录
|
||||
* 基于社区标准实践,优先从环境变量获取配置的工作目录
|
||||
* @returns {string} 工作目录路径
|
||||
*/
|
||||
function getMCPWorkingDirectory() {
|
||||
// 策略1:WORKSPACE_FOLDER_PATHS(VS Code/Cursor标准环境变量)
|
||||
const workspacePaths = process.env.WORKSPACE_FOLDER_PATHS;
|
||||
if (workspacePaths) {
|
||||
// 取第一个工作区路径(多工作区情况)
|
||||
const firstPath = workspacePaths.split(path.delimiter)[0];
|
||||
if (firstPath && isValidDirectory(firstPath)) {
|
||||
console.error(`[执行上下文] 使用WORKSPACE_FOLDER_PATHS: ${firstPath}`);
|
||||
return firstPath;
|
||||
}
|
||||
}
|
||||
|
||||
// 策略2:PROMPTX_WORKSPACE(PromptX专用环境变量)
|
||||
const promptxWorkspace = process.env.PROMPTX_WORKSPACE;
|
||||
if (promptxWorkspace && isValidDirectory(promptxWorkspace)) {
|
||||
console.error(`[执行上下文] 使用PROMPTX_WORKSPACE: ${promptxWorkspace}`);
|
||||
return promptxWorkspace;
|
||||
}
|
||||
|
||||
// 策略3:PWD环境变量(某些情况下可用)
|
||||
const pwd = process.env.PWD;
|
||||
if (pwd && isValidDirectory(pwd) && pwd !== process.cwd()) {
|
||||
console.error(`[执行上下文] 使用PWD环境变量: ${pwd}`);
|
||||
return pwd;
|
||||
}
|
||||
|
||||
// 策略4:项目根目录智能推测(向上查找项目标识)
|
||||
const projectRoot = findProjectRoot(process.cwd());
|
||||
if (projectRoot && projectRoot !== process.cwd()) {
|
||||
console.error(`[执行上下文] 智能推测项目根目录: ${projectRoot}`);
|
||||
return projectRoot;
|
||||
}
|
||||
|
||||
// 策略5:回退到process.cwd()
|
||||
console.error(`[执行上下文] 回退到process.cwd(): ${process.cwd()}`);
|
||||
console.error(`[执行上下文] 提示:建议在MCP配置中添加 "env": {"PROMPTX_WORKSPACE": "你的项目目录"}`);
|
||||
return process.cwd();
|
||||
}
|
||||
|
||||
/**
|
||||
* 向上查找项目根目录
|
||||
* @param {string} startDir 开始查找的目录
|
||||
* @returns {string|null} 项目根目录或null
|
||||
*/
|
||||
function findProjectRoot(startDir) {
|
||||
const projectMarkers = [
|
||||
'.promptx',
|
||||
'package.json',
|
||||
'.git',
|
||||
'pyproject.toml',
|
||||
'Cargo.toml',
|
||||
'go.mod',
|
||||
'pom.xml',
|
||||
'build.gradle',
|
||||
'.gitignore'
|
||||
];
|
||||
|
||||
let currentDir = path.resolve(startDir);
|
||||
const root = path.parse(currentDir).root;
|
||||
|
||||
while (currentDir !== root) {
|
||||
// 检查是否包含项目标识文件
|
||||
for (const marker of projectMarkers) {
|
||||
const markerPath = path.join(currentDir, marker);
|
||||
if (fs.existsSync(markerPath)) {
|
||||
return currentDir;
|
||||
}
|
||||
}
|
||||
|
||||
// 向上一级目录
|
||||
const parentDir = path.dirname(currentDir);
|
||||
if (parentDir === currentDir) break; // 防止无限循环
|
||||
currentDir = parentDir;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证目录是否有效
|
||||
* @param {string} dir 要验证的目录路径
|
||||
* @returns {boolean} 目录是否有效
|
||||
*/
|
||||
function isValidDirectory(dir) {
|
||||
try {
|
||||
if (!dir || typeof dir !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const resolvedDir = path.resolve(dir);
|
||||
const stat = fs.statSync(resolvedDir);
|
||||
|
||||
return stat.isDirectory();
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取调试信息
|
||||
* @returns {Object} 调试信息对象
|
||||
*/
|
||||
function getDebugInfo() {
|
||||
const context = getExecutionContext();
|
||||
|
||||
return {
|
||||
processArgv: process.argv,
|
||||
processCwd: process.cwd(),
|
||||
detectedMode: context.mode,
|
||||
detectedWorkingDirectory: context.workingDirectory,
|
||||
environmentVariables: {
|
||||
WORKSPACE_FOLDER_PATHS: process.env.WORKSPACE_FOLDER_PATHS || 'undefined',
|
||||
PROMPTX_WORKSPACE: process.env.PROMPTX_WORKSPACE || 'undefined',
|
||||
PWD: process.env.PWD || 'undefined'
|
||||
},
|
||||
nodeVersion: process.version,
|
||||
platform: process.platform
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
getExecutionContext,
|
||||
isValidDirectory,
|
||||
getDebugInfo
|
||||
};
|
||||
172
src/tests/adapters/mcp-output-adapter.unit.test.js
Normal file
172
src/tests/adapters/mcp-output-adapter.unit.test.js
Normal file
@ -0,0 +1,172 @@
|
||||
const { MCPOutputAdapter } = require('../../lib/adapters/MCPOutputAdapter');
|
||||
|
||||
describe('MCPOutputAdapter 单元测试', () => {
|
||||
let adapter;
|
||||
|
||||
beforeEach(() => {
|
||||
adapter = new MCPOutputAdapter();
|
||||
});
|
||||
|
||||
describe('基础功能测试', () => {
|
||||
test('MCPOutputAdapter类应该能创建', () => {
|
||||
expect(adapter).toBeDefined();
|
||||
expect(adapter).toBeInstanceOf(MCPOutputAdapter);
|
||||
});
|
||||
|
||||
test('应该有convertToMCPFormat方法', () => {
|
||||
expect(typeof adapter.convertToMCPFormat).toBe('function');
|
||||
});
|
||||
|
||||
test('应该有sanitizeText方法', () => {
|
||||
expect(typeof adapter.sanitizeText).toBe('function');
|
||||
});
|
||||
|
||||
test('应该有handleError方法', () => {
|
||||
expect(typeof adapter.handleError).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('文本转换测试', () => {
|
||||
test('应该保留emoji和中文字符', () => {
|
||||
const input = '🎯 PromptX 系统初始化完成!';
|
||||
const result = adapter.convertToMCPFormat(input);
|
||||
|
||||
expect(result.content).toBeDefined();
|
||||
expect(result.content[0].type).toBe('text');
|
||||
expect(result.content[0].text).toContain('🎯');
|
||||
expect(result.content[0].text).toContain('PromptX');
|
||||
});
|
||||
|
||||
test('应该保留markdown格式', () => {
|
||||
const input = '## 🎯 角色激活总结\n✅ **assistant 角色已完全激活!**';
|
||||
const result = adapter.convertToMCPFormat(input);
|
||||
|
||||
expect(result.content[0].text).toContain('##');
|
||||
expect(result.content[0].text).toContain('**');
|
||||
expect(result.content[0].text).toContain('✅');
|
||||
});
|
||||
|
||||
test('应该处理复杂的PromptX输出格式', () => {
|
||||
const input = `============================================================
|
||||
🎯 锦囊目的:激活特定AI角色,分析并生成具体的思维模式、行为模式和知识学习计划
|
||||
============================================================
|
||||
|
||||
📜 锦囊内容:
|
||||
🎭 **角色激活完成:assistant** - 所有技能已自动加载`;
|
||||
|
||||
const result = adapter.convertToMCPFormat(input);
|
||||
|
||||
expect(result.content[0].text).toContain('🎯');
|
||||
expect(result.content[0].text).toContain('📜');
|
||||
expect(result.content[0].text).toContain('🎭');
|
||||
expect(result.content[0].text).toContain('====');
|
||||
});
|
||||
|
||||
test('应该处理多行内容', () => {
|
||||
const input = `行1\n行2\n行3`;
|
||||
const result = adapter.convertToMCPFormat(input);
|
||||
|
||||
expect(result.content[0].text).toContain('行1');
|
||||
expect(result.content[0].text).toContain('行2');
|
||||
expect(result.content[0].text).toContain('行3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('对象输入处理测试', () => {
|
||||
test('应该处理PouchOutput对象', () => {
|
||||
const mockPouchOutput = {
|
||||
toString: () => '🎯 模拟的PouchOutput输出'
|
||||
};
|
||||
|
||||
const result = adapter.convertToMCPFormat(mockPouchOutput);
|
||||
expect(result.content[0].text).toBe('🎯 模拟的PouchOutput输出');
|
||||
});
|
||||
|
||||
test('应该处理普通对象', () => {
|
||||
const input = { message: '测试消息', status: 'success' };
|
||||
const result = adapter.convertToMCPFormat(input);
|
||||
|
||||
expect(result.content[0].text).toContain('message');
|
||||
expect(result.content[0].text).toContain('测试消息');
|
||||
});
|
||||
|
||||
test('应该处理null和undefined', () => {
|
||||
const nullResult = adapter.convertToMCPFormat(null);
|
||||
const undefinedResult = adapter.convertToMCPFormat(undefined);
|
||||
|
||||
expect(nullResult.content[0].text).toBe('null');
|
||||
expect(undefinedResult.content[0].text).toBe('undefined');
|
||||
});
|
||||
});
|
||||
|
||||
describe('错误处理测试', () => {
|
||||
test('应该处理转换错误', () => {
|
||||
const result = adapter.handleError(new Error('测试错误'));
|
||||
|
||||
expect(result.content[0].text).toContain('❌');
|
||||
expect(result.content[0].text).toContain('测试错误');
|
||||
expect(result.isError).toBe(true);
|
||||
});
|
||||
|
||||
test('应该处理未知错误', () => {
|
||||
const result = adapter.handleError('字符串错误');
|
||||
|
||||
expect(result.content[0].text).toContain('❌');
|
||||
expect(result.content[0].text).toContain('字符串错误');
|
||||
expect(result.isError).toBe(true);
|
||||
});
|
||||
|
||||
test('错误输出应该符合MCP格式', () => {
|
||||
const result = adapter.handleError(new Error('测试'));
|
||||
|
||||
expect(result.content).toBeDefined();
|
||||
expect(Array.isArray(result.content)).toBe(true);
|
||||
expect(result.content[0].type).toBe('text');
|
||||
expect(typeof result.content[0].text).toBe('string');
|
||||
});
|
||||
});
|
||||
|
||||
describe('边界情况测试', () => {
|
||||
test('应该处理空字符串', () => {
|
||||
const result = adapter.convertToMCPFormat('');
|
||||
expect(result.content[0].text).toBe('');
|
||||
});
|
||||
|
||||
test('应该处理非常长的文本', () => {
|
||||
const longText = 'a'.repeat(10000);
|
||||
const result = adapter.convertToMCPFormat(longText);
|
||||
expect(result.content[0].text).toBe(longText);
|
||||
});
|
||||
|
||||
test('应该处理特殊字符', () => {
|
||||
const specialChars = '\\n\\r\\t"\'{|}[]()';
|
||||
const result = adapter.convertToMCPFormat(specialChars);
|
||||
expect(result.content[0].text).toContain(specialChars);
|
||||
});
|
||||
});
|
||||
|
||||
describe('输出格式验证测试', () => {
|
||||
test('输出应该始终符合MCP content格式', () => {
|
||||
const inputs = [
|
||||
'simple text',
|
||||
'🎯 emoji text',
|
||||
{ object: 'data' },
|
||||
['array', 'data'],
|
||||
null,
|
||||
undefined
|
||||
];
|
||||
|
||||
inputs.forEach(input => {
|
||||
const result = adapter.convertToMCPFormat(input);
|
||||
|
||||
// 验证MCP标准格式
|
||||
expect(result).toHaveProperty('content');
|
||||
expect(Array.isArray(result.content)).toBe(true);
|
||||
expect(result.content).toHaveLength(1);
|
||||
expect(result.content[0]).toHaveProperty('type', 'text');
|
||||
expect(result.content[0]).toHaveProperty('text');
|
||||
expect(typeof result.content[0].text).toBe('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
308
src/tests/commands/mcp-server.unit.test.js
Normal file
308
src/tests/commands/mcp-server.unit.test.js
Normal file
@ -0,0 +1,308 @@
|
||||
const { exec } = require('child_process');
|
||||
const { promisify } = require('util');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// 测试辅助函数
|
||||
function normalizeOutput(output) {
|
||||
return output
|
||||
.replace(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/g, 'TIMESTAMP')
|
||||
.replace(/\[\d+ms\]/g, '[TIME]')
|
||||
.replace(/PS [^>]+>/g, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
describe('MCP Server 项目结构验证', () => {
|
||||
test('现有CLI入口文件存在', () => {
|
||||
expect(fs.existsSync('src/bin/promptx.js')).toBe(true);
|
||||
});
|
||||
|
||||
test('commands目录已创建', () => {
|
||||
expect(fs.existsSync('src/lib/commands')).toBe(true);
|
||||
});
|
||||
|
||||
test('MCP SDK依赖已安装', () => {
|
||||
const pkg = require('../../../package.json');
|
||||
expect(pkg.dependencies['@modelcontextprotocol/sdk']).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CLI函数调用基线测试', () => {
|
||||
let cli;
|
||||
|
||||
beforeEach(() => {
|
||||
// 重新导入以确保清洁状态
|
||||
delete require.cache[require.resolve('../../lib/core/pouch')];
|
||||
cli = require('../../lib/core/pouch').cli;
|
||||
});
|
||||
|
||||
test('cli.execute函数可用性', () => {
|
||||
expect(typeof cli.execute).toBe('function');
|
||||
});
|
||||
|
||||
test('init命令函数调用', async () => {
|
||||
const result = await cli.execute('init', []);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.toString()).toContain('🎯');
|
||||
}, 10000);
|
||||
|
||||
test('hello命令函数调用', async () => {
|
||||
const result = await cli.execute('hello', []);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.toString()).toContain('🎯');
|
||||
}, 10000);
|
||||
|
||||
test('action命令函数调用', async () => {
|
||||
const result = await cli.execute('action', ['assistant']);
|
||||
expect(result).toBeDefined();
|
||||
expect(result.toString()).toContain('⚡');
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe('MCP适配器单元测试', () => {
|
||||
let mcpServer;
|
||||
|
||||
beforeEach(() => {
|
||||
try {
|
||||
const { MCPServerCommand } = require('../../lib/commands/MCPServerCommand');
|
||||
mcpServer = new MCPServerCommand();
|
||||
} catch (error) {
|
||||
mcpServer = null;
|
||||
}
|
||||
});
|
||||
|
||||
describe('基础结构测试', () => {
|
||||
test('MCPServerCommand类应该能导入', () => {
|
||||
expect(() => {
|
||||
require('../../lib/commands/MCPServerCommand');
|
||||
}).not.toThrow();
|
||||
});
|
||||
|
||||
test('MCPServerCommand应该有必要方法', () => {
|
||||
if (!mcpServer) {
|
||||
expect(true).toBe(true); // 跳过测试如果类还没实现
|
||||
return;
|
||||
}
|
||||
|
||||
expect(typeof mcpServer.execute).toBe('function');
|
||||
expect(typeof mcpServer.getToolDefinitions).toBe('function');
|
||||
expect(typeof mcpServer.convertMCPToCliParams).toBe('function');
|
||||
expect(typeof mcpServer.callTool).toBe('function');
|
||||
expect(typeof mcpServer.log).toBe('function');
|
||||
});
|
||||
|
||||
test('调试模式应该可配置', () => {
|
||||
if (!mcpServer) {
|
||||
expect(true).toBe(true);
|
||||
return;
|
||||
}
|
||||
|
||||
expect(typeof mcpServer.debug).toBe('boolean');
|
||||
expect(typeof mcpServer.log).toBe('function');
|
||||
});
|
||||
});
|
||||
|
||||
describe('参数转换测试', () => {
|
||||
test('promptx_init参数转换', () => {
|
||||
if (!mcpServer) {
|
||||
expect(true).toBe(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = mcpServer.convertMCPToCliParams('promptx_init', {});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
test('promptx_action参数转换', () => {
|
||||
if (!mcpServer) {
|
||||
expect(true).toBe(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = mcpServer.convertMCPToCliParams('promptx_action', {
|
||||
role: 'product-manager'
|
||||
});
|
||||
expect(result).toEqual(['product-manager']);
|
||||
});
|
||||
|
||||
test('promptx_learn参数转换', () => {
|
||||
if (!mcpServer) {
|
||||
expect(true).toBe(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = mcpServer.convertMCPToCliParams('promptx_learn', {
|
||||
resource: 'thought://creativity'
|
||||
});
|
||||
expect(result).toEqual(['thought://creativity']);
|
||||
});
|
||||
|
||||
test('promptx_remember参数转换', () => {
|
||||
if (!mcpServer) {
|
||||
expect(true).toBe(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = mcpServer.convertMCPToCliParams('promptx_remember', {
|
||||
content: '测试内容',
|
||||
tags: '测试 标签'
|
||||
});
|
||||
expect(result).toEqual(['测试内容', '--tags', '测试 标签']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('工具调用测试', () => {
|
||||
test('init工具调用', async () => {
|
||||
if (!mcpServer) {
|
||||
expect(true).toBe(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await mcpServer.callTool('promptx_init', {});
|
||||
expect(result.content).toBeDefined();
|
||||
expect(result.content[0].type).toBe('text');
|
||||
expect(result.content[0].text).toContain('🎯');
|
||||
}, 15000);
|
||||
|
||||
test('hello工具调用', async () => {
|
||||
if (!mcpServer) {
|
||||
expect(true).toBe(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await mcpServer.callTool('promptx_hello', {});
|
||||
expect(result.content).toBeDefined();
|
||||
expect(result.content[0].text).toContain('🎯');
|
||||
}, 15000);
|
||||
|
||||
test('action工具调用', async () => {
|
||||
if (!mcpServer) {
|
||||
expect(true).toBe(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await mcpServer.callTool('promptx_action', {
|
||||
role: 'assistant'
|
||||
});
|
||||
expect(result.content).toBeDefined();
|
||||
expect(result.content[0].text).toContain('⚡');
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
describe('错误处理测试', () => {
|
||||
test('无效工具名处理', async () => {
|
||||
if (!mcpServer) {
|
||||
expect(true).toBe(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await mcpServer.callTool('invalid_tool', {});
|
||||
expect(result.content[0].text).toContain('❌');
|
||||
expect(result.isError).toBe(true);
|
||||
});
|
||||
|
||||
test('缺少必需参数处理', async () => {
|
||||
if (!mcpServer) {
|
||||
expect(true).toBe(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await mcpServer.callTool('promptx_action', {});
|
||||
expect(result.content[0].text).toContain('❌');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('MCP vs CLI 一致性测试', () => {
|
||||
let mcpServer;
|
||||
let cli;
|
||||
|
||||
beforeEach(() => {
|
||||
try {
|
||||
const { MCPServerCommand } = require('../../lib/commands/MCPServerCommand');
|
||||
mcpServer = new MCPServerCommand();
|
||||
cli = require('../../lib/core/pouch').cli;
|
||||
} catch (error) {
|
||||
mcpServer = null;
|
||||
cli = null;
|
||||
}
|
||||
});
|
||||
|
||||
test('init: MCP vs CLI 输出一致性', async () => {
|
||||
if (!mcpServer || !cli) {
|
||||
expect(true).toBe(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// 通过MCP调用
|
||||
const mcpResult = await mcpServer.callTool('promptx_init', {});
|
||||
const mcpOutput = normalizeOutput(mcpResult.content[0].text);
|
||||
|
||||
// 直接CLI函数调用
|
||||
const cliResult = await cli.execute('init', []);
|
||||
const cliOutput = normalizeOutput(cliResult.toString());
|
||||
|
||||
// 验证输出一致性
|
||||
expect(mcpOutput).toBe(cliOutput);
|
||||
}, 15000);
|
||||
|
||||
test('action: MCP vs CLI 输出一致性', async () => {
|
||||
if (!mcpServer || !cli) {
|
||||
expect(true).toBe(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const role = 'assistant';
|
||||
|
||||
const mcpResult = await mcpServer.callTool('promptx_action', { role });
|
||||
const mcpOutput = normalizeOutput(mcpResult.content[0].text);
|
||||
|
||||
const cliResult = await cli.execute('action', [role]);
|
||||
const cliOutput = normalizeOutput(cliResult.toString());
|
||||
|
||||
expect(mcpOutput).toBe(cliOutput);
|
||||
}, 15000);
|
||||
});
|
||||
|
||||
describe('MCP协议通信测试', () => {
|
||||
test('工具定义获取', () => {
|
||||
let mcpServer;
|
||||
try {
|
||||
const { MCPServerCommand } = require('../../lib/commands/MCPServerCommand');
|
||||
mcpServer = new MCPServerCommand();
|
||||
} catch (error) {
|
||||
expect(true).toBe(true); // 跳过如果还没实现
|
||||
return;
|
||||
}
|
||||
|
||||
const tools = mcpServer.getToolDefinitions();
|
||||
expect(tools).toHaveLength(6);
|
||||
|
||||
const toolNames = tools.map(t => t.name);
|
||||
expect(toolNames).toContain('promptx_init');
|
||||
expect(toolNames).toContain('promptx_hello');
|
||||
expect(toolNames).toContain('promptx_action');
|
||||
expect(toolNames).toContain('promptx_learn');
|
||||
expect(toolNames).toContain('promptx_recall');
|
||||
expect(toolNames).toContain('promptx_remember');
|
||||
});
|
||||
|
||||
test('工具Schema验证', () => {
|
||||
let mcpServer;
|
||||
try {
|
||||
const { MCPServerCommand } = require('../../lib/commands/MCPServerCommand');
|
||||
mcpServer = new MCPServerCommand();
|
||||
} catch (error) {
|
||||
expect(true).toBe(true);
|
||||
return;
|
||||
}
|
||||
|
||||
const tools = mcpServer.getToolDefinitions();
|
||||
const actionTool = tools.find(t => t.name === 'promptx_action');
|
||||
|
||||
expect(actionTool.inputSchema.properties.role).toBeDefined();
|
||||
expect(actionTool.inputSchema.required).toContain('role');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user