All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 39s
- Streamable HTTP 传输,/mcp 端点,Bearer token 鉴权(data/mcp.json,已忽略) - 项目/配置完整增删改查工具,删除为两步式确认(delete_* + confirm_delete_*) - 写操作遵循系统模式,readonly 下拒绝 - 依赖 @modelcontextprotocol/sdk,新增单元测试
114 lines
3.5 KiB
JavaScript
114 lines
3.5 KiB
JavaScript
/**
|
||
* MCP HTTP 传输层
|
||
* 以 Streamable HTTP 协议在 /mcp 端点暴露 MCP 服务。
|
||
* 鉴权:基于 data/mcp.json 中配置的 token,要求请求头 Authorization: Bearer <token>。
|
||
*/
|
||
const crypto = require("crypto");
|
||
const express = require("express");
|
||
const { randomUUID } = require("node:crypto");
|
||
const {
|
||
StreamableHTTPServerTransport,
|
||
} = require("@modelcontextprotocol/sdk/server/streamableHttp.js");
|
||
const { isInitializeRequest } = require("@modelcontextprotocol/sdk/types.js");
|
||
const { createMcpServer } = require("./tools");
|
||
const { loadMcpConfig } = require("../persistence/jsonAdapter");
|
||
|
||
const router = express.Router();
|
||
|
||
// sessionId -> transport,用于会话复用
|
||
const transports = new Map();
|
||
|
||
/**
|
||
* 校验请求携带的 Bearer token 是否与配置中的 token 匹配
|
||
* @param {string} [header] - Authorization 请求头
|
||
* @returns {boolean} 是否鉴权通过
|
||
*/
|
||
function isAuthorized(header) {
|
||
const mcp = loadMcpConfig();
|
||
// 未配置 token 时视为未启用鉴权
|
||
if (!mcp.token) {
|
||
return true;
|
||
}
|
||
const expected = mcp.token;
|
||
const provided = header && header.startsWith("Bearer ") ? header.slice(7).trim() : "";
|
||
if (!provided || provided.length !== expected.length) {
|
||
return false;
|
||
}
|
||
// 使用常量时间比较,避免时序侧信道
|
||
const a = Buffer.from(provided);
|
||
const b = Buffer.from(expected);
|
||
return crypto.timingSafeEqual(a, b);
|
||
}
|
||
|
||
/**
|
||
* 鉴权中间件:token 校验失败返回 401
|
||
*/
|
||
router.use((req, res, next) => {
|
||
if (!isAuthorized(req.headers.authorization)) {
|
||
return res
|
||
.status(401)
|
||
.json({ jsonrpc: "2.0", error: { code: -32001, message: "Unauthorized: invalid or missing token" }, id: null });
|
||
}
|
||
next();
|
||
});
|
||
|
||
/**
|
||
* 根据会话 ID 或其下的 transport 处理请求
|
||
* 若带 mcp-session-id 且存在则复用;否则仅初始化请求可新建会话
|
||
*/
|
||
async function handleRequest(req, res) {
|
||
const sessionId = req.headers["mcp-session-id"];
|
||
const jsonBody = req.body;
|
||
|
||
try {
|
||
let transport;
|
||
if (sessionId && transports.get(sessionId)) {
|
||
transport = transports.get(sessionId);
|
||
} else if (!sessionId && isInitializeRequest(jsonBody)) {
|
||
transport = new StreamableHTTPServerTransport({
|
||
sessionIdGenerator: () => randomUUID(),
|
||
onsessioninitialized: (sid) => {
|
||
transports.set(sid, transport);
|
||
},
|
||
});
|
||
transport.onclose = () => {
|
||
if (transport.sessionId) {
|
||
transports.delete(transport.sessionId);
|
||
}
|
||
};
|
||
const server = createMcpServer();
|
||
await server.connect(transport);
|
||
await transport.handleRequest(req, res, req.body);
|
||
return;
|
||
} else {
|
||
res.status(400).json({
|
||
jsonrpc: "2.0",
|
||
error: { code: -32000, message: "Bad Request: missing or invalid mcp-session-id" },
|
||
id: null,
|
||
});
|
||
return;
|
||
}
|
||
await transport.handleRequest(req, res, req.body);
|
||
} catch (err) {
|
||
console.error("MCP 请求处理失败:", err);
|
||
if (!res.headersSent) {
|
||
res.status(500).json({
|
||
jsonrpc: "2.0",
|
||
error: { code: -32603, message: "Internal server error" },
|
||
id: null,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
// POST /mcp:初始化与工具调用
|
||
router.post("/", (req, res) => handleRequest(req, res));
|
||
|
||
// GET /mcp:建立 SSE 流(会话恢复/服务器通知)
|
||
router.get("/", (req, res) => handleRequest(req, res));
|
||
|
||
// DELETE /mcp:会话终止
|
||
router.delete("/", (req, res) => handleRequest(req, res));
|
||
|
||
module.exports = router;
|