import express, { Request, Response } from "express";
import cors from "cors";
import { randomUUID } from "node:crypto";
import "dotenv/config";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { CallToolRequestSchema, ListToolsRequestSchema, CallToolResult, Tool } from "@modelcontextprotocol/sdk/types.js";
// 导入业务工具
import { convertStrategy } from "./tools/convertStrategy.js";
// 会话存储
interface Session { id: string; server: Server; createdAt: Date; lastActivity: Date }
const sessions = new Map<string, Session>();
function createMCPServer(): Server {
const server = new Server(
{ name: "Quant2Ptrader-MCP", version: "1.0.0" },
{ capabilities: { tools: {} } }
);
const tools: Tool[] = [
{
name: convertStrategy.name,
description: convertStrategy.description,
inputSchema: convertStrategy.parameters as any
}
];
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools }));
server.setRequestHandler(CallToolRequestSchema, async (request): Promise<CallToolResult> => {
const { name, arguments: args } = request.params as any;
switch (name) {
case "convert_joinquant_to_ptrade":
return await convertStrategy.run(args);
default:
throw new Error(`Unknown tool: ${name}`);
}
});
return server;
}
const app = express();
const PORT = Number(process.env.PORT) || 3000;
app.use(cors({ origin: "*" }));
app.use(express.json({ limit: "10mb" }));
// 健康检查
app.get("/health", (_req: Request, res: Response) => {
res.json({
status: "healthy",
service: "Quant2Ptrader-MCP",
transport: "streamable-http",
activeSessions: sessions.size
});
});
// Streamable HTTP 主端点
app.all("/mcp", async (req: Request, res: Response) => {
const sessionIdHeader = req.headers["mcp-session-id"] as string | undefined;
const method = req.method.toUpperCase();
if (method === "POST") {
const body = req.body;
if (!body) return res.status(400).json({ jsonrpc: "2.0", error: { code: -32600, message: "Empty body" }, id: null });
// 忽略通知
const isNotification = (body.id === undefined || body.id === null) && typeof body.method === "string" && body.method.startsWith("notifications/");
if (isNotification) {
if (sessionIdHeader && sessions.has(sessionIdHeader)) sessions.get(sessionIdHeader)!.lastActivity = new Date();
return res.status(204).end();
}
// 初始化/会话管理
const isInit = body.method === "initialize";
let session: Session | undefined;
if (sessionIdHeader && sessions.has(sessionIdHeader)) {
session = sessions.get(sessionIdHeader)!;
session.lastActivity = new Date();
} else if (isInit) {
const newId = randomUUID();
const server = createMCPServer();
session = { id: newId, server, createdAt: new Date(), lastActivity: new Date() };
sessions.set(newId, session);
res.setHeader("Mcp-Session-Id", newId);
} else {
return res.status(400).json({ jsonrpc: "2.0", error: { code: -32000, message: "No session and not initialize" }, id: null });
}
// 处理核心方法
if (body.method === "initialize") {
return res.json({
jsonrpc: "2.0",
result: {
protocolVersion: "2024-11-05",
capabilities: { tools: {} },
serverInfo: { name: "Quant2Ptrader-MCP", version: "1.0.0" }
},
id: body.id
});
}
if (body.method === "tools/list") {
const tools = [
{
name: convertStrategy.name,
description: convertStrategy.description,
inputSchema: convertStrategy.parameters
}
];
return res.json({ jsonrpc: "2.0", result: { tools }, id: body.id });
}
if (body.method === "tools/call") {
const { name, arguments: args } = body.params;
let result: any;
switch (name) {
case "convert_joinquant_to_ptrade":
result = await convertStrategy.run(args);
break;
default:
throw new Error(`Unknown tool: ${name}`);
}
return res.json({ jsonrpc: "2.0", result, id: body.id });
}
return res.status(400).json({ jsonrpc: "2.0", error: { code: -32601, message: `Method not found: ${body.method}` }, id: body.id });
}
return res.status(405).json({ jsonrpc: "2.0", error: { code: -32600, message: "Method Not Allowed" }, id: null });
});
// 启动
app.listen(PORT, () => {
console.log(`🚀 Quant2Ptrader MCP Server - Streamable HTTP Mode`);
console.log(`📍 MCP endpoint: http://localhost:${PORT}/mcp`);
console.log(`💚 Health check: http://localhost:${PORT}/health`);
});