@cloudbase/cloudbase-mcp 1.6.0 → 1.7.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 +10 -0
- package/dist/auth.js +4 -0
- package/dist/cloudbase-manager.js +81 -10
- package/dist/index.js +3 -0
- package/dist/interactive-server.js +1621 -0
- package/dist/tools/env.js +59 -13
- package/dist/tools/interactive.js +198 -0
- package/dist/utils/logger.js +119 -0
- package/package.json +7 -1
package/dist/tools/env.js
CHANGED
|
@@ -1,26 +1,72 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { getCloudBaseManager } from '../cloudbase-manager.js';
|
|
3
3
|
import { logout } from '../auth.js';
|
|
4
|
+
import { clearUserEnvId, _promptAndSetEnvironmentId } from './interactive.js';
|
|
4
5
|
export function registerEnvTools(server) {
|
|
5
|
-
//
|
|
6
|
-
server.tool("
|
|
6
|
+
// login - 登录云开发环境
|
|
7
|
+
server.tool("login", "登录云开发环境并选择要使用的环境", {
|
|
8
|
+
forceUpdate: z.boolean().optional().describe("是否强制重新选择环境")
|
|
9
|
+
}, async ({ forceUpdate = false }) => {
|
|
10
|
+
try {
|
|
11
|
+
const { selectedEnvId, cancelled, error, noEnvs } = await _promptAndSetEnvironmentId(false);
|
|
12
|
+
if (error) {
|
|
13
|
+
return { content: [{ type: "text", text: error }] };
|
|
14
|
+
}
|
|
15
|
+
if (noEnvs) {
|
|
16
|
+
return { content: [{ type: "text", text: "当前账户下暂无可用的云开发环境,请先在腾讯云控制台创建环境" }] };
|
|
17
|
+
}
|
|
18
|
+
if (cancelled) {
|
|
19
|
+
return { content: [{ type: "text", text: "用户取消了登录" }] };
|
|
20
|
+
}
|
|
21
|
+
if (selectedEnvId) {
|
|
22
|
+
return {
|
|
23
|
+
content: [{
|
|
24
|
+
type: "text",
|
|
25
|
+
text: `✅ 登录成功,当前环境: ${selectedEnvId}`
|
|
26
|
+
}]
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
throw new Error("登录失败");
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
return {
|
|
33
|
+
content: [{
|
|
34
|
+
type: "text",
|
|
35
|
+
text: `登录失败: ${error instanceof Error ? error.message : String(error)}`
|
|
36
|
+
}]
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
// logout - 退出云开发环境
|
|
41
|
+
server.tool("logout", "退出云开发环境", {
|
|
7
42
|
confirm: z.literal("yes").describe("确认操作,默认传 yes")
|
|
8
|
-
}, async (
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
43
|
+
}, async () => {
|
|
44
|
+
try {
|
|
45
|
+
// 登出账户
|
|
46
|
+
await logout();
|
|
47
|
+
// 清理环境ID配置
|
|
48
|
+
await clearUserEnvId();
|
|
49
|
+
return {
|
|
50
|
+
content: [{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: "✅ 已退出登录"
|
|
53
|
+
}]
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
return {
|
|
58
|
+
content: [{
|
|
59
|
+
type: "text",
|
|
60
|
+
text: `退出失败: ${error instanceof Error ? error.message : String(error)}`
|
|
61
|
+
}]
|
|
62
|
+
};
|
|
63
|
+
}
|
|
18
64
|
});
|
|
19
65
|
// listEnvs
|
|
20
66
|
server.tool("listEnvs", "获取所有云开发环境信息", {
|
|
21
67
|
confirm: z.literal("yes").describe("确认操作,默认传 yes")
|
|
22
68
|
}, async () => {
|
|
23
|
-
const cloudbase = await getCloudBaseManager();
|
|
69
|
+
const cloudbase = await getCloudBaseManager({ requireEnvId: false });
|
|
24
70
|
const result = await cloudbase.env.listEnvs();
|
|
25
71
|
return {
|
|
26
72
|
content: [
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { getInteractiveServer } from "../interactive-server.js";
|
|
3
|
+
import { getCloudBaseManager } from '../cloudbase-manager.js';
|
|
4
|
+
import { getLoginState } from '../auth.js';
|
|
5
|
+
import { debug, warn } from '../utils/logger.js';
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
export function registerInteractiveTools(server) {
|
|
10
|
+
// 统一的交互式对话工具
|
|
11
|
+
server.tool("interactiveDialog", "统一的交互式对话工具,支持需求澄清和任务确认,当需要和用户确认下一步的操作的时候,可以调用这个工具的clarify,如果有敏感的操作,需要用户确认,可以调用这个工具的confirm", {
|
|
12
|
+
type: z.enum(['clarify', 'confirm']).describe("交互类型: clarify=需求澄清, confirm=任务确认"),
|
|
13
|
+
message: z.string().optional().describe("对话消息内容"),
|
|
14
|
+
options: z.array(z.string()).optional().describe("可选的预设选项"),
|
|
15
|
+
forceUpdate: z.boolean().optional().describe("是否强制更新环境ID配置"),
|
|
16
|
+
risks: z.array(z.string()).optional().describe("操作风险提示")
|
|
17
|
+
}, async ({ type, message, options, forceUpdate = false, risks }) => {
|
|
18
|
+
try {
|
|
19
|
+
switch (type) {
|
|
20
|
+
case 'clarify': {
|
|
21
|
+
if (!message) {
|
|
22
|
+
throw new Error("需求澄清必须提供message参数");
|
|
23
|
+
}
|
|
24
|
+
const interactiveServer = getInteractiveServer();
|
|
25
|
+
const result = await interactiveServer.clarifyRequest(message, options);
|
|
26
|
+
if (result.cancelled) {
|
|
27
|
+
return { content: [{ type: "text", text: "用户取消了需求澄清" }] };
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
content: [{
|
|
31
|
+
type: "text",
|
|
32
|
+
text: `📝 用户澄清反馈:\n${result.data.response}`
|
|
33
|
+
}]
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
case 'confirm': {
|
|
37
|
+
if (!message) {
|
|
38
|
+
throw new Error("任务确认必须提供message参数");
|
|
39
|
+
}
|
|
40
|
+
let dialogMessage = `🎯 即将执行任务:\n${message}`;
|
|
41
|
+
if (risks && risks.length > 0) {
|
|
42
|
+
dialogMessage += `\n\n⚠️ 风险提示:\n${risks.map(risk => `• ${risk}`).join('\n')}`;
|
|
43
|
+
}
|
|
44
|
+
dialogMessage += `\n\n是否继续执行此任务?`;
|
|
45
|
+
const dialogOptions = options || ["确认执行", "取消操作", "需要修改任务"];
|
|
46
|
+
const interactiveServer = getInteractiveServer();
|
|
47
|
+
const result = await interactiveServer.clarifyRequest(dialogMessage, dialogOptions);
|
|
48
|
+
if (result.cancelled || result.data.response.includes('取消')) {
|
|
49
|
+
return { content: [{ type: "text", text: "❌ 用户取消了任务执行" }] };
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
content: [{
|
|
53
|
+
type: "text",
|
|
54
|
+
text: `✅ 用户确认: ${result.data.response}`
|
|
55
|
+
}]
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
return {
|
|
62
|
+
content: [{
|
|
63
|
+
type: "text",
|
|
64
|
+
text: `交互对话出错: ${error instanceof Error ? error.message : String(error)}`
|
|
65
|
+
}]
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// 封装了获取环境、提示选择、保存配置的核心逻辑
|
|
71
|
+
export async function _promptAndSetEnvironmentId(autoSelectSingle) {
|
|
72
|
+
// 1. 确保用户已登录
|
|
73
|
+
const loginState = await getLoginState();
|
|
74
|
+
debug('loginState', loginState);
|
|
75
|
+
if (!loginState) {
|
|
76
|
+
debug('请先登录云开发账户');
|
|
77
|
+
return { selectedEnvId: null, cancelled: false, error: "请先登录云开发账户" };
|
|
78
|
+
}
|
|
79
|
+
// 2. 获取可用环境列表
|
|
80
|
+
const cloudbase = await getCloudBaseManager({ requireEnvId: false });
|
|
81
|
+
const envResult = await cloudbase.env.listEnvs();
|
|
82
|
+
debug('envResult', envResult);
|
|
83
|
+
if (!envResult || !envResult.EnvList || envResult.EnvList.length === 0) {
|
|
84
|
+
return { selectedEnvId: null, cancelled: false, noEnvs: true };
|
|
85
|
+
}
|
|
86
|
+
const { EnvList } = envResult;
|
|
87
|
+
let selectedEnvId = null;
|
|
88
|
+
// 3. 根据情况选择或提示用户选择
|
|
89
|
+
if (autoSelectSingle && EnvList.length === 1 && EnvList[0].EnvId) {
|
|
90
|
+
selectedEnvId = EnvList[0].EnvId;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const interactiveServer = getInteractiveServer();
|
|
94
|
+
const result = await interactiveServer.collectEnvId(EnvList);
|
|
95
|
+
if (result.cancelled) {
|
|
96
|
+
return { selectedEnvId: null, cancelled: true };
|
|
97
|
+
}
|
|
98
|
+
selectedEnvId = result.data.envId;
|
|
99
|
+
}
|
|
100
|
+
// 4. 保存并设置环境ID
|
|
101
|
+
if (selectedEnvId) {
|
|
102
|
+
await saveEnvIdToUserConfig(selectedEnvId);
|
|
103
|
+
process.env.CLOUDBASE_ENV_ID = selectedEnvId;
|
|
104
|
+
}
|
|
105
|
+
return { selectedEnvId, cancelled: false };
|
|
106
|
+
}
|
|
107
|
+
// 获取用户配置文件路径
|
|
108
|
+
function getUserConfigPath() {
|
|
109
|
+
return path.join(os.homedir(), '.cloudbase-env-id');
|
|
110
|
+
}
|
|
111
|
+
// 保存环境ID到用户配置文件
|
|
112
|
+
async function saveEnvIdToUserConfig(envId) {
|
|
113
|
+
const configPath = getUserConfigPath();
|
|
114
|
+
try {
|
|
115
|
+
const config = {
|
|
116
|
+
envId,
|
|
117
|
+
updatedAt: new Date().toISOString(),
|
|
118
|
+
version: '1.0'
|
|
119
|
+
};
|
|
120
|
+
await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
|
|
121
|
+
// 环境ID已保存 - 静默操作避免干扰MCP返回值
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
console.error('保存环境ID配置失败:', error);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// 从用户配置文件读取环境ID
|
|
129
|
+
async function loadEnvIdFromUserConfig() {
|
|
130
|
+
const configPath = getUserConfigPath();
|
|
131
|
+
try {
|
|
132
|
+
const configContent = await fs.readFile(configPath, 'utf8');
|
|
133
|
+
const config = JSON.parse(configContent);
|
|
134
|
+
const envId = config.envId || null;
|
|
135
|
+
if (!envId) {
|
|
136
|
+
warn(`Config file ${configPath} found, but 'envId' property is missing or empty.`);
|
|
137
|
+
}
|
|
138
|
+
return envId;
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
// 文件不存在是正常情况,不应告警。只在文件存在但有问题时告警。
|
|
142
|
+
if (err.code !== 'ENOENT') {
|
|
143
|
+
warn(`Failed to load envId from config file at ${configPath}. Error: ${err.message}`);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
debug(`Env config file not found at ${configPath}, which is expected if not set.`);
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// 检查并设置环境ID
|
|
152
|
+
export async function ensureEnvId() {
|
|
153
|
+
// 优先使用进程环境变量
|
|
154
|
+
if (process.env.CLOUDBASE_ENV_ID) {
|
|
155
|
+
return process.env.CLOUDBASE_ENV_ID;
|
|
156
|
+
}
|
|
157
|
+
// 从用户配置文件读取
|
|
158
|
+
const envId = await loadEnvIdFromUserConfig();
|
|
159
|
+
if (envId) {
|
|
160
|
+
// 设置到进程环境变量中
|
|
161
|
+
process.env.CLOUDBASE_ENV_ID = envId;
|
|
162
|
+
return envId;
|
|
163
|
+
}
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
// 清理用户环境ID配置
|
|
167
|
+
export async function clearUserEnvId() {
|
|
168
|
+
const configPath = getUserConfigPath();
|
|
169
|
+
try {
|
|
170
|
+
await fs.unlink(configPath);
|
|
171
|
+
// 清理进程环境变量
|
|
172
|
+
delete process.env.CLOUDBASE_ENV_ID;
|
|
173
|
+
delete process.env.TENCENTCLOUD_SECRETID;
|
|
174
|
+
delete process.env.TENCENTCLOUD_SECRETKEY;
|
|
175
|
+
delete process.env.TENCENTCLOUD_SESSIONTOKEN;
|
|
176
|
+
// 环境ID配置已清理 - 静默操作
|
|
177
|
+
}
|
|
178
|
+
catch (error) {
|
|
179
|
+
// 文件不存在或删除失败,忽略错误
|
|
180
|
+
// 环境ID配置文件不存在或已清理 - 静默操作
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// 自动设置环境ID(无需MCP工具调用)
|
|
184
|
+
export async function autoSetupEnvironmentId() {
|
|
185
|
+
try {
|
|
186
|
+
const { selectedEnvId, cancelled, error, noEnvs } = await _promptAndSetEnvironmentId(true);
|
|
187
|
+
if (error || noEnvs || cancelled) {
|
|
188
|
+
debug('Auto setup environment ID interrupted or failed silently.', { error, noEnvs, cancelled });
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
debug('Auto setup environment ID successful.', { selectedEnvId });
|
|
192
|
+
return selectedEnvId;
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
console.error('自动配置环境ID时出错:', error);
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
export var LogLevel;
|
|
5
|
+
(function (LogLevel) {
|
|
6
|
+
LogLevel[LogLevel["DEBUG"] = 0] = "DEBUG";
|
|
7
|
+
LogLevel[LogLevel["INFO"] = 1] = "INFO";
|
|
8
|
+
LogLevel[LogLevel["WARN"] = 2] = "WARN";
|
|
9
|
+
LogLevel[LogLevel["ERROR"] = 3] = "ERROR";
|
|
10
|
+
})(LogLevel || (LogLevel = {}));
|
|
11
|
+
class Logger {
|
|
12
|
+
enabled;
|
|
13
|
+
level;
|
|
14
|
+
logFile;
|
|
15
|
+
useConsole;
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
// 通过环境变量控制是否启用日志
|
|
18
|
+
this.enabled = options.enabled ?? (process.env.MCP_DEBUG === 'true' || process.env.NODE_ENV === 'development');
|
|
19
|
+
this.level = options.level ?? LogLevel.INFO;
|
|
20
|
+
this.useConsole = options.console ?? false;
|
|
21
|
+
// 默认日志文件路径
|
|
22
|
+
this.logFile = options.logFile ?? path.join(os.tmpdir(), 'cloudbase-mcp.log');
|
|
23
|
+
}
|
|
24
|
+
async writeLog(level, message, data) {
|
|
25
|
+
if (!this.enabled || level < this.level)
|
|
26
|
+
return;
|
|
27
|
+
const timestamp = new Date().toISOString();
|
|
28
|
+
const levelName = LogLevel[level];
|
|
29
|
+
const logMessage = data
|
|
30
|
+
? `[${timestamp}] [${levelName}] ${message} ${JSON.stringify(data, null, 2)}`
|
|
31
|
+
: `[${timestamp}] [${levelName}] ${message}`;
|
|
32
|
+
// 输出到控制台(仅在调试模式下)
|
|
33
|
+
if (this.useConsole && process.env.NODE_ENV === 'development') {
|
|
34
|
+
console.error(logMessage); // 使用 stderr 避免污染 stdout
|
|
35
|
+
}
|
|
36
|
+
// 写入日志文件
|
|
37
|
+
if (this.logFile) {
|
|
38
|
+
try {
|
|
39
|
+
await fs.appendFile(this.logFile, logMessage + '\n');
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
// 静默处理日志写入错误,避免影响主要功能
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
debug(message, data) {
|
|
47
|
+
this.writeLog(LogLevel.DEBUG, message, data);
|
|
48
|
+
}
|
|
49
|
+
info(message, data) {
|
|
50
|
+
this.writeLog(LogLevel.INFO, message, data);
|
|
51
|
+
}
|
|
52
|
+
warn(message, data) {
|
|
53
|
+
this.writeLog(LogLevel.WARN, message, data);
|
|
54
|
+
}
|
|
55
|
+
error(message, data) {
|
|
56
|
+
this.writeLog(LogLevel.ERROR, message, data);
|
|
57
|
+
}
|
|
58
|
+
// 设置日志级别
|
|
59
|
+
setLevel(level) {
|
|
60
|
+
this.level = level;
|
|
61
|
+
}
|
|
62
|
+
// 启用/禁用日志
|
|
63
|
+
setEnabled(enabled) {
|
|
64
|
+
this.enabled = enabled;
|
|
65
|
+
}
|
|
66
|
+
// 获取日志文件路径
|
|
67
|
+
getLogFile() {
|
|
68
|
+
return this.logFile;
|
|
69
|
+
}
|
|
70
|
+
// 清理日志文件
|
|
71
|
+
async clearLogs() {
|
|
72
|
+
if (this.logFile) {
|
|
73
|
+
try {
|
|
74
|
+
await fs.writeFile(this.logFile, '');
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// 静默处理
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// 读取日志内容
|
|
82
|
+
async getLogs(maxLines = 1000) {
|
|
83
|
+
if (!this.logFile)
|
|
84
|
+
return [];
|
|
85
|
+
try {
|
|
86
|
+
const content = await fs.readFile(this.logFile, 'utf-8');
|
|
87
|
+
const lines = content.split('\n').filter(line => line.trim());
|
|
88
|
+
// 返回最近的 maxLines 行
|
|
89
|
+
return lines.slice(-maxLines);
|
|
90
|
+
}
|
|
91
|
+
catch (error) {
|
|
92
|
+
return [`读取日志文件失败: ${error instanceof Error ? error.message : String(error)}`];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
// 获取日志状态信息
|
|
96
|
+
getStatus() {
|
|
97
|
+
return {
|
|
98
|
+
enabled: this.enabled,
|
|
99
|
+
level: LogLevel[this.level],
|
|
100
|
+
logFile: this.logFile,
|
|
101
|
+
useConsole: this.useConsole
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// 创建全局 logger 实例
|
|
106
|
+
export const logger = new Logger({
|
|
107
|
+
enabled: process.env.MCP_DEBUG === 'true',
|
|
108
|
+
level: LogLevel.INFO,
|
|
109
|
+
console: process.env.NODE_ENV === 'development'
|
|
110
|
+
});
|
|
111
|
+
// 便捷的导出函数
|
|
112
|
+
export const debug = (message, data) => logger.error(message, data);
|
|
113
|
+
export const info = (message, data) => logger.info(message, data);
|
|
114
|
+
export const warn = (message, data) => logger.warn(message, data);
|
|
115
|
+
export const error = (message, data) => logger.error(message, data);
|
|
116
|
+
// 日志管理函数
|
|
117
|
+
export const getLogs = (maxLines) => logger.getLogs(maxLines);
|
|
118
|
+
export const getLoggerStatus = () => logger.getStatus();
|
|
119
|
+
export const clearLogs = () => logger.clearLogs();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudbase/cloudbase-mcp",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"description": "腾讯云开发 MCP Server,支持静态托管/环境查询/",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -30,11 +30,17 @@
|
|
|
30
30
|
"@cloudbase/toolbox": "^0.7.5",
|
|
31
31
|
"@modelcontextprotocol/sdk": "1.9.0",
|
|
32
32
|
"@types/unzipper": "^0.10.11",
|
|
33
|
+
"cors": "^2.8.5",
|
|
34
|
+
"express": "^5.1.0",
|
|
35
|
+
"open": "^10.1.2",
|
|
33
36
|
"unzipper": "^0.12.3",
|
|
37
|
+
"ws": "^8.18.2",
|
|
34
38
|
"zod": "^3.24.3"
|
|
35
39
|
},
|
|
36
40
|
"devDependencies": {
|
|
41
|
+
"@types/express": "^5.0.3",
|
|
37
42
|
"@types/node": "^22.14.1",
|
|
43
|
+
"@types/ws": "^8.18.1",
|
|
38
44
|
"typescript": "^5.8.3"
|
|
39
45
|
}
|
|
40
46
|
}
|