@cloudbase/cloudbase-mcp 1.7.3 → 1.7.5

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 CHANGED
@@ -132,7 +132,7 @@ https://github.com/user-attachments/assets/2b402fa6-c5c4-495a-b85b-f5d4a25daa4a
132
132
  "mcpServers": {
133
133
  "cloudbase-mcp": {
134
134
  "command": "npx",
135
- "args": ["@cloudbase/cloudbase-mcp@latest"]
135
+ "args": ["-y", "@cloudbase/cloudbase-mcp@latest"]
136
136
  }
137
137
  }
138
138
  }
@@ -162,7 +162,7 @@ https://github.com/user-attachments/assets/2b402fa6-c5c4-495a-b85b-f5d4a25daa4a
162
162
  "mcpServers": {
163
163
  "cloudbase-mcp": {
164
164
  "command": "npx",
165
- "args": ["@cloudbase/cloudbase-mcp@latest"]
165
+ "args": ["-y", "@cloudbase/cloudbase-mcp@latest"]
166
166
  }
167
167
  }
168
168
  }
@@ -190,7 +190,7 @@ https://github.com/user-attachments/assets/2b402fa6-c5c4-495a-b85b-f5d4a25daa4a
190
190
  "mcpServers": {
191
191
  "cloudbase-mcp": {
192
192
  "command": "npx",
193
- "args": ["@cloudbase/cloudbase-mcp@latest"]
193
+ "args": ["-y", "@cloudbase/cloudbase-mcp@latest"]
194
194
  }
195
195
  }
196
196
  }
@@ -284,7 +284,7 @@ https://github.com/user-attachments/assets/2b402fa6-c5c4-495a-b85b-f5d4a25daa4a
284
284
  "mcpServers": {
285
285
  "cloudbase-mcp": {
286
286
  "command": "npx",
287
- "args": ["@cloudbase/cloudbase-mcp@latest"]
287
+ "args": ["-y", "@cloudbase/cloudbase-mcp@latest"]
288
288
  }
289
289
  }
290
290
  }
@@ -312,7 +312,7 @@ https://github.com/user-attachments/assets/2b402fa6-c5c4-495a-b85b-f5d4a25daa4a
312
312
  "mcpServers": {
313
313
  "cloudbase-mcp": {
314
314
  "command": "npx",
315
- "args": ["@cloudbase/cloudbase-mcp@latest"]
315
+ "args": ["-y", "@cloudbase/cloudbase-mcp@latest"]
316
316
  }
317
317
  }
318
318
  }
@@ -1,84 +1,122 @@
1
1
  import { getLoginState } from './auth.js';
2
- import { ensureEnvId, autoSetupEnvironmentId } from './tools/interactive.js';
2
+ import { loadEnvIdFromUserConfig, saveEnvIdToUserConfig, autoSetupEnvironmentId } from './tools/interactive.js';
3
3
  import CloudBase from "@cloudbase/manager-node";
4
4
  import { debug, error } from './utils/logger.js';
