@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.
Files changed (2) hide show
  1. package/dist/http.js +114 -216
  2. 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 host = '0.0.0.0';
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
- host = args[i + 1];
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
- host = args[i + 1];
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 HOST 指定监听地址 (默认: 0.0.0.0)
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
- host,
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, host, stateless, timeout } = parseArgs();
1387
+ const { port, hosts, stateless, timeout } = parseArgs();
1407
1388
  const version = getPackageVersion();
1408
- const timeoutMs = 60 * timeout * 1000;
1409
- const cleanupInterval = setInterval(()=>{
1410
- cleanupExpiredSessions(timeoutMs).catch((error)=>{
1411
- console.error(`[${new Date().toISOString()}] 会话清理错误:`, error);
1412
- });
1413
- }, 60000);
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, mcp-session-id');
1419
- res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id');
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
- const sessionId = req.headers['mcp-session-id'];
1442
- if ('POST' === req.method) {
1443
- let body = '';
1444
- req.on('data', (chunk)=>{
1445
- body += chunk.toString();
1446
- });
1447
- await new Promise((resolve)=>{
1448
- req.on('end', async ()=>{
1449
- try {
1450
- const parsedBody = body ? JSON.parse(body) : void 0;
1451
- const isInitialize = parsedBody?.method === 'initialize';
1452
- if (isInitialize) {
1453
- const server = createMCPServer();
1454
- const transport = new StreamableHTTPServerTransport({
1455
- sessionIdGenerator: stateless ? void 0 : ()=>randomUUID()
1456
- });
1457
- await server.connect(transport);
1458
- transport.onclose = ()=>{
1459
- const sid = transport.sessionId;
1460
- if (sid) {
1461
- console.log(`[${new Date().toISOString()}] 会话关闭: ${sid}`);
1462
- sessions.delete(sid);
1463
- }
1464
- };
1465
- await transport.handleRequest(req, res, parsedBody);
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
- resolve();
1527
- });
1461
+ }
1462
+ resolve();
1528
1463
  });
1529
- return;
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 请求 (初始化请求无需 session ID)',
1612
- GET: '/mcp - SSE 流 (需要 mcp-session-id 头)',
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
- 'mcp-session-id': '会话 ID (初始化响应后获取,后续请求需携带)'
1486
+ 'Mcp-Session-Id': '会话 ID (初始化响应后获取,后续请求需携带)'
1619
1487
  },
1620
1488
  usage: {
1621
- step1: '发送 initialize 请求获取 session ID',
1622
- step2: '后续请求携带 mcp-session-id ',
1623
- example: `curl -X POST http://${'0.0.0.0' === host ? 'localhost' : host}:${port}/mcp -H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -d '{"jsonrpc":"2.0","method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test","version":"1.0"}},"id":1}'`
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
- httpServer.listen(port, host, ()=>{
1636
- console.log(`
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
- clearInterval(cleanupInterval);
1655
- const sessionEntries = Array.from(sessions.entries());
1656
- for (const [sessionId, session] of sessionEntries){
1657
- console.log(`关闭会话: ${sessionId}`);
1658
- await session.transport.close();
1546
+ try {
1547
+ await transport.close();
1548
+ console.log('Transport 已关闭');
1549
+ } catch (error) {
1550
+ console.error('关闭 transport 时出错:', error);
1659
1551
  }
1660
- sessions.clear();
1661
- httpServer.close(()=>{
1662
- console.log('服务器已关闭');
1663
- process.exit(0);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@douyinfe/semi-mcp",
3
- "version": "1.0.10",
3
+ "version": "1.0.12",
4
4
  "description": "Semi Design MCP Server - Model Context Protocol server for Semi Design components and documentation",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",