mcp-log-query-server 1.0.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/README.md +151 -0
- package/config.js +474 -0
- package/index.js +555 -0
- package/package.json +42 -0
- package/server-sse.js +203 -0
- package/ssh-client.js +326 -0
package/index.js
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Log Query MCP Server
|
|
5
|
+
*
|
|
6
|
+
* 提供以下工具:
|
|
7
|
+
* - query_log: 查询服务日志
|
|
8
|
+
* - search_log: 搜索日志关键词
|
|
9
|
+
* - list_services: 列出可用服务
|
|
10
|
+
* - test_connection: 测试 SSH 连接
|
|
11
|
+
* - list_pods: 列出 pods 及状态
|
|
12
|
+
* - describe_pod: 获取 pod 详情
|
|
13
|
+
* - get_pod_logs: 获取 pod 日志
|
|
14
|
+
* - get_events: 获取 namespace 事件
|
|
15
|
+
* - trace_log: 根据 traceId 跨服务查询日志
|
|
16
|
+
* - detect_context: 根据工作目录自动检测 namespace 和服务
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
20
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
21
|
+
import {
|
|
22
|
+
CallToolRequestSchema,
|
|
23
|
+
ListToolsRequestSchema,
|
|
24
|
+
} from '@modelcontextprotocol/sdk/types.js';
|
|
25
|
+
|
|
26
|
+
import { queryLog, testConnection, executeKubectl } from './ssh-client.js';
|
|
27
|
+
import { findService, getAllServices, DEFAULTS, DEFAULT_NAMESPACE, SERVICES, NAMESPACES, detectContextFromPath } from './config.js';
|
|
28
|
+
|
|
29
|
+
// 创建 MCP Server
|
|
30
|
+
const server = new Server(
|
|
31
|
+
{
|
|
32
|
+
name: 'mcp-log-query',
|
|
33
|
+
version: '2.0.0',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
capabilities: {
|
|
37
|
+
tools: {},
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
// 定义工具列表
|
|
43
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
44
|
+
return {
|
|
45
|
+
tools: [
|
|
46
|
+
{
|
|
47
|
+
name: 'query_log',
|
|
48
|
+
description: '查询服务容器的日志文件。返回最近的日志内容。',
|
|
49
|
+
inputSchema: {
|
|
50
|
+
type: 'object',
|
|
51
|
+
properties: {
|
|
52
|
+
service: {
|
|
53
|
+
type: 'string',
|
|
54
|
+
description: '服务名称,如 clife-senior-health、clife-senior-archive,或别名如 health、archive'
|
|
55
|
+
},
|
|
56
|
+
namespace: {
|
|
57
|
+
type: 'string',
|
|
58
|
+
description: 'K8s namespace,如 saas-itest、whood-itest。不指定则使用服务默认配置'
|
|
59
|
+
},
|
|
60
|
+
lines: {
|
|
61
|
+
type: 'number',
|
|
62
|
+
description: '返回的日志行数,默认 100',
|
|
63
|
+
default: 100
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
required: ['service']
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'search_log',
|
|
71
|
+
description: '在服务日志中搜索关键词。支持正则表达式。',
|
|
72
|
+
inputSchema: {
|
|
73
|
+
type: 'object',
|
|
74
|
+
properties: {
|
|
75
|
+
service: {
|
|
76
|
+
type: 'string',
|
|
77
|
+
description: '服务名称或别名'
|
|
78
|
+
},
|
|
79
|
+
namespace: {
|
|
80
|
+
type: 'string',
|
|
81
|
+
description: 'K8s namespace,如 saas-itest、whood-itest。不指定则使用服务默认配置'
|
|
82
|
+
},
|
|
83
|
+
keyword: {
|
|
84
|
+
type: 'string',
|
|
85
|
+
description: '搜索关键词,支持正则表达式'
|
|
86
|
+
},
|
|
87
|
+
context_lines: {
|
|
88
|
+
type: 'number',
|
|
89
|
+
description: '显示匹配行前后的上下文行数,默认 5',
|
|
90
|
+
default: 5
|
|
91
|
+
},
|
|
92
|
+
case_sensitive: {
|
|
93
|
+
type: 'boolean',
|
|
94
|
+
description: '是否区分大小写,默认 false',
|
|
95
|
+
default: false
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
required: ['service', 'keyword']
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
name: 'list_services',
|
|
103
|
+
description: '列出所有可查询日志的服务',
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: 'object',
|
|
106
|
+
properties: {}
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
name: 'test_connection',
|
|
111
|
+
description: '测试到堡垒机的 SSH 连接是否正常',
|
|
112
|
+
inputSchema: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: {}
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
// ========== 新增 K8s 工具 ==========
|
|
118
|
+
{
|
|
119
|
+
name: 'list_pods',
|
|
120
|
+
description: '列出指定 namespace 的所有 pods 及其状态,用于快速定位问题 pod',
|
|
121
|
+
inputSchema: {
|
|
122
|
+
type: 'object',
|
|
123
|
+
properties: {
|
|
124
|
+
namespace: {
|
|
125
|
+
type: 'string',
|
|
126
|
+
description: 'K8s namespace,默认 saas-itest',
|
|
127
|
+
default: 'saas-itest'
|
|
128
|
+
},
|
|
129
|
+
label: {
|
|
130
|
+
type: 'string',
|
|
131
|
+
description: '标签选择器,如 app=clife-senior-health'
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
name: 'describe_pod',
|
|
138
|
+
description: '获取 pod 详细信息,包括事件、状态、退出码等,用于排查 pod 崩溃原因',
|
|
139
|
+
inputSchema: {
|
|
140
|
+
type: 'object',
|
|
141
|
+
properties: {
|
|
142
|
+
pod: {
|
|
143
|
+
type: 'string',
|
|
144
|
+
description: 'Pod 名称或名称模式(支持部分匹配)'
|
|
145
|
+
},
|
|
146
|
+
namespace: {
|
|
147
|
+
type: 'string',
|
|
148
|
+
description: 'K8s namespace,默认 saas-itest',
|
|
149
|
+
default: 'saas-itest'
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
required: ['pod']
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: 'get_pod_logs',
|
|
157
|
+
description: '获取 pod 日志,支持查看崩溃前的日志(--previous)',
|
|
158
|
+
inputSchema: {
|
|
159
|
+
type: 'object',
|
|
160
|
+
properties: {
|
|
161
|
+
pod: {
|
|
162
|
+
type: 'string',
|
|
163
|
+
description: 'Pod 名称或名称模式'
|
|
164
|
+
},
|
|
165
|
+
namespace: {
|
|
166
|
+
type: 'string',
|
|
167
|
+
description: 'K8s namespace,默认 saas-itest',
|
|
168
|
+
default: 'saas-itest'
|
|
169
|
+
},
|
|
170
|
+
previous: {
|
|
171
|
+
type: 'boolean',
|
|
172
|
+
description: '是否查看上一个容器的日志(崩溃前日志),默认 false',
|
|
173
|
+
default: false
|
|
174
|
+
},
|
|
175
|
+
tail: {
|
|
176
|
+
type: 'number',
|
|
177
|
+
description: '返回的日志行数,默认 100',
|
|
178
|
+
default: 100
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
required: ['pod']
|
|
182
|
+
}
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: 'get_events',
|
|
186
|
+
description: '获取 namespace 级别的 K8s 事件,用于排查集群问题',
|
|
187
|
+
inputSchema: {
|
|
188
|
+
type: 'object',
|
|
189
|
+
properties: {
|
|
190
|
+
namespace: {
|
|
191
|
+
type: 'string',
|
|
192
|
+
description: 'K8s namespace,默认 saas-itest',
|
|
193
|
+
default: 'saas-itest'
|
|
194
|
+
},
|
|
195
|
+
pod: {
|
|
196
|
+
type: 'string',
|
|
197
|
+
description: '过滤指定 pod 的事件(可选)'
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
name: 'trace_log',
|
|
204
|
+
description: '根据 traceId 跨服务查询日志,用于追踪完整调用链',
|
|
205
|
+
inputSchema: {
|
|
206
|
+
type: 'object',
|
|
207
|
+
properties: {
|
|
208
|
+
traceId: {
|
|
209
|
+
type: 'string',
|
|
210
|
+
description: '链路追踪 ID'
|
|
211
|
+
},
|
|
212
|
+
namespace: {
|
|
213
|
+
type: 'string',
|
|
214
|
+
description: 'K8s namespace,如 saas-itest、whood-itest。不指定则使用服务默认配置'
|
|
215
|
+
},
|
|
216
|
+
services: {
|
|
217
|
+
type: 'array',
|
|
218
|
+
items: { type: 'string' },
|
|
219
|
+
description: '要搜索的服务列表,不指定则搜索所有服务'
|
|
220
|
+
},
|
|
221
|
+
context_lines: {
|
|
222
|
+
type: 'number',
|
|
223
|
+
description: '显示匹配行前后的上下文行数,默认 3',
|
|
224
|
+
default: 3
|
|
225
|
+
}
|
|
226
|
+
},
|
|
227
|
+
required: ['traceId']
|
|
228
|
+
}
|
|
229
|
+
},
|
|
230
|
+
// ========== 上下文检测工具 ==========
|
|
231
|
+
{
|
|
232
|
+
name: 'detect_context',
|
|
233
|
+
description: '根据当前工作目录自动检测对应的 namespace 和服务名。AI 可以先调用此工具获取上下文,再调用 query_log 等工具时传入正确的 namespace。',
|
|
234
|
+
inputSchema: {
|
|
235
|
+
type: 'object',
|
|
236
|
+
properties: {
|
|
237
|
+
workspace_path: {
|
|
238
|
+
type: 'string',
|
|
239
|
+
description: '当前工作目录路径,如 D:\\shulian\\whood\\clife-senior-mall 或 /home/user/shulian/saas/clife-senior-health'
|
|
240
|
+
}
|
|
241
|
+
},
|
|
242
|
+
required: ['workspace_path']
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
]
|
|
246
|
+
};
|
|
247
|
+
});
|
|
248
|
+
// 处理工具调用
|
|
249
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
250
|
+
const { name, arguments: args } = request.params;
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
switch (name) {
|
|
254
|
+
case 'query_log': {
|
|
255
|
+
// 支持传入 namespace 参数覆盖默认值
|
|
256
|
+
const service = findService(args.service, args.namespace);
|
|
257
|
+
if (!service) {
|
|
258
|
+
return {
|
|
259
|
+
content: [{
|
|
260
|
+
type: 'text',
|
|
261
|
+
text: `错误: 未找到服务 "${args.service}"。使用 list_services 查看可用服务。`
|
|
262
|
+
}]
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const lines = args.lines || DEFAULTS.lines;
|
|
267
|
+
const command = `tail -${lines}`;
|
|
268
|
+
|
|
269
|
+
console.error(`[MCP] 查询日志: ${service.name} (namespace: ${service.namespace}), 命令: ${command}`);
|
|
270
|
+
const result = await queryLog(service, command);
|
|
271
|
+
|
|
272
|
+
return {
|
|
273
|
+
content: [{
|
|
274
|
+
type: 'text',
|
|
275
|
+
text: `## ${service.name} 日志 (namespace: ${service.namespace}, 最近 ${lines} 行)\n\n\`\`\`\n${result}\n\`\`\``
|
|
276
|
+
}]
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
case 'search_log': {
|
|
281
|
+
// 支持传入 namespace 参数覆盖默认值
|
|
282
|
+
const service = findService(args.service, args.namespace);
|
|
283
|
+
if (!service) {
|
|
284
|
+
return {
|
|
285
|
+
content: [{
|
|
286
|
+
type: 'text',
|
|
287
|
+
text: `错误: 未找到服务 "${args.service}"。使用 list_services 查看可用服务。`
|
|
288
|
+
}]
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const keyword = args.keyword;
|
|
293
|
+
const contextLines = args.context_lines || 5;
|
|
294
|
+
const caseSensitive = args.case_sensitive || false;
|
|
295
|
+
|
|
296
|
+
const grepFlags = caseSensitive ? '' : '-i';
|
|
297
|
+
const command = `grep ${grepFlags} -C ${contextLines} "${keyword}"`;
|
|
298
|
+
|
|
299
|
+
console.error(`[MCP] 搜索日志: ${service.name} (namespace: ${service.namespace}), 关键词: ${keyword}`);
|
|
300
|
+
const result = await queryLog(service, command);
|
|
301
|
+
|
|
302
|
+
return {
|
|
303
|
+
content: [{
|
|
304
|
+
type: 'text',
|
|
305
|
+
text: `## ${service.name} 日志搜索结果 (namespace: ${service.namespace})\n\n**关键词**: ${keyword}\n\n\`\`\`\n${result || '未找到匹配内容'}\n\`\`\``
|
|
306
|
+
}]
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
case 'list_services': {
|
|
311
|
+
const services = getAllServices();
|
|
312
|
+
const list = services.map(s =>
|
|
313
|
+
`- **${s.name}**: ${s.description}\n 别名: ${s.aliases.join(', ')}`
|
|
314
|
+
).join('\n');
|
|
315
|
+
|
|
316
|
+
return {
|
|
317
|
+
content: [{
|
|
318
|
+
type: 'text',
|
|
319
|
+
text: `## 可用服务列表\n\n${list}`
|
|
320
|
+
}]
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
case 'test_connection': {
|
|
325
|
+
console.error('[MCP] 测试 SSH 连接');
|
|
326
|
+
const result = await testConnection();
|
|
327
|
+
|
|
328
|
+
return {
|
|
329
|
+
content: [{
|
|
330
|
+
type: 'text',
|
|
331
|
+
text: `## SSH 连接测试\n\n✅ ${result.message}`
|
|
332
|
+
}]
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ========== 新增 K8s 工具处理 ==========
|
|
337
|
+
case 'list_pods': {
|
|
338
|
+
const namespace = args.namespace || DEFAULT_NAMESPACE;
|
|
339
|
+
let cmd = `kubectl get pods -n ${namespace} -o wide`;
|
|
340
|
+
if (args.label) {
|
|
341
|
+
cmd += ` -l ${args.label}`;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
console.error(`[MCP] 列出 pods: namespace=${namespace}`);
|
|
345
|
+
const result = await executeKubectl(cmd);
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
content: [{
|
|
349
|
+
type: 'text',
|
|
350
|
+
text: `## Pods 列表 (namespace: ${namespace})\n\n\`\`\`\n${result}\n\`\`\``
|
|
351
|
+
}]
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
case 'describe_pod': {
|
|
356
|
+
const namespace = args.namespace || DEFAULT_NAMESPACE;
|
|
357
|
+
const podPattern = args.pod;
|
|
358
|
+
|
|
359
|
+
// 先查找匹配的 pod
|
|
360
|
+
const findCmd = `kubectl get pod -n ${namespace} -o name | grep ${podPattern} | head -1`;
|
|
361
|
+
console.error(`[MCP] 查找 pod: ${podPattern}`);
|
|
362
|
+
|
|
363
|
+
const describeCmd = `kubectl describe $(kubectl get pod -n ${namespace} -o name | grep ${podPattern} | head -1) -n ${namespace}`;
|
|
364
|
+
const result = await executeKubectl(describeCmd);
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
content: [{
|
|
368
|
+
type: 'text',
|
|
369
|
+
text: `## Pod 详情: ${podPattern}\n\n\`\`\`\n${result}\n\`\`\``
|
|
370
|
+
}]
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
case 'get_pod_logs': {
|
|
375
|
+
const namespace = args.namespace || DEFAULT_NAMESPACE;
|
|
376
|
+
const podPattern = args.pod;
|
|
377
|
+
const previous = args.previous || false;
|
|
378
|
+
const tail = args.tail || 100;
|
|
379
|
+
|
|
380
|
+
let cmd = `kubectl logs $(kubectl get pod -n ${namespace} -o name | grep ${podPattern} | head -1) -n ${namespace} --tail=${tail}`;
|
|
381
|
+
if (previous) {
|
|
382
|
+
cmd += ' --previous';
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
console.error(`[MCP] 获取 pod 日志: ${podPattern}, previous=${previous}`);
|
|
386
|
+
const result = await executeKubectl(cmd);
|
|
387
|
+
|
|
388
|
+
const logType = previous ? '崩溃前日志' : '当前日志';
|
|
389
|
+
return {
|
|
390
|
+
content: [{
|
|
391
|
+
type: 'text',
|
|
392
|
+
text: `## Pod 日志: ${podPattern} (${logType})\n\n\`\`\`\n${result}\n\`\`\``
|
|
393
|
+
}]
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
case 'get_events': {
|
|
398
|
+
const namespace = args.namespace || DEFAULT_NAMESPACE;
|
|
399
|
+
let cmd = `kubectl get events -n ${namespace} --sort-by='.lastTimestamp'`;
|
|
400
|
+
|
|
401
|
+
if (args.pod) {
|
|
402
|
+
cmd = `kubectl get events -n ${namespace} --field-selector involvedObject.name=${args.pod} --sort-by='.lastTimestamp'`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
console.error(`[MCP] 获取事件: namespace=${namespace}`);
|
|
406
|
+
const result = await executeKubectl(cmd);
|
|
407
|
+
|
|
408
|
+
return {
|
|
409
|
+
content: [{
|
|
410
|
+
type: 'text',
|
|
411
|
+
text: `## K8s 事件 (namespace: ${namespace})\n\n\`\`\`\n${result}\n\`\`\``
|
|
412
|
+
}]
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
case 'trace_log': {
|
|
417
|
+
const traceId = args.traceId;
|
|
418
|
+
const contextLines = args.context_lines || 3;
|
|
419
|
+
const targetNamespace = args.namespace || null; // 支持指定 namespace
|
|
420
|
+
let servicesToSearch = args.services || [];
|
|
421
|
+
|
|
422
|
+
// 如果没有指定服务,搜索所有服务
|
|
423
|
+
if (servicesToSearch.length === 0) {
|
|
424
|
+
servicesToSearch = Object.keys(SERVICES);
|
|
425
|
+
} else {
|
|
426
|
+
// 解析服务别名
|
|
427
|
+
servicesToSearch = servicesToSearch.map(s => {
|
|
428
|
+
const service = findService(s, targetNamespace);
|
|
429
|
+
return service ? service.name : s;
|
|
430
|
+
}).filter(Boolean);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
console.error(`[MCP] 追踪日志: traceId=${traceId}, namespace=${targetNamespace || 'default'}, 服务数=${servicesToSearch.length}`);
|
|
434
|
+
|
|
435
|
+
const results = [];
|
|
436
|
+
for (const serviceName of servicesToSearch) {
|
|
437
|
+
// 使用 findService 获取服务配置,支持 namespace 覆盖
|
|
438
|
+
const service = findService(serviceName, targetNamespace);
|
|
439
|
+
if (!service) continue;
|
|
440
|
+
|
|
441
|
+
try {
|
|
442
|
+
const command = `grep -i -C ${contextLines} "${traceId}"`;
|
|
443
|
+
const result = await queryLog(service, command);
|
|
444
|
+
|
|
445
|
+
if (result && result.trim() && !result.includes('未找到')) {
|
|
446
|
+
results.push({
|
|
447
|
+
service: serviceName,
|
|
448
|
+
namespace: service.namespace,
|
|
449
|
+
logs: result
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
} catch (err) {
|
|
453
|
+
// 忽略单个服务的错误,继续搜索其他服务
|
|
454
|
+
console.error(`[MCP] 搜索 ${serviceName} 失败: ${err.message}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (results.length === 0) {
|
|
459
|
+
return {
|
|
460
|
+
content: [{
|
|
461
|
+
type: 'text',
|
|
462
|
+
text: `## TraceId 追踪结果\n\n**traceId**: ${traceId}\n**namespace**: ${targetNamespace || '默认'}\n\n❌ 未在任何服务中找到匹配的日志`
|
|
463
|
+
}]
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const output = results.map(r =>
|
|
468
|
+
`### ${r.service} (${r.namespace})\n\`\`\`\n${r.logs}\n\`\`\``
|
|
469
|
+
).join('\n\n');
|
|
470
|
+
|
|
471
|
+
return {
|
|
472
|
+
content: [{
|
|
473
|
+
type: 'text',
|
|
474
|
+
text: `## TraceId 追踪结果\n\n**traceId**: ${traceId}\n**namespace**: ${targetNamespace || '默认'}\n**匹配服务数**: ${results.length}\n\n${output}`
|
|
475
|
+
}]
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
case 'detect_context': {
|
|
480
|
+
const workspacePath = args.workspace_path;
|
|
481
|
+
const result = detectContextFromPath(workspacePath);
|
|
482
|
+
|
|
483
|
+
if (!result.success) {
|
|
484
|
+
return {
|
|
485
|
+
content: [{
|
|
486
|
+
type: 'text',
|
|
487
|
+
text: `## 上下文检测失败\n\n**错误**: ${result.error}\n**默认 namespace**: ${result.namespace}`
|
|
488
|
+
}]
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// 构建返回信息
|
|
493
|
+
let responseText = `## 上下文检测结果\n\n`;
|
|
494
|
+
responseText += `**工作目录**: ${result.originalPath}\n`;
|
|
495
|
+
responseText += `**检测到的 namespace**: ${result.namespace}\n`;
|
|
496
|
+
responseText += `**namespace 来源**: ${result.namespaceSource}\n`;
|
|
497
|
+
|
|
498
|
+
if (result.serviceName) {
|
|
499
|
+
responseText += `**检测到的服务**: ${result.serviceName}\n`;
|
|
500
|
+
if (result.service) {
|
|
501
|
+
responseText += `**服务描述**: ${result.service.description}\n`;
|
|
502
|
+
responseText += `**服务别名**: ${result.service.aliases.join(', ')}\n`;
|
|
503
|
+
}
|
|
504
|
+
} else {
|
|
505
|
+
responseText += `**检测到的服务**: 未能从路径中识别服务名\n`;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
responseText += `\n### 建议\n`;
|
|
509
|
+
responseText += `在调用 query_log、search_log 等工具时,请使用:\n`;
|
|
510
|
+
responseText += `- **namespace**: \`${result.namespace}\`\n`;
|
|
511
|
+
if (result.serviceName) {
|
|
512
|
+
responseText += `- **service**: \`${result.serviceName}\`\n`;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
console.error(`[MCP] 上下文检测: path=${workspacePath}, namespace=${result.namespace}, service=${result.serviceName}`);
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
content: [{
|
|
519
|
+
type: 'text',
|
|
520
|
+
text: responseText
|
|
521
|
+
}]
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
default:
|
|
526
|
+
return {
|
|
527
|
+
content: [{
|
|
528
|
+
type: 'text',
|
|
529
|
+
text: `错误: 未知工具 "${name}"`
|
|
530
|
+
}]
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
} catch (error) {
|
|
534
|
+
console.error(`[MCP] 错误: ${error.message}`);
|
|
535
|
+
return {
|
|
536
|
+
content: [{
|
|
537
|
+
type: 'text',
|
|
538
|
+
text: `## 执行错误\n\n❌ ${error.message}`
|
|
539
|
+
}],
|
|
540
|
+
isError: true
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
// 启动服务器
|
|
546
|
+
async function main() {
|
|
547
|
+
const transport = new StdioServerTransport();
|
|
548
|
+
await server.connect(transport);
|
|
549
|
+
console.error('[MCP] Log Query Server v2.0 已启动');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
main().catch((error) => {
|
|
553
|
+
console.error('[MCP] 启动失败:', error);
|
|
554
|
+
process.exit(1);
|
|
555
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "mcp-log-query-server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "MCP Server for querying server logs via SSH jump host",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"mcp-log-query": "./index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"index.js",
|
|
12
|
+
"config.js",
|
|
13
|
+
"ssh-client.js",
|
|
14
|
+
"server-sse.js",
|
|
15
|
+
"README.md"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node index.js",
|
|
19
|
+
"start:sse": "node server-sse.js"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
23
|
+
"cors": "^2.8.5",
|
|
24
|
+
"express": "^4.18.2",
|
|
25
|
+
"ssh2": "^1.15.0"
|
|
26
|
+
},
|
|
27
|
+
"keywords": [
|
|
28
|
+
"mcp",
|
|
29
|
+
"ssh",
|
|
30
|
+
"log",
|
|
31
|
+
"kubernetes",
|
|
32
|
+
"model-context-protocol"
|
|
33
|
+
],
|
|
34
|
+
"author": "huawang1258",
|
|
35
|
+
"license": "MIT",
|
|
36
|
+
"repository": {
|
|
37
|
+
"type": "git",
|
|
38
|
+
"url": "https://github.com/huawang1258/mcp-services.git"
|
|
39
|
+
},
|
|
40
|
+
"homepage": "https://github.com/huawang1258/mcp-services#readme"
|
|
41
|
+
}
|
|
42
|
+
|