@junjun-org/bd-ke-mcp 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.
Files changed (4) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +284 -0
  3. package/index.js +1088 -0
  4. package/package.json +39 -0
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 章文俊
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md ADDED
@@ -0,0 +1,284 @@
1
+ # BD-KE MCP Server
2
+
3
+ 这是一个符合 MCP (Model Context Protocol) 规范的 Node.js 服务器,用于管理 KE (Knowledge Engine) 应用。
4
+
5
+ ## 快速开始
6
+
7
+ ### 1. 安装
8
+
9
+ **使用 npx(推荐):**
10
+
11
+ ```bash
12
+ npx bd-ke-mcp
13
+ ```
14
+
15
+ **全局安装:**
16
+
17
+ ```bash
18
+ npm install -g bd-ke-mcp
19
+ ```
20
+
21
+ **本地开发:**
22
+
23
+ ```bash
24
+ cd /path/to/bd-ke-mcp
25
+ npm install
26
+ ```
27
+
28
+ ### 2. 配置 Cursor
29
+
30
+ 在 Cursor 的 MCP 配置文件中添加:
31
+
32
+ **使用 npx(推荐):**
33
+
34
+ ```json
35
+ {
36
+ "mcpServers": {
37
+ "bd-ke-mcp": {
38
+ "command": "npx",
39
+ "args": ["-y", "bd-ke-mcp"],
40
+ "env": {
41
+ "PROJECT_ROOT": "${workspaceFolder}"
42
+ }
43
+ }
44
+ }
45
+ }
46
+ ```
47
+
48
+ **本地开发模式:**
49
+
50
+ ```json
51
+ {
52
+ "mcpServers": {
53
+ "bd-ke-mcp": {
54
+ "command": "node",
55
+ "args": ["/path/to/bd-ke-mcp/index.js"],
56
+ "env": {
57
+ "PROJECT_ROOT": "${workspaceFolder}"
58
+ }
59
+ }
60
+ }
61
+ }
62
+ ```
63
+
64
+ 配置文件位置:
65
+ - 通过 Cursor 设置界面:Settings → Features → Model Context Protocol
66
+
67
+ ### 3. 配置项目
68
+
69
+ 在项目根目录创建 `.mcp/bd-ke-mcp.json`:
70
+
71
+ ```json
72
+ {
73
+ "domain": "https://ke.example.com",
74
+ "access_token": "your_access_token_here",
75
+ "tenant": "your_tenant",
76
+ "project": "your_project",
77
+ "application": "your_application",
78
+ "env": "production"
79
+ }
80
+ ```
81
+
82
+ **注意:**
83
+ - 配置采用扁平化结构,所有配置项直接放在顶层
84
+ - 配置文件包含敏感信息,建议添加到 `.gitignore`
85
+ - 可通过环境变量覆盖配置(使用 `BD_KE_MCP_` 前缀)
86
+
87
+ ### 4. 使用
88
+
89
+ 配置完成后,重启 Cursor。MCP 服务器会自动启动,可以在 Cursor 中使用以下工具。
90
+
91
+ ## 项目结构
92
+
93
+ ```
94
+ bd-ke-mcp/
95
+ ├── index.js # MCP服务器主文件(包含所有工具实现)
96
+ ├── package.json # 项目配置文件
97
+ ├── .mcp/ # 配置目录
98
+ │ └── bd-ke-mcp.json.example # 配置示例
99
+ ├── README.md # 项目说明文档
100
+ └── LICENSE # 许可证
101
+ ```
102
+
103
+ ## 配置说明
104
+
105
+ ### 配置文件位置
106
+
107
+ `.mcp/bd-ke-mcp.json` - 项目根目录下的 `.mcp` 目录
108
+
109
+ ### 配置项
110
+
111
+ | 配置项 | 类型 | 必需 | 描述 |
112
+ |--------|------|------|------|
113
+ | `domain` | string | 是 | KE服务地址 |
114
+ | `access_token` | string | 是 | 授权token |
115
+ | `tenant` | string | 是 | 租户名称 |
116
+ | `project` | string | 是 | 项目名称 |
117
+ | `application` | string | 是 | 应用名称 |
118
+ | `env` | string | 是 | 环境(如:development, staging, production) |
119
+
120
+ **配置说明:**
121
+ - `tenant` 和 `project` 组合后形成 KE 的命名空间(namespace),格式为 `tenant-project`
122
+ - 例如:`tenant=app`, `project=xxx`,则实际的命名空间为 `app-xxx`
123
+ - 在 KE API 调用中,URL 路径中的 `namespace` 参数使用的是 `tenant-project` 格式
124
+
125
+ ### 环境变量
126
+
127
+ 配置可通过环境变量设置(优先级高于配置文件):
128
+
129
+ ```bash
130
+ export BD_KE_MCP_DOMAIN="https://ke.example.com"
131
+ export BD_KE_MCP_ACCESS_TOKEN="your_access_token"
132
+ export BD_KE_MCP_TENANT="your_tenant"
133
+ export BD_KE_MCP_PROJECT="your_project"
134
+ export BD_KE_MCP_APPLICATION="your_application"
135
+ export BD_KE_MCP_ENV="production"
136
+ ```
137
+
138
+ 环境变量命名规则:
139
+ - 使用 `BD_KE_MCP_` 前缀
140
+ - 配置键全部大写:`BD_KE_MCP_DOMAIN` → `domain`
141
+
142
+ ### 配置查找逻辑
143
+
144
+ 1. 从 `PROJECT_ROOT` 环境变量获取项目根目录
145
+ 2. 在项目根目录查找 `.mcp/bd-ke-mcp.json`
146
+ 3. 加载配置文件后,用环境变量覆盖同名配置
147
+
148
+ ## 可用工具
149
+
150
+ ### ke_app_restart
151
+
152
+ 重启 KE 应用(通过删除 Pod 触发自动重建)。
153
+
154
+ **使用方式:**
155
+
156
+ 在 Cursor 中直接调用,无需参数。工具会自动从项目配置中读取应用信息。
157
+
158
+ ### ke_pipeline_info
159
+
160
+ 获取 KE 流水线信息,包括流水线名称和执行参数。
161
+
162
+ **使用方式:**
163
+
164
+ 在 Cursor 中直接调用,无需参数。工具会从项目配置中读取流水线地址和授权token,返回流水线名称和可修改的执行参数。
165
+
166
+ ### ke_pipeline_create
167
+
168
+ 执行 KE 流水线(创建流水线运行时)。
169
+
170
+ **使用方式:**
171
+
172
+ 在 Cursor 中调用,需要传入 `globalArguments` 参数(参数字典)。工具会从项目配置中读取流水线地址和授权token,执行流水线。
173
+
174
+ **参数:**
175
+ - `globalArguments` (必需): 全局参数字典,键为参数名,值为参数值
176
+ - `deadline` (可选): 超时时间(秒),默认 86400
177
+ - `review` (可选): 审核信息,默认为空字符串
178
+
179
+ ### ke_tenants_list
180
+
181
+ 列出所有的租户。
182
+
183
+ ### ke_projects_list
184
+
185
+ 列出所有的项目。
186
+
187
+ ### ke_app_list
188
+
189
+ 列出所有的应用。
190
+
191
+ ### ke_env_list
192
+
193
+ 获取当前租户下的所有环境列表。
194
+
195
+ **参数:**
196
+ - `projectName` (可选): 已废弃,保留以保持向后兼容,实际不使用此参数
197
+
198
+ ### ke_pods_list
199
+
200
+ 获取指定应用的Pod实例列表。
201
+
202
+ **参数:**
203
+ - `appName` (必需): 应用名称,可通过 `ke_app_list` 获取
204
+
205
+ **返回信息:**
206
+ - 实例名称
207
+ - 实例状态(Running、Pending 等)
208
+ - 创建时间
209
+ - 容器IP
210
+ - 运行节点
211
+ - 重启次数
212
+
213
+ ### ke_pod_log_list
214
+
215
+ 获取指定应用的Pod日志列表。
216
+
217
+ **参数:**
218
+ - `namespace` (必需): 命名空间,格式为 tenant-project(如:app-sheyang)
219
+ - `service` (必需): 服务名称(应用名称),可通过 `ke_app_list` 获取
220
+ - `env` (必需): 环境名称,可通过 `ke_env_list` 获取
221
+ - `start_time` (必需): 开始时间(纳秒时间戳)
222
+ - `end_time` (必需): 结束时间(纳秒时间戳)
223
+ - `size` (可选): 返回记录数量,默认100
224
+ - `offset` (可选): 偏移量,默认0
225
+ - `sort` (可选): 排序方式,默认desc(降序),可选asc(升序)
226
+
227
+ **返回信息:**
228
+ - 日志内容(body)
229
+ - 时间戳
230
+ - Pod名称
231
+ - Pod IP
232
+ - 日志级别
233
+
234
+ ## 开发
235
+
236
+ ### 运行本地开发
237
+
238
+ ```bash
239
+ # 安装依赖
240
+ npm install
241
+
242
+ # 运行服务
243
+ npm start
244
+ ```
245
+
246
+ ### 调试模式
247
+
248
+ 设置 `BD_KE_MCP_DEBUG` 环境变量可启用 DEBUG 级别日志:
249
+
250
+ ```bash
251
+ BD_KE_MCP_DEBUG=1 npm start
252
+ ```
253
+
254
+ ## 从 Python 版本迁移
255
+
256
+ 如果你之前使用的是 Python 版本,迁移步骤如下:
257
+
258
+ 1. **配置文件兼容**:`.mcp/bd-ke-mcp.json` 配置格式完全相同,无需修改
259
+ 2. **更新 Cursor 配置**:将 MCP 配置中的 `command` 从 `python3` 改为 `node` 或 `npx`
260
+ 3. **功能完全兼容**:所有 9 个工具的功能和参数完全一致
261
+
262
+ ## 常见问题
263
+
264
+ ### 1. 配置文件未加载
265
+ - **原因**:项目根目录未正确识别
266
+ - **解决**:设置 `PROJECT_ROOT` 环境变量指向项目根目录
267
+
268
+ ### 2. 网络请求超时
269
+ - **原因**:KE 服务响应慢或网络问题
270
+ - **解决**:检查 `domain` 配置是否正确,确保网络可达
271
+
272
+ ### 3. 认证失败
273
+ - **原因**:`access_token` 无效或过期
274
+ - **解决**:更新配置文件中的 `access_token`
275
+
276
+ ## 日志
277
+
278
+ 日志输出到 stderr,可在 Cursor 的 MCP 日志面板查看。
279
+
280
+ 设置 `BD_KE_MCP_DEBUG` 环境变量可启用 DEBUG 级别日志。
281
+
282
+ ## 许可证
283
+
284
+ MIT License
package/index.js ADDED
@@ -0,0 +1,1088 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * BD-KE MCP Server
4
+ * MCP (Model Context Protocol) tools for Knowledge Engine application management
5
+ */
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8
+ import { z } from "zod";
9
+ import fs from "fs";
10
+ import path from "path";
11
+ import os from "os";
12
+
13
+ // ==================== 常量定义 ====================
14
+ const VERSION = "1.0.0";
15
+ const CONFIG_FILE_NAME = "bd-ke-mcp.json";
16
+ const FETCH_TIMEOUT = 30000; // 30秒超时
17
+
18
+ // ==================== 日志系统 ====================
19
+ /**
20
+ * 日志输出到 stderr(Cursor 会捕获显示)
21
+ * 注意:不能使用 console.log,会污染 MCP 的 stdio 通信通道
22
+ */
23
+ const logger = {
24
+ info: (message, data) => {
25
+ const timestamp = new Date().toISOString();
26
+ const dataStr = data ? ` | ${JSON.stringify(data)}` : '';
27
+ console.error(`[${timestamp}] INFO ${message}${dataStr}`);
28
+ },
29
+ warn: (message, data) => {
30
+ const timestamp = new Date().toISOString();
31
+ const dataStr = data ? ` | ${JSON.stringify(data)}` : '';
32
+ console.error(`[${timestamp}] WARN ${message}${dataStr}`);
33
+ },
34
+ error: (message, data) => {
35
+ const timestamp = new Date().toISOString();
36
+ const dataStr = data ? ` | ${JSON.stringify(data)}` : '';
37
+ console.error(`[${timestamp}] ERROR ${message}${dataStr}`);
38
+ },
39
+ debug: (message, data) => {
40
+ if (process.env.BD_KE_MCP_DEBUG) {
41
+ const timestamp = new Date().toISOString();
42
+ const dataStr = data ? ` | ${JSON.stringify(data)}` : '';
43
+ console.error(`[${timestamp}] DEBUG ${message}${dataStr}`);
44
+ }
45
+ }
46
+ };
47
+
48
+ // ==================== 配置管理 ====================
49
+
50
+ /**
51
+ * 获取项目根目录(直接从 PROJECT_ROOT 环境变量获取)
52
+ */
53
+ function getProjectRoot() {
54
+ return process.env.PROJECT_ROOT || null;
55
+ }
56
+
57
+ /**
58
+ * 加载配置文件
59
+ * 直接从 PROJECT_ROOT/.mcp/bd-ke-mcp.json 加载
60
+ */
61
+ function loadConfig() {
62
+ const config = {};
63
+ const projectRoot = getProjectRoot();
64
+
65
+ if (!projectRoot) {
66
+ logger.warn("PROJECT_ROOT 环境变量未设置,无法加载配置文件");
67
+ return config;
68
+ }
69
+
70
+ const configFile = path.join(projectRoot, ".mcp", CONFIG_FILE_NAME);
71
+
72
+ if (!fs.existsSync(configFile)) {
73
+ logger.warn("配置文件不存在", { path: configFile });
74
+ return config;
75
+ }
76
+
77
+ try {
78
+ const fileContent = fs.readFileSync(configFile, "utf8");
79
+ const fileConfig = JSON.parse(fileContent);
80
+ Object.assign(config, fileConfig);
81
+ logger.info("已加载配置文件", { path: configFile });
82
+ } catch (error) {
83
+ logger.error("配置文件加载失败", { error: error.message });
84
+ }
85
+
86
+ return config;
87
+ }
88
+
89
+ // 全局配置
90
+ let globalConfig = loadConfig();
91
+
92
+ /**
93
+ * 重新加载配置(确保获取最新配置)
94
+ */
95
+ function reloadConfig() {
96
+ globalConfig = loadConfig();
97
+ return globalConfig;
98
+ }
99
+
100
+ /**
101
+ * 获取配置值
102
+ */
103
+ function getConfig(key, defaultValue = null) {
104
+ return globalConfig[key] ?? defaultValue;
105
+ }
106
+
107
+ /**
108
+ * 获取租户命名空间(tenant-project 格式)
109
+ */
110
+ function getTenantNamespace() {
111
+ const tenant = getConfig("tenant");
112
+ const project = getConfig("project");
113
+ if (!tenant || !project) {
114
+ throw new Error("配置项 tenant 或 project 未设置");
115
+ }
116
+ return `${tenant}-${project}`;
117
+ }
118
+
119
+ // ==================== HTTP 请求 ====================
120
+
121
+ /**
122
+ * 带超时的 fetch 请求
123
+ */
124
+ async function fetchWithTimeout(url, options = {}, timeoutMs = FETCH_TIMEOUT) {
125
+ const controller = new AbortController();
126
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
127
+
128
+ try {
129
+ const response = await fetch(url, {
130
+ ...options,
131
+ signal: controller.signal
132
+ });
133
+ clearTimeout(timeoutId);
134
+ return response;
135
+ } catch (error) {
136
+ clearTimeout(timeoutId);
137
+ if (error.name === "AbortError") {
138
+ throw new Error(`请求超时(${timeoutMs}ms)`);
139
+ }
140
+ throw error;
141
+ }
142
+ }
143
+
144
+ /**
145
+ * KE API 请求
146
+ */
147
+ async function keRequest(method, urlPath, options = {}) {
148
+ const domain = getConfig("domain");
149
+ const accessToken = getConfig("access_token");
150
+
151
+ if (!domain) {
152
+ throw new Error("配置项 domain 未设置");
153
+ }
154
+ if (!accessToken) {
155
+ throw new Error("配置项 access_token 未设置");
156
+ }
157
+
158
+ const url = `${domain.replace(/\/+$/, "")}/${urlPath.replace(/^\/+/, "")}`;
159
+
160
+ logger.debug("KE API 请求", { method, url });
161
+
162
+ const headers = {
163
+ "leo-user-token": accessToken,
164
+ "Content-Type": "application/json",
165
+ ...options.headers
166
+ };
167
+
168
+ const response = await fetchWithTimeout(url, {
169
+ method,
170
+ headers,
171
+ body: options.body ? JSON.stringify(options.body) : undefined
172
+ });
173
+
174
+ if (!response.ok) {
175
+ const errorText = await response.text();
176
+ throw new Error(`HTTP ${response.status}: ${errorText}`);
177
+ }
178
+
179
+ const data = await response.json();
180
+
181
+ if (data.rtnCode !== "000000") {
182
+ throw new Error(data.rtnMsg || "API 返回错误");
183
+ }
184
+
185
+ return data;
186
+ }
187
+
188
+ // ==================== 工具实现 ====================
189
+
190
+ /**
191
+ * ke_app_restart - 重启 KE 应用(通过删除 Pod)
192
+ */
193
+ async function keAppRestart() {
194
+ reloadConfig();
195
+
196
+ const application = getConfig("application");
197
+ const env = getConfig("env");
198
+
199
+ if (!application || !env) {
200
+ throw new Error("配置项 application 或 env 未设置");
201
+ }
202
+
203
+ const namespace = getTenantNamespace();
204
+
205
+ logger.info("重启应用", { application, env, namespace });
206
+
207
+ // 1. 获取应用状态
208
+ const statusUrl = `amp/v4/namespace/${namespace}/env/${env}/applications/${application}/appstatus/view?canary=false`;
209
+ const statusData = await keRequest("GET", statusUrl);
210
+
211
+ // 2. 提取 Pod 名称
212
+ const podNames = [];
213
+ const workloads = statusData.rtnData?.workloads || [];
214
+ for (const workload of workloads) {
215
+ const pods = workload.pods || [];
216
+ for (const pod of pods) {
217
+ if (pod.name) {
218
+ podNames.push(pod.name);
219
+ }
220
+ }
221
+ }
222
+
223
+ logger.info(`找到 ${podNames.length} 个 Pod`);
224
+
225
+ if (podNames.length === 0) {
226
+ return "未找到任何 Pod 节点";
227
+ }
228
+
229
+ // 3. 删除每个 Pod
230
+ const deletedPods = [];
231
+ const failedPods = [];
232
+
233
+ for (const podName of podNames) {
234
+ try {
235
+ const deleteUrl = `amp/v4/namespace/${namespace}/env/${env}/applications/${application}/pods/${podName}/delete?cluster=${env}`;
236
+ await keRequest("DELETE", deleteUrl);
237
+ deletedPods.push(podName);
238
+ logger.info(`已删除 Pod: ${podName}`);
239
+ } catch (error) {
240
+ failedPods.push({ pod: podName, error: error.message });
241
+ logger.error(`删除 Pod 失败: ${podName}`, { error: error.message });
242
+ }
243
+ }
244
+
245
+ // 4. 构建返回消息
246
+ let result = `KE 应用重启操作完成\n`;
247
+ result += `应用: ${application}\n`;
248
+ result += `环境: ${env}\n\n`;
249
+ result += `成功重启的 Pod 节点 (${deletedPods.length} 个):\n`;
250
+
251
+ if (deletedPods.length > 0) {
252
+ for (const pod of deletedPods) {
253
+ result += ` - ${pod}\n`;
254
+ }
255
+ } else {
256
+ result += " (无)\n";
257
+ }
258
+
259
+ if (failedPods.length > 0) {
260
+ result += `\n删除失败的 Pod 节点 (${failedPods.length} 个):\n`;
261
+ for (const failed of failedPods) {
262
+ result += ` - ${failed.pod}: ${failed.error}\n`;
263
+ }
264
+ }
265
+
266
+ return result;
267
+ }
268
+
269
+ /**
270
+ * ke_pipeline_list - 获取流水线列表
271
+ */
272
+ async function kePipelineList(page = 1, pageSize = 20) {
273
+ reloadConfig();
274
+
275
+ const domain = getConfig("domain");
276
+ const tenant = getConfig("tenant");
277
+ const project = getConfig("project");
278
+ const application = getConfig("application");
279
+
280
+ if (!tenant || !project) {
281
+ throw new Error("配置项 tenant 或 project 未设置");
282
+ }
283
+
284
+ // 构建 label 参数
285
+ const label = {
286
+ tenantName: tenant,
287
+ projectName: project
288
+ };
289
+ if (application) {
290
+ label.applicationName = application;
291
+ }
292
+
293
+ // URL 编码 label 参数
294
+ const labelEncoded = encodeURIComponent(JSON.stringify(label));
295
+ const group = `${tenant}-${project}`;
296
+
297
+ const url = `pipeline/v1/group/${group}/pipelines/list?page=${page}&pageSize=${pageSize}&searchValue=&label=${labelEncoded}`;
298
+
299
+ logger.info("获取流水线列表", { group, application });
300
+
301
+ const data = await keRequest("GET", url);
302
+ const pipelinesData = data.rtnData?.data || [];
303
+ const envMapping = data.rtnData?.env || {};
304
+ const totalCount = data.rtnData?.count || 0;
305
+
306
+ if (pipelinesData.length === 0) {
307
+ return "未找到任何流水线";
308
+ }
309
+
310
+ let result = `找到 ${totalCount} 条流水线:\n\n`;
311
+
312
+ for (let i = 0; i < pipelinesData.length; i++) {
313
+ const pipeline = pipelinesData[i];
314
+ const pipelineName = pipeline.name || "";
315
+ const pipelineAlias = pipeline.alias || "";
316
+ const creator = pipeline.creator || "";
317
+ const createAt = (pipeline.create_at || "").substring(0, 19).replace("T", " ");
318
+ const pipelineGroup = pipeline.group || group;
319
+ const env = envMapping[pipelineName] || "";
320
+
321
+ // 构建流水线地址
322
+ const pipelineUrl = `${domain}/pipeline/v1/group/${pipelineGroup}/pipelines/${pipelineName}/view`;
323
+
324
+ result += `${i + 1}. ${pipelineAlias || pipelineName}\n`;
325
+ result += ` 流水线名称: ${pipelineName}\n`;
326
+ result += ` 环境: ${env}\n`;
327
+ result += ` 创建者: ${creator}\n`;
328
+ result += ` 创建时间: ${createAt}\n`;
329
+ result += ` 流水线地址: ${pipelineUrl}\n\n`;
330
+ }
331
+
332
+ return result.trim();
333
+ }
334
+
335
+ /**
336
+ * ke_pipeline_info - 获取流水线信息
337
+ */
338
+ async function kePipelineInfo(pipelineUrl) {
339
+ reloadConfig();
340
+
341
+ if (!pipelineUrl) {
342
+ throw new Error("参数 pipelineUrl 不能为空");
343
+ }
344
+
345
+ const accessToken = getConfig("access_token");
346
+ if (!accessToken) {
347
+ throw new Error("配置项 access_token 未设置");
348
+ }
349
+
350
+ logger.info("获取流水线信息", { url: pipelineUrl });
351
+
352
+ const response = await fetchWithTimeout(pipelineUrl, {
353
+ method: "GET",
354
+ headers: {
355
+ "leo-user-token": accessToken,
356
+ "Content-Type": "application/json"
357
+ }
358
+ });
359
+
360
+ if (!response.ok) {
361
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
362
+ }
363
+
364
+ const data = await response.json();
365
+
366
+ if (data.rtnCode !== "000000") {
367
+ throw new Error(data.rtnMsg || "获取流水线信息失败");
368
+ }
369
+
370
+ // 提取流水线信息
371
+ const rtnData = data.rtnData || {};
372
+ const pipelineData = rtnData.data || {};
373
+ const pipelineName = pipelineData.name || "未知";
374
+ const metaData = pipelineData.meta_data || {};
375
+ const globalParams = metaData.global || [];
376
+
377
+ let result = `流水线名称: ${pipelineName}\n\n`;
378
+ result += "执行参数(可修改):\n\n";
379
+
380
+ for (const param of globalParams) {
381
+ const paramName = param.name || "";
382
+ const paramDesc = param.desc || "";
383
+ const paramValue = param.value || "";
384
+ result += ` ${paramName}: ${paramValue} # ${paramDesc}\n`;
385
+ }
386
+
387
+ result += "\n" + "=".repeat(60) + "\n";
388
+ result += "请使用 ke_pipeline_create 工具执行流水线,传入修改后的参数。\n";
389
+ result += '参数格式: {"参数名": "参数值", ...}';
390
+
391
+ return result;
392
+ }
393
+
394
+ /**
395
+ * ke_pipeline_create - 执行流水线
396
+ */
397
+ async function kePipelineCreate(pipelineUrl, globalArguments, deadline = 86400, review = "") {
398
+ reloadConfig();
399
+
400
+ if (!pipelineUrl) {
401
+ throw new Error("参数 pipelineUrl 不能为空");
402
+ }
403
+
404
+ const accessToken = getConfig("access_token");
405
+ if (!accessToken) {
406
+ throw new Error("配置项 access_token 未设置");
407
+ }
408
+ if (!globalArguments || Object.keys(globalArguments).length === 0) {
409
+ throw new Error("参数 globalArguments 不能为空");
410
+ }
411
+
412
+ // 将 /view 替换为 /runtimes/create
413
+ const createUrl = pipelineUrl.replace("/view", "/runtimes/create");
414
+
415
+ logger.info("执行流水线", { url: createUrl, params: globalArguments });
416
+
417
+ const response = await fetchWithTimeout(createUrl, {
418
+ method: "POST",
419
+ headers: {
420
+ "leo-user-token": accessToken,
421
+ "Content-Type": "application/json"
422
+ },
423
+ body: JSON.stringify({
424
+ deadline,
425
+ globalArguments,
426
+ review
427
+ })
428
+ });
429
+
430
+ if (!response.ok) {
431
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
432
+ }
433
+
434
+ const data = await response.json();
435
+
436
+ if (data.rtnCode !== "000000") {
437
+ throw new Error(data.rtnMsg || "创建流水线失败");
438
+ }
439
+
440
+ let result = "KE 流水线执行已启动\n\n";
441
+ result += `参数数量: ${Object.keys(globalArguments).length}\n\n`;
442
+ result += "执行参数:\n";
443
+
444
+ for (const [key, value] of Object.entries(globalArguments)) {
445
+ result += ` ${key}: ${value}\n`;
446
+ }
447
+
448
+ if (data.rtnData) {
449
+ result += `\n执行结果:\n ${JSON.stringify(data.rtnData)}`;
450
+ }
451
+
452
+ return result;
453
+ }
454
+
455
+ /**
456
+ * ke_tenants_list - 列出所有租户
457
+ */
458
+ async function keTenantsList() {
459
+ reloadConfig();
460
+
461
+ const url = "user-center/v4/tenants/list?page=1&pageSize=100&type=all&single=true";
462
+ const data = await keRequest("GET", url);
463
+
464
+ const tenantsData = data.rtnData?.data || [];
465
+
466
+ if (tenantsData.length === 0) {
467
+ return "未找到任何租户";
468
+ }
469
+
470
+ let result = `找到 ${tenantsData.length} 个租户:\n\n`;
471
+
472
+ for (const tenant of tenantsData) {
473
+ result += ` 租户ID: ${tenant.tenantId || ""}\n`;
474
+ result += ` 租户名称: ${tenant.tenantName || ""}\n`;
475
+ result += ` 租户描述: ${tenant.tenantDesc || ""}\n\n`;
476
+ }
477
+
478
+ return result.trim();
479
+ }
480
+
481
+ /**
482
+ * ke_projects_list - 列出所有项目
483
+ */
484
+ async function keProjectsList() {
485
+ reloadConfig();
486
+
487
+ const tenant = getConfig("tenant");
488
+ if (!tenant) {
489
+ throw new Error("配置项 tenant 未设置");
490
+ }
491
+
492
+ const url = `user-center/v4/tenants/${tenant}/projects/list`;
493
+ const data = await keRequest("GET", url);
494
+
495
+ const projectsData = data.rtnData?.data || [];
496
+
497
+ if (projectsData.length === 0) {
498
+ return "未找到任何项目";
499
+ }
500
+
501
+ let result = `找到 ${projectsData.length} 个项目:\n\n`;
502
+
503
+ for (const project of projectsData) {
504
+ result += ` 租户名称: ${project.tenantName || ""}\n`;
505
+ result += ` 项目ID: ${project.projectId || ""}\n`;
506
+ result += ` 项目名称: ${project.projectName || ""}\n`;
507
+ result += ` 项目描述: ${project.projectDesc || ""}\n`;
508
+ result += ` 命名空间: ${project.namespace || ""}\n\n`;
509
+ }
510
+
511
+ return result.trim();
512
+ }
513
+
514
+ /**
515
+ * ke_app_list - 列出所有应用
516
+ */
517
+ async function keAppList() {
518
+ reloadConfig();
519
+
520
+ const namespace = getTenantNamespace();
521
+ const url = `amp/v4/namespace/${namespace}/applications/list?page=1&pageSize=999`;
522
+ const data = await keRequest("GET", url);
523
+
524
+ const appsData = data.rtnData?.data || [];
525
+
526
+ if (appsData.length === 0) {
527
+ return "未找到任何应用";
528
+ }
529
+
530
+ let result = `找到 ${appsData.length} 个应用:\n\n`;
531
+
532
+ for (const app of appsData) {
533
+ result += ` 应用名称: ${app.name || ""}\n`;
534
+ result += ` 显示名称: ${app.displayName || ""}\n`;
535
+ result += ` 分组: ${app.group || ""}\n\n`;
536
+ }
537
+
538
+ return result.trim();
539
+ }
540
+
541
+ /**
542
+ * ke_env_list - 获取环境列表
543
+ */
544
+ async function keEnvList() {
545
+ reloadConfig();
546
+
547
+ const tenant = getConfig("tenant");
548
+ if (!tenant) {
549
+ throw new Error("配置项 tenant 未设置");
550
+ }
551
+
552
+ const url = `user-center/v4/tenants/${tenant}/clusters/all/list`;
553
+ const data = await keRequest("GET", url);
554
+
555
+ const clustersData = data.rtnData?.data || [];
556
+ const envList = clustersData.map(c => c.name).filter(Boolean);
557
+
558
+ if (envList.length === 0) {
559
+ return `租户 ${tenant} 未找到任何环境`;
560
+ }
561
+
562
+ let result = `租户 ${tenant} 的环境列表(共 ${envList.length} 个):\n\n`;
563
+
564
+ for (const envName of envList) {
565
+ result += ` - ${envName}\n`;
566
+ }
567
+
568
+ return result;
569
+ }
570
+
571
+ /**
572
+ * ke_pods_list - 获取 Pod 列表
573
+ */
574
+ async function kePodsList(appName) {
575
+ reloadConfig();
576
+
577
+ if (!appName) {
578
+ throw new Error("参数 appName 不能为空");
579
+ }
580
+
581
+ const namespace = getTenantNamespace();
582
+ const env = getConfig("env");
583
+
584
+ if (!env) {
585
+ throw new Error("配置项 env 未设置");
586
+ }
587
+
588
+ const url = `amp/v4/namespace/${namespace}/env/${env}/applications/${appName}/appstatus/view?canary=false`;
589
+ const data = await keRequest("GET", url);
590
+
591
+ // 提取 Pod 列表
592
+ const podsList = [];
593
+ const workloads = data.rtnData?.workloads || [];
594
+
595
+ for (const workload of workloads) {
596
+ const pods = workload.pods || [];
597
+ for (const pod of pods) {
598
+ const unstructured = pod.unstructured || {};
599
+ const spec = unstructured.spec || {};
600
+ const status = unstructured.status || {};
601
+
602
+ // 计算重启次数
603
+ const containerStatuses = status.containerStatuses || [];
604
+ let totalRestartCount = 0;
605
+ for (const cs of containerStatuses) {
606
+ totalRestartCount += cs.restartCount || 0;
607
+ }
608
+
609
+ podsList.push({
610
+ name: pod.name || "",
611
+ status: pod.status || "",
612
+ createTime: (pod.createTime || "").substring(0, 19),
613
+ podIp: pod.podIp || "",
614
+ nodeName: spec.nodeName || "",
615
+ restartCount: totalRestartCount
616
+ });
617
+ }
618
+ }
619
+
620
+ if (podsList.length === 0) {
621
+ return `应用 ${appName} 未找到任何 Pod 实例`;
622
+ }
623
+
624
+ let result = `应用 ${appName} 的 Pod 列表(共 ${podsList.length} 个实例):\n\n`;
625
+ result += `${"实例名称".padEnd(45)} ${"状态".padEnd(10)} ${"创建时间".padEnd(22)} ${"容器IP".padEnd(16)} ${"运行节点".padEnd(20)} ${"重启次数"}\n`;
626
+ result += "-".repeat(130) + "\n";
627
+
628
+ for (const pod of podsList) {
629
+ result += `${pod.name.padEnd(45)} ${pod.status.padEnd(10)} ${pod.createTime.padEnd(22)} ${pod.podIp.padEnd(16)} ${pod.nodeName.padEnd(20)} ${pod.restartCount}\n`;
630
+ }
631
+
632
+ return result;
633
+ }
634
+
635
+ /**
636
+ * ke_pod_log_list - 获取 Pod 日志列表
637
+ */
638
+ async function kePodLogList(namespace, service, env, startTime, endTime, size = 100, offset = 0, sort = "desc") {
639
+ reloadConfig();
640
+
641
+ if (!namespace) throw new Error("参数 namespace 不能为空");
642
+ if (!service) throw new Error("参数 service 不能为空");
643
+ if (!env) throw new Error("参数 env 不能为空");
644
+ if (!startTime) throw new Error("参数 start_time 不能为空");
645
+ if (!endTime) throw new Error("参数 end_time 不能为空");
646
+
647
+ const tenant = getConfig("tenant");
648
+ if (!tenant) {
649
+ throw new Error("配置项 tenant 未设置");
650
+ }
651
+
652
+ const url = `armilla/v1/group/${tenant}/env/${env}/log/list`;
653
+
654
+ const requestBody = {
655
+ keys: [],
656
+ sort,
657
+ offset,
658
+ size,
659
+ log_source: "application",
660
+ serviceNames: [],
661
+ start_time: startTime,
662
+ end_time: endTime,
663
+ tags: [
664
+ {
665
+ key: "_namespace_name_",
666
+ keyKind: "FixedLabel",
667
+ keyType: "string",
668
+ operator: "Equals",
669
+ stringValues: [namespace]
670
+ },
671
+ {
672
+ key: "_service_name_",
673
+ keyKind: "FixedLabel",
674
+ keyType: "string",
675
+ operator: "Equals",
676
+ stringValues: [service]
677
+ }
678
+ ],
679
+ log_version: "v3"
680
+ };
681
+
682
+ const data = await keRequest("POST", url, { body: requestBody });
683
+ const records = data.rtnData?.records || [];
684
+
685
+ if (records.length === 0) {
686
+ return `未找到任何日志记录(namespace: ${namespace}, service: ${service}, env: ${env})`;
687
+ }
688
+
689
+ let result = `Pod 日志列表(共 ${records.length} 条记录):\n`;
690
+ result += `命名空间: ${namespace}\n`;
691
+ result += `服务名称: ${service}\n`;
692
+ result += `环境: ${env}\n\n`;
693
+
694
+ for (const record of records) {
695
+ const body = record.body || "";
696
+ const time = record.time;
697
+ const stringTags = record.stringTags || {};
698
+ const podName = stringTags._pod_name_ || "";
699
+ const podIp = stringTags._pod_ip_ || "";
700
+ const level = stringTags._level_ || "";
701
+
702
+ // 格式化时间戳
703
+ let timeStr = "";
704
+ if (time) {
705
+ try {
706
+ const timeSeconds = Math.floor(Number(time) / 1000000000);
707
+ const date = new Date(timeSeconds * 1000);
708
+ timeStr = date.toISOString().replace("T", " ").substring(0, 19);
709
+ } catch {
710
+ timeStr = String(time);
711
+ }
712
+ }
713
+
714
+ result += `[${timeStr}] [${podName}] [${podIp}] [${level}]\n`;
715
+ result += ` ${body}\n\n`;
716
+ }
717
+
718
+ return result;
719
+ }
720
+
721
+ /**
722
+ * ke_pod_exec - 在 Pod 容器中执行命令
723
+ */
724
+ async function kePodExec(tenant, project, podName, cluster, command, container = "", nodeName = "") {
725
+ reloadConfig();
726
+
727
+ if (!tenant) throw new Error("参数 tenant 不能为空");
728
+ if (!project) throw new Error("参数 project 不能为空");
729
+ if (!podName) throw new Error("参数 podName 不能为空");
730
+ if (!cluster) throw new Error("参数 cluster 不能为空");
731
+ if (!command) throw new Error("参数 command 不能为空");
732
+
733
+ const namespace = `${tenant}-${project}`;
734
+
735
+ // 构建 exec API URL
736
+ // 根据 KE 平台的 API 结构,exec 接口可能在 amp/v4 下
737
+ const url = `amp/v4/namespace/${namespace}/env/${cluster}/pods/${podName}/exec`;
738
+
739
+ logger.info("执行 Pod 命令", { namespace, podName, cluster, command, container, nodeName });
740
+
741
+ const requestBody = {
742
+ command: command.split(/\s+/), // 将命令字符串拆分为数组
743
+ container: container || undefined,
744
+ nodeName: nodeName || undefined
745
+ };
746
+
747
+ // 移除 undefined 字段
748
+ Object.keys(requestBody).forEach(key => {
749
+ if (requestBody[key] === undefined) {
750
+ delete requestBody[key];
751
+ }
752
+ });
753
+
754
+ try {
755
+ const data = await keRequest("POST", url, { body: requestBody });
756
+
757
+ const execResult = data.rtnData || {};
758
+ const stdout = execResult.stdout || "";
759
+ const stderr = execResult.stderr || "";
760
+ const exitCode = execResult.exitCode !== undefined ? execResult.exitCode : (data.rtnCode === "000000" ? 0 : 1);
761
+
762
+ let result = `命令执行完成\n`;
763
+ result += `Pod: ${podName}\n`;
764
+ result += `命名空间: ${namespace}\n`;
765
+ result += `环境: ${cluster}\n`;
766
+ result += `命令: ${command}\n`;
767
+ result += `退出码: ${exitCode}\n\n`;
768
+
769
+ if (stdout) {
770
+ result += `标准输出:\n${stdout}\n`;
771
+ }
772
+
773
+ if (stderr) {
774
+ result += `错误输出:\n${stderr}\n`;
775
+ }
776
+
777
+ if (!stdout && !stderr) {
778
+ result += `(无输出)`;
779
+ }
780
+
781
+ return result;
782
+ } catch (error) {
783
+ // 如果 POST 失败,尝试 GET 方式(某些平台可能使用 GET)
784
+ logger.debug("POST 方式失败,尝试 GET 方式", { error: error.message });
785
+
786
+ const getUrl = `${url}?command=${encodeURIComponent(command)}${container ? `&container=${encodeURIComponent(container)}` : ''}${nodeName ? `&nodeName=${encodeURIComponent(nodeName)}` : ''}`;
787
+
788
+ try {
789
+ const data = await keRequest("GET", getUrl);
790
+ const execResult = data.rtnData || {};
791
+ const stdout = execResult.stdout || "";
792
+ const stderr = execResult.stderr || "";
793
+ const exitCode = execResult.exitCode !== undefined ? execResult.exitCode : 0;
794
+
795
+ let result = `命令执行完成\n`;
796
+ result += `Pod: ${podName}\n`;
797
+ result += `命名空间: ${namespace}\n`;
798
+ result += `环境: ${cluster}\n`;
799
+ result += `命令: ${command}\n`;
800
+ result += `退出码: ${exitCode}\n\n`;
801
+
802
+ if (stdout) {
803
+ result += `标准输出:\n${stdout}\n`;
804
+ }
805
+
806
+ if (stderr) {
807
+ result += `错误输出:\n${stderr}\n`;
808
+ }
809
+
810
+ if (!stdout && !stderr) {
811
+ result += `(无输出)`;
812
+ }
813
+
814
+ return result;
815
+ } catch (getError) {
816
+ throw new Error(`执行命令失败: ${error.message}。如果 KE 平台不支持 exec API,请使用 webshell 手动操作。`);
817
+ }
818
+ }
819
+ }
820
+
821
+ // ==================== MCP 服务器 ====================
822
+
823
+ const server = new McpServer({
824
+ name: "bd-ke-mcp",
825
+ version: VERSION
826
+ });
827
+
828
+ // 注册工具:ke_app_restart
829
+ server.registerTool(
830
+ "ke_app_restart",
831
+ {
832
+ description: "重启 KE 应用(通过删除 Pod 触发自动重建)",
833
+ inputSchema: {}
834
+ },
835
+ async () => {
836
+ try {
837
+ const result = await keAppRestart();
838
+ return { content: [{ type: "text", text: result }] };
839
+ } catch (error) {
840
+ logger.error("ke_app_restart 失败", { error: error.message });
841
+ return { content: [{ type: "text", text: `错误: ${error.message}` }], isError: true };
842
+ }
843
+ }
844
+ );
845
+
846
+ // 注册工具:ke_pipeline_list
847
+ server.registerTool(
848
+ "ke_pipeline_list",
849
+ {
850
+ description: "获取 KE 流水线列表,根据配置文件中的 tenant、project、application 筛选",
851
+ inputSchema: {
852
+ page: z.number().optional().default(1).describe("页码,默认 1"),
853
+ pageSize: z.number().optional().default(20).describe("每页数量,默认 20")
854
+ }
855
+ },
856
+ async ({ page, pageSize }) => {
857
+ try {
858
+ const result = await kePipelineList(page, pageSize);
859
+ return { content: [{ type: "text", text: result }] };
860
+ } catch (error) {
861
+ logger.error("ke_pipeline_list 失败", { error: error.message });
862
+ return { content: [{ type: "text", text: `错误: ${error.message}` }], isError: true };
863
+ }
864
+ }
865
+ );
866
+
867
+ // 注册工具:ke_pipeline_info
868
+ server.registerTool(
869
+ "ke_pipeline_info",
870
+ {
871
+ description: "获取 KE 流水线信息,包括流水线名称和执行参数",
872
+ inputSchema: {
873
+ pipelineUrl: z.string().describe("流水线地址(如:http://pro.kubeease.cn/pipeline/v1/group/tenant-project/pipelines/app-xxx/view)")
874
+ }
875
+ },
876
+ async ({ pipelineUrl }) => {
877
+ try {
878
+ const result = await kePipelineInfo(pipelineUrl);
879
+ return { content: [{ type: "text", text: result }] };
880
+ } catch (error) {
881
+ logger.error("ke_pipeline_info 失败", { error: error.message });
882
+ return { content: [{ type: "text", text: `错误: ${error.message}` }], isError: true };
883
+ }
884
+ }
885
+ );
886
+
887
+ // 注册工具:ke_pipeline_create
888
+ server.registerTool(
889
+ "ke_pipeline_create",
890
+ {
891
+ description: "执行 KE 流水线(创建流水线运行时)",
892
+ inputSchema: {
893
+ pipelineUrl: z.string().describe("流水线地址(如:http://pro.kubeease.cn/pipeline/v1/group/tenant-project/pipelines/app-xxx/view)"),
894
+ globalArguments: z.record(z.string()).describe("全局参数字典,键为参数名,值为参数值"),
895
+ deadline: z.number().optional().default(86400).describe("超时时间(秒),默认 86400"),
896
+ review: z.string().optional().default("").describe("审核信息")
897
+ }
898
+ },
899
+ async ({ pipelineUrl, globalArguments, deadline, review }) => {
900
+ try {
901
+ const result = await kePipelineCreate(pipelineUrl, globalArguments, deadline, review);
902
+ return { content: [{ type: "text", text: result }] };
903
+ } catch (error) {
904
+ logger.error("ke_pipeline_create 失败", { error: error.message });
905
+ return { content: [{ type: "text", text: `错误: ${error.message}` }], isError: true };
906
+ }
907
+ }
908
+ );
909
+
910
+ // 注册工具:ke_tenants_list
911
+ server.registerTool(
912
+ "ke_tenants_list",
913
+ {
914
+ description: "列出所有的租户",
915
+ inputSchema: {}
916
+ },
917
+ async () => {
918
+ try {
919
+ const result = await keTenantsList();
920
+ return { content: [{ type: "text", text: result }] };
921
+ } catch (error) {
922
+ logger.error("ke_tenants_list 失败", { error: error.message });
923
+ return { content: [{ type: "text", text: `错误: ${error.message}` }], isError: true };
924
+ }
925
+ }
926
+ );
927
+
928
+ // 注册工具:ke_projects_list
929
+ server.registerTool(
930
+ "ke_projects_list",
931
+ {
932
+ description: "列出所有的项目",
933
+ inputSchema: {}
934
+ },
935
+ async () => {
936
+ try {
937
+ const result = await keProjectsList();
938
+ return { content: [{ type: "text", text: result }] };
939
+ } catch (error) {
940
+ logger.error("ke_projects_list 失败", { error: error.message });
941
+ return { content: [{ type: "text", text: `错误: ${error.message}` }], isError: true };
942
+ }
943
+ }
944
+ );
945
+
946
+ // 注册工具:ke_app_list
947
+ server.registerTool(
948
+ "ke_app_list",
949
+ {
950
+ description: "列出所有的应用",
951
+ inputSchema: {}
952
+ },
953
+ async () => {
954
+ try {
955
+ const result = await keAppList();
956
+ return { content: [{ type: "text", text: result }] };
957
+ } catch (error) {
958
+ logger.error("ke_app_list 失败", { error: error.message });
959
+ return { content: [{ type: "text", text: `错误: ${error.message}` }], isError: true };
960
+ }
961
+ }
962
+ );
963
+
964
+ // 注册工具:ke_env_list
965
+ server.registerTool(
966
+ "ke_env_list",
967
+ {
968
+ description: "获取当前租户下的所有环境列表",
969
+ inputSchema: {
970
+ projectName: z.string().optional().describe("项目名称(已废弃,保留以保持向后兼容)")
971
+ }
972
+ },
973
+ async () => {
974
+ try {
975
+ const result = await keEnvList();
976
+ return { content: [{ type: "text", text: result }] };
977
+ } catch (error) {
978
+ logger.error("ke_env_list 失败", { error: error.message });
979
+ return { content: [{ type: "text", text: `错误: ${error.message}` }], isError: true };
980
+ }
981
+ }
982
+ );
983
+
984
+ // 注册工具:ke_pods_list
985
+ server.registerTool(
986
+ "ke_pods_list",
987
+ {
988
+ description: "获取指定应用的 Pod 实例列表,包含实例名称、状态、创建时间、容器IP、运行节点、重启次数等信息",
989
+ inputSchema: {
990
+ appName: z.string().describe("应用名称,可通过 ke_app_list 获取")
991
+ }
992
+ },
993
+ async ({ appName }) => {
994
+ try {
995
+ const result = await kePodsList(appName);
996
+ return { content: [{ type: "text", text: result }] };
997
+ } catch (error) {
998
+ logger.error("ke_pods_list 失败", { error: error.message });
999
+ return { content: [{ type: "text", text: `错误: ${error.message}` }], isError: true };
1000
+ }
1001
+ }
1002
+ );
1003
+
1004
+ // 注册工具:ke_pod_log_list
1005
+ server.registerTool(
1006
+ "ke_pod_log_list",
1007
+ {
1008
+ description: "获取指定应用的 Pod 日志列表",
1009
+ inputSchema: {
1010
+ namespace: z.string().describe("命名空间,格式为 tenant-project(如:app-sheyang)"),
1011
+ service: z.string().describe("服务名称(应用名称),可通过 ke_app_list 获取"),
1012
+ env: z.string().describe("环境名称,可通过 ke_env_list 获取"),
1013
+ start_time: z.number().describe("开始时间(纳秒时间戳)"),
1014
+ end_time: z.number().describe("结束时间(纳秒时间戳)"),
1015
+ size: z.number().optional().default(100).describe("返回记录数量,默认 100"),
1016
+ offset: z.number().optional().default(0).describe("偏移量,默认 0"),
1017
+ sort: z.string().optional().default("desc").describe("排序方式,默认 desc(降序),可选 asc(升序)")
1018
+ }
1019
+ },
1020
+ async ({ namespace, service, env, start_time, end_time, size, offset, sort }) => {
1021
+ try {
1022
+ const result = await kePodLogList(namespace, service, env, start_time, end_time, size, offset, sort);
1023
+ return { content: [{ type: "text", text: result }] };
1024
+ } catch (error) {
1025
+ logger.error("ke_pod_log_list 失败", { error: error.message });
1026
+ return { content: [{ type: "text", text: `错误: ${error.message}` }], isError: true };
1027
+ }
1028
+ }
1029
+ );
1030
+
1031
+ // 注册工具:ke_pod_exec
1032
+ server.registerTool(
1033
+ "ke_pod_exec",
1034
+ {
1035
+ description: "在 Pod 容器中执行命令(如:ls, pwd, cat 等)",
1036
+ inputSchema: {
1037
+ tenant: z.string().describe("租户名称"),
1038
+ project: z.string().describe("项目名称"),
1039
+ podName: z.string().describe("Pod 名称"),
1040
+ cluster: z.string().describe("集群/环境名称(如:funeng-test)"),
1041
+ command: z.string().describe("要执行的命令(如:ls, pwd, cat /etc/hosts)"),
1042
+ container: z.string().optional().describe("容器名称(可选,如果 Pod 有多个容器)"),
1043
+ nodeName: z.string().optional().describe("节点名称(可选)")
1044
+ }
1045
+ },
1046
+ async ({ tenant, project, podName, cluster, command, container, nodeName }) => {
1047
+ try {
1048
+ const result = await kePodExec(tenant, project, podName, cluster, command, container, nodeName);
1049
+ return { content: [{ type: "text", text: result }] };
1050
+ } catch (error) {
1051
+ logger.error("ke_pod_exec 失败", { error: error.message });
1052
+ return { content: [{ type: "text", text: `错误: ${error.message}` }], isError: true };
1053
+ }
1054
+ }
1055
+ );
1056
+
1057
+ // ==================== 启动服务 ====================
1058
+
1059
+ async function main() {
1060
+ const transport = new StdioServerTransport();
1061
+ await server.connect(transport);
1062
+
1063
+ logger.info("BD-KE MCP 服务已启动", {
1064
+ version: VERSION,
1065
+ nodeVersion: process.version,
1066
+ platform: os.platform(),
1067
+ pid: process.pid,
1068
+ projectRoot: getProjectRoot(),
1069
+ tools: [
1070
+ "ke_app_restart",
1071
+ "ke_pipeline_list",
1072
+ "ke_pipeline_info",
1073
+ "ke_pipeline_create",
1074
+ "ke_tenants_list",
1075
+ "ke_projects_list",
1076
+ "ke_app_list",
1077
+ "ke_env_list",
1078
+ "ke_pods_list",
1079
+ "ke_pod_log_list",
1080
+ "ke_pod_exec"
1081
+ ]
1082
+ });
1083
+ }
1084
+
1085
+ main().catch((err) => {
1086
+ logger.error("MCP 服务启动失败", { error: err.message, stack: err.stack });
1087
+ process.exit(1);
1088
+ });
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@junjun-org/bd-ke-mcp",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "description": "MCP (Model Context Protocol) tools for BD-KE - Knowledge Engine application management",
6
+ "main": "index.js",
7
+ "bin": {
8
+ "bd-ke-mcp": "index.js"
9
+ },
10
+ "scripts": {
11
+ "start": "node index.js",
12
+ "start:stdio": "node index.js"
13
+ },
14
+ "keywords": [
15
+ "mcp",
16
+ "model-context-protocol",
17
+ "ke",
18
+ "knowledge-engine",
19
+ "kubernetes",
20
+ "pod",
21
+ "pipeline",
22
+ "cursor",
23
+ "ai"
24
+ ],
25
+ "author": "",
26
+ "license": "MIT",
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ },
30
+ "dependencies": {
31
+ "@modelcontextprotocol/sdk": "^1.18.2",
32
+ "zod": "^3.25.76"
33
+ },
34
+ "files": [
35
+ "index.js",
36
+ "README.md",
37
+ "LICENSE"
38
+ ]
39
+ }