feature: support more transport of mcp server
This commit is contained in:
11
docs/claude-desktop-config-simple.json
Normal file
11
docs/claude-desktop-config-simple.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"promptx": {
|
||||||
|
"command": "node",
|
||||||
|
"args": [
|
||||||
|
"/Users/YOUR_USERNAME/WorkSpaces/DeepracticeProjects/PromptX/src/bin/promptx.js",
|
||||||
|
"mcp-server"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
11
docs/claude-desktop-config-windows.json
Normal file
11
docs/claude-desktop-config-windows.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"promptx": {
|
||||||
|
"command": "node",
|
||||||
|
"args": [
|
||||||
|
"C:\\Users\\YOUR_USERNAME\\WorkSpaces\\DeepracticeProjects\\PromptX\\src\\bin\\promptx.js",
|
||||||
|
"mcp-server"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
291
docs/mcp-http-sse-implementation-plan.md
Normal file
291
docs/mcp-http-sse-implementation-plan.md
Normal file
@ -0,0 +1,291 @@
|
|||||||
|
# MCP Streamable HTTP 传输实现规划
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档规划在 PromptX 项目中实现 MCP (Model Context Protocol) Streamable HTTP 传输的技术方案,同时提供 SSE 向后兼容支持。
|
||||||
|
|
||||||
|
## 背景分析
|
||||||
|
|
||||||
|
### 当前状态
|
||||||
|
- PromptX 目前仅支持 stdio 传输方式 (`MCPServerCommand.js`)
|
||||||
|
- 使用 `@modelcontextprotocol/sdk@1.12.1`,已包含 SSE 传输支持
|
||||||
|
- 启动方式:`pnpm start mcp-server` (默认 stdio)
|
||||||
|
|
||||||
|
### 需求驱动
|
||||||
|
- 需要支持基于 HTTP 的 MCP 服务器实例
|
||||||
|
- 为 Web 客户端和远程访问提供现代化支持
|
||||||
|
- 采用最新 MCP 协议标准,确保长期兼容性
|
||||||
|
- 提供更灵活的部署选项
|
||||||
|
|
||||||
|
## 技术方案
|
||||||
|
|
||||||
|
### 传输协议选择
|
||||||
|
|
||||||
|
#### Streamable HTTP 传输(主要方案)
|
||||||
|
- **状态**: MCP 协议当前推荐的标准传输方式
|
||||||
|
- **特点**:
|
||||||
|
- 统一 HTTP POST 端点
|
||||||
|
- 无状态连接,支持 SSE 可选升级
|
||||||
|
- 支持会话管理和连接恢复
|
||||||
|
- **优势**:
|
||||||
|
- 现代化架构,更好的可扩展性
|
||||||
|
- 简化客户端实现
|
||||||
|
- 更好的负载均衡支持
|
||||||
|
- 符合 REST 架构原则
|
||||||
|
|
||||||
|
#### SSE 传输(兼容方案)
|
||||||
|
- **状态**: 在协议版本 2024-11-05 中被标记为弃用
|
||||||
|
- **特点**: 双端点架构(GET 建立 SSE 流,POST 接收消息)
|
||||||
|
- **适用**: 向后兼容现有客户端,过渡期使用
|
||||||
|
|
||||||
|
### 实现架构
|
||||||
|
|
||||||
|
#### 方案一:扩展现有 MCPServerCommand
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- 保持代码统一性
|
||||||
|
- 复用现有逻辑
|
||||||
|
- 最小化改动
|
||||||
|
|
||||||
|
**实现路径**:
|
||||||
|
```javascript
|
||||||
|
// MCPServerCommand.js 修改
|
||||||
|
async execute(options = {}) {
|
||||||
|
const { transport = 'stdio', port = 3000 } = options;
|
||||||
|
|
||||||
|
switch (transport) {
|
||||||
|
case 'stdio':
|
||||||
|
return this.startStdioServer();
|
||||||
|
case 'http':
|
||||||
|
return this.startStreamableHttpServer(port);
|
||||||
|
case 'sse':
|
||||||
|
return this.startSSEServer(port); // 兼容支持
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported transport: ${transport}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方案二:创建专用 HTTP 服务器命令
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- 职责分离,代码清晰
|
||||||
|
- 便于独立测试和维护
|
||||||
|
- 避免原有功能的副作用
|
||||||
|
|
||||||
|
**实现路径**:
|
||||||
|
```
|
||||||
|
src/lib/commands/
|
||||||
|
├── MCPServerCommand.js # stdio 传输
|
||||||
|
├── MCPStreamableHttpCommand.js # Streamable HTTP 传输(主要)
|
||||||
|
└── index.js # 命令导出
|
||||||
|
```
|
||||||
|
|
||||||
|
### 详细设计
|
||||||
|
|
||||||
|
#### Streamable HTTP 服务器实现
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 基础架构
|
||||||
|
class MCPStreamableHttpCommand {
|
||||||
|
constructor() {
|
||||||
|
this.name = 'promptx-mcp-streamable-http-server';
|
||||||
|
this.version = '1.0.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(options = {}) {
|
||||||
|
const {
|
||||||
|
transport = 'http', // 'http' | 'sse'
|
||||||
|
port = 3000,
|
||||||
|
host = 'localhost'
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
if (transport === 'http') {
|
||||||
|
return this.startStreamableHttpServer(port, host);
|
||||||
|
} else if (transport === 'sse') {
|
||||||
|
return this.startSSEServer(port, host); // 兼容支持
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async startStreamableHttpServer(port, host) {
|
||||||
|
// 使用 StreamableHttpServerTransport
|
||||||
|
// 实现现代化统一端点架构
|
||||||
|
}
|
||||||
|
|
||||||
|
async startSSEServer(port, host) {
|
||||||
|
// 使用 Express + SSEServerTransport
|
||||||
|
// 向后兼容双端点架构
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 端点设计
|
||||||
|
|
||||||
|
**Streamable HTTP 端点**(主要):
|
||||||
|
- `POST /mcp` - 统一入口端点
|
||||||
|
- 接收所有 JSON-RPC 消息
|
||||||
|
- 支持可选 SSE 流式响应
|
||||||
|
- 支持会话管理(sessionId)
|
||||||
|
- 无状态设计,便于负载均衡
|
||||||
|
|
||||||
|
**SSE 传输端点**(兼容):
|
||||||
|
- `GET /mcp` - 建立 SSE 连接
|
||||||
|
- `POST /messages` - 接收客户端消息
|
||||||
|
|
||||||
|
#### 配置选项
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 命令行参数
|
||||||
|
{
|
||||||
|
transport: 'stdio' | 'http' | 'sse', // 'http' 为推荐默认值
|
||||||
|
port: number, // HTTP 端口 (默认: 3000)
|
||||||
|
host: string, // 绑定地址 (默认: localhost)
|
||||||
|
cors: boolean, // CORS 支持 (默认: false)
|
||||||
|
auth: boolean, // 认证开关 (默认: false)
|
||||||
|
streaming: boolean, // SSE 流式响应 (默认: true)
|
||||||
|
maxConnections: number // 最大连接数 (默认: 100)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实现计划
|
||||||
|
|
||||||
|
### 阶段 1: Streamable HTTP 传输支持(主要目标)
|
||||||
|
|
||||||
|
**目标**: 实现 MCP 推荐的 Streamable HTTP 传输
|
||||||
|
|
||||||
|
**任务**:
|
||||||
|
1. 创建 `MCPStreamableHttpCommand.js`
|
||||||
|
2. 实现 StreamableHttpServerTransport 集成
|
||||||
|
3. 支持统一端点架构和可选 SSE 升级
|
||||||
|
4. 集成现有 MCP 工具处理逻辑
|
||||||
|
5. 添加命令行参数支持
|
||||||
|
6. 编写单元测试
|
||||||
|
|
||||||
|
**预期成果**:
|
||||||
|
```bash
|
||||||
|
# 启动 Streamable HTTP 服务器
|
||||||
|
pnpm start mcp-server --transport http --port 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 阶段 2: SSE 传输兼容支持
|
||||||
|
|
||||||
|
**目标**: 实现 SSE 传输的向后兼容
|
||||||
|
|
||||||
|
**任务**:
|
||||||
|
1. 在同一命令中添加 SSE 传输支持
|
||||||
|
2. 实现 SSE 双端点架构
|
||||||
|
3. 添加传输类型切换逻辑
|
||||||
|
4. 性能优化和错误处理
|
||||||
|
5. 兼容性测试
|
||||||
|
|
||||||
|
**预期成果**:
|
||||||
|
```bash
|
||||||
|
# 启动 SSE 服务器(兼容模式)
|
||||||
|
pnpm start mcp-server --transport sse --port 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 阶段 3: 生产化增强
|
||||||
|
|
||||||
|
**目标**: 完善生产环境特性
|
||||||
|
|
||||||
|
**任务**:
|
||||||
|
1. CORS 跨域支持
|
||||||
|
2. 认证机制集成
|
||||||
|
3. 连接池和限流
|
||||||
|
4. 监控和日志增强
|
||||||
|
5. Docker 部署支持
|
||||||
|
|
||||||
|
**预期成果**:
|
||||||
|
- 生产就绪的 Streamable HTTP MCP 服务器
|
||||||
|
- 完整的部署文档
|
||||||
|
- 性能基准测试报告
|
||||||
|
|
||||||
|
## 配置管理
|
||||||
|
|
||||||
|
### 环境变量支持
|
||||||
|
```bash
|
||||||
|
MCP_TRANSPORT=http # 传输类型(推荐默认值)
|
||||||
|
MCP_PORT=3000 # 服务端口
|
||||||
|
MCP_HOST=localhost # 绑定地址
|
||||||
|
MCP_CORS_ENABLED=false # CORS 开关
|
||||||
|
MCP_STREAMING=true # SSE 流式响应
|
||||||
|
MCP_MAX_CONNECTIONS=100 # 最大连接数
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## 测试策略
|
||||||
|
|
||||||
|
### 单元测试
|
||||||
|
- 传输类型选择逻辑
|
||||||
|
- HTTP 端点处理
|
||||||
|
- 错误处理机制
|
||||||
|
- 参数验证
|
||||||
|
|
||||||
|
### 集成测试
|
||||||
|
- 完整 MCP 会话流程
|
||||||
|
- 多客户端并发连接
|
||||||
|
- 传输协议兼容性
|
||||||
|
- 工具调用端到端测试
|
||||||
|
|
||||||
|
|
||||||
|
## 部署考虑
|
||||||
|
|
||||||
|
### 开发环境
|
||||||
|
- 本地调试支持
|
||||||
|
- 热重载机制
|
||||||
|
- 详细日志输出
|
||||||
|
|
||||||
|
### 生产环境
|
||||||
|
- 进程管理 (PM2)
|
||||||
|
- 反向代理 (Nginx)
|
||||||
|
- HTTPS 支持
|
||||||
|
- 监控告警
|
||||||
|
|
||||||
|
## 兼容性
|
||||||
|
|
||||||
|
### MCP 客户端兼容性
|
||||||
|
- Claude Desktop
|
||||||
|
- MCP Inspector
|
||||||
|
- 自定义 MCP 客户端
|
||||||
|
|
||||||
|
### 协议版本兼容性
|
||||||
|
- 支持当前协议版本
|
||||||
|
- 向后兼容弃用特性
|
||||||
|
- 平滑迁移路径
|
||||||
|
|
||||||
|
## 风险评估
|
||||||
|
|
||||||
|
### 技术风险
|
||||||
|
- SSE 传输弃用风险 → 优先实现 Streamable HTTP
|
||||||
|
- 并发性能瓶颈 → 连接池和限流机制
|
||||||
|
- 内存泄漏风险 → 完善资源清理
|
||||||
|
|
||||||
|
### 维护风险
|
||||||
|
- 代码复杂度增加 → 清晰的架构分层
|
||||||
|
- 测试覆盖率下降 → 完善的测试策略
|
||||||
|
|
||||||
|
## 成功指标
|
||||||
|
|
||||||
|
### 功能指标
|
||||||
|
- [ ] 支持 Streamable HTTP 传输启动
|
||||||
|
- [ ] 支持 SSE 兼容传输
|
||||||
|
- [ ] 多传输类型无缝切换
|
||||||
|
- [ ] 完整的工具调用功能
|
||||||
|
|
||||||
|
### 性能指标
|
||||||
|
- 支持 > 50 并发连接
|
||||||
|
- 消息延迟 < 100ms
|
||||||
|
- 内存使用 < 500MB
|
||||||
|
|
||||||
|
### 质量指标
|
||||||
|
- 测试覆盖率 > 80%
|
||||||
|
- 零安全漏洞
|
||||||
|
- 完整的文档覆盖
|
||||||
|
|
||||||
|
## 参考资料
|
||||||
|
|
||||||
|
- [MCP 官方文档 - Transports](https://modelcontextprotocol.io/docs/concepts/transports)
|
||||||
|
- [MCP SDK 示例 - Streamable HTTP Server](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/simpleStreamableHttp.js)
|
||||||
|
- [MCP SDK 示例 - SSE Server](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/simpleSseServer.js)
|
||||||
|
- [Streamable HTTP 实现指南](https://blog.christianposta.com/ai/understanding-mcp-recent-change-around-http-sse/)
|
||||||
|
- [MCP 协议变更说明](https://blog.christianposta.com/ai/understanding-mcp-recent-change-around-http-sse/)
|
||||||
864
docs/mcp-streamable-http-implementation-plan.md
Normal file
864
docs/mcp-streamable-http-implementation-plan.md
Normal file
@ -0,0 +1,864 @@
|
|||||||
|
# MCP Streamable HTTP 传输实现规划
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档规划在 PromptX 项目中实现 MCP (Model Context Protocol) Streamable HTTP 传输的技术方案,同时提供 SSE 向后兼容支持。
|
||||||
|
|
||||||
|
## 背景分析
|
||||||
|
|
||||||
|
### 当前状态
|
||||||
|
- PromptX 目前仅支持 stdio 传输方式 (`MCPServerCommand.js`)
|
||||||
|
- 使用 `@modelcontextprotocol/sdk@1.12.1`,已包含 SSE 传输支持
|
||||||
|
- 启动方式:`pnpm start mcp-server` (默认 stdio)
|
||||||
|
|
||||||
|
### 需求驱动
|
||||||
|
- 需要支持基于 HTTP 的 MCP 服务器实例
|
||||||
|
- 为 Web 客户端和远程访问提供现代化支持
|
||||||
|
- 采用最新 MCP 协议标准,确保长期兼容性
|
||||||
|
- 提供更灵活的部署选项
|
||||||
|
|
||||||
|
## 技术方案
|
||||||
|
|
||||||
|
### 依赖管理
|
||||||
|
|
||||||
|
基于官方示例和稳定性考虑,本实现使用 Express.js 框架:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# MCP SDK(已安装)
|
||||||
|
@modelcontextprotocol/sdk@1.12.1
|
||||||
|
|
||||||
|
# Express 框架(新增)
|
||||||
|
express@^5.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
**选择 Express.js 的原因:**
|
||||||
|
1. **官方示例一致性** - MCP SDK 官方示例均使用 Express.js
|
||||||
|
2. **测试稳定性** - Express 提供更完善的中间件和错误处理机制
|
||||||
|
3. **开发效率** - 简化 CORS、JSON 解析等常见 HTTP 处理需求
|
||||||
|
4. **社区支持** - 成熟的生态系统和丰富的文档资源
|
||||||
|
|
||||||
|
### 传输协议选择
|
||||||
|
|
||||||
|
#### Streamable HTTP 传输(主要方案)
|
||||||
|
- **状态**: MCP 协议当前推荐的标准传输方式
|
||||||
|
- **特点**:
|
||||||
|
- 统一 HTTP POST 端点
|
||||||
|
- 无状态连接,支持 SSE 可选升级
|
||||||
|
- 支持会话管理和连接恢复
|
||||||
|
- **优势**:
|
||||||
|
- 现代化架构,更好的可扩展性
|
||||||
|
- 简化客户端实现
|
||||||
|
- 更好的负载均衡支持
|
||||||
|
- 符合 REST 架构原则
|
||||||
|
|
||||||
|
#### SSE 传输(兼容方案)
|
||||||
|
- **状态**: 在协议版本 2024-11-05 中被标记为弃用
|
||||||
|
- **特点**: 双端点架构(GET 建立 SSE 流,POST 接收消息)
|
||||||
|
- **适用**: 向后兼容现有客户端,过渡期使用
|
||||||
|
|
||||||
|
### 实现架构
|
||||||
|
|
||||||
|
#### 方案一:扩展现有 MCPServerCommand
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- 保持代码统一性
|
||||||
|
- 复用现有逻辑
|
||||||
|
- 最小化改动
|
||||||
|
|
||||||
|
**实现路径**:
|
||||||
|
```javascript
|
||||||
|
// MCPServerCommand.js 修改
|
||||||
|
async execute(options = {}) {
|
||||||
|
const { transport = 'stdio', port = 3000 } = options;
|
||||||
|
|
||||||
|
switch (transport) {
|
||||||
|
case 'stdio':
|
||||||
|
return this.startStdioServer();
|
||||||
|
case 'http':
|
||||||
|
return this.startStreamableHttpServer(port);
|
||||||
|
case 'sse':
|
||||||
|
return this.startSSEServer(port); // 兼容支持
|
||||||
|
default:
|
||||||
|
throw new Error(`Unsupported transport: ${transport}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方案二:创建专用 HTTP 服务器命令
|
||||||
|
|
||||||
|
**优势**:
|
||||||
|
- 职责分离,代码清晰
|
||||||
|
- 便于独立测试和维护
|
||||||
|
- 避免原有功能的副作用
|
||||||
|
|
||||||
|
**实现路径**:
|
||||||
|
```
|
||||||
|
src/lib/commands/
|
||||||
|
├── MCPServerCommand.js # stdio 传输
|
||||||
|
├── MCPStreamableHttpCommand.js # Streamable HTTP 传输(主要)
|
||||||
|
└── index.js # 命令导出
|
||||||
|
```
|
||||||
|
|
||||||
|
### 详细设计
|
||||||
|
|
||||||
|
#### Streamable HTTP 服务器实现
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 基础架构
|
||||||
|
class MCPStreamableHttpCommand {
|
||||||
|
constructor() {
|
||||||
|
this.name = 'promptx-mcp-streamable-http-server';
|
||||||
|
this.version = '1.0.0';
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(options = {}) {
|
||||||
|
const {
|
||||||
|
transport = 'http', // 'http' | 'sse'
|
||||||
|
port = 3000,
|
||||||
|
host = 'localhost'
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
if (transport === 'http') {
|
||||||
|
return this.startStreamableHttpServer(port, host);
|
||||||
|
} else if (transport === 'sse') {
|
||||||
|
return this.startSSEServer(port, host); // 兼容支持
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async startStreamableHttpServer(port, host) {
|
||||||
|
// 使用 Express + StreamableHttpServerTransport
|
||||||
|
// 实现现代化统一端点架构
|
||||||
|
const app = express();
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(corsMiddleware);
|
||||||
|
app.post('/mcp', handleMCPPostRequest);
|
||||||
|
// 健康检查和其他端点
|
||||||
|
}
|
||||||
|
|
||||||
|
async startSSEServer(port, host) {
|
||||||
|
// 使用 Express + SSEServerTransport
|
||||||
|
// 向后兼容双端点架构
|
||||||
|
const app = express();
|
||||||
|
app.get('/mcp', handleSSEConnection);
|
||||||
|
app.post('/messages', handleSSEMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 端点设计
|
||||||
|
|
||||||
|
**Streamable HTTP 端点**(主要):
|
||||||
|
- `POST /mcp` - 统一入口端点
|
||||||
|
- 接收所有 JSON-RPC 消息
|
||||||
|
- 支持可选 SSE 流式响应
|
||||||
|
- 支持会话管理(sessionId)
|
||||||
|
- 无状态设计,便于负载均衡
|
||||||
|
|
||||||
|
**SSE 传输端点**(兼容):
|
||||||
|
- `GET /mcp` - 建立 SSE 连接
|
||||||
|
- `POST /messages` - 接收客户端消息
|
||||||
|
|
||||||
|
#### 配置选项
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 命令行参数
|
||||||
|
{
|
||||||
|
transport: 'stdio' | 'http' | 'sse', // 'http' 为推荐默认值
|
||||||
|
port: number, // HTTP 端口 (默认: 3000)
|
||||||
|
host: string, // 绑定地址 (默认: localhost)
|
||||||
|
cors: boolean, // CORS 支持 (默认: false)
|
||||||
|
auth: boolean, // 认证开关 (默认: false)
|
||||||
|
streaming: boolean, // SSE 流式响应 (默认: true)
|
||||||
|
maxConnections: number // 最大连接数 (默认: 100)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实现计划
|
||||||
|
|
||||||
|
### 阶段 1: Streamable HTTP 传输支持(主要目标)
|
||||||
|
|
||||||
|
**目标**: 实现 MCP 推荐的 Streamable HTTP 传输
|
||||||
|
|
||||||
|
**任务**:
|
||||||
|
1. 创建 `MCPStreamableHttpCommand.js`
|
||||||
|
2. 实现 StreamableHttpServerTransport 集成
|
||||||
|
3. 支持统一端点架构和可选 SSE 升级
|
||||||
|
4. 集成现有 MCP 工具处理逻辑
|
||||||
|
5. 添加命令行参数支持
|
||||||
|
6. 编写单元测试
|
||||||
|
|
||||||
|
**预期成果**:
|
||||||
|
```bash
|
||||||
|
# 启动 Streamable HTTP 服务器
|
||||||
|
pnpm start mcp-server --transport http --port 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 阶段 2: SSE 传输兼容支持
|
||||||
|
|
||||||
|
**目标**: 实现 SSE 传输的向后兼容
|
||||||
|
|
||||||
|
**任务**:
|
||||||
|
1. 在同一命令中添加 SSE 传输支持
|
||||||
|
2. 实现 SSE 双端点架构
|
||||||
|
3. 添加传输类型切换逻辑
|
||||||
|
4. 性能优化和错误处理
|
||||||
|
5. 兼容性测试
|
||||||
|
|
||||||
|
**预期成果**:
|
||||||
|
```bash
|
||||||
|
# 启动 SSE 服务器(兼容模式)
|
||||||
|
pnpm start mcp-server --transport sse --port 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 阶段 3: 生产化增强
|
||||||
|
|
||||||
|
**目标**: 完善生产环境特性
|
||||||
|
|
||||||
|
**任务**:
|
||||||
|
1. CORS 跨域支持
|
||||||
|
2. 认证机制集成
|
||||||
|
3. 连接池和限流
|
||||||
|
4. 监控和日志增强
|
||||||
|
5. Docker 部署支持
|
||||||
|
|
||||||
|
**预期成果**:
|
||||||
|
- 生产就绪的 Streamable HTTP MCP 服务器
|
||||||
|
- 完整的部署文档
|
||||||
|
- 性能基准测试报告
|
||||||
|
|
||||||
|
## 配置管理
|
||||||
|
|
||||||
|
### 环境变量支持
|
||||||
|
```bash
|
||||||
|
MCP_TRANSPORT=http # 传输类型(推荐默认值)
|
||||||
|
MCP_PORT=3000 # 服务端口
|
||||||
|
MCP_HOST=localhost # 绑定地址
|
||||||
|
MCP_CORS_ENABLED=false # CORS 开关
|
||||||
|
MCP_STREAMING=true # SSE 流式响应
|
||||||
|
MCP_MAX_CONNECTIONS=100 # 最大连接数
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置文件支持
|
||||||
|
```json
|
||||||
|
// package.json scripts 扩展
|
||||||
|
{
|
||||||
|
"scripts": {
|
||||||
|
"mcp:stdio": "node src/bin/promptx.js mcp-server",
|
||||||
|
"mcp:http": "node src/bin/promptx.js mcp-server --transport http",
|
||||||
|
"mcp:sse": "node src/bin/promptx.js mcp-server --transport sse",
|
||||||
|
"mcp:dev": "MCP_DEBUG=true node src/bin/promptx.js mcp-server --transport http --port 3001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试策略
|
||||||
|
|
||||||
|
### 单元测试
|
||||||
|
- 传输类型选择逻辑
|
||||||
|
- HTTP 端点处理
|
||||||
|
- 错误处理机制
|
||||||
|
- 参数验证
|
||||||
|
|
||||||
|
### 集成测试
|
||||||
|
- 完整 MCP 会话流程
|
||||||
|
- 多客户端并发连接
|
||||||
|
- 传输协议兼容性
|
||||||
|
- 工具调用端到端测试
|
||||||
|
|
||||||
|
### 性能测试
|
||||||
|
- 并发连接压力测试
|
||||||
|
- 消息吞吐量测试
|
||||||
|
- 内存和 CPU 使用率监控
|
||||||
|
|
||||||
|
## 部署考虑
|
||||||
|
|
||||||
|
### 开发环境
|
||||||
|
- 本地调试支持
|
||||||
|
- 热重载机制
|
||||||
|
- 详细日志输出
|
||||||
|
|
||||||
|
### 生产环境
|
||||||
|
- 进程管理 (PM2)
|
||||||
|
- 反向代理 (Nginx)
|
||||||
|
- HTTPS 支持
|
||||||
|
- 监控告警
|
||||||
|
|
||||||
|
## 客户端配置指南
|
||||||
|
|
||||||
|
### Claude Desktop 配置
|
||||||
|
|
||||||
|
#### 推荐配置(官方标准方式)
|
||||||
|
|
||||||
|
**配置文件路径**:
|
||||||
|
- **macOS**: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||||
|
- **Windows**: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||||
|
|
||||||
|
##### 方式一:Stdio 传输(推荐,最简单)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"promptx": {
|
||||||
|
"command": "node",
|
||||||
|
"args": [
|
||||||
|
"/absolute/path/to/PromptX/src/bin/promptx.js",
|
||||||
|
"mcp-server"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows 示例**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"promptx": {
|
||||||
|
"command": "node",
|
||||||
|
"args": [
|
||||||
|
"C:\\Users\\你的用户名\\WorkSpaces\\DeepracticeProjects\\PromptX\\src\\bin\\promptx.js",
|
||||||
|
"mcp-server"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
##### 方式二:使用 npx 运行(如果发布到 npm)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"promptx": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": [
|
||||||
|
"-y",
|
||||||
|
"dpml-prompt",
|
||||||
|
"mcp-server"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### HTTP 传输配置(高级用法)
|
||||||
|
|
||||||
|
⚠️ **注意**: HTTP 传输配置比较复杂,仅在有特殊需求时使用。
|
||||||
|
|
||||||
|
##### 跨平台 HTTP 配置
|
||||||
|
|
||||||
|
**macOS/Linux** (有 curl):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"promptx-http": {
|
||||||
|
"command": "curl",
|
||||||
|
"args": [
|
||||||
|
"-X", "POST",
|
||||||
|
"-H", "Content-Type: application/json",
|
||||||
|
"-H", "Accept: application/json, text/event-stream",
|
||||||
|
"--data-binary", "@-",
|
||||||
|
"http://localhost:3000/mcp"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Windows** (使用 Node.js 脚本):
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"promptx-http": {
|
||||||
|
"command": "node",
|
||||||
|
"args": [
|
||||||
|
"C:\\path\\to\\PromptX\\scripts\\mcp-http-client.js"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 生产环境配置
|
||||||
|
|
||||||
|
对于生产环境,建议使用以下配置:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"promptx-prod": {
|
||||||
|
"command": "curl",
|
||||||
|
"args": [
|
||||||
|
"-X", "POST",
|
||||||
|
"-H", "Content-Type: application/json",
|
||||||
|
"-H", "Accept: application/json, text/event-stream",
|
||||||
|
"-H", "User-Agent: Claude-Desktop/1.0",
|
||||||
|
"--timeout", "30",
|
||||||
|
"--retry", "3",
|
||||||
|
"--data-binary", "@-",
|
||||||
|
"https://your-domain.com/mcp"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MCP_DEBUG": "false",
|
||||||
|
"HTTP_TIMEOUT": "30000"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### SSE 传输配置(兼容模式)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"promptx-sse": {
|
||||||
|
"command": "curl",
|
||||||
|
"args": [
|
||||||
|
"-X", "GET",
|
||||||
|
"-H", "Accept: text/event-stream",
|
||||||
|
"-H", "Cache-Control: no-cache",
|
||||||
|
"http://localhost:3000/mcp"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MCP_DEBUG": "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置文件管理
|
||||||
|
|
||||||
|
#### 配置文件创建步骤
|
||||||
|
|
||||||
|
1. **查找配置文件位置**
|
||||||
|
```bash
|
||||||
|
# macOS
|
||||||
|
ls -la ~/Library/Application\ Support/Claude/
|
||||||
|
|
||||||
|
# Windows (PowerShell)
|
||||||
|
ls $env:APPDATA\Claude\
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **创建配置文件**(如果不存在)
|
||||||
|
```bash
|
||||||
|
# macOS
|
||||||
|
mkdir -p ~/Library/Application\ Support/Claude/
|
||||||
|
touch ~/Library/Application\ Support/Claude/claude_desktop_config.json
|
||||||
|
|
||||||
|
# Windows (PowerShell)
|
||||||
|
New-Item -ItemType Directory -Force -Path $env:APPDATA\Claude\
|
||||||
|
New-Item -ItemType File -Force -Path $env:APPDATA\Claude\claude_desktop_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **验证配置**
|
||||||
|
```bash
|
||||||
|
# 测试配置文件语法
|
||||||
|
cat ~/Library/Application\ Support/Claude/claude_desktop_config.json | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 配置文件模板
|
||||||
|
|
||||||
|
我们提供了一个完整的配置文件模板:`docs/claude-desktop-config-example.json`
|
||||||
|
|
||||||
|
你可以直接复制这个文件到你的 Claude Desktop 配置目录:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# macOS
|
||||||
|
cp docs/claude-desktop-config-example.json ~/Library/Application\ Support/Claude/claude_desktop_config.json
|
||||||
|
|
||||||
|
# Windows (PowerShell)
|
||||||
|
Copy-Item docs/claude-desktop-config-example.json $env:APPDATA\Claude\claude_desktop_config.json
|
||||||
|
```
|
||||||
|
|
||||||
|
**重要**: 记得将配置文件中的 `/Users/YOUR_USERNAME/` 替换为你的实际用户路径。
|
||||||
|
|
||||||
|
#### 快速配置脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# 文件名: setup-claude-config.sh
|
||||||
|
|
||||||
|
# 获取当前项目路径
|
||||||
|
PROJECT_PATH=$(pwd)
|
||||||
|
|
||||||
|
# 获取用户名
|
||||||
|
USERNAME=$(whoami)
|
||||||
|
|
||||||
|
# Claude Desktop 配置路径
|
||||||
|
CLAUDE_CONFIG_DIR="$HOME/Library/Application Support/Claude"
|
||||||
|
CLAUDE_CONFIG_FILE="$CLAUDE_CONFIG_DIR/claude_desktop_config.json"
|
||||||
|
|
||||||
|
# 创建配置目录
|
||||||
|
mkdir -p "$CLAUDE_CONFIG_DIR"
|
||||||
|
|
||||||
|
# 生成配置文件
|
||||||
|
cat > "$CLAUDE_CONFIG_FILE" << EOF
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"promptx-http": {
|
||||||
|
"command": "curl",
|
||||||
|
"args": [
|
||||||
|
"-X", "POST",
|
||||||
|
"-H", "Content-Type: application/json",
|
||||||
|
"-H", "Accept: application/json, text/event-stream",
|
||||||
|
"--data-binary", "@-",
|
||||||
|
"http://localhost:3000/mcp"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MCP_DEBUG": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"promptx-stdio": {
|
||||||
|
"command": "node",
|
||||||
|
"args": [
|
||||||
|
"$PROJECT_PATH/src/bin/promptx.js",
|
||||||
|
"mcp-server"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MCP_DEBUG": "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"globalShortcut": "Cmd+Shift+.",
|
||||||
|
"theme": "auto"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "✅ Claude Desktop 配置已生成: $CLAUDE_CONFIG_FILE"
|
||||||
|
echo "🔄 请重启 Claude Desktop 以加载新配置"
|
||||||
|
```
|
||||||
|
|
||||||
|
使用方法:
|
||||||
|
```bash
|
||||||
|
chmod +x setup-claude-config.sh
|
||||||
|
./setup-claude-config.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 多环境配置
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"promptx-dev": {
|
||||||
|
"command": "curl",
|
||||||
|
"args": [
|
||||||
|
"-X", "POST",
|
||||||
|
"-H", "Content-Type: application/json",
|
||||||
|
"-H", "Accept: application/json, text/event-stream",
|
||||||
|
"--data-binary", "@-",
|
||||||
|
"http://localhost:3000/mcp"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MCP_DEBUG": "true",
|
||||||
|
"NODE_ENV": "development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"promptx-staging": {
|
||||||
|
"command": "curl",
|
||||||
|
"args": [
|
||||||
|
"-X", "POST",
|
||||||
|
"-H", "Content-Type: application/json",
|
||||||
|
"-H", "Accept: application/json, text/event-stream",
|
||||||
|
"--data-binary", "@-",
|
||||||
|
"https://staging.your-domain.com/mcp"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MCP_DEBUG": "false",
|
||||||
|
"NODE_ENV": "staging"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"promptx-prod": {
|
||||||
|
"command": "curl",
|
||||||
|
"args": [
|
||||||
|
"-X", "POST",
|
||||||
|
"-H", "Content-Type: application/json",
|
||||||
|
"-H", "Accept: application/json, text/event-stream",
|
||||||
|
"-H", "Authorization: Bearer YOUR_API_TOKEN",
|
||||||
|
"--timeout", "30",
|
||||||
|
"--retry", "3",
|
||||||
|
"--data-binary", "@-",
|
||||||
|
"https://api.your-domain.com/mcp"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MCP_DEBUG": "false",
|
||||||
|
"NODE_ENV": "production"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义客户端实现
|
||||||
|
|
||||||
|
#### JavaScript/TypeScript 客户端
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
import { McpClient } from '@modelcontextprotocol/sdk/client/mcp.js';
|
||||||
|
import { HttpTransport } from '@modelcontextprotocol/sdk/client/http.js';
|
||||||
|
|
||||||
|
// Streamable HTTP 客户端
|
||||||
|
const transport = new HttpTransport({
|
||||||
|
baseUrl: 'http://localhost:3000/mcp',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const client = new McpClient({
|
||||||
|
name: 'promptx-client',
|
||||||
|
version: '1.0.0'
|
||||||
|
}, {
|
||||||
|
capabilities: {
|
||||||
|
tools: {}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await client.connect(transport);
|
||||||
|
|
||||||
|
// 调用工具示例
|
||||||
|
const result = await client.callTool('promptx_hello', {});
|
||||||
|
console.log(result);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Python 客户端
|
||||||
|
|
||||||
|
```python
|
||||||
|
import asyncio
|
||||||
|
import aiohttp
|
||||||
|
import json
|
||||||
|
|
||||||
|
class PromptXClient:
|
||||||
|
def __init__(self, base_url="http://localhost:3000"):
|
||||||
|
self.base_url = base_url
|
||||||
|
self.session_id = None
|
||||||
|
|
||||||
|
async def initialize(self):
|
||||||
|
"""初始化 MCP 连接"""
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
payload = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "initialize",
|
||||||
|
"params": {
|
||||||
|
"protocolVersion": "2024-11-05",
|
||||||
|
"capabilities": {
|
||||||
|
"tools": {}
|
||||||
|
},
|
||||||
|
"clientInfo": {
|
||||||
|
"name": "promptx-python-client",
|
||||||
|
"version": "1.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": 1
|
||||||
|
}
|
||||||
|
|
||||||
|
async with session.post(
|
||||||
|
f"{self.base_url}/mcp",
|
||||||
|
json=payload,
|
||||||
|
headers={"Content-Type": "application/json"}
|
||||||
|
) as response:
|
||||||
|
result = await response.json()
|
||||||
|
self.session_id = response.headers.get('mcp-session-id')
|
||||||
|
return result
|
||||||
|
|
||||||
|
async def call_tool(self, tool_name, arguments=None):
|
||||||
|
"""调用 PromptX 工具"""
|
||||||
|
if not self.session_id:
|
||||||
|
await self.initialize()
|
||||||
|
|
||||||
|
async with aiohttp.ClientSession() as session:
|
||||||
|
payload = {
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": tool_name,
|
||||||
|
"arguments": arguments or {}
|
||||||
|
},
|
||||||
|
"id": 2
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"mcp-session-id": self.session_id
|
||||||
|
}
|
||||||
|
|
||||||
|
async with session.post(
|
||||||
|
f"{self.base_url}/mcp",
|
||||||
|
json=payload,
|
||||||
|
headers=headers
|
||||||
|
) as response:
|
||||||
|
return await response.json()
|
||||||
|
|
||||||
|
# 使用示例
|
||||||
|
async def main():
|
||||||
|
client = PromptXClient()
|
||||||
|
|
||||||
|
# 调用角色发现工具
|
||||||
|
result = await client.call_tool('promptx_hello')
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
# 激活产品经理角色
|
||||||
|
result = await client.call_tool('promptx_action', {'role': 'product-manager'})
|
||||||
|
print(result)
|
||||||
|
|
||||||
|
asyncio.run(main())
|
||||||
|
```
|
||||||
|
|
||||||
|
### MCP Inspector 配置
|
||||||
|
|
||||||
|
使用 MCP Inspector 进行调试和测试:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 安装 MCP Inspector
|
||||||
|
npm install -g @modelcontextprotocol/inspector
|
||||||
|
|
||||||
|
# 连接到 PromptX HTTP 服务器
|
||||||
|
mcp-inspector http://localhost:3000/mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
### 服务器启动命令
|
||||||
|
|
||||||
|
在配置客户端之前,确保 PromptX 服务器已启动:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 启动 Streamable HTTP 服务器(推荐)
|
||||||
|
pnpm start mcp-server --transport http --port 3000
|
||||||
|
|
||||||
|
# 启动 SSE 服务器(兼容模式)
|
||||||
|
pnpm start mcp-server --transport sse --port 3000
|
||||||
|
|
||||||
|
# 启动时启用调试日志
|
||||||
|
MCP_DEBUG=true pnpm start mcp-server --transport http --port 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
### 连接测试
|
||||||
|
|
||||||
|
#### 健康检查
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 测试服务器是否运行
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
|
||||||
|
# 预期响应
|
||||||
|
{
|
||||||
|
"status": "ok",
|
||||||
|
"name": "promptx-mcp-streamable-http-server",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"transport": "http"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 工具列表获取
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 获取可用工具列表(无需会话ID)
|
||||||
|
curl -X POST http://localhost:3000/mcp \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "Accept: application/json, text/event-stream" \
|
||||||
|
-d '{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "tools/list",
|
||||||
|
"id": 1
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意**: 必须包含 `Accept: application/json, text/event-stream` 头,否则会收到406错误。
|
||||||
|
|
||||||
|
#### 工具调用测试
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 调用角色发现工具
|
||||||
|
curl -X POST http://localhost:3000/mcp \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-H "mcp-session-id: YOUR_SESSION_ID" \
|
||||||
|
-d '{
|
||||||
|
"jsonrpc": "2.0",
|
||||||
|
"method": "tools/call",
|
||||||
|
"params": {
|
||||||
|
"name": "promptx_hello",
|
||||||
|
"arguments": {}
|
||||||
|
},
|
||||||
|
"id": 2
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 故障排除
|
||||||
|
|
||||||
|
#### 常见问题
|
||||||
|
|
||||||
|
1. **连接被拒绝**
|
||||||
|
```bash
|
||||||
|
# 检查服务器是否运行
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
# 检查端口是否被占用
|
||||||
|
lsof -i :3000
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **CORS 错误**
|
||||||
|
```bash
|
||||||
|
# 启动时启用 CORS(如果需要)
|
||||||
|
pnpm start mcp-server --transport http --port 3000 --cors
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **会话 ID 错误**
|
||||||
|
- 确保在工具调用时包含正确的 `mcp-session-id` 头
|
||||||
|
- 对于新连接,先发送 `initialize` 请求
|
||||||
|
|
||||||
|
4. **工具调用失败**
|
||||||
|
```bash
|
||||||
|
# 启用调试模式查看详细日志
|
||||||
|
MCP_DEBUG=true pnpm start mcp-server --transport http --port 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 兼容性
|
||||||
|
|
||||||
|
### MCP 客户端兼容性
|
||||||
|
- Claude Desktop (通过 HTTP 配置)
|
||||||
|
- MCP Inspector
|
||||||
|
- 自定义 JavaScript/TypeScript 客户端
|
||||||
|
- 自定义 Python 客户端
|
||||||
|
- 任何支持 HTTP JSON-RPC 的客户端
|
||||||
|
|
||||||
|
### 协议版本兼容性
|
||||||
|
- 支持当前协议版本 (2024-11-05)
|
||||||
|
- 向后兼容弃用特性 (SSE 传输)
|
||||||
|
- 平滑迁移路径
|
||||||
|
|
||||||
|
## 风险评估
|
||||||
|
|
||||||
|
### 技术风险
|
||||||
|
- SSE 传输弃用风险 → 优先实现 Streamable HTTP
|
||||||
|
- 并发性能瓶颈 → 连接池和限流机制
|
||||||
|
- 内存泄漏风险 → 完善资源清理
|
||||||
|
|
||||||
|
### 维护风险
|
||||||
|
- 代码复杂度增加 → 清晰的架构分层
|
||||||
|
- 测试覆盖率下降 → 完善的测试策略
|
||||||
|
|
||||||
|
## 成功指标
|
||||||
|
|
||||||
|
### 功能指标
|
||||||
|
- [ ] 支持 Streamable HTTP 传输启动
|
||||||
|
- [ ] 支持 SSE 兼容传输
|
||||||
|
- [ ] 多传输类型无缝切换
|
||||||
|
- [ ] 完整的工具调用功能
|
||||||
|
|
||||||
|
### 性能指标
|
||||||
|
- 支持 > 50 并发连接
|
||||||
|
- 消息延迟 < 100ms
|
||||||
|
- 内存使用 < 500MB
|
||||||
|
|
||||||
|
### 质量指标
|
||||||
|
- 测试覆盖率 > 80%
|
||||||
|
- 零安全漏洞
|
||||||
|
- 完整的文档覆盖
|
||||||
|
|
||||||
|
## 参考资料
|
||||||
|
|
||||||
|
- [MCP 官方文档 - Transports](https://modelcontextprotocol.io/docs/concepts/transports)
|
||||||
|
- [MCP SDK 示例 - Streamable HTTP Server](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/simpleStreamableHttp.js)
|
||||||
|
- [MCP SDK 示例 - SSE Server](https://github.com/modelcontextprotocol/typescript-sdk/blob/main/examples/server/simpleSseServer.js)
|
||||||
|
- [Streamable HTTP 实现指南](https://blog.christianposta.com/ai/understanding-mcp-recent-change-around-http-sse/)
|
||||||
|
- [MCP 协议变更说明](https://blog.christianposta.com/ai/understanding-mcp-recent-change-around-http-sse/)
|
||||||
140
docs/usage/mcp-quick-start.md
Normal file
140
docs/usage/mcp-quick-start.md
Normal file
@ -0,0 +1,140 @@
|
|||||||
|
# PromptX MCP 快速上手
|
||||||
|
|
||||||
|
## 启动服务器
|
||||||
|
|
||||||
|
### 本地模式(推荐)
|
||||||
|
```bash
|
||||||
|
npx -f -y dpml-prompt@snapshot mcp-server
|
||||||
|
```
|
||||||
|
|
||||||
|
### HTTP 模式(远程访问)
|
||||||
|
```bash
|
||||||
|
npx -f -y dpml-prompt@snapshot mcp-server --transport http --port 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
检查服务器状态:
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3000/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## 客户端配置
|
||||||
|
|
||||||
|
### Claude Desktop(仅本地模式)
|
||||||
|
|
||||||
|
**配置文件位置:**
|
||||||
|
- macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
||||||
|
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"promptx": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-f", "-y", "dpml-prompt@snapshot", "mcp-server"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### VS Code
|
||||||
|
|
||||||
|
创建 `.vscode/mcp.json`:
|
||||||
|
|
||||||
|
**本地模式:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"servers": {
|
||||||
|
"promptx": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-f", "-y", "dpml-prompt@snapshot", "mcp-server"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP 模式:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"servers": {
|
||||||
|
"promptx": {
|
||||||
|
"type": "http",
|
||||||
|
"url": "http://localhost:3000/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Cursor
|
||||||
|
|
||||||
|
**本地模式:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"promptx": {
|
||||||
|
"command": "npx",
|
||||||
|
"args": ["-f", "-y", "dpml-prompt@snapshot", "mcp-server"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP 模式:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"promptx": {
|
||||||
|
"url": "http://localhost:3000/mcp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### LibreChat
|
||||||
|
|
||||||
|
编辑 `librechat.yaml`:
|
||||||
|
|
||||||
|
**本地模式:**
|
||||||
|
```yaml
|
||||||
|
mcpServers:
|
||||||
|
promptx:
|
||||||
|
command: npx
|
||||||
|
args:
|
||||||
|
- -f
|
||||||
|
- -y
|
||||||
|
- dpml-prompt@snapshot
|
||||||
|
- mcp-server
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTTP 模式:**
|
||||||
|
```yaml
|
||||||
|
mcpServers:
|
||||||
|
promptx:
|
||||||
|
type: streamable-http
|
||||||
|
url: http://localhost:3000/mcp
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试工具
|
||||||
|
|
||||||
|
重启客户端后,尝试使用以下工具:
|
||||||
|
|
||||||
|
- `promptx_hello` - 查看可用角色
|
||||||
|
- `promptx_action` - 激活角色(需要参数:role)
|
||||||
|
- `promptx_learn` - 学习资源(需要参数:resource)
|
||||||
|
- `promptx_recall` - 查看记忆
|
||||||
|
- `promptx_remember` - 保存记忆(需要参数:content)
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
**服务器启动失败:**
|
||||||
|
- 检查 Node.js 版本:`node --version`(需要 >= 14)
|
||||||
|
- 确认网络连接正常(npx 需要下载包)
|
||||||
|
|
||||||
|
**客户端连接失败:**
|
||||||
|
- 检查配置文件 JSON/YAML 语法
|
||||||
|
- 重启客户端应用
|
||||||
|
- 确认 npx 可以运行:`npx -f -y dpml-prompt@snapshot --help`
|
||||||
|
|
||||||
|
**HTTP 模式报错:**
|
||||||
|
- 确认服务器正在运行
|
||||||
|
- 检查防火墙设置
|
||||||
|
- 使用 `curl` 测试连接
|
||||||
@ -52,6 +52,7 @@
|
|||||||
"chalk": "^4.1.2",
|
"chalk": "^4.1.2",
|
||||||
"commander": "^11.0.0",
|
"commander": "^11.0.0",
|
||||||
"env-paths": "2.2.1",
|
"env-paths": "2.2.1",
|
||||||
|
"express": "^5.1.0",
|
||||||
"find-monorepo-root": "^1.0.3",
|
"find-monorepo-root": "^1.0.3",
|
||||||
"find-pkg-dir": "^2.0.0",
|
"find-pkg-dir": "^2.0.0",
|
||||||
"find-up": "^7.0.0",
|
"find-up": "^7.0.0",
|
||||||
@ -63,7 +64,8 @@
|
|||||||
"resolve": "^1.22.10",
|
"resolve": "^1.22.10",
|
||||||
"resolve-package": "^1.0.1",
|
"resolve-package": "^1.0.1",
|
||||||
"semver": "^7.5.0",
|
"semver": "^7.5.0",
|
||||||
"yaml": "^2.3.0"
|
"yaml": "^2.3.0",
|
||||||
|
"zod": "^3.25.62"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@changesets/changelog-github": "^0.5.1",
|
"@changesets/changelog-github": "^0.5.1",
|
||||||
|
|||||||
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@ -26,6 +26,9 @@ importers:
|
|||||||
env-paths:
|
env-paths:
|
||||||
specifier: 2.2.1
|
specifier: 2.2.1
|
||||||
version: 2.2.1
|
version: 2.2.1
|
||||||
|
express:
|
||||||
|
specifier: ^5.1.0
|
||||||
|
version: 5.1.0
|
||||||
find-monorepo-root:
|
find-monorepo-root:
|
||||||
specifier: ^1.0.3
|
specifier: ^1.0.3
|
||||||
version: 1.0.3
|
version: 1.0.3
|
||||||
@ -65,6 +68,9 @@ importers:
|
|||||||
yaml:
|
yaml:
|
||||||
specifier: ^2.3.0
|
specifier: ^2.3.0
|
||||||
version: 2.8.0
|
version: 2.8.0
|
||||||
|
zod:
|
||||||
|
specifier: ^3.25.62
|
||||||
|
version: 3.25.62
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@changesets/changelog-github':
|
'@changesets/changelog-github':
|
||||||
specifier: ^0.5.1
|
specifier: ^0.5.1
|
||||||
@ -2950,8 +2956,8 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
zod: ^3.24.1
|
zod: ^3.24.1
|
||||||
|
|
||||||
zod@3.25.53:
|
zod@3.25.62:
|
||||||
resolution: {integrity: sha512-BKOKoY3XcGUVkqaalCtFK15LhwR0G0i65AClFpWSXLN2gJNBGlTktukHgwexCTa/dAacPPp9ReryXPWyeZF4LQ==}
|
resolution: {integrity: sha512-YCxsr4DmhPcrKPC9R1oBHQNlQzlJEyPAId//qTau/vBee9uO8K6prmRq4eMkOyxvBfH4wDPIPdLx9HVMWIY3xA==}
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
@ -3566,8 +3572,8 @@ snapshots:
|
|||||||
express-rate-limit: 7.5.0(express@5.1.0)
|
express-rate-limit: 7.5.0(express@5.1.0)
|
||||||
pkce-challenge: 5.0.0
|
pkce-challenge: 5.0.0
|
||||||
raw-body: 3.0.0
|
raw-body: 3.0.0
|
||||||
zod: 3.25.53
|
zod: 3.25.62
|
||||||
zod-to-json-schema: 3.24.5(zod@3.25.53)
|
zod-to-json-schema: 3.24.5(zod@3.25.62)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@ -6454,8 +6460,8 @@ snapshots:
|
|||||||
|
|
||||||
yocto-queue@1.2.1: {}
|
yocto-queue@1.2.1: {}
|
||||||
|
|
||||||
zod-to-json-schema@3.24.5(zod@3.25.53):
|
zod-to-json-schema@3.24.5(zod@3.25.62):
|
||||||
dependencies:
|
dependencies:
|
||||||
zod: 3.25.53
|
zod: 3.25.62
|
||||||
|
|
||||||
zod@3.25.53: {}
|
zod@3.25.62: {}
|
||||||
|
|||||||
111
scripts/setup-claude-config.sh
Normal file
111
scripts/setup-claude-config.sh
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# PromptX Claude Desktop 配置生成脚本
|
||||||
|
# 使用方法: ./scripts/setup-claude-config.sh
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔧 设置 Claude Desktop 配置..."
|
||||||
|
|
||||||
|
# 获取当前项目路径
|
||||||
|
PROJECT_PATH=$(pwd)
|
||||||
|
|
||||||
|
# 获取用户名
|
||||||
|
USERNAME=$(whoami)
|
||||||
|
|
||||||
|
# Claude Desktop 配置路径
|
||||||
|
CLAUDE_CONFIG_DIR="$HOME/Library/Application Support/Claude"
|
||||||
|
CLAUDE_CONFIG_FILE="$CLAUDE_CONFIG_DIR/claude_desktop_config.json"
|
||||||
|
|
||||||
|
# 检查操作系统
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
# macOS
|
||||||
|
CLAUDE_CONFIG_DIR="$HOME/Library/Application Support/Claude"
|
||||||
|
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
|
||||||
|
# Windows
|
||||||
|
CLAUDE_CONFIG_DIR="$APPDATA/Claude"
|
||||||
|
else
|
||||||
|
echo "❌ 不支持的操作系统: $OSTYPE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CLAUDE_CONFIG_FILE="$CLAUDE_CONFIG_DIR/claude_desktop_config.json"
|
||||||
|
|
||||||
|
echo "📁 配置目录: $CLAUDE_CONFIG_DIR"
|
||||||
|
echo "📄 配置文件: $CLAUDE_CONFIG_FILE"
|
||||||
|
|
||||||
|
# 创建配置目录
|
||||||
|
mkdir -p "$CLAUDE_CONFIG_DIR"
|
||||||
|
|
||||||
|
# 备份现有配置
|
||||||
|
if [ -f "$CLAUDE_CONFIG_FILE" ]; then
|
||||||
|
BACKUP_FILE="${CLAUDE_CONFIG_FILE}.backup.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
cp "$CLAUDE_CONFIG_FILE" "$BACKUP_FILE"
|
||||||
|
echo "💾 已备份现有配置到: $BACKUP_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 生成配置文件
|
||||||
|
cat > "$CLAUDE_CONFIG_FILE" << EOF
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"promptx-http": {
|
||||||
|
"command": "curl",
|
||||||
|
"args": [
|
||||||
|
"-X", "POST",
|
||||||
|
"-H", "Content-Type: application/json",
|
||||||
|
"-H", "Accept: application/json, text/event-stream",
|
||||||
|
"--data-binary", "@-",
|
||||||
|
"http://localhost:3000/mcp"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MCP_DEBUG": "false"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"promptx-stdio": {
|
||||||
|
"command": "node",
|
||||||
|
"args": [
|
||||||
|
"$PROJECT_PATH/src/bin/promptx.js",
|
||||||
|
"mcp-server"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"MCP_DEBUG": "false"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"globalShortcut": "Cmd+Shift+.",
|
||||||
|
"theme": "auto"
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "✅ Claude Desktop 配置已生成"
|
||||||
|
echo "📝 配置文件内容:"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
cat "$CLAUDE_CONFIG_FILE"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
# 验证JSON格式
|
||||||
|
if command -v jq &> /dev/null; then
|
||||||
|
if jq . "$CLAUDE_CONFIG_FILE" > /dev/null 2>&1; then
|
||||||
|
echo "✅ JSON 格式验证通过"
|
||||||
|
else
|
||||||
|
echo "❌ JSON 格式验证失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "⚠️ 建议安装 jq 来验证 JSON 格式: brew install jq"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🚀 下一步操作:"
|
||||||
|
echo "1. 启动 PromptX HTTP 服务器:"
|
||||||
|
echo " pnpm start mcp-server --transport http --port 3000"
|
||||||
|
echo ""
|
||||||
|
echo "2. 重启 Claude Desktop 应用"
|
||||||
|
echo ""
|
||||||
|
echo "3. 在 Claude Desktop 中应该能看到 PromptX 工具"
|
||||||
|
|
||||||
|
# 检查服务器是否运行
|
||||||
|
if curl -s http://localhost:3000/health > /dev/null 2>&1; then
|
||||||
|
echo "✅ PromptX HTTP 服务器正在运行"
|
||||||
|
else
|
||||||
|
echo "⚠️ PromptX HTTP 服务器未运行,请先启动服务器"
|
||||||
|
fi
|
||||||
77
scripts/setup-claude-simple.sh
Normal file
77
scripts/setup-claude-simple.sh
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# PromptX Claude Desktop 简单配置脚本(官方标准方式)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "🔧 设置 Claude Desktop 配置(官方标准方式)..."
|
||||||
|
|
||||||
|
# 获取当前项目路径
|
||||||
|
PROJECT_PATH=$(pwd)
|
||||||
|
|
||||||
|
# Claude Desktop 配置路径
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
# macOS
|
||||||
|
CLAUDE_CONFIG_DIR="$HOME/Library/Application Support/Claude"
|
||||||
|
elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "win32" ]]; then
|
||||||
|
# Windows
|
||||||
|
CLAUDE_CONFIG_DIR="$APPDATA/Claude"
|
||||||
|
else
|
||||||
|
echo "❌ 不支持的操作系统: $OSTYPE"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
CLAUDE_CONFIG_FILE="$CLAUDE_CONFIG_DIR/claude_desktop_config.json"
|
||||||
|
|
||||||
|
echo "📁 配置目录: $CLAUDE_CONFIG_DIR"
|
||||||
|
echo "📄 配置文件: $CLAUDE_CONFIG_FILE"
|
||||||
|
|
||||||
|
# 创建配置目录
|
||||||
|
mkdir -p "$CLAUDE_CONFIG_DIR"
|
||||||
|
|
||||||
|
# 备份现有配置
|
||||||
|
if [ -f "$CLAUDE_CONFIG_FILE" ]; then
|
||||||
|
BACKUP_FILE="${CLAUDE_CONFIG_FILE}.backup.$(date +%Y%m%d_%H%M%S)"
|
||||||
|
cp "$CLAUDE_CONFIG_FILE" "$BACKUP_FILE"
|
||||||
|
echo "💾 已备份现有配置到: $BACKUP_FILE"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 生成简单配置文件(stdio传输)
|
||||||
|
cat > "$CLAUDE_CONFIG_FILE" << EOF
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"promptx": {
|
||||||
|
"command": "node",
|
||||||
|
"args": [
|
||||||
|
"$PROJECT_PATH/src/bin/promptx.js",
|
||||||
|
"mcp-server"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
echo "✅ Claude Desktop 配置已生成(stdio 传输方式)"
|
||||||
|
echo "📝 配置文件内容:"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
cat "$CLAUDE_CONFIG_FILE"
|
||||||
|
echo "----------------------------------------"
|
||||||
|
|
||||||
|
# 验证JSON格式
|
||||||
|
if command -v jq &> /dev/null; then
|
||||||
|
if jq . "$CLAUDE_CONFIG_FILE" > /dev/null 2>&1; then
|
||||||
|
echo "✅ JSON 格式验证通过"
|
||||||
|
else
|
||||||
|
echo "❌ JSON 格式验证失败"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "🚀 下一步操作:"
|
||||||
|
echo "1. 重启 Claude Desktop 应用"
|
||||||
|
echo "2. 在 Claude Desktop 中应该能看到 PromptX 工具"
|
||||||
|
echo ""
|
||||||
|
echo "💡 提示:"
|
||||||
|
echo "- 使用 stdio 传输,无需单独启动 HTTP 服务器"
|
||||||
|
echo "- 这是官方推荐的最简单配置方式"
|
||||||
|
echo "- 如需 HTTP 传输,请参考文档中的高级配置"
|
||||||
@ -8,6 +8,7 @@ const packageJson = require('../../package.json')
|
|||||||
const { cli } = require('../lib/core/pouch')
|
const { cli } = require('../lib/core/pouch')
|
||||||
// 导入MCP Server命令
|
// 导入MCP Server命令
|
||||||
const { MCPServerCommand } = require('../lib/commands/MCPServerCommand')
|
const { MCPServerCommand } = require('../lib/commands/MCPServerCommand')
|
||||||
|
const { MCPStreamableHttpCommand } = require('../lib/commands/MCPStreamableHttpCommand')
|
||||||
|
|
||||||
// 创建主程序
|
// 创建主程序
|
||||||
const program = new Command()
|
const program = new Command()
|
||||||
@ -66,10 +67,36 @@ program
|
|||||||
program
|
program
|
||||||
.command('mcp-server')
|
.command('mcp-server')
|
||||||
.description('🔌 启动MCP Server,支持Claude Desktop等AI应用接入')
|
.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) => {
|
.action(async (options) => {
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
// 输出到stderr,不污染MCP的stdout通信
|
// 输出到stderr,不污染MCP的stdout通信
|
||||||
console.error(chalk.red(`❌ MCP Server 启动失败: ${error.message}`));
|
console.error(chalk.red(`❌ MCP Server 启动失败: ${error.message}`));
|
||||||
@ -121,7 +148,9 @@ ${chalk.cyan('示例:')}
|
|||||||
promptx remember "测试→预发布→生产"
|
promptx remember "测试→预发布→生产"
|
||||||
|
|
||||||
${chalk.gray('# 7️⃣ 启动MCP服务')}
|
${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状态机:')}
|
${chalk.cyan('🔄 PATEOAS状态机:')}
|
||||||
每个锦囊输出都包含 PATEOAS 导航,引导 AI 发现下一步操作
|
每个锦囊输出都包含 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