mcp-log-query-server 3.5.2 → 3.6.0

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/index.js CHANGED
@@ -58,6 +58,23 @@ function safeStringify(obj, maxLen = 200) {
58
58
  }
59
59
  }
60
60
 
61
+ // 合并多个 AbortSignal:任何一个 abort 则聚合 signal abort
62
+ // Node 20+ 原生支持 AbortSignal.any;低版本回退到手工监听
63
+ function anySignal(signals) {
64
+ const valid = signals.filter(Boolean);
65
+ if (valid.length === 0) return undefined;
66
+ if (valid.length === 1) return valid[0];
67
+ if (typeof AbortSignal.any === 'function') return AbortSignal.any(valid);
68
+ // 回退方案
69
+ const ctrl = new AbortController();
70
+ const onAbort = () => ctrl.abort();
71
+ for (const s of valid) {
72
+ if (s.aborted) { ctrl.abort(); break; }
73
+ s.addEventListener('abort', onAbort, { once: true });
74
+ }
75
+ return ctrl.signal;
76
+ }
77
+
61
78
  // 进程级安全网:只记录日志,不退出进程
62
79
  // 退出会导致 stdio 断开,整个 MCP 不可用直到 IDE 重启;单次请求错误不应拖死服务
63
80
  process.on('unhandledRejection', (err) => log(`[unhandledRejection] ${err && err.stack || err}`));
