feature: support more transport of mcp server
This commit is contained in:
@ -8,6 +8,7 @@ const packageJson = require('../../package.json')
|
||||
const { cli } = require('../lib/core/pouch')
|
||||
// 导入MCP Server命令
|
||||
const { MCPServerCommand } = require('../lib/commands/MCPServerCommand')
|
||||
const { MCPStreamableHttpCommand } = require('../lib/commands/MCPStreamableHttpCommand')
|
||||
|
||||
// 创建主程序
|
||||
const program = new Command()
|
||||
@ -66,10 +67,36 @@ program
|
||||
program
|
||||
.command('mcp-server')
|
||||
.description('🔌 启动MCP Server,支持Claude Desktop等AI应用接入')
|
||||
.option('-t, --transport <type>', '传输类型 (stdio|http|sse)', 'stdio')
|
||||
.option('-p, --port <number>', 'HTTP端口号 (仅http/sse传输)', '3000')
|
||||
.option('--host <address>', '绑定地址 (仅http/sse传输)', 'localhost')
|
||||
.option('--cors', '启用CORS (仅http/sse传输)', false)
|
||||
.option('--debug', '启用调试模式', false)
|
||||
.action(async (options) => {
|
||||
try {
|
||||
const mcpServer = new MCPServerCommand();
|
||||
await mcpServer.execute();
|
||||
// 设置调试模式
|
||||
if (options.debug) {
|
||||
process.env.MCP_DEBUG = 'true';
|
||||
}
|
||||
|
||||
// 根据传输类型选择命令
|
||||
if (options.transport === 'stdio') {
|
||||
const mcpServer = new MCPServerCommand();
|
||||
await mcpServer.execute();
|
||||
} else if (options.transport === 'http' || options.transport === 'sse') {
|
||||
const mcpHttpServer = new MCPStreamableHttpCommand();
|
||||
const serverOptions = {
|
||||
transport: options.transport,
|
||||
port: parseInt(options.port),
|
||||
host: options.host,
|
||||
cors: options.cors
|
||||
};
|
||||
|
||||
console.error(chalk.green(`🚀 启动 ${options.transport.toUpperCase()} MCP Server 在 ${options.host}:${options.port}...`));
|
||||
await mcpHttpServer.execute(serverOptions);
|
||||
} else {
|
||||
throw new Error(`不支持的传输类型: ${options.transport}。支持的类型: stdio, http, sse`);
|
||||
}
|
||||
} catch (error) {
|
||||
// 输出到stderr,不污染MCP的stdout通信
|
||||
console.error(chalk.red(`❌ MCP Server 启动失败: ${error.message}`));
|
||||
@ -121,7 +148,9 @@ ${chalk.cyan('示例:')}
|
||||
promptx remember "测试→预发布→生产"
|
||||
|
||||
${chalk.gray('# 7️⃣ 启动MCP服务')}
|
||||
promptx mcp-server
|
||||
promptx mcp-server # stdio传输(默认)
|
||||
promptx mcp-server -t http -p 3000 # HTTP传输
|
||||
promptx mcp-server -t sse -p 3001 # SSE传输
|
||||
|
||||
${chalk.cyan('🔄 PATEOAS状态机:')}
|
||||
每个锦囊输出都包含 PATEOAS 导航,引导 AI 发现下一步操作
|
||||
|
||||
619
src/lib/commands/MCPStreamableHttpCommand.js
Normal file
619
src/lib/commands/MCPStreamableHttpCommand.js
Normal file
@ -0,0 +1,619 @@
|
||||
const express = require('express');
|
||||
const { randomUUID } = require('node:crypto');
|
||||
const { McpServer } = require('@modelcontextprotocol/sdk/server/mcp.js');
|
||||
const { StreamableHTTPServerTransport } = require('@modelcontextprotocol/sdk/server/streamableHttp.js');
|
||||
const { SSEServerTransport } = require('@modelcontextprotocol/sdk/server/sse.js');
|
||||
const { isInitializeRequest } = require('@modelcontextprotocol/sdk/types.js');
|
||||
const { cli } = require('../core/pouch');
|
||||
const { MCPOutputAdapter } = require('../adapters/MCPOutputAdapter');
|
||||
|
||||
/**
|
||||
* MCP Streamable HTTP Server Command
|
||||
* 实现基于 Streamable HTTP 传输的 MCP 服务器
|
||||
* 同时提供 SSE 向后兼容支持
|
||||
*/
|
||||
class MCPStreamableHttpCommand {
|
||||
constructor() {
|
||||
this.name = 'promptx-mcp-streamable-http-server';
|
||||
this.version = '1.0.0';
|
||||
this.transport = 'http';
|
||||
this.port = 3000;
|
||||
this.host = 'localhost';
|
||||
this.transports = {}; // 存储会话传输
|
||||
this.outputAdapter = new MCPOutputAdapter();
|
||||
this.debug = process.env.MCP_DEBUG === 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行命令
|
||||
*/
|
||||
async execute(options = {}) {
|
||||
const {
|
||||
transport = 'http',
|
||||
port = 3000,
|
||||
host = 'localhost'
|
||||
} = options;
|
||||
|
||||
// 验证传输类型
|
||||
if (!['http', 'sse'].includes(transport)) {
|
||||
throw new Error(`Unsupported transport: ${transport}`);
|
||||
}
|
||||
|
||||
// 验证配置
|
||||
this.validatePort(port);
|
||||
this.validateHost(host);
|
||||
|
||||
if (transport === 'http') {
|
||||
return this.startStreamableHttpServer(port, host);
|
||||
} else if (transport === 'sse') {
|
||||
return this.startSSEServer(port, host);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 Streamable HTTP 服务器
|
||||
*/
|
||||
async startStreamableHttpServer(port, host) {
|
||||
this.log(`🚀 启动 Streamable HTTP MCP Server...`);
|
||||
|
||||
const app = express();
|
||||
|
||||
// 中间件设置
|
||||
app.use(express.json());
|
||||
app.use(this.corsMiddleware.bind(this));
|
||||
|
||||
// 健康检查端点
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({
|
||||
status: 'ok',
|
||||
name: this.name,
|
||||
version: this.version,
|
||||
transport: 'http'
|
||||
});
|
||||
});
|
||||
|
||||
// MCP 端点
|
||||
app.post('/mcp', this.handleMCPPostRequest.bind(this));
|
||||
app.get('/mcp', this.handleMCPGetRequest.bind(this));
|
||||
app.delete('/mcp', this.handleMCPDeleteRequest.bind(this));
|
||||
|
||||
// 错误处理中间件
|
||||
app.use(this.errorHandler.bind(this));
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = app.listen(port, host, () => {
|
||||
this.log(`✅ Streamable HTTP MCP Server 运行在 http://${host}:${port}`);
|
||||
this.server = server;
|
||||
resolve(server);
|
||||
});
|
||||
|
||||
server.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* CORS 中间件
|
||||
*/
|
||||
corsMiddleware(req, res, next) {
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept, mcp-session-id');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.sendStatus(200);
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
/**
|
||||
* 错误处理中间件
|
||||
*/
|
||||
errorHandler(error, req, res, next) {
|
||||
this.log('Express 错误处理:', error);
|
||||
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32603,
|
||||
message: 'Internal server error'
|
||||
},
|
||||
id: null
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动 SSE 服务器
|
||||
*/
|
||||
async startSSEServer(port, host) {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
this.log(`🚀 启动 SSE MCP Server...`);
|
||||
|
||||
// 健康检查端点
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', name: this.name, version: this.version, transport: 'sse' });
|
||||
});
|
||||
|
||||
// SSE 端点 - 建立事件流
|
||||
app.get('/mcp', async (req, res) => {
|
||||
await this.handleSSEConnection(req, res);
|
||||
});
|
||||
|
||||
// 消息端点 - 接收客户端 JSON-RPC 消息
|
||||
app.post('/messages', async (req, res) => {
|
||||
await this.handleSSEMessage(req, res);
|
||||
});
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const server = app.listen(port, host, () => {
|
||||
this.log(`✅ SSE MCP Server 运行在 http://${host}:${port}`);
|
||||
resolve(server);
|
||||
});
|
||||
|
||||
server.on('error', reject);
|
||||
this.server = server;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SSE 连接建立
|
||||
*/
|
||||
async handleSSEConnection(req, res) {
|
||||
this.log('建立 SSE 连接');
|
||||
|
||||
try {
|
||||
// 创建 SSE 传输
|
||||
const transport = new SSEServerTransport('/messages', res);
|
||||
const sessionId = transport.sessionId;
|
||||
|
||||
// 存储传输
|
||||
this.transports[sessionId] = transport;
|
||||
|
||||
// 设置关闭处理程序
|
||||
transport.onclose = () => {
|
||||
this.log(`SSE 传输关闭: ${sessionId}`);
|
||||
delete this.transports[sessionId];
|
||||
};
|
||||
|
||||
// 连接到 MCP 服务器
|
||||
const server = this.setupMCPServer();
|
||||
await server.connect(transport);
|
||||
|
||||
this.log(`SSE 流已建立,会话ID: ${sessionId}`);
|
||||
} catch (error) {
|
||||
this.log('建立 SSE 连接错误:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).send('Error establishing SSE connection');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SSE 消息
|
||||
*/
|
||||
async handleSSEMessage(req, res) {
|
||||
this.log('收到 SSE 消息:', req.body);
|
||||
|
||||
try {
|
||||
// 从查询参数获取会话ID
|
||||
const sessionId = req.query.sessionId;
|
||||
|
||||
if (!sessionId) {
|
||||
res.status(400).send('Missing sessionId parameter');
|
||||
return;
|
||||
}
|
||||
|
||||
const transport = this.transports[sessionId];
|
||||
if (!transport) {
|
||||
res.status(404).send('Session not found');
|
||||
return;
|
||||
}
|
||||
|
||||
// 处理消息
|
||||
await transport.handlePostMessage(req, res, req.body);
|
||||
} catch (error) {
|
||||
this.log('处理 SSE 消息错误:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).send('Error handling request');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 MCP 服务器
|
||||
*/
|
||||
setupMCPServer() {
|
||||
const server = new McpServer({
|
||||
name: this.name,
|
||||
version: this.version
|
||||
}, {
|
||||
capabilities: {
|
||||
tools: {},
|
||||
logging: {}
|
||||
}
|
||||
});
|
||||
|
||||
// 注册所有 PromptX 工具
|
||||
this.setupMCPTools(server);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置 MCP 工具
|
||||
*/
|
||||
setupMCPTools(server) {
|
||||
const { z } = require('zod');
|
||||
|
||||
// 注册 promptx_init 工具
|
||||
server.tool('promptx_init', '🏗️ [环境初始化锦囊] 初始化PromptX工作环境,创建配置目录,准备专业能力增强系统', {}, async (args, extra) => {
|
||||
this.log('🔧 调用工具: promptx_init');
|
||||
return await this.callTool('promptx_init', {});
|
||||
});
|
||||
|
||||
// 注册 promptx_hello 工具
|
||||
server.tool('promptx_hello', '👋 [角色发现锦囊] 让AI浏览专业角色库(产品经理、Java开发者、设计师等),当需要专业能力时使用,引导角色激活', {}, async (args, extra) => {
|
||||
this.log('🔧 调用工具: promptx_hello');
|
||||
return await this.callTool('promptx_hello', {});
|
||||
});
|
||||
|
||||
// 注册 promptx_action 工具
|
||||
server.tool('promptx_action', '⚡ [专家变身锦囊] 让AI获得指定专业角色的思维模式和核心能力,即时变身领域专家,开始提供专业服务', {
|
||||
role: z.string().describe('要激活的角色ID,如:copywriter, product-manager, java-backend-developer')
|
||||
}, async (args, extra) => {
|
||||
this.log(`🔧 调用工具: promptx_action 参数: ${JSON.stringify(args)}`);
|
||||
return await this.callTool('promptx_action', args);
|
||||
});
|
||||
|
||||
// 注册 promptx_learn 工具
|
||||
server.tool('promptx_learn', '📚 [专业深化锦囊] 让AI学习特定领域的思维模式和执行模式(如敏捷开发、产品设计),强化当前专家角色能力', {
|
||||
resource: z.string().describe('资源URL,支持格式:thought://creativity, execution://best-practice, knowledge://scrum')
|
||||
}, async (args, extra) => {
|
||||
this.log(`🔧 调用工具: promptx_learn 参数: ${JSON.stringify(args)}`);
|
||||
return await this.callTool('promptx_learn', args);
|
||||
});
|
||||
|
||||
// 注册 promptx_recall 工具
|
||||
server.tool('promptx_recall', '🔍 [经验检索锦囊] 让AI从专业记忆库中检索相关经验和最佳实践,当需要基于历史经验工作时使用', {
|
||||
query: z.string().optional().describe('检索关键词或描述,可选参数,不提供则返回所有记忆')
|
||||
}, async (args, extra) => {
|
||||
this.log(`🔧 调用工具: promptx_recall 参数: ${JSON.stringify(args)}`);
|
||||
return await this.callTool('promptx_recall', args);
|
||||
});
|
||||
|
||||
// 注册 promptx_remember 工具
|
||||
server.tool('promptx_remember', '💾 [知识积累锦囊] 让AI将重要经验和专业知识保存到记忆库,构建可复用的专业知识体系,供未来检索应用', {
|
||||
content: z.string().describe('要保存的重要信息或经验'),
|
||||
tags: z.string().optional().describe('自定义标签,用空格分隔,可选')
|
||||
}, async (args, extra) => {
|
||||
this.log(`🔧 调用工具: promptx_remember 参数: ${JSON.stringify(args)}`);
|
||||
return await this.callTool('promptx_remember', 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']
|
||||
}
|
||||
}
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 MCP POST 请求
|
||||
*/
|
||||
async handleMCPPostRequest(req, res) {
|
||||
this.log('收到 MCP 请求:', req.body);
|
||||
|
||||
try {
|
||||
// 检查现有会话 ID
|
||||
const sessionId = req.headers['mcp-session-id'];
|
||||
let transport;
|
||||
|
||||
if (sessionId && this.transports[sessionId]) {
|
||||
// 复用现有传输
|
||||
transport = this.transports[sessionId];
|
||||
} else if (!sessionId && isInitializeRequest(req.body)) {
|
||||
// 新的初始化请求
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: () => randomUUID(),
|
||||
onsessioninitialized: (sessionId) => {
|
||||
this.log(`会话初始化: ${sessionId}`);
|
||||
this.transports[sessionId] = transport;
|
||||
}
|
||||
});
|
||||
|
||||
// 设置关闭处理程序
|
||||
transport.onclose = () => {
|
||||
const sid = transport.sessionId;
|
||||
if (sid && this.transports[sid]) {
|
||||
this.log(`传输关闭: ${sid}`);
|
||||
delete this.transports[sid];
|
||||
}
|
||||
};
|
||||
|
||||
// 连接到 MCP 服务器
|
||||
const server = this.setupMCPServer();
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
return;
|
||||
} else if (!sessionId && this.isStatelessRequest(req.body)) {
|
||||
// 无状态请求(如 tools/list, prompts/list 等)
|
||||
transport = new StreamableHTTPServerTransport({
|
||||
sessionIdGenerator: undefined // 无状态模式
|
||||
});
|
||||
|
||||
// 连接到 MCP 服务器
|
||||
const server = this.setupMCPServer();
|
||||
await server.connect(transport);
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
return;
|
||||
} else {
|
||||
// 无效请求
|
||||
return res.status(400).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32000,
|
||||
message: 'Bad Request: No valid session ID provided'
|
||||
},
|
||||
id: null
|
||||
});
|
||||
}
|
||||
|
||||
// 处理现有传输的请求
|
||||
await transport.handleRequest(req, res, req.body);
|
||||
} catch (error) {
|
||||
this.log('处理 MCP 请求错误:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
jsonrpc: '2.0',
|
||||
error: {
|
||||
code: -32603,
|
||||
message: 'Internal server error'
|
||||
},
|
||||
id: null
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 MCP GET 请求(SSE)
|
||||
*/
|
||||
async handleMCPGetRequest(req, res) {
|
||||
const sessionId = req.headers['mcp-session-id'];
|
||||
if (!sessionId || !this.transports[sessionId]) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid or missing session ID'
|
||||
});
|
||||
}
|
||||
|
||||
this.log(`建立 SSE 流: ${sessionId}`);
|
||||
const transport = this.transports[sessionId];
|
||||
await transport.handleRequest(req, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 MCP DELETE 请求(会话终止)
|
||||
*/
|
||||
async handleMCPDeleteRequest(req, res) {
|
||||
const sessionId = req.headers['mcp-session-id'];
|
||||
if (!sessionId || !this.transports[sessionId]) {
|
||||
return res.status(400).json({
|
||||
error: 'Invalid or missing session ID'
|
||||
});
|
||||
}
|
||||
|
||||
this.log(`终止会话: ${sessionId}`);
|
||||
try {
|
||||
const transport = this.transports[sessionId];
|
||||
await transport.handleRequest(req, res);
|
||||
} catch (error) {
|
||||
this.log('处理会话终止错误:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({
|
||||
error: 'Error processing session termination'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用工具
|
||||
*/
|
||||
async callTool(toolName, args) {
|
||||
try {
|
||||
// 将 MCP 参数转换为 CLI 函数调用参数
|
||||
const cliArgs = this.convertMCPToCliParams(toolName, args);
|
||||
this.log(`🎯 CLI调用: ${toolName} -> ${JSON.stringify(cliArgs)}`);
|
||||
|
||||
// 直接调用 PromptX CLI 函数
|
||||
const result = await cli.execute(toolName.replace('promptx_', ''), cliArgs, true);
|
||||
this.log(`✅ CLI执行完成: ${toolName}`);
|
||||
|
||||
// 返回新 MCP SDK 格式的响应
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
type: 'text',
|
||||
text: typeof result === 'string' ? result : JSON.stringify(result, null, 2)
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
this.log(`❌ 工具调用失败: ${toolName} - ${error.message}`);
|
||||
throw error; // 让 MCP SDK 处理错误
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换 MCP 参数为 CLI 函数调用参数
|
||||
*/
|
||||
convertMCPToCliParams(toolName, mcpArgs) {
|
||||
const paramMapping = {
|
||||
'promptx_init': () => [],
|
||||
'promptx_hello': () => [],
|
||||
'promptx_action': (args) => args && args.role ? [args.role] : [],
|
||||
'promptx_learn': (args) => args && args.resource ? [args.resource] : [],
|
||||
'promptx_recall': (args) => {
|
||||
if (!args || !args.query || typeof args.query !== 'string' || args.query.trim() === '') {
|
||||
return [];
|
||||
}
|
||||
return [args.query];
|
||||
},
|
||||
'promptx_remember': (args) => {
|
||||
if (!args || !args.content) {
|
||||
throw new Error('content 参数是必需的');
|
||||
}
|
||||
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 || {});
|
||||
}
|
||||
|
||||
/**
|
||||
* 调试日志
|
||||
*/
|
||||
log(message, ...args) {
|
||||
if (this.debug) {
|
||||
console.error(`[MCP DEBUG] ${message}`, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证端口号
|
||||
*/
|
||||
validatePort(port) {
|
||||
if (typeof port !== 'number') {
|
||||
throw new Error('Port must be a number');
|
||||
}
|
||||
if (port < 1 || port > 65535) {
|
||||
throw new Error('Port must be between 1 and 65535');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证主机地址
|
||||
*/
|
||||
validateHost(host) {
|
||||
if (!host || typeof host !== 'string' || host.trim() === '') {
|
||||
throw new Error('Host cannot be empty');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为无状态请求(不需要会话ID)
|
||||
*/
|
||||
isStatelessRequest(requestBody) {
|
||||
if (!requestBody || !requestBody.method) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 这些方法可以无状态处理
|
||||
const statelessMethods = [
|
||||
'tools/list',
|
||||
'prompts/list',
|
||||
'resources/list'
|
||||
];
|
||||
|
||||
return statelessMethods.includes(requestBody.method);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { MCPStreamableHttpCommand };
|
||||
7
src/lib/commands/index.js
Normal file
7
src/lib/commands/index.js
Normal file
@ -0,0 +1,7 @@
|
||||
const { MCPServerCommand } = require('./MCPServerCommand');
|
||||
const { MCPStreamableHttpCommand } = require('./MCPStreamableHttpCommand');
|
||||
|
||||
module.exports = {
|
||||
MCPServerCommand,
|
||||
MCPStreamableHttpCommand
|
||||
};
|
||||
181
src/tests/commands/MCPSSEServer.integration.test.js
Normal file
181
src/tests/commands/MCPSSEServer.integration.test.js
Normal file
@ -0,0 +1,181 @@
|
||||
const { MCPStreamableHttpCommand } = require('../../lib/commands/MCPStreamableHttpCommand');
|
||||
const http = require('http');
|
||||
|
||||
describe('MCP SSE Server Integration Tests', () => {
|
||||
let command;
|
||||
let port;
|
||||
|
||||
beforeEach(() => {
|
||||
command = new MCPStreamableHttpCommand();
|
||||
port = 3001 + Math.floor(Math.random() * 1000);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (command.server && command.server.close) {
|
||||
await new Promise((resolve) => {
|
||||
command.server.close(resolve);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('SSE Transport', () => {
|
||||
it('should start SSE server and handle dual endpoints', async () => {
|
||||
// 启动 SSE 服务器
|
||||
await command.execute({
|
||||
transport: 'sse',
|
||||
port,
|
||||
host: 'localhost'
|
||||
});
|
||||
|
||||
// 等待服务器启动
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
// 测试健康检查端点
|
||||
const healthResponse = await makeHttpRequest({
|
||||
hostname: 'localhost',
|
||||
port,
|
||||
path: '/health',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
expect(healthResponse.statusCode).toBe(200);
|
||||
const healthData = JSON.parse(healthResponse.data);
|
||||
expect(healthData.status).toBe('ok');
|
||||
}, 10000);
|
||||
|
||||
it('should establish SSE stream on GET /mcp', async () => {
|
||||
await command.execute({ transport: 'sse', port, host: 'localhost' });
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
// 尝试建立 SSE 连接
|
||||
const sseResponse = await makeHttpRequest({
|
||||
hostname: 'localhost',
|
||||
port,
|
||||
path: '/mcp',
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'text/event-stream',
|
||||
'Cache-Control': 'no-cache'
|
||||
}
|
||||
});
|
||||
|
||||
expect(sseResponse.statusCode).toBe(200);
|
||||
expect(sseResponse.headers['content-type']).toContain('text/event-stream');
|
||||
}, 10000);
|
||||
|
||||
it('should handle POST messages to /messages endpoint', async () => {
|
||||
await command.execute({ transport: 'sse', port, host: 'localhost' });
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
// 先建立 SSE 连接获取会话ID
|
||||
const sseResponse = await makeHttpRequest({
|
||||
hostname: 'localhost',
|
||||
port,
|
||||
path: '/mcp',
|
||||
method: 'GET',
|
||||
headers: { 'Accept': 'text/event-stream' }
|
||||
});
|
||||
|
||||
// 解析 SSE 响应获取会话ID
|
||||
const sseData = sseResponse.data;
|
||||
const endpointMatch = sseData.match(/event: endpoint\ndata: (.+)/);
|
||||
let sessionId = 'test-session';
|
||||
|
||||
if (endpointMatch) {
|
||||
const endpointData = JSON.parse(endpointMatch[1]);
|
||||
const urlObj = new URL(endpointData.uri);
|
||||
sessionId = urlObj.searchParams.get('sessionId');
|
||||
}
|
||||
|
||||
// 发送初始化请求到 /messages 端点
|
||||
const initRequest = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {},
|
||||
clientInfo: { name: 'test-client', version: '1.0.0' }
|
||||
},
|
||||
id: 1
|
||||
};
|
||||
|
||||
const response = await makeHttpRequest({
|
||||
hostname: 'localhost',
|
||||
port,
|
||||
path: `/messages?sessionId=${sessionId}`,
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
}, JSON.stringify(initRequest));
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
}, 10000);
|
||||
});
|
||||
|
||||
describe('Transport Type Selection', () => {
|
||||
it('should start different transports based on parameter', async () => {
|
||||
// 测试默认 HTTP 传输
|
||||
const httpCommand = new MCPStreamableHttpCommand();
|
||||
const httpPort = port + 100;
|
||||
await httpCommand.execute({ transport: 'http', port: httpPort });
|
||||
|
||||
const httpHealth = await makeHttpRequest({
|
||||
hostname: 'localhost',
|
||||
port: httpPort,
|
||||
path: '/health',
|
||||
method: 'GET'
|
||||
});
|
||||
expect(httpHealth.statusCode).toBe(200);
|
||||
|
||||
// 清理
|
||||
if (httpCommand.server) {
|
||||
await new Promise(resolve => httpCommand.server.close(resolve));
|
||||
}
|
||||
|
||||
// 测试 SSE 传输
|
||||
const sseCommand = new MCPStreamableHttpCommand();
|
||||
const ssePort = port + 200;
|
||||
await sseCommand.execute({ transport: 'sse', port: ssePort });
|
||||
|
||||
const sseHealth = await makeHttpRequest({
|
||||
hostname: 'localhost',
|
||||
port: ssePort,
|
||||
path: '/health',
|
||||
method: 'GET'
|
||||
});
|
||||
expect(sseHealth.statusCode).toBe(200);
|
||||
|
||||
// 清理
|
||||
if (sseCommand.server) {
|
||||
await new Promise(resolve => sseCommand.server.close(resolve));
|
||||
}
|
||||
}, 15000);
|
||||
});
|
||||
});
|
||||
|
||||
// Helper function to make HTTP requests
|
||||
function makeHttpRequest(options, data = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request(options, (res) => {
|
||||
let responseData = '';
|
||||
res.on('data', (chunk) => {
|
||||
responseData += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
resolve({
|
||||
statusCode: res.statusCode,
|
||||
headers: res.headers,
|
||||
data: responseData
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
|
||||
if (data) {
|
||||
req.write(data);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
261
src/tests/commands/MCPStreamableHttpCommand.integration.test.js
Normal file
261
src/tests/commands/MCPStreamableHttpCommand.integration.test.js
Normal file
@ -0,0 +1,261 @@
|
||||
const { MCPStreamableHttpCommand } = require('../../lib/commands/MCPStreamableHttpCommand');
|
||||
const http = require('http');
|
||||
|
||||
describe('MCPStreamableHttpCommand Integration Tests', () => {
|
||||
let command;
|
||||
let server;
|
||||
let port;
|
||||
|
||||
beforeEach(() => {
|
||||
command = new MCPStreamableHttpCommand();
|
||||
port = 3001 + Math.floor(Math.random() * 1000); // 随机端口避免冲突
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (server && server.close) {
|
||||
await new Promise((resolve) => {
|
||||
server.close(resolve);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('Streamable HTTP Server', () => {
|
||||
it('should start server and respond to health check', async () => {
|
||||
// 启动服务器
|
||||
const serverPromise = command.execute({
|
||||
transport: 'http',
|
||||
port,
|
||||
host: 'localhost'
|
||||
});
|
||||
|
||||
// 等待服务器启动
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 发送健康检查请求
|
||||
const response = await makeHttpRequest({
|
||||
hostname: 'localhost',
|
||||
port,
|
||||
path: '/health',
|
||||
method: 'GET'
|
||||
});
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
}, 5000);
|
||||
|
||||
it('should handle MCP initialize request', async () => {
|
||||
// 启动服务器
|
||||
await command.execute({
|
||||
transport: 'http',
|
||||
port,
|
||||
host: 'localhost'
|
||||
});
|
||||
|
||||
// 等待服务器启动
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 发送初始化请求
|
||||
const initRequest = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {},
|
||||
clientInfo: {
|
||||
name: 'test-client',
|
||||
version: '1.0.0'
|
||||
}
|
||||
},
|
||||
id: 1
|
||||
};
|
||||
|
||||
const response = await makeHttpRequest({
|
||||
hostname: 'localhost',
|
||||
port,
|
||||
path: '/mcp',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json, text/event-stream'
|
||||
}
|
||||
}, JSON.stringify(initRequest));
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const responseData = JSON.parse(response.data);
|
||||
expect(responseData.jsonrpc).toBe('2.0');
|
||||
expect(responseData.id).toBe(1);
|
||||
}, 5000);
|
||||
|
||||
it('should handle tools/list request', async () => {
|
||||
// 启动服务器
|
||||
await command.execute({
|
||||
transport: 'http',
|
||||
port,
|
||||
host: 'localhost'
|
||||
});
|
||||
|
||||
// 等待服务器启动
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 先初始化
|
||||
const initRequest = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'initialize',
|
||||
params: {
|
||||
protocolVersion: '2024-11-05',
|
||||
capabilities: {},
|
||||
clientInfo: { name: 'test-client', version: '1.0.0' }
|
||||
},
|
||||
id: 1
|
||||
};
|
||||
|
||||
const initResponse = await makeHttpRequest({
|
||||
hostname: 'localhost',
|
||||
port,
|
||||
path: '/mcp',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json, text/event-stream'
|
||||
}
|
||||
}, JSON.stringify(initRequest));
|
||||
|
||||
const sessionId = JSON.parse(initResponse.data).result?.sessionId;
|
||||
|
||||
// 发送工具列表请求
|
||||
const toolsRequest = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'tools/list',
|
||||
params: {},
|
||||
id: 2
|
||||
};
|
||||
|
||||
const response = await makeHttpRequest({
|
||||
hostname: 'localhost',
|
||||
port,
|
||||
path: '/mcp',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json, text/event-stream',
|
||||
'mcp-session-id': sessionId || 'test-session'
|
||||
}
|
||||
}, JSON.stringify(toolsRequest));
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const responseData = JSON.parse(response.data);
|
||||
expect(responseData.result.tools).toBeDefined();
|
||||
expect(Array.isArray(responseData.result.tools)).toBe(true);
|
||||
expect(responseData.result.tools.length).toBe(6);
|
||||
}, 5000);
|
||||
|
||||
it('should handle tool call request', async () => {
|
||||
// 启动服务器
|
||||
await command.execute({
|
||||
transport: 'http',
|
||||
port,
|
||||
host: 'localhost'
|
||||
});
|
||||
|
||||
// 等待服务器启动
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
// 发送工具调用请求
|
||||
const toolCallRequest = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'promptx_hello',
|
||||
arguments: {}
|
||||
},
|
||||
id: 3
|
||||
};
|
||||
|
||||
const response = await makeHttpRequest({
|
||||
hostname: 'localhost',
|
||||
port,
|
||||
path: '/mcp',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json, text/event-stream',
|
||||
'mcp-session-id': 'test-session'
|
||||
}
|
||||
}, JSON.stringify(toolCallRequest));
|
||||
|
||||
expect(response.statusCode).toBe(200);
|
||||
const responseData = JSON.parse(response.data);
|
||||
expect(responseData.result).toBeDefined();
|
||||
}, 5000);
|
||||
});
|
||||
|
||||
describe('Error Handling', () => {
|
||||
it('should handle invalid JSON requests', async () => {
|
||||
await command.execute({ transport: 'http', port, host: 'localhost' });
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const response = await makeHttpRequest({
|
||||
hostname: 'localhost',
|
||||
port,
|
||||
path: '/mcp',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json, text/event-stream'
|
||||
}
|
||||
}, 'invalid json');
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
}, 5000);
|
||||
|
||||
it('should handle missing session ID for non-initialize requests', async () => {
|
||||
await command.execute({ transport: 'http', port, host: 'localhost' });
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const request = {
|
||||
jsonrpc: '2.0',
|
||||
method: 'tools/list',
|
||||
params: {},
|
||||
id: 1
|
||||
};
|
||||
|
||||
const response = await makeHttpRequest({
|
||||
hostname: 'localhost',
|
||||
port,
|
||||
path: '/mcp',
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Accept': 'application/json, text/event-stream'
|
||||
}
|
||||
}, JSON.stringify(request));
|
||||
|
||||
expect(response.statusCode).toBe(400);
|
||||
}, 5000);
|
||||
});
|
||||
});
|
||||
|
||||
// Helper function to make HTTP requests
|
||||
function makeHttpRequest(options, data = null) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.request(options, (res) => {
|
||||
let responseData = '';
|
||||
res.on('data', (chunk) => {
|
||||
responseData += chunk;
|
||||
});
|
||||
res.on('end', () => {
|
||||
resolve({
|
||||
statusCode: res.statusCode,
|
||||
headers: res.headers,
|
||||
data: responseData
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', reject);
|
||||
|
||||
if (data) {
|
||||
req.write(data);
|
||||
}
|
||||
req.end();
|
||||
});
|
||||
}
|
||||
178
src/tests/commands/MCPStreamableHttpCommand.unit.test.js
Normal file
178
src/tests/commands/MCPStreamableHttpCommand.unit.test.js
Normal file
@ -0,0 +1,178 @@
|
||||
const { MCPStreamableHttpCommand } = require('../../lib/commands/MCPStreamableHttpCommand');
|
||||
|
||||
describe('MCPStreamableHttpCommand', () => {
|
||||
let command;
|
||||
|
||||
beforeEach(() => {
|
||||
command = new MCPStreamableHttpCommand();
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should initialize with correct name and version', () => {
|
||||
expect(command.name).toBe('promptx-mcp-streamable-http-server');
|
||||
expect(command.version).toBe('1.0.0');
|
||||
});
|
||||
|
||||
it('should have default configuration', () => {
|
||||
expect(command.transport).toBe('http');
|
||||
expect(command.port).toBe(3000);
|
||||
expect(command.host).toBe('localhost');
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
it('should throw error when transport type is unsupported', async () => {
|
||||
await expect(command.execute({ transport: 'unsupported' }))
|
||||
.rejects
|
||||
.toThrow('Unsupported transport: unsupported');
|
||||
});
|
||||
|
||||
it('should start Streamable HTTP server with default options', async () => {
|
||||
const mockStartStreamableHttpServer = jest.fn().mockResolvedValue();
|
||||
command.startStreamableHttpServer = mockStartStreamableHttpServer;
|
||||
|
||||
await command.execute();
|
||||
|
||||
expect(mockStartStreamableHttpServer).toHaveBeenCalledWith(3000, 'localhost');
|
||||
});
|
||||
|
||||
it('should start Streamable HTTP server with custom options', async () => {
|
||||
const mockStartStreamableHttpServer = jest.fn().mockResolvedValue();
|
||||
command.startStreamableHttpServer = mockStartStreamableHttpServer;
|
||||
|
||||
await command.execute({ transport: 'http', port: 4000, host: '0.0.0.0' });
|
||||
|
||||
expect(mockStartStreamableHttpServer).toHaveBeenCalledWith(4000, '0.0.0.0');
|
||||
});
|
||||
|
||||
it('should start SSE server when transport is sse', async () => {
|
||||
const mockStartSSEServer = jest.fn().mockResolvedValue();
|
||||
command.startSSEServer = mockStartSSEServer;
|
||||
|
||||
await command.execute({ transport: 'sse', port: 3001 });
|
||||
|
||||
expect(mockStartSSEServer).toHaveBeenCalledWith(3001, 'localhost');
|
||||
});
|
||||
});
|
||||
|
||||
describe('startStreamableHttpServer', () => {
|
||||
it('should create Express app and listen on specified port', async () => {
|
||||
// Mock Express
|
||||
const mockApp = {
|
||||
use: jest.fn(),
|
||||
post: jest.fn(),
|
||||
get: jest.fn(),
|
||||
delete: jest.fn(),
|
||||
listen: jest.fn((port, callback) => callback())
|
||||
};
|
||||
const mockExpress = jest.fn(() => mockApp);
|
||||
mockExpress.json = jest.fn();
|
||||
|
||||
// Mock the method to avoid actual server startup
|
||||
const originalMethod = command.startStreamableHttpServer;
|
||||
command.startStreamableHttpServer = jest.fn().mockImplementation(async (port, host) => {
|
||||
expect(port).toBe(3000);
|
||||
expect(host).toBe('localhost');
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
await command.startStreamableHttpServer(3000, 'localhost');
|
||||
|
||||
expect(command.startStreamableHttpServer).toHaveBeenCalledWith(3000, 'localhost');
|
||||
});
|
||||
});
|
||||
|
||||
describe('startSSEServer', () => {
|
||||
it('should create Express app with dual endpoints', async () => {
|
||||
// Mock the method to avoid actual server startup
|
||||
command.startSSEServer = jest.fn().mockImplementation(async (port, host) => {
|
||||
expect(port).toBe(3000);
|
||||
expect(host).toBe('localhost');
|
||||
return Promise.resolve();
|
||||
});
|
||||
|
||||
await command.startSSEServer(3000, 'localhost');
|
||||
|
||||
expect(command.startSSEServer).toHaveBeenCalledWith(3000, 'localhost');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setupMCPServer', () => {
|
||||
it('should create MCP server with correct configuration', () => {
|
||||
const server = command.setupMCPServer();
|
||||
|
||||
expect(server).toBeDefined();
|
||||
// We'll verify the server has the correct tools in integration tests
|
||||
});
|
||||
});
|
||||
|
||||
describe('getToolDefinitions', () => {
|
||||
it('should return all PromptX tools', () => {
|
||||
const tools = command.getToolDefinitions();
|
||||
|
||||
expect(Array.isArray(tools)).toBe(true);
|
||||
expect(tools.length).toBe(6); // All PromptX tools
|
||||
|
||||
const toolNames = tools.map(tool => tool.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');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleMCPRequest', () => {
|
||||
it('should handle tool calls correctly', async () => {
|
||||
const mockReq = {
|
||||
body: {
|
||||
jsonrpc: '2.0',
|
||||
method: 'tools/call',
|
||||
params: {
|
||||
name: 'promptx_hello',
|
||||
arguments: {}
|
||||
},
|
||||
id: 1
|
||||
},
|
||||
headers: {}
|
||||
};
|
||||
|
||||
const mockRes = {
|
||||
json: jest.fn(),
|
||||
status: jest.fn().mockReturnThis(),
|
||||
headersSent: false
|
||||
};
|
||||
|
||||
// Mock CLI execution
|
||||
const mockCli = {
|
||||
execute: jest.fn().mockResolvedValue('Hello response')
|
||||
};
|
||||
|
||||
command.cli = mockCli;
|
||||
command.handleMCPRequest = jest.fn().mockImplementation(async (req, res) => {
|
||||
expect(req.body.method).toBe('tools/call');
|
||||
res.json({ result: 'success' });
|
||||
});
|
||||
|
||||
await command.handleMCPRequest(mockReq, mockRes);
|
||||
|
||||
expect(command.handleMCPRequest).toHaveBeenCalledWith(mockReq, mockRes);
|
||||
});
|
||||
});
|
||||
|
||||
describe('configuration validation', () => {
|
||||
it('should validate port number', () => {
|
||||
expect(() => command.validatePort(3000)).not.toThrow();
|
||||
expect(() => command.validatePort('invalid')).toThrow('Port must be a number');
|
||||
expect(() => command.validatePort(70000)).toThrow('Port must be between 1 and 65535');
|
||||
});
|
||||
|
||||
it('should validate host address', () => {
|
||||
expect(() => command.validateHost('localhost')).not.toThrow();
|
||||
expect(() => command.validateHost('0.0.0.0')).not.toThrow();
|
||||
expect(() => command.validateHost('192.168.1.1')).not.toThrow();
|
||||
expect(() => command.validateHost('')).toThrow('Host cannot be empty');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user