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 +62 -19
- package/loki-client.js +506 -483
- package/package.json +1 -1
- package/ssh-client.js +56 -6
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.
|
|
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
|
|
364
|
-
handleToolCall(name, args),
|
|
365
|
-
|
|
366
|
-
|
|
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.
|
|
903
|
+
log(`[MCP] Log Query Server v3.6.0 已启动 (SSH排队超时 + Loki体超时 + 文件日志 + cancel signal 透传)`);
|
|
861
904
|
if (logPath) log(`[MCP] 本地日志文件: ${logPath}`);
|
|
862
905
|
}
|
|
863
906
|
|