@douyinfe/semi-mcp 1.0.10 → 1.0.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/http.js +50 -202
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createServer } from "http";
|
|
3
|
-
import { randomUUID } from "crypto";
|
|
4
3
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
5
4
|
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
6
5
|
import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -1368,7 +1367,6 @@ Options:
|
|
|
1368
1367
|
Endpoints:
|
|
1369
1368
|
POST /mcp MCP 消息端点 (Streamable HTTP)
|
|
1370
1369
|
GET /mcp SSE 流端点 (用于服务器推送)
|
|
1371
|
-
DELETE /mcp 关闭会话
|
|
1372
1370
|
GET /health 健康检查端点
|
|
1373
1371
|
`);
|
|
1374
1372
|
process.exit(0);
|
|
@@ -1380,43 +1378,22 @@ Endpoints:
|
|
|
1380
1378
|
timeout
|
|
1381
1379
|
};
|
|
1382
1380
|
}
|
|
1383
|
-
const sessions = new Map();
|
|
1384
|
-
function touchSession(sessionId) {
|
|
1385
|
-
const session = sessions.get(sessionId);
|
|
1386
|
-
if (session) session.lastActivity = Date.now();
|
|
1387
|
-
}
|
|
1388
|
-
async function cleanupExpiredSessions(timeoutMs) {
|
|
1389
|
-
const now = Date.now();
|
|
1390
|
-
const expiredSessions = [];
|
|
1391
|
-
sessions.forEach((session, sessionId)=>{
|
|
1392
|
-
if (now - session.lastActivity > timeoutMs) expiredSessions.push(sessionId);
|
|
1393
|
-
});
|
|
1394
|
-
for (const sessionId of expiredSessions){
|
|
1395
|
-
const session = sessions.get(sessionId);
|
|
1396
|
-
if (session) {
|
|
1397
|
-
console.log(`[${new Date().toISOString()}] 会话超时清理: ${sessionId} (空闲 ${Math.round((now - session.lastActivity) / 1000 / 60)} 分钟)`);
|
|
1398
|
-
try {
|
|
1399
|
-
await session.transport.close();
|
|
1400
|
-
} catch {}
|
|
1401
|
-
sessions.delete(sessionId);
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
}
|
|
1405
1381
|
async function main() {
|
|
1406
1382
|
const { port, host, stateless, timeout } = parseArgs();
|
|
1407
1383
|
const version = getPackageVersion();
|
|
1408
|
-
const
|
|
1409
|
-
const
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
}
|
|
1384
|
+
const server = createMCPServer();
|
|
1385
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1386
|
+
sessionIdGenerator: stateless ? void 0 : ()=>crypto.randomUUID()
|
|
1387
|
+
});
|
|
1388
|
+
await server.connect(transport);
|
|
1389
|
+
console.log(`[${new Date().toISOString()}] MCP 服务器已启动`);
|
|
1390
|
+
console.log(`[${new Date().toISOString()}] 模式: ${stateless ? '无状态 (Stateless)' : '有状态 (Stateful)'}`);
|
|
1414
1391
|
const httpServer = createServer(async (req, res)=>{
|
|
1415
1392
|
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
1416
1393
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
1417
1394
|
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
|
|
1418
|
-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type,
|
|
1419
|
-
res.setHeader('Access-Control-Expose-Headers', '
|
|
1395
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id');
|
|
1396
|
+
res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
|
|
1420
1397
|
if ('OPTIONS' === req.method) {
|
|
1421
1398
|
res.writeHead(204);
|
|
1422
1399
|
res.end();
|
|
@@ -1432,168 +1409,42 @@ async function main() {
|
|
|
1432
1409
|
version,
|
|
1433
1410
|
transport: 'streamable-http',
|
|
1434
1411
|
stateless,
|
|
1435
|
-
sessionTimeout: `${timeout} minutes
|
|
1436
|
-
activeSessions: sessions.size
|
|
1412
|
+
sessionTimeout: `${timeout} minutes`
|
|
1437
1413
|
}));
|
|
1438
1414
|
return;
|
|
1439
1415
|
}
|
|
1440
1416
|
if ('/mcp' === url.pathname) {
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
const newSessionId = transport.sessionId;
|
|
1467
|
-
if (newSessionId) {
|
|
1468
|
-
sessions.set(newSessionId, {
|
|
1469
|
-
transport,
|
|
1470
|
-
lastActivity: Date.now()
|
|
1471
|
-
});
|
|
1472
|
-
console.log(`[${new Date().toISOString()}] 新会话创建: ${newSessionId}`);
|
|
1473
|
-
}
|
|
1474
|
-
} else {
|
|
1475
|
-
if (!sessionId) {
|
|
1476
|
-
res.writeHead(400, {
|
|
1477
|
-
'Content-Type': 'application/json'
|
|
1478
|
-
});
|
|
1479
|
-
res.end(JSON.stringify({
|
|
1480
|
-
jsonrpc: '2.0',
|
|
1481
|
-
error: {
|
|
1482
|
-
code: -32000,
|
|
1483
|
-
message: 'Bad Request: Missing mcp-session-id header'
|
|
1484
|
-
},
|
|
1485
|
-
id: parsedBody?.id ?? null
|
|
1486
|
-
}));
|
|
1487
|
-
resolve();
|
|
1488
|
-
return;
|
|
1489
|
-
}
|
|
1490
|
-
const session = sessions.get(sessionId);
|
|
1491
|
-
if (!session) {
|
|
1492
|
-
res.writeHead(404, {
|
|
1493
|
-
'Content-Type': 'application/json'
|
|
1494
|
-
});
|
|
1495
|
-
res.end(JSON.stringify({
|
|
1496
|
-
jsonrpc: '2.0',
|
|
1497
|
-
error: {
|
|
1498
|
-
code: -32000,
|
|
1499
|
-
message: 'Not Found: Session not found or expired'
|
|
1500
|
-
},
|
|
1501
|
-
id: parsedBody?.id ?? null
|
|
1502
|
-
}));
|
|
1503
|
-
resolve();
|
|
1504
|
-
return;
|
|
1505
|
-
}
|
|
1506
|
-
touchSession(sessionId);
|
|
1507
|
-
await session.transport.handleRequest(req, res, parsedBody);
|
|
1508
|
-
}
|
|
1509
|
-
} catch (error) {
|
|
1510
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1511
|
-
console.error(`[${new Date().toISOString()}] 请求处理错误:`, errorMessage);
|
|
1512
|
-
if (!res.headersSent) {
|
|
1513
|
-
res.writeHead(500, {
|
|
1514
|
-
'Content-Type': 'application/json'
|
|
1515
|
-
});
|
|
1516
|
-
res.end(JSON.stringify({
|
|
1517
|
-
jsonrpc: '2.0',
|
|
1518
|
-
error: {
|
|
1519
|
-
code: -32000,
|
|
1520
|
-
message: errorMessage
|
|
1521
|
-
},
|
|
1522
|
-
id: null
|
|
1523
|
-
}));
|
|
1524
|
-
}
|
|
1417
|
+
let body = '';
|
|
1418
|
+
req.on('data', (chunk)=>{
|
|
1419
|
+
body += chunk.toString();
|
|
1420
|
+
});
|
|
1421
|
+
await new Promise((resolve)=>{
|
|
1422
|
+
req.on('end', async ()=>{
|
|
1423
|
+
try {
|
|
1424
|
+
const parsedBody = body ? JSON.parse(body) : void 0;
|
|
1425
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
1426
|
+
console.log(`[${new Date().toISOString()}] ${req.method} ${url.pathname} - ${res.statusCode}`);
|
|
1427
|
+
} catch (error) {
|
|
1428
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1429
|
+
console.error(`[${new Date().toISOString()}] 请求处理错误:`, errorMessage);
|
|
1430
|
+
if (!res.headersSent) {
|
|
1431
|
+
res.writeHead(500, {
|
|
1432
|
+
'Content-Type': 'application/json'
|
|
1433
|
+
});
|
|
1434
|
+
res.end(JSON.stringify({
|
|
1435
|
+
jsonrpc: '2.0',
|
|
1436
|
+
error: {
|
|
1437
|
+
code: -32000,
|
|
1438
|
+
message: errorMessage
|
|
1439
|
+
},
|
|
1440
|
+
id: null
|
|
1441
|
+
}));
|
|
1525
1442
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1443
|
+
}
|
|
1444
|
+
resolve();
|
|
1528
1445
|
});
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
if ('GET' === req.method) {
|
|
1532
|
-
if (!sessionId) {
|
|
1533
|
-
res.writeHead(400, {
|
|
1534
|
-
'Content-Type': 'application/json'
|
|
1535
|
-
});
|
|
1536
|
-
res.end(JSON.stringify({
|
|
1537
|
-
jsonrpc: '2.0',
|
|
1538
|
-
error: {
|
|
1539
|
-
code: -32000,
|
|
1540
|
-
message: 'Bad Request: Missing mcp-session-id header'
|
|
1541
|
-
},
|
|
1542
|
-
id: null
|
|
1543
|
-
}));
|
|
1544
|
-
return;
|
|
1545
|
-
}
|
|
1546
|
-
const session = sessions.get(sessionId);
|
|
1547
|
-
if (!session) {
|
|
1548
|
-
res.writeHead(404, {
|
|
1549
|
-
'Content-Type': 'application/json'
|
|
1550
|
-
});
|
|
1551
|
-
res.end(JSON.stringify({
|
|
1552
|
-
jsonrpc: '2.0',
|
|
1553
|
-
error: {
|
|
1554
|
-
code: -32000,
|
|
1555
|
-
message: 'Not Found: Session not found'
|
|
1556
|
-
},
|
|
1557
|
-
id: null
|
|
1558
|
-
}));
|
|
1559
|
-
return;
|
|
1560
|
-
}
|
|
1561
|
-
touchSession(sessionId);
|
|
1562
|
-
await session.transport.handleRequest(req, res);
|
|
1563
|
-
return;
|
|
1564
|
-
}
|
|
1565
|
-
if ('DELETE' === req.method) {
|
|
1566
|
-
if (!sessionId) {
|
|
1567
|
-
res.writeHead(400, {
|
|
1568
|
-
'Content-Type': 'application/json'
|
|
1569
|
-
});
|
|
1570
|
-
res.end(JSON.stringify({
|
|
1571
|
-
error: 'Missing mcp-session-id header'
|
|
1572
|
-
}));
|
|
1573
|
-
return;
|
|
1574
|
-
}
|
|
1575
|
-
const session = sessions.get(sessionId);
|
|
1576
|
-
if (session) {
|
|
1577
|
-
await session.transport.close();
|
|
1578
|
-
sessions.delete(sessionId);
|
|
1579
|
-
console.log(`[${new Date().toISOString()}] 会话已删除: ${sessionId}`);
|
|
1580
|
-
res.writeHead(200, {
|
|
1581
|
-
'Content-Type': 'application/json'
|
|
1582
|
-
});
|
|
1583
|
-
res.end(JSON.stringify({
|
|
1584
|
-
success: true,
|
|
1585
|
-
message: '会话已关闭'
|
|
1586
|
-
}));
|
|
1587
|
-
} else {
|
|
1588
|
-
res.writeHead(404, {
|
|
1589
|
-
'Content-Type': 'application/json'
|
|
1590
|
-
});
|
|
1591
|
-
res.end(JSON.stringify({
|
|
1592
|
-
error: '会话不存在'
|
|
1593
|
-
}));
|
|
1594
|
-
}
|
|
1595
|
-
return;
|
|
1596
|
-
}
|
|
1446
|
+
});
|
|
1447
|
+
return;
|
|
1597
1448
|
}
|
|
1598
1449
|
if ('/' === url.pathname && 'GET' === req.method) {
|
|
1599
1450
|
res.writeHead(200, {
|
|
@@ -1608,19 +1459,18 @@ async function main() {
|
|
|
1608
1459
|
sessionTimeout: `${timeout} minutes`,
|
|
1609
1460
|
endpoints: {
|
|
1610
1461
|
mcp: {
|
|
1611
|
-
POST: '/mcp - 发送 MCP 请求
|
|
1612
|
-
GET: '/mcp - SSE 流 (需要
|
|
1613
|
-
DELETE: '/mcp - 关闭会话'
|
|
1462
|
+
POST: '/mcp - 发送 MCP 请求',
|
|
1463
|
+
GET: '/mcp - SSE 流 (需要 Mcp-Session-Id 头)'
|
|
1614
1464
|
},
|
|
1615
1465
|
health: '/health - 健康检查'
|
|
1616
1466
|
},
|
|
1617
1467
|
headers: {
|
|
1618
|
-
'
|
|
1468
|
+
'Mcp-Session-Id': '会话 ID (初始化响应后获取,后续请求需携带)'
|
|
1619
1469
|
},
|
|
1620
1470
|
usage: {
|
|
1621
|
-
step1: '
|
|
1622
|
-
step2: '
|
|
1623
|
-
|
|
1471
|
+
step1: '客户端发送 initialize 请求',
|
|
1472
|
+
step2: '服务器返回响应并包含 Mcp-Session-Id header',
|
|
1473
|
+
step3: '后续请求携带 Mcp-Session-Id header'
|
|
1624
1474
|
}
|
|
1625
1475
|
}, null, 2));
|
|
1626
1476
|
return;
|
|
@@ -1644,20 +1494,18 @@ async function main() {
|
|
|
1644
1494
|
║ 端点: ║
|
|
1645
1495
|
║ POST /mcp 发送 MCP 请求 ║
|
|
1646
1496
|
║ GET /mcp SSE 流 (服务器推送) ║
|
|
1647
|
-
║ DELETE /mcp 关闭会话 ║
|
|
1648
1497
|
║ GET /health 健康检查 ║
|
|
1649
1498
|
╚══════════════════════════════════════════════════════════════╝
|
|
1650
1499
|
`);
|
|
1651
1500
|
});
|
|
1652
1501
|
const shutdown = async ()=>{
|
|
1653
1502
|
console.log('\n正在关闭服务器...');
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1503
|
+
try {
|
|
1504
|
+
await transport.close();
|
|
1505
|
+
console.log('Transport 已关闭');
|
|
1506
|
+
} catch (error) {
|
|
1507
|
+
console.error('关闭 transport 时出错:', error);
|
|
1659
1508
|
}
|
|
1660
|
-
sessions.clear();
|
|
1661
1509
|
httpServer.close(()=>{
|
|
1662
1510
|
console.log('服务器已关闭');
|
|
1663
1511
|
process.exit(0);
|
package/package.json
CHANGED