@@ -67,7 +84,7 @@ process.on('uncaughtException', (err) => log(`[uncaughtException] ${err && err.s
67
84
  const server = new Server(
68
85
  {
69
86
  name: 'mcp-log-query',
70
- version: '3.5.2',
87
+ version: '3.6.0',
71
88
  },
72
89
  {
73
90
  capabilities: {
@@ -347,29 +364,50 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
347
364
  };
348
365
  });
349
366
  // 处理工具调用
350
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
367
+ server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
351
368
  const { name, arguments: args } = request.params;
352
369
  const startTime = Date.now();
370
+ // SDK 传入的 signal:Cascade 发 notifications/cancelled 时 signal.aborted=true
371
+ const signal = extra && extra.signal;
353
372
  log(`[Tool] → ${name} start args=${safeStringify(args)}`);
354
373
 
374
+ // 提前 cancel:立即抛错,让 SDK 检测到 signal.aborted 不发 response
375
+ if (signal && signal.aborted) {
376
+ log(`[Tool] ⊗ ${name} 收到请求时已 aborted,立即返回`);
377
+ throw new Error('Request cancelled before handler');
378
+ }
379
+
355
380
  // 看门狗:仅记录长时间未完成的请求,不再退出进程
356
- // withTimeout(REQUEST_TIMEOUT) 已经保证单次请求超时会抛错
357
381
  const watchdog = setTimeout(() => {
358
382
  log(`[Watchdog] ${name} 仍在运行超过 ${WATCHDOG_WARN_TIMEOUT}ms(仅记录,不退出进程)`);
359
383
  }, WATCHDOG_WARN_TIMEOUT);
360
384
  watchdog.unref();
361
385
 
386
+ // cancel race:signal abort 时立即 reject,handler 不再等下游
387
+ const cancelPromise = new Promise((_, reject) => {
388
+ if (!signal) return;
389
+ const onAbort = () => {
390
+ log(`[Tool] ⊗ ${name} 收到 cancel signal (${Date.now() - startTime}ms)`);
391
+ reject(new Error('CANCELLED'));
392
+ };
393
+ signal.addEventListener('abort', onAbort, { once: true });
394
+ });
395
+
362
396
  try {
363
- const result = await withTimeout(
364
- handleToolCall(name, args),
365
- REQUEST_TIMEOUT,
366
- name
367
- );
397
+ const result = await Promise.race([
398
+ withTimeout(handleToolCall(name, args, signal), REQUEST_TIMEOUT, name),
399
+ cancelPromise,
400
+ ]);
368
401
  clearTimeout(watchdog);
369
402
  log(`[Tool] ✓ ${name} done ${Date.now() - startTime}ms`);
370
403
  return result;
371
404
  } catch (error) {
372
405
  clearTimeout(watchdog);
406
+ // 取消场景:抛错让 SDK 知道(SDK 检测 signal.aborted 不发 response)
407
+ if (signal && signal.aborted) {
408
+ log(`[Tool] ⊗ ${name} CANCELLED ${Date.now() - startTime}ms`);
409
+ throw error;
410
+ }
373
411
  log(`[Tool] ✗ ${name} FAIL ${Date.now() - startTime}ms: ${error.message}`);
374
412
  return {
375
413
  content: [{ type: 'text', text: `## 执行错误\n\n❌ ${error.message}` }],
@@ -380,8 +418,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
380
418
 
381
419
  /**
382
420
  * 实际的工具调用处理逻辑
421
+ * @param {string} name - 工具名
422
+ * @param {object} args - 工具参数
423
+ * @param {AbortSignal} [signal] - Cascade 传入的取消信号;层层传给 Loki/SSH/kubectl
383
424
  */
384
- async function handleToolCall(name, args) {
425
+ async function handleToolCall(name, args, signal) {
385
426
  try {
386
427
  switch (name) {
387
428
  case 'query_log': {
@@ -400,6 +441,7 @@ async function handleToolCall(name, args) {
400
441
  const timeOpts = { maxLines };
401
442
  if (args.from) timeOpts.from = parseTimeStr(args.from);
402
443
  if (args.to) timeOpts.to = parseTimeStr(args.to);
444
+ timeOpts.signal = signal;
403
445
 
404
446
  const lokiResult = await queryLokiAutoRange(envKey, expr, timeOpts);
405
447
 
@@ -428,7 +470,7 @@ async function handleToolCall(name, args) {
428
470
  const command = `tail -${lines}`;
429
471
 
430
472
  console.error(`[MCP] 查询日志: ${service.name} (namespace: ${service.namespace}), 命令: ${command}`);
431
- const result = await queryLog(service, command);
473
+ const result = await queryLog(service, command, { signal });
432
474
 
433
475
  return {
434
476
  content: [{
@@ -454,6 +496,7 @@ async function handleToolCall(name, args) {
454
496
  const timeOpts = { maxLines: 200 };
455
497
  if (args.from) timeOpts.from = parseTimeStr(args.from);
456
498
  if (args.to) timeOpts.to = parseTimeStr(args.to);
499
+ timeOpts.signal = signal;
457
500
 
458
501
  const lokiResult = await queryLokiAutoRange(envKey, expr, timeOpts);
459
502
 
@@ -490,7 +533,7 @@ async function handleToolCall(name, args) {
490
533
  const command = `grep ${grepFlags} -C ${contextLines} "${keyword}"`;
491
534
 
492
535
  console.error(`[MCP] 搜索日志: ${service.name} (namespace: ${service.namespace}), 关键词: ${keyword}`);
493
- const result = await queryLog(service, command);
536
+ const result = await queryLog(service, command, { signal });
494
537
 
495
538
  return {
496
539
  content: [{
@@ -535,7 +578,7 @@ async function handleToolCall(name, args) {
535
578
  }
536
579
 
537
580
  console.error(`[MCP] 列出 pods: namespace=${namespace}`);
538
- const result = await executeKubectl(cmd);
581
+ const result = await executeKubectl(cmd, { signal });
539
582
 
540
583
  return {
541
584
  content: [{
@@ -554,7 +597,7 @@ async function handleToolCall(name, args) {
554
597
  console.error(`[MCP] 查找 pod: ${podPattern}`);
555
598
 
556
599
  const describeCmd = `kubectl describe $(kubectl get pod -n ${namespace} -o name | grep ${podPattern} | head -1) -n ${namespace}`;
557
- const result = await executeKubectl(describeCmd);
600
+ const result = await executeKubectl(describeCmd, { signal });
558
601
 
559
602
  return {
560
603
  content: [{
@@ -576,7 +619,7 @@ async function handleToolCall(name, args) {
576
619
  }
577
620
 
578
621
  console.error(`[MCP] 获取 pod 日志: ${podPattern}, previous=${previous}`);
579
- const result = await executeKubectl(cmd);
622
+ const result = await executeKubectl(cmd, { signal });
580
623
 
581
624
  const logType = previous ? '崩溃前日志' : '当前日志';
582
625
  return {
@@ -596,7 +639,7 @@ async function handleToolCall(name, args) {
596
639
  }
597
640
 
598
641
  console.error(`[MCP] 获取事件: namespace=${namespace}`);
599
- const result = await executeKubectl(cmd);
642
+ const result = await executeKubectl(cmd, { signal });
600
643
 
601
644
  return {
602
645
  content: [{
@@ -633,7 +676,7 @@ async function handleToolCall(name, args) {
633
676
  const dirName = getLokiServiceDirName(svc);
634
677
  const expr = buildServiceLogQL(project, dirName, traceId, envKey);
635
678
  console.error(`[MCP] Loki trace: env=${envKey}, service=${svc}, traceId=${traceId}`);
636
- const r = await queryLokiAutoRange(envKey, expr, { ...timeOpts, maxLines: 500 });
679
+ const r = await queryLokiAutoRange(envKey, expr, { ...timeOpts, maxLines: 500, signal });
637
680
  allLogs.push(...r.logs);
638
681
  allLabels.push(...r.labels);
639
682
  }
@@ -642,7 +685,7 @@ async function handleToolCall(name, args) {
642
685
  // 未指定服务:按项目一次查询所有服务(高效!)
643
686
  const expr = buildProjectLogQL(project, traceId, envKey);
644
687
  console.error(`[MCP] Loki trace (全项目): env=${envKey}, project=${project}, traceId=${traceId}`);
645
- lokiResult = await queryLokiAutoRange(envKey, expr, { ...timeOpts, maxLines: 1000 });
688
+ lokiResult = await queryLokiAutoRange(envKey, expr, { ...timeOpts, maxLines: 1000, signal });
646
689
  }
647
690
 
648
691
  if (lokiResult.logs.length === 0) {
@@ -703,7 +746,7 @@ async function handleToolCall(name, args) {
703
746
 
704
747
  try {
705
748
  const command = `grep -i -C ${contextLines} "${traceId}"`;
706
- const result = await queryLog(service, command, { timeout: TRACE_PER_SERVICE });
749
+ const result = await queryLog(service, command, { timeout: TRACE_PER_SERVICE, signal });
707
750
 
708
751
  if (result && result.trim() && !result.includes('未找到')) {
709
752
  results.push({ service: serviceName, namespace: service.namespace, logs: result });
@@ -857,7 +900,7 @@ async function main() {
857
900
  const transport = new StdioServerTransport();
858
901
  await server.connect(transport);
859
902
  const logPath = getLogFilePath();
860
- log(`[MCP] Log Query Server v3.5.2 已启动 (超时保护 + SSH 并发限制 + 排队超时 + 文件日志 + 进程不自杀)`);
903
+ log(`[MCP] Log Query Server v3.6.0 已启动 (SSH排队超时 + Loki体超时 + 文件日志 + cancel signal 透传)`);
861
904
  if (logPath) log(`[MCP] 本地日志文件: ${logPath}`);
862
905
  }
863
906