freature: 支持mcp 协议

This commit is contained in:
7.
2025-06-06 16:16:12 +08:00
parent 8d34022d31
commit 11824a5ff3
12 changed files with 3598 additions and 154 deletions

View 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 };

View 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: '要激活的角色IDcopywriter, 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 };

View File

@ -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
}
}

View 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() {
// 策略1WORKSPACE_FOLDER_PATHSVS 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;
}
}
// 策略2PROMPTX_WORKSPACEPromptX专用环境变量
const promptxWorkspace = process.env.PROMPTX_WORKSPACE;
if (promptxWorkspace && isValidDirectory(promptxWorkspace)) {
console.error(`[执行上下文] 使用PROMPTX_WORKSPACE: ${promptxWorkspace}`);
return promptxWorkspace;
}
// 策略3PWD环境变量某些情况下可用
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
};