@cloudbase/cloudbase-mcp 1.6.0 → 1.7.1

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/dist/tools/env.js CHANGED
@@ -1,26 +1,74 @@
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';
5
+ import { debug } from '../utils/logger.js';
4
6
  export function registerEnvTools(server) {
5
- // logout
6
- server.tool("logout", "登出当前云开发账户", {
7
+ // login - 登录云开发环境
8
+ server.tool("login", "登录云开发环境并选择要使用的环境", {
9
+ forceUpdate: z.boolean().optional().describe("是否强制重新选择环境")
10
+ }, async ({ forceUpdate = false }) => {
11
+ try {
12
+ const { selectedEnvId, cancelled, error, noEnvs } = await _promptAndSetEnvironmentId(false);
13
+ debug("login", { selectedEnvId, cancelled, error, noEnvs });
14
+ if (error) {
15
+ return { content: [{ type: "text", text: error }] };
16
+ }
17
+ if (noEnvs) {
18
+ return { content: [{ type: "text", text: "当前账户下暂无可用的云开发环境,请先在腾讯云控制台创建环境" }] };
19
+ }
20
+ if (cancelled) {
21
+ return { content: [{ type: "text", text: "用户取消了登录" }] };
22
+ }
23
+ if (selectedEnvId) {
24
+ return {
25
+ content: [{
26
+ type: "text",
27
+ text: `✅ 登录成功,当前环境: ${selectedEnvId}`
28
+ }]
29
+ };
30
+ }
31
+ throw new Error("登录失败");
32
+ }
33
+ catch (error) {
34
+ return {
35
+ content: [{
36
+ type: "text",
37
+ text: `登录失败: ${error instanceof Error ? error.message : String(error)}`
38
+ }]
39
+ };
40
+ }
41
+ });
42
+ // logout - 退出云开发环境
43
+ server.tool("logout", "退出云开发环境", {
7
44
  confirm: z.literal("yes").describe("确认操作,默认传 yes")
8
- }, async ({ confirm }) => {
9
- const result = await logout();
10
- return {
11
- content: [
12
- {
13
- type: "text",
14
- text: "success"
15
- }
16
- ]
17
- };
45
+ }, async () => {
46
+ try {
47
+ // 登出账户
48
+ await logout();
49
+ // 清理环境ID配置
50
+ await clearUserEnvId();
51
+ return {
52
+ content: [{
53
+ type: "text",
54
+ text: "✅ 已退出登录"
55
+ }]
56
+ };
57
+ }
58
+ catch (error) {
59
+ return {
60
+ content: [{
61
+ type: "text",
62
+ text: `退出失败: ${error instanceof Error ? error.message : String(error)}`
63
+ }]
64
+ };
65
+ }
18
66
  });
19
67
  // listEnvs
20
68
  server.tool("listEnvs", "获取所有云开发环境信息", {
21
69
  confirm: z.literal("yes").describe("确认操作,默认传 yes")
22
70
  }, async () => {
23
- const cloudbase = await getCloudBaseManager();
71
+ const cloudbase = await getCloudBaseManager({ requireEnvId: false });
24
72
  const result = await cloudbase.env.listEnvs();
25
73
  return {
26
74
  content: [
@@ -96,109 +96,16 @@ export function registerHostingTools(server) {
96
96
  ]
97
97
  };
98
98
  });
99
- // // setWebsiteDocument - 配置静态网站文档
100
- // server.tool(
101
- // "setWebsiteDocument",
102
- // "配置静态网站的错误文档、索引文档和重定向规则",
103
- // {
104
- // indexDocument: z.string().describe("索引文档路径"),
105
- // errorDocument: z.string().optional().describe("错误文档路径"),
106
- // routingRules: z.array(z.object({
107
- // keyPrefixEquals: z.string().optional(),
108
- // httpErrorCodeReturnedEquals: z.string().optional(),
109
- // replaceKeyWith: z.string().optional(),
110
- // replaceKeyPrefixWith: z.string().optional()
111
- // })).optional().describe("重定向规则")
112
- // },
113
- // async ({ indexDocument, errorDocument, routingRules }) => {
114
- // const cloudbase = await getCloudBaseManager()
115
- // const result = await cloudbase.hosting.setWebsiteDocument({
116
- // indexDocument,
117
- // errorDocument,
118
- // routingRules
119
- // });
120
- // return {
121
- // content: [
122
- // {
123
- // type: "text",
124
- // text: JSON.stringify(result, null, 2)
125
- // }
126
- // ]
127
- // };
128
- // }
129
- // );
130
- // createHostingDomain - 绑定自定义域名
131
- server.tool("createHostingDomain", "绑定自定义域名", {
132
- domain: z.string().describe("自定义域名"),
133
- certId: z.string().describe("证书ID")
134
- }, async ({ domain, certId }) => {
135
- const cloudbase = await getCloudBaseManager();
136
- const result = await cloudbase.hosting.CreateHostingDomain({
137
- domain,
138
- certId
139
- });
140
- return {
141
- content: [
142
- {
143
- type: "text",
144
- text: JSON.stringify(result, null, 2)
145
- }
146
- ]
147
- };
148
- });
149
- // deleteHostingDomain - 解绑自定义域名
150
- server.tool("deleteHostingDomain", "解绑自定义域名", {
151
- domain: z.string().describe("自定义域名")
152
- }, async ({ domain }) => {
153
- const cloudbase = await getCloudBaseManager();
154
- const result = await cloudbase.hosting.deleteHostingDomain({
155
- domain
156
- });
157
- return {
158
- content: [
159
- {
160
- type: "text",
161
- text: JSON.stringify(result, null, 2)
162
- }
163
- ]
164
- };
165
- });
166
- // getWebsiteConfig - 获取静态网站配置
167
- server.tool("getWebsiteConfig", "获取静态网站配置", {
168
- confirm: z.literal("yes").describe("确认操作,默认传 yes")
169
- }, async () => {
170
- const cloudbase = await getCloudBaseManager();
171
- const result = await cloudbase.hosting.getWebsiteConfig();
172
- return {
173
- content: [
174
- {
175
- type: "text",
176
- text: JSON.stringify(result, null, 2)
177
- }
178
- ]
179
- };
180
- });
181
- // tcbCheckResource - 获取域名配置
182
- server.tool("tcbCheckResource", "获取域名配置", {
183
- domains: z.array(z.string()).describe("域名列表")
184
- }, async ({ domains }) => {
185
- const cloudbase = await getCloudBaseManager();
186
- const result = await cloudbase.hosting.tcbCheckResource({
187
- domains
188
- });
189
- return {
190
- content: [
191
- {
192
- type: "text",
193
- text: JSON.stringify(result, null, 2)
194
- }
195
- ]
196
- };
197
- });
198
- // tcbModifyAttribute - 修改域名配置
199
- server.tool("tcbModifyAttribute", "修改域名配置", {
200
- domain: z.string().describe("域名"),
201
- domainId: z.number().describe("域名ID"),
99
+ // domainManagement - 统一的域名管理工具
100
+ server.tool("domainManagement", "统一的域名管理工具,支持绑定、解绑、查询和修改域名配置", {
101
+ action: z.enum(["create", "delete", "check", "modify"]).describe("操作类型: create=绑定域名, delete=解绑域名, check=查询域名配置, modify=修改域名配置"),
102
+ // 绑定域名参数
103
+ domain: z.string().optional().describe("域名"),
104
+ certId: z.string().optional().describe("证书ID(绑定域名时必需)"),
105
+ // 查询域名参数
106
+ domains: z.array(z.string()).optional().describe("域名列表(查询配置时使用)"),
107
+ // 修改域名参数
108
+ domainId: z.number().optional().describe("域名ID(修改配置时必需)"),
202
109
  domainConfig: z.object({
203
110
  Refer: z.object({
204
111
  Switch: z.string(),
@@ -222,14 +129,67 @@ export function registerHostingTools(server) {
222
129
  Switch: z.string(),
223
130
  Qps: z.number().optional()
224
131
  }).optional()
225
- }).describe("域名配置")
226
- }, async ({ domain, domainId, domainConfig }) => {
132
+ }).optional().describe("域名配置(修改配置时使用)")
133
+ }, async ({ action, domain, certId, domains, domainId, domainConfig }) => {
227
134
  const cloudbase = await getCloudBaseManager();
228
- const result = await cloudbase.hosting.tcbModifyAttribute({
229
- domain,
230
- domainId,
231
- domainConfig
232
- });
135
+ let result;
136
+ switch (action) {
137
+ case "create":
138
+ if (!domain || !certId) {
139
+ throw new Error("绑定域名需要提供域名和证书ID");
140
+ }
141
+ result = await cloudbase.hosting.CreateHostingDomain({
142
+ domain,
143
+ certId
144
+ });
145
+ break;
146
+ case "delete":
147
+ if (!domain) {
148
+ throw new Error("解绑域名需要提供域名");
149
+ }
150
+ result = await cloudbase.hosting.deleteHostingDomain({
151
+ domain
152
+ });
153
+ break;
154
+ case "check":
155
+ if (!domains || domains.length === 0) {
156
+ throw new Error("查询域名配置需要提供域名列表");
157
+ }
158
+ result = await cloudbase.hosting.tcbCheckResource({
159
+ domains
160
+ });
161
+ break;
162
+ case "modify":
163
+ if (!domain || domainId === undefined || !domainConfig) {
164
+ throw new Error("修改域名配置需要提供域名、域名ID和配置信息");
165
+ }
166
+ result = await cloudbase.hosting.tcbModifyAttribute({
167
+ domain,
168
+ domainId,
169
+ domainConfig
170
+ });
171
+ break;
172
+ default:
173
+ throw new Error(`不支持的操作类型: ${action}`);
174
+ }
175
+ return {
176
+ content: [
177
+ {
178
+ type: "text",
179
+ text: JSON.stringify({
180
+ action,
181
+ result
182
+ }, null, 2)
183
+ }
184
+ ]
185
+ };
186
+ });
187
+ // getWebsiteConfig - 获取静态网站配置
188
+ server.tool("getWebsiteConfig", "获取静态网站配置", {
189
+ confirm: z.literal("yes").describe("确认操作,默认传 yes")
190
+ }, async () => {
191
+ const cloudbase = await getCloudBaseManager();
192
+ const result = await cloudbase.hosting.getWebsiteConfig();
233
193
  return {
234
194
  content: [
235
195
  {
@@ -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}`
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 && result.data.includes && result.data.includes('取消'))) {
49
+ return { content: [{ type: "text", text: "❌ 用户取消了任务执行" }] };
50
+ }
51
+ return {
52
+ content: [{
53
+ type: "text",
54
+ text: `✅ 用户确认: ${result.data}`
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;
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
+ }
@@ -97,6 +97,27 @@ async function copyFileIfNotExists(src, dest) {
97
97
  return { copied: false, reason: `复制失败: ${error instanceof Error ? error.message : '未知错误'}` };
98
98
  }
99
99
  }
100
+ // 复制文件,支持覆盖模式
101
+ async function copyFile(src, dest, overwrite = false) {
102
+ try {
103
+ const destExists = fs.existsSync(dest);
104
+ // 如果目标文件存在且不允许覆盖
105
+ if (destExists && !overwrite) {
106
+ return { copied: false, reason: '文件已存在', action: 'skipped' };
107
+ }
108
+ // 创建目标目录
109
+ await fsPromises.mkdir(path.dirname(dest), { recursive: true });
110
+ // 复制文件
111
+ await fsPromises.copyFile(src, dest);
112
+ return {
113
+ copied: true,
114
+ action: destExists ? 'overwritten' : 'created'
115
+ };
116
+ }
117
+ catch (error) {
118
+ return { copied: false, reason: `复制失败: ${error instanceof Error ? error.message : '未知错误'}` };
119
+ }
120
+ }
100
121
  export function registerSetupTools(server) {
101
122
  server.tool("downloadTemplate", `自动下载并部署CloudBase项目模板。
102
123
 
@@ -105,9 +126,10 @@ export function registerSetupTools(server) {
105
126
  - miniprogram: 微信小程序 + 云开发模板
106
127
  - rules: 只包含AI编辑器配置文件(包含Cursor、WindSurf、CodeBuddy等所有主流编辑器配置),适合在已有项目中补充AI编辑器配置
107
128
 
108
- 工具会自动下载模板到临时目录,解压后如果检测到WORKSPACE_FOLDER_PATHS环境变量,则复制到项目目录(不覆盖已存在文件)。`, {
109
- template: z.enum(["react", "miniprogram", "rules"]).describe("要下载的模板类型")
110
- }, async ({ template }) => {
129
+ 工具会自动下载模板到临时目录,解压后如果检测到WORKSPACE_FOLDER_PATHS环境变量,则复制到项目目录。`, {
130
+ template: z.enum(["react", "miniprogram", "rules"]).describe("要下载的模板类型"),
131
+ overwrite: z.boolean().optional().describe("是否覆盖已存在的文件,默认为false(不覆盖)")
132
+ }, async ({ template, overwrite = false }) => {
111
133
  try {
112
134
  const templateConfig = TEMPLATES[template];
113
135
  if (!templateConfig) {
@@ -131,16 +153,22 @@ export function registerSetupTools(server) {
131
153
  // 检查是否需要复制到项目目录
132
154
  const workspaceFolder = process.env.WORKSPACE_FOLDER_PATHS;
133
155
  let finalFiles = [];
134
- let copiedCount = 0;
156
+ let createdCount = 0;
157
+ let overwrittenCount = 0;
135
158
  let skippedCount = 0;
136
159
  const results = [];
137
160
  if (workspaceFolder) {
138
161
  for (const relativePath of extractedFiles) {
139
162
  const srcPath = path.join(extractDir, relativePath);
140
163
  const destPath = path.join(workspaceFolder, relativePath);
141
- const copyResult = await copyFileIfNotExists(srcPath, destPath);
164
+ const copyResult = await copyFile(srcPath, destPath, overwrite);
142
165
  if (copyResult.copied) {
143
- copiedCount++;
166
+ if (copyResult.action === 'overwritten') {
167
+ overwrittenCount++;
168
+ }
169
+ else {
170
+ createdCount++;
171
+ }
144
172
  finalFiles.push(destPath);
145
173
  }
146
174
  else {
@@ -149,8 +177,20 @@ export function registerSetupTools(server) {
149
177
  }
150
178
  }
151
179
  results.push(`✅ ${templateConfig.description} 同步完成`);
152
- results.push(`📁 保存在临时目录: ${extractDir}`);
153
- results.push(`📊 复制了 ${copiedCount} 个文件${skippedCount > 0 ? `,跳过 ${skippedCount} 个已存在文件` : ''}`);
180
+ results.push(`📁 临时目录: ${extractDir}`);
181
+ const stats = [];
182
+ if (createdCount > 0)
183
+ stats.push(`新建 ${createdCount} 个文件`);
184
+ if (overwrittenCount > 0)
185
+ stats.push(`覆盖 ${overwrittenCount} 个文件`);
186
+ if (skippedCount > 0)
187
+ stats.push(`跳过 ${skippedCount} 个已存在文件`);
188
+ if (stats.length > 0) {
189
+ results.push(`📊 ${stats.join(',')}`);
190
+ }
191
+ if (overwrite || overwrittenCount > 0 || skippedCount > 0) {
192
+ results.push(`🔄 覆盖模式: ${overwrite ? '启用' : '禁用'}`);
193
+ }
154
194
  }
155
195
  else {
156
196
  finalFiles = extractedFiles.map(relativePath => path.join(extractDir, relativePath));