5
- let initializationPromise = null;
6
- let noEnvIdInitializationPromise = null;
7
- const INITIALIZATION_TIMEOUT = 30000; // 30 seconds
8
- export function getCloudBaseManager(options = {}) {
9
- const { requireEnvId = true } = options;
10
- if (requireEnvId) {
11
- if (initializationPromise) {
12
- return initializationPromise;
5
+ const ENV_ID_TIMEOUT = 30000; // 30 seconds
6
+ // 统一的环境ID管理类
7
+ class EnvironmentManager {
8
+ cachedEnvId = null;
9
+ envIdPromise = null;
10
+ // 重置缓存
11
+ reset() {
12
+ this.cachedEnvId = null;
13
+ this.envIdPromise = null;
14
+ delete process.env.CLOUDBASE_ENV_ID;
15
+ }
16
+ // 获取环境ID的核心逻辑
17
+ async getEnvId() {
18
+ // 1. 优先使用内存缓存
19
+ if (this.cachedEnvId) {
20
+ debug('使用内存缓存的环境ID:', this.cachedEnvId);
21
+ return this.cachedEnvId;
13
22
  }
14
- const executor = async () => {
15
- try {
16
- // 检查并确保环境ID已配置
17
- let userEnvId = await ensureEnvId();
18
- if (!userEnvId) {
19
- debug("未找到环境ID,尝试自动设置...");
20
- userEnvId = await autoSetupEnvironmentId();
21
- if (!userEnvId) {
22
- throw new Error("CloudBase Environment ID not found after auto setup. Please set CLOUDBASE_ENV_ID or run setupEnvironmentId tool.");
23
- }
24
- }
25
- const loginState = await getLoginState();
26
- const { envId, secretId, secretKey, token } = loginState;
27
- const manager = new CloudBase({
28
- secretId,
29
- secretKey,
30
- envId: userEnvId,
31
- token,
32
- proxy: process.env.http_proxy
33
- });
34
- return manager;
35
- }
36
- catch (err) {
37
- error('Failed to initialize CloudBase Manager:', err);
38
- throw err;
39
- }
40
- };
23
+ // 2. 如果正在获取中,等待结果
24
+ if (this.envIdPromise) {
25
+ return this.envIdPromise;
26
+ }
27
+ // 3. 开始获取环境ID
28
+ this.envIdPromise = this._fetchEnvId();
29
+ // 增加超时保护
41
30
  const timeoutPromise = new Promise((_, reject) => {
42
31
  const id = setTimeout(() => {
43
32
  clearTimeout(id);
44
- reject(new Error(`CloudBase Manager initialization timed out after ${INITIALIZATION_TIMEOUT / 1000} seconds.`));
45
- }, INITIALIZATION_TIMEOUT);
46
- });
47
- initializationPromise = Promise.race([executor(), timeoutPromise]);
48
- initializationPromise.catch(() => {
49
- initializationPromise = null;
33
+ reject(new Error(`EnvId 获取超时(${ENV_ID_TIMEOUT / 1000}秒)`));
34
+ }, ENV_ID_TIMEOUT);
50
35
  });
51
- return initializationPromise;
52
- }
53
- if (noEnvIdInitializationPromise) {
54
- return noEnvIdInitializationPromise;
55
- }
56
- const noEnvIdExecutor = async () => {
57
36
  try {
58
- const loginState = await getLoginState();
59
- const { secretId, secretKey, token } = loginState;
60
- const manager = new CloudBase({
61
- secretId,
62
- secretKey,
63
- token,
64
- proxy: process.env.http_proxy
65
- });
66
- return manager;
37
+ const result = await Promise.race([this.envIdPromise, timeoutPromise]);
38
+ return result;
67
39
  }
68
40
  catch (err) {
69
- error('Failed to initialize CloudBase Manager (no envId):', err instanceof Error ? err.message : String(err));
41
+ this.envIdPromise = null;
70
42
  throw err;
71
43
  }
72
- };
73
- const timeoutPromise = new Promise((_, reject) => {
74
- const id = setTimeout(() => {
75
- clearTimeout(id);
76
- reject(new Error(`CloudBase Manager (no envId) initialization timed out after ${INITIALIZATION_TIMEOUT / 1000} seconds.`));
77
- }, INITIALIZATION_TIMEOUT);
78
- });
79
- noEnvIdInitializationPromise = Promise.race([noEnvIdExecutor(), timeoutPromise]);
80
- noEnvIdInitializationPromise.catch(() => {
81
- noEnvIdInitializationPromise = null;
82
- });
83
- return noEnvIdInitializationPromise;
44
+ }
45
+ async _fetchEnvId() {
46
+ try {
47
+ // 1. 检查进程环境变量
48
+ if (process.env.CLOUDBASE_ENV_ID) {
49
+ debug('使用进程环境变量的环境ID:', process.env.CLOUDBASE_ENV_ID);
50
+ this.cachedEnvId = process.env.CLOUDBASE_ENV_ID;
51
+ return this.cachedEnvId;
52
+ }
53
+ // 2. 从配置文件读取
54
+ const fileEnvId = await loadEnvIdFromUserConfig();
55
+ if (fileEnvId) {
56
+ debug('从配置文件读取到环境ID:', fileEnvId);
57
+ this._setCachedEnvId(fileEnvId);
58
+ return fileEnvId;
59
+ }
60
+ // 3. 自动设置环境ID
61
+ debug('未找到环境ID,尝试自动设置...');
62
+ const autoEnvId = await autoSetupEnvironmentId();
63
+ if (!autoEnvId) {
64
+ throw new Error("CloudBase Environment ID not found after auto setup. Please set CLOUDBASE_ENV_ID or run setupEnvironmentId tool.");
65
+ }
66
+ debug('自动设置环境ID成功:', autoEnvId);
67
+ this._setCachedEnvId(autoEnvId);
68
+ return autoEnvId;
69
+ }
70
+ finally {
71
+ this.envIdPromise = null;
72
+ }
73
+ }
74
+ // 统一设置缓存的方法
75
+ _setCachedEnvId(envId) {
76
+ this.cachedEnvId = envId;
77
+ process.env.CLOUDBASE_ENV_ID = envId;
78
+ debug('已更新环境ID缓存:', envId);
79
+ }
80
+ // 手动设置环境ID(用于外部调用)
81
+ async setEnvId(envId) {
82
+ this._setCachedEnvId(envId);
83
+ // 同步保存到配置文件
84
+ await saveEnvIdToUserConfig(envId);
85
+ debug('手动设置环境ID并保存到文件:', envId);
86
+ }
87
+ }
88
+ // 全局实例
89
+ const envManager = new EnvironmentManager();
90
+ // 导出函数保持兼容性
91
+ export function resetCloudBaseManagerCache() {
92
+ envManager.reset();
93
+ }
94
+ /**
95
+ * 每次都实时获取最新的 token/secretId/secretKey
96
+ */
97
+ export async function getCloudBaseManager(options = {}) {
98
+ const { requireEnvId = true } = options;
99
+ try {
100
+ const loginState = await getLoginState();
101
+ const { envId: loginEnvId, secretId, secretKey, token } = loginState;
102
+ let finalEnvId;
103
+ if (requireEnvId) {
104
+ finalEnvId = await envManager.getEnvId();
105
+ }
106
+ // envId 优先顺序:获取到的envId > loginState中的envId > undefined
107
+ const manager = new CloudBase({
108
+ secretId,
109
+ secretKey,
110
+ envId: finalEnvId || loginEnvId,
111
+ token,
112
+ proxy: process.env.http_proxy
113
+ });
114
+ return manager;
115
+ }
116
+ catch (err) {
117
+ error('Failed to initialize CloudBase Manager:', err instanceof Error ? err.message : String(err));
118
+ throw err;
119
+ }
84
120
  }
