@douyinfe/semi-mcp 1.0.10 → 1.0.12
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 +114 -216
- 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";
|
|
@@ -1330,20 +1329,20 @@ function createMCPServer() {
|
|
|
1330
1329
|
function parseArgs() {
|
|
1331
1330
|
const args = process.argv.slice(2);
|
|
1332
1331
|
let port = 3000;
|
|
1333
|
-
let
|
|
1332
|
+
let hosts = [];
|
|
1334
1333
|
let stateless = false;
|
|
1335
1334
|
let timeout = 30;
|
|
1336
1335
|
for(let i = 0; i < args.length; i++)if ('--port' === args[i] && args[i + 1]) {
|
|
1337
1336
|
port = parseInt(args[i + 1], 10);
|
|
1338
1337
|
i++;
|
|
1339
1338
|
} else if ('--host' === args[i] && args[i + 1]) {
|
|
1340
|
-
|
|
1339
|
+
hosts = args[i + 1].split(',').map((h)=>h.trim());
|
|
1341
1340
|
i++;
|
|
1342
1341
|
} else if ('-p' === args[i] && args[i + 1]) {
|
|
1343
1342
|
port = parseInt(args[i + 1], 10);
|
|
1344
1343
|
i++;
|
|
1345
1344
|
} else if ('-h' === args[i] && args[i + 1]) {
|
|
1346
|
-
|
|
1345
|
+
hosts = args[i + 1].split(',').map((h)=>h.trim());
|
|
1347
1346
|
i++;
|
|
1348
1347
|
} else if ('--stateless' === args[i]) stateless = true;
|
|
1349
1348
|
else if ('--timeout' === args[i] && args[i + 1]) {
|
|
@@ -1360,7 +1359,12 @@ Usage: semi-mcp-http [options]
|
|
|
1360
1359
|
|
|
1361
1360
|
Options:
|
|
1362
1361
|
--port, -p PORT 指定监听端口 (默认: 3000)
|
|
1363
|
-
--host, -h
|
|
1362
|
+
--host, -h HOSTS 指定监听地址,多个地址用逗号分隔 (默认: ::)
|
|
1363
|
+
:: 表示 IPv6 任意地址(自动支持 IPv4)
|
|
1364
|
+
0.0.0.0 表示 IPv4 任意地址
|
|
1365
|
+
::1 表示 IPv6 本地回环
|
|
1366
|
+
127.0.0.1 表示 IPv4 本地回环
|
|
1367
|
+
注意: 如果同时指定 0.0.0.0 和 ::,只使用 ::
|
|
1364
1368
|
--stateless 无状态模式,不生成 session ID
|
|
1365
1369
|
--timeout, -t MINUTES 会话超时时间,单位分钟 (默认: 30)
|
|
1366
1370
|
--help 显示帮助信息
|
|
@@ -1368,55 +1372,46 @@ Options:
|
|
|
1368
1372
|
Endpoints:
|
|
1369
1373
|
POST /mcp MCP 消息端点 (Streamable HTTP)
|
|
1370
1374
|
GET /mcp SSE 流端点 (用于服务器推送)
|
|
1371
|
-
DELETE /mcp 关闭会话
|
|
1372
1375
|
GET /health 健康检查端点
|
|
1373
1376
|
`);
|
|
1374
1377
|
process.exit(0);
|
|
1375
1378
|
}
|
|
1376
1379
|
return {
|
|
1377
1380
|
port,
|
|
1378
|
-
|
|
1381
|
+
hosts,
|
|
1379
1382
|
stateless,
|
|
1380
1383
|
timeout
|
|
1381
1384
|
};
|
|
1382
1385
|
}
|
|
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
1386
|
async function main() {
|
|
1406
|
-
const { port,
|
|
1387
|
+
const { port, hosts, stateless, timeout } = parseArgs();
|
|
1407
1388
|
const version = getPackageVersion();
|
|
1408
|
-
|
|
1409
|
-
const
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1389
|
+
let processedHosts = hosts;
|
|
1390
|
+
const hasIPv4All = hosts.includes('0.0.0.0');
|
|
1391
|
+
const hasIPv6All = hosts.includes('::');
|
|
1392
|
+
if (hasIPv4All && hasIPv6All) {
|
|
1393
|
+
processedHosts = hosts.filter((h)=>'0.0.0.0' !== h);
|
|
1394
|
+
console.log(`[${new Date().toISOString()}] 检测到同时监听 IPv4 和 IPv6,使用 :: (IPv6) 统一监听`);
|
|
1395
|
+
}
|
|
1396
|
+
if (0 === processedHosts.length) {
|
|
1397
|
+
processedHosts = [
|
|
1398
|
+
'::'
|
|
1399
|
+
];
|
|
1400
|
+
console.log(`[${new Date().toISOString()}] 使用默认配置: 监听 IPv6 (::)`);
|
|
1401
|
+
}
|
|
1402
|
+
const server = createMCPServer();
|
|
1403
|
+
const transport = new StreamableHTTPServerTransport({
|
|
1404
|
+
sessionIdGenerator: stateless ? void 0 : ()=>crypto.randomUUID()
|
|
1405
|
+
});
|
|
1406
|
+
await server.connect(transport);
|
|
1407
|
+
console.log(`[${new Date().toISOString()}] MCP 服务器已启动`);
|
|
1408
|
+
console.log(`[${new Date().toISOString()}] 模式: ${stateless ? '无状态 (Stateless)' : '有状态 (Stateful)'}`);
|
|
1414
1409
|
const httpServer = createServer(async (req, res)=>{
|
|
1415
1410
|
const url = new URL(req.url || '/', `http://${req.headers.host}`);
|
|
1416
1411
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
1417
1412
|
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', '
|
|
1413
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Mcp-Session-Id');
|
|
1414
|
+
res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
|
|
1420
1415
|
if ('OPTIONS' === req.method) {
|
|
1421
1416
|
res.writeHead(204);
|
|
1422
1417
|
res.end();
|
|
@@ -1432,168 +1427,42 @@ async function main() {
|
|
|
1432
1427
|
version,
|
|
1433
1428
|
transport: 'streamable-http',
|
|
1434
1429
|
stateless,
|
|
1435
|
-
sessionTimeout: `${timeout} minutes
|
|
1436
|
-
activeSessions: sessions.size
|
|
1430
|
+
sessionTimeout: `${timeout} minutes`
|
|
1437
1431
|
}));
|
|
1438
1432
|
return;
|
|
1439
1433
|
}
|
|
1440
1434
|
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
|
-
}
|
|
1435
|
+
let body = '';
|
|
1436
|
+
req.on('data', (chunk)=>{
|
|
1437
|
+
body += chunk.toString();
|
|
1438
|
+
});
|
|
1439
|
+
await new Promise((resolve)=>{
|
|
1440
|
+
req.on('end', async ()=>{
|
|
1441
|
+
try {
|
|
1442
|
+
const parsedBody = body ? JSON.parse(body) : void 0;
|
|
1443
|
+
await transport.handleRequest(req, res, parsedBody);
|
|
1444
|
+
console.log(`[${new Date().toISOString()}] ${req.method} ${url.pathname} - ${res.statusCode}`);
|
|
1445
|
+
} catch (error) {
|
|
1446
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1447
|
+
console.error(`[${new Date().toISOString()}] 请求处理错误:`, errorMessage);
|
|
1448
|
+
if (!res.headersSent) {
|
|
1449
|
+
res.writeHead(500, {
|
|
1450
|
+
'Content-Type': 'application/json'
|
|
1451
|
+
});
|
|
1452
|
+
res.end(JSON.stringify({
|
|
1453
|
+
jsonrpc: '2.0',
|
|
1454
|
+
error: {
|
|
1455
|
+
code: -32000,
|
|
1456
|
+
message: errorMessage
|
|
1457
|
+
},
|
|
1458
|
+
id: null
|
|
1459
|
+
}));
|
|
1525
1460
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1461
|
+
}
|
|
1462
|
+
resolve();
|
|
1528
1463
|
});
|
|
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
|
-
}
|
|
1464
|
+
});
|
|
1465
|
+
return;
|
|
1597
1466
|
}
|
|
1598
1467
|
if ('/' === url.pathname && 'GET' === req.method) {
|
|
1599
1468
|
res.writeHead(200, {
|
|
@@ -1608,19 +1477,18 @@ async function main() {
|
|
|
1608
1477
|
sessionTimeout: `${timeout} minutes`,
|
|
1609
1478
|
endpoints: {
|
|
1610
1479
|
mcp: {
|
|
1611
|
-
POST: '/mcp - 发送 MCP 请求
|
|
1612
|
-
GET: '/mcp - SSE 流 (需要
|
|
1613
|
-
DELETE: '/mcp - 关闭会话'
|
|
1480
|
+
POST: '/mcp - 发送 MCP 请求',
|
|
1481
|
+
GET: '/mcp - SSE 流 (需要 Mcp-Session-Id 头)'
|
|
1614
1482
|
},
|
|
1615
1483
|
health: '/health - 健康检查'
|
|
1616
1484
|
},
|
|
1617
1485
|
headers: {
|
|
1618
|
-
'
|
|
1486
|
+
'Mcp-Session-Id': '会话 ID (初始化响应后获取,后续请求需携带)'
|
|
1619
1487
|
},
|
|
1620
1488
|
usage: {
|
|
1621
|
-
step1: '
|
|
1622
|
-
step2: '
|
|
1623
|
-
|
|
1489
|
+
step1: '客户端发送 initialize 请求',
|
|
1490
|
+
step2: '服务器返回响应并包含 Mcp-Session-Id header',
|
|
1491
|
+
step3: '后续请求携带 Mcp-Session-Id header'
|
|
1624
1492
|
}
|
|
1625
1493
|
}, null, 2));
|
|
1626
1494
|
return;
|
|
@@ -1632,35 +1500,65 @@ async function main() {
|
|
|
1632
1500
|
error: '未知的端点'
|
|
1633
1501
|
}));
|
|
1634
1502
|
});
|
|
1635
|
-
|
|
1636
|
-
|
|
1503
|
+
const servers = [];
|
|
1504
|
+
let startedCount = 0;
|
|
1505
|
+
console.log(`
|
|
1637
1506
|
╔══════════════════════════════════════════════════════════════╗
|
|
1638
1507
|
║ Semi MCP Server (Streamable HTTP) v${version.padEnd(10)} ║
|
|
1639
1508
|
╠══════════════════════════════════════════════════════════════╣
|
|
1640
|
-
║ 服务地址: http://${('0.0.0.0' === host ? 'localhost' : host).padEnd(15)}:${String(port).padEnd(5)} ║
|
|
1641
1509
|
║ 模式: ${stateless ? '无状态 (Stateless)' : '有状态 (Stateful) '} ║
|
|
1642
1510
|
║ 会话超时: ${String(timeout).padEnd(3)} 分钟 ║
|
|
1643
|
-
║
|
|
1644
|
-
|
|
1511
|
+
║ ║`);
|
|
1512
|
+
const formatHost = (h)=>{
|
|
1513
|
+
if ('::' === h) return ':: (所有 IPv6)';
|
|
1514
|
+
if ('0.0.0.0' === h) return '0.0.0.0 (所有 IPv4)';
|
|
1515
|
+
if ('::1' === h) return '::1 (IPv6 本地)';
|
|
1516
|
+
if ('127.0.0.1' === h) return '127.0.0.1 (IPv4 本地)';
|
|
1517
|
+
return h;
|
|
1518
|
+
};
|
|
1519
|
+
processedHosts.forEach((host, index)=>{
|
|
1520
|
+
httpServer.on('error', (err)=>{
|
|
1521
|
+
const displayHost = formatHost(host);
|
|
1522
|
+
console.log(`║ ✗ 端点 ${index + 1}: http://${displayHost}:${port}`);
|
|
1523
|
+
console.error(`[${new Date().toISOString()}] 启动失败 [${host}]:`, err.message);
|
|
1524
|
+
});
|
|
1525
|
+
httpServer.listen(port, host, ()=>{
|
|
1526
|
+
startedCount++;
|
|
1527
|
+
const displayHost = formatHost(host);
|
|
1528
|
+
console.log(`║ ✓ 端点 ${index + 1}: http://${displayHost}:${port}`);
|
|
1529
|
+
if (startedCount === processedHosts.length) {
|
|
1530
|
+
console.log(`║ ║
|
|
1531
|
+
║ 可用端点: ║
|
|
1645
1532
|
║ POST /mcp 发送 MCP 请求 ║
|
|
1646
1533
|
║ GET /mcp SSE 流 (服务器推送) ║
|
|
1647
|
-
║ DELETE /mcp 关闭会话 ║
|
|
1648
1534
|
║ GET /health 健康检查 ║
|
|
1649
1535
|
╚══════════════════════════════════════════════════════════════╝
|
|
1650
1536
|
`);
|
|
1537
|
+
console.log(`[${new Date().toISOString()}] 所有服务器已启动,监听 ${processedHosts.length} 个地址`);
|
|
1538
|
+
console.log(`[${new Date().toISOString()}] 总计监听: ${processedHosts.join(', ')}`);
|
|
1539
|
+
}
|
|
1540
|
+
});
|
|
1541
|
+
servers.push(httpServer);
|
|
1651
1542
|
});
|
|
1543
|
+
processedHosts.length;
|
|
1652
1544
|
const shutdown = async ()=>{
|
|
1653
1545
|
console.log('\n正在关闭服务器...');
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1546
|
+
try {
|
|
1547
|
+
await transport.close();
|
|
1548
|
+
console.log('Transport 已关闭');
|
|
1549
|
+
} catch (error) {
|
|
1550
|
+
console.error('关闭 transport 时出错:', error);
|
|
1659
1551
|
}
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1552
|
+
let closedCount = 0;
|
|
1553
|
+
servers.forEach((server, index)=>{
|
|
1554
|
+
server.close(()=>{
|
|
1555
|
+
closedCount++;
|
|
1556
|
+
console.log(`[${new Date().toISOString()}] 服务器 ${index + 1}/${servers.length} 已关闭`);
|
|
1557
|
+
if (closedCount === servers.length) {
|
|
1558
|
+
console.log('所有服务器已关闭');
|
|
1559
|
+
process.exit(0);
|
|
1560
|
+
}
|
|
1561
|
+
});
|
|
1664
1562
|
});
|
|
1665
1563
|
};
|
|
1666
1564
|
process.on('SIGINT', shutdown);
|
package/package.json
CHANGED