121
+ // 导出环境管理器实例供其他地方使用
122
+ export { envManager };
@@ -17,13 +17,25 @@ export class InteractiveServer {
17
17
  sessionData = new Map();
18
18
  // 固定端口配置
19
19
  DEFAULT_PORT = 3721;
20
- FALLBACK_PORTS = [3722, 3723, 3724, 3725];
20
+ FALLBACK_PORTS = [3722, 3723, 3724, 3725, 3726, 3727, 3728, 3729, 3730, 3731, 3732, 3733, 3734, 3735];
21
21
  constructor() {
22
22
  this.app = express();
23
23
  this.server = http.createServer(this.app);
24
24
  this.wss = new WebSocketServer({ server: this.server });
25
25
  this.setupExpress();
26
26
  this.setupWebSocket();
27
+ // 确保进程退出时清理资源
28
+ process.on('exit', () => this.cleanup());
29
+ process.on('SIGINT', () => this.cleanup());
30
+ process.on('SIGTERM', () => this.cleanup());
31
+ }
32
+ cleanup() {
33
+ if (this.isRunning) {
34
+ debug('Cleaning up interactive server resources...');
35
+ this.server.close();
36
+ this.wss.close();
37
+ this.isRunning = false;
38
+ }
27
39
  }
28
40
  setupExpress() {
29
41
  this.app.use(express.json());
@@ -148,32 +160,41 @@ export class InteractiveServer {
148
160
  let currentIndex = 0;
149
161
  const tryNextPort = () => {
150
162
  if (currentIndex >= tryPorts.length) {
151
- const err = new Error('All ports are in use, failed to start server');
163
+ const err = new Error(`All ${tryPorts.length} ports are in use (${tryPorts.join(', ')}), failed to start server`);
152
164
  error('Server start failed', err);
153
165
  reject(err);
154
166
  return;
155
167
  }
156
168
  const portToTry = tryPorts[currentIndex];
157
169
  currentIndex++;
158
- debug(`Trying to start server on port ${portToTry}`);
159
- // 清除之前的错误监听器
170
+ debug(`Trying to start server on port ${portToTry} (attempt ${currentIndex}/${tryPorts.length})`);
171
+ // 清除之前的所有监听器
160
172
  this.server.removeAllListeners('error');
161
- this.server.on('error', (err) => {
173
+ this.server.removeAllListeners('listening');
174
+ // 设置错误处理
175
+ const errorHandler = (err) => {
162
176
  if (err.code === 'EADDRINUSE') {
163
177
  warn(`Port ${portToTry} is in use, trying next port...`);
178
+ // 清理当前尝试
179
+ this.server.removeAllListeners('error');
180
+ this.server.removeAllListeners('listening');
164
181
  tryNextPort();
165
182
  }
166
183
  else {
167
184
  error('Server error', err);
168
185
  reject(err);
169
186
  }
170
- });
171
- this.server.listen(portToTry, '127.0.0.1', () => {
187
+ };
188
+ // 设置成功监听处理
189
+ const listeningHandler = () => {
172
190
  const address = this.server.address();
173
191
  if (address && typeof address === 'object') {
174
192
  this.port = address.port;
175
193
  this.isRunning = true;
176
194
  info(`Interactive server started successfully on http://localhost:${this.port}`);
195
+ // 移除临时监听器
196
+ this.server.removeListener('error', errorHandler);
197
+ this.server.removeListener('listening', listeningHandler);
177
198
  resolve(this.port);
178
199
  }
179
200
  else {
@@ -181,19 +202,61 @@ export class InteractiveServer {
181
202
  error('Server start error', err);
182
203
  reject(err);
183
204
  }
184
- });
205
+ };
206
+ this.server.once('error', errorHandler);
207
+ this.server.once('listening', listeningHandler);
208
+ try {
209
+ this.server.listen(portToTry, '127.0.0.1');
210
+ }
211
+ catch (err) {
212
+ error(`Failed to bind to port ${portToTry}:`, err);
213
+ tryNextPort();
214
+ }
185
215
  };
186
216
  tryNextPort();
187
217
  });
188
218
  }
189
219
  async stop() {
190
- if (!this.isRunning)
220
+ if (!this.isRunning) {
221
+ debug('Interactive server is not running, nothing to stop');
191
222
  return;
192
- return new Promise((resolve) => {
193
- this.server.close(() => {
223
+ }
224
+ info('Stopping interactive server...');
225
+ return new Promise((resolve, reject) => {
226
+ // 设置超时,防止无限等待
227
+ const timeout = setTimeout(() => {
228
+ warn('Server close timeout, forcing cleanup');
194
229
  this.isRunning = false;
230
+ this.port = 0;
195
231
  resolve();
196
- });
232
+ }, 5000);
233
+ try {
234
+ // 首先关闭WebSocket服务器
235
+ this.wss.close(() => {
236
+ debug('WebSocket server closed');
237
+ });
238
+ // 然后关闭HTTP服务器
239
+ this.server.close((err) => {
240
+ clearTimeout(timeout);
241
+ if (err) {
242
+ error('Error closing server:', err);
243
+ reject(err);
244
+ }
245
+ else {
246
+ info('Interactive server stopped successfully');
247
+ this.isRunning = false;
248
+ this.port = 0;
249
+ resolve();
250
+ }
251
+ });
252
+ }
253
+ catch (err) {
254
+ clearTimeout(timeout);
255
+ error('Error stopping server:', err);
256
+ this.isRunning = false;
257
+ this.port = 0;
258
+ reject(err);
259
+ }
197
260
  });
198
261
  }
199
262
  async collectEnvId(availableEnvs) {
@@ -2438,6 +2501,14 @@ export class InteractiveServer {
2438
2501
  </body>
2439
2502
  </html>`;
2440
2503
  }
2504
+ // 公共方法获取运行状态
2505
+ get running() {
2506
+ return this.isRunning;
2507
+ }
2508
+ // 公共方法获取端口
2509
+ get currentPort() {
2510
+ return this.port;
2511
+ }
2441
2512
  }
2442
2513
  // 单例实例
2443
2514
  let interactiveServerInstance = null;
@@ -2447,3 +2518,27 @@ export function getInteractiveServer() {
2447
2518
  }
2448
2519
  return interactiveServerInstance;
2449
2520
  }
2521
+ export async function resetInteractiveServer() {
2522
+ if (interactiveServerInstance) {
2523
+ try {
2524
+ await interactiveServerInstance.stop();
2525
+ }
2526
+ catch (err) {
2527
+ error('Error stopping existing server instance:', err);
2528
+ }
2529
+ interactiveServerInstance = null;
2530
+ }
2531
+ }
2532
+ export async function getInteractiveServerSafe() {
2533
+ // 如果当前实例存在但不在运行状态,先清理
2534
+ if (interactiveServerInstance && !interactiveServerInstance.running) {
2535
+ try {
2536
+ await interactiveServerInstance.stop();
2537
+ }
2538
+ catch (err) {
2539
+ debug('Error stopping non-running server:', err);
2540
+ }
2541
+ interactiveServerInstance = null;
2542
+ }
2543
+ return getInteractiveServer();
2544
+ }
package/dist/tools/env.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { getCloudBaseManager } from '../cloudbase-manager.js';
2
+ import { getCloudBaseManager, resetCloudBaseManagerCache } from '../cloudbase-manager.js';
3
3
  import { logout } from '../auth.js';
4
4
  import { clearUserEnvId, _promptAndSetEnvironmentId } from './interactive.js';
5
5
  import { debug } from '../utils/logger.js';
@@ -48,6 +48,7 @@ export function registerEnvTools(server) {
48
48
  await logout();
49
49
  // 清理环境ID配置
50
50
  await clearUserEnvId();
51
+ await resetCloudBaseManagerCache();
51
52
  return {
52
53
  content: [{
53
54
  type: "text",
@@ -97,10 +97,10 @@ export async function _promptAndSetEnvironmentId(autoSelectSingle) {
97
97
  }
98
98
  selectedEnvId = result.data;
99
99
  }
100
- // 4. 保存并设置环境ID
100
+ // 4. 保存环境ID配置
101
101
  if (selectedEnvId) {
102
102
  await saveEnvIdToUserConfig(selectedEnvId);
103
- process.env.CLOUDBASE_ENV_ID = selectedEnvId;
103
+ debug('环境ID已保存到配置文件:', selectedEnvId);
104
104
  }
105
105
  return { selectedEnvId, cancelled: false };
106
106
  }
@@ -109,7 +109,7 @@ function getUserConfigPath() {
109
109
  return path.join(os.homedir(), '.cloudbase-env-id');
110
110
  }
111
111
  // 保存环境ID到用户配置文件
112
- async function saveEnvIdToUserConfig(envId) {
112
+ export async function saveEnvIdToUserConfig(envId) {
113
113
  const configPath = getUserConfigPath();
114
114
  try {
115
115
  const config = {
@@ -118,7 +118,7 @@ async function saveEnvIdToUserConfig(envId) {
118
118
  version: '1.0'
119
119
  };
120
120
  await fs.writeFile(configPath, JSON.stringify(config, null, 2), 'utf8');
121
- // 环境ID已保存 - 静默操作避免干扰MCP返回值
121
+ debug('环境ID配置已保存到文件:', configPath);
122
122
  }
123
123
  catch (error) {
124
124
  console.error('保存环境ID配置失败:', error);
@@ -126,7 +126,7 @@ async function saveEnvIdToUserConfig(envId) {
126
126
  }
127
127
  }
128
128
  // 从用户配置文件读取环境ID
129
- async function loadEnvIdFromUserConfig() {
129
+ export async function loadEnvIdFromUserConfig() {
130
130
  const configPath = getUserConfigPath();
131
131
  try {
132
132
  const configContent = await fs.readFile(configPath, 'utf8');
@@ -135,6 +135,9 @@ async function loadEnvIdFromUserConfig() {
135
135
  if (!envId) {
136
136
  warn(`Config file ${configPath} found, but 'envId' property is missing or empty.`);
137
137
  }
138
+ else {
139
+ debug('从配置文件加载环境ID:', envId);
140
+ }
138
141
  return envId;
139
142
  }
140
143
  catch (err) {
@@ -148,36 +151,16 @@ async function loadEnvIdFromUserConfig() {
148
151
  return null;
149
152
  }
150
153
  }
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
154
  // 清理用户环境ID配置
167
155
  export async function clearUserEnvId() {
168
156
  const configPath = getUserConfigPath();
169
157
  try {
170
158
  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配置已清理 - 静默操作
159
+ debug('环境ID配置文件已删除:', configPath);
177
160
  }
178
161
  catch (error) {
179
162
  // 文件不存在或删除失败,忽略错误
180
- // 环境ID配置文件不存在或已清理 - 静默操作
163
+ debug('环境ID配置文件不存在或已清理:', configPath);
181
164
  }
182
165
  }
183
166
  // 自动设置环境ID(无需MCP工具调用)
@@ -12,6 +12,10 @@ const TEMPLATES = {
12
12
  description: "React + CloudBase 全栈应用模板",
13
13
  url: "https://static.cloudbase.net/cloudbase-examples/web-cloudbase-react-template.zip"
14
14
  },
15
+ "vue": {
16
+ description: "Vue + CloudBase 全栈应用模板",
17
+ url: "https://static.cloudbase.net/cloudbase-examples/web-cloudbase-vue-template.zip"
18
+ },
15
19
  "miniprogram": {
16
20
  description: "微信小程序 + 云开发模板",
17
21
  url: "https://static.cloudbase.net/cloudbase-examples/miniprogram-cloudbase-miniprogram-template.zip"
@@ -123,11 +127,12 @@ export function registerSetupTools(server) {
123
127
 
124
128
  支持的模板:
125
129
  - react: React + CloudBase 全栈应用模板
130
+ - vue: Vue + CloudBase 全栈应用模板
126
131
  - miniprogram: 微信小程序 + 云开发模板
127
132
  - rules: 只包含AI编辑器配置文件(包含Cursor、WindSurf、CodeBuddy等所有主流编辑器配置),适合在已有项目中补充AI编辑器配置
128
133
 
129
134
  工具会自动下载模板到临时目录,解压后如果检测到WORKSPACE_FOLDER_PATHS环境变量,则复制到项目目录。`, {
130
- template: z.enum(["react", "miniprogram", "rules"]).describe("要下载的模板类型"),
135
+ template: z.enum(["react", "vue", "miniprogram", "rules"]).describe("要下载的模板类型"),
131
136
  overwrite: z.boolean().optional().describe("是否覆盖已存在的文件,默认为false(不覆盖)")
132
137
  }, async ({ template, overwrite = false }) => {
133
138
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cloudbase/cloudbase-mcp",
3
- "version": "1.7.3",
3
+ "version": "1.7.5",
4
4
  "description": "腾讯云开发 MCP Server,支持静态托管/环境查询/",
5
5
  "main": "index.js",
6
6
  "type": "module",