@cloudbase/cloudbase-mcp 2.17.0 → 2.18.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 CHANGED
@@ -118,15 +118,22 @@ AI 编程工具(如 OpenClaw、Cursor、CodeBuddy)解决了**代码生成**
118
118
  }
119
119
  ```
120
120
 
121
- **托管模式可选:通过 URL 控制启用哪些插件**
121
+ **托管模式可选:通过 URL 控制插件启用范围**
122
122
 
123
- 在 `url` 里加上 query 参数可禁用指定插件,例如禁用 `rag` 和 `env`:
123
+ 在 `url` 里可通过 query 参数控制插件启用范围:
124
+
125
+ - `enable_plugins`:仅启用指定插件,多个插件使用逗号分隔,例如只启用 `env` 和 `database`
126
+ - `disable_plugins`:从默认插件集合中禁用指定插件,多个插件使用逗号分隔,例如禁用 `rag` 和 `env`
124
127
 
125
128
  ```
126
- https://tcb-api.cloud.tencent.com/mcp/v1?env_id=YOUR_ENV_ID&disable_plugins=rag&disable_plugins=env
129
+ # 只启用指定插件
130
+ https://tcb-api.cloud.tencent.com/mcp/v1?env_id=YOUR_ENV_ID&enable_plugins=env,database
131
+
132
+ # 禁用指定插件
133
+ https://tcb-api.cloud.tencent.com/mcp/v1?env_id=YOUR_ENV_ID&disable_plugins=rag,env
127
134
  ```
128
135
 
129
- 当前可配置的插件名:`env`, `database`, `functions`, `hosting`, `storage`, `setup`, `rag`, `cloudrun`, `gateway`, `download`, `security-rule`, `invite-code`, `capi`。
136
+ 当前可配置的插件名以 `mcp/src/server.ts` 为准,建议优先使用 canonical 名称:`env`, `database`, `functions`, `hosting`, `storage`, `setup`, `rag`, `download`, `gateway`, `cloudrun`, `app-auth`, `permissions`, `logs`, `agents`, `invite-code`, `capi`, `apps`。
130
137
 
131
138
  > [!TIP]
132
139
  > **推荐使用 CloudBase AI CLI**
@@ -431,9 +438,9 @@ CodeBuddy 已内置 CloudBase MCP,无需配置即可使用。
431
438
  | **官方文档** | [查看文档](https://docs.cloudbase.net/) | 完整的云开发文档 |
432
439
  | **Issue 反馈** | [提交问题](https://github.com/TencentCloudBase/CloudBase-AI-ToolKit/issues) | Bug 反馈和功能请求 |
433
440
 
434
- ## Star History
441
+ ## 项目活跃度
435
442
 
436
- [![Star History Chart](https://api.star-history.com/svg?repos=TencentCloudBase/CloudBase-AI-ToolKit&type=Timeline)](https://github.com/TencentCloudBase/CloudBase-AI-ToolKit)
443
+ ![Repo Activity](https://repobeats.axiom.co/api/embed/6cd6ed00da4384e43b24805c197f584626946dda.svg "Repobeats analytics image")
437
444
 
438
445
  ## Contributors
439
446
 
package/dist/341.index.js CHANGED
@@ -26,6 +26,85 @@ function isVSCodeEnvironment() {
26
26
  process.env.VSCODE_PID ||
27
27
  process.env.TERM_PROGRAM === "vscode");
28
28
  }
29
+ /**
30
+ * Validates that a URL is safe to open, blocking SSRF targets.
31
+ * Only allows http/https schemes and rejects private/reserved IP ranges
32
+ * and sensitive hostnames.
33
+ */
34
+ function isUrlSafeToOpen(url) {
35
+ let parsed;
36
+ try {
37
+ parsed = new URL(url);
38
+ }
39
+ catch {
40
+ return { safe: false, reason: "Invalid URL format" };
41
+ }
42
+ // Only allow http and https schemes
43
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
44
+ return { safe: false, reason: `Unsupported scheme: ${parsed.protocol}` };
45
+ }
46
+ const hostname = parsed.hostname.toLowerCase();
47
+ // Block common sensitive / internal hostnames
48
+ const blockedHostnames = [
49
+ "localhost",
50
+ "localhost.localdomain",
51
+ "ip6-localhost",
52
+ "ip6-loopback",
53
+ "metadata.google.internal",
54
+ "metadata.internal",
55
+ ];
56
+ if (blockedHostnames.includes(hostname)) {
57
+ return { safe: false, reason: `Blocked hostname: ${hostname}` };
58
+ }
59
+ // Block hostnames ending with .internal or .local
60
+ if (hostname.endsWith(".internal") || hostname.endsWith(".local")) {
61
+ return { safe: false, reason: `Blocked internal hostname: ${hostname}` };
62
+ }
63
+ // Block IP addresses in private/reserved ranges (RFC 1918, loopback, link-local, etc.)
64
+ const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
65
+ const match = ipv4Regex.exec(hostname);
66
+ if (match) {
67
+ const octets = [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10), parseInt(match[4], 10)];
68
+ // Loopback: 127.0.0.0/8
69
+ if (octets[0] === 127) {
70
+ return { safe: false, reason: "Blocked loopback IP address" };
71
+ }
72
+ // Link-local: 169.254.0.0/16
73
+ if (octets[0] === 169 && octets[1] === 254) {
74
+ return { safe: false, reason: "Blocked link-local IP address" };
75
+ }
76
+ // RFC 1918 private: 10.0.0.0/8
77
+ if (octets[0] === 10) {
78
+ return { safe: false, reason: "Blocked private IP address (10.x.x.x)" };
79
+ }
80
+ // RFC 1918 private: 172.16.0.0/12
81
+ if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) {
82
+ return { safe: false, reason: "Blocked private IP address (172.16-31.x.x)" };
83
+ }
84
+ // RFC 1918 private: 192.168.0.0/16
85
+ if (octets[0] === 192 && octets[1] === 168) {
86
+ return { safe: false, reason: "Blocked private IP address (192.168.x.x)" };
87
+ }
88
+ // Broadcast: 0.0.0.0
89
+ if (octets[0] === 0 && octets[1] === 0 && octets[2] === 0 && octets[3] === 0) {
90
+ return { safe: false, reason: "Blocked broadcast address" };
91
+ }
92
+ }
93
+ // Block IPv6 loopback and link-local
94
+ if (hostname.startsWith("[") && hostname.endsWith("]")) {
95
+ const ipv6 = hostname.slice(1, -1).toLowerCase();
96
+ if (ipv6 === "::1" || ipv6 === "0:0:0:0:0:0:0:1" || ipv6 === "::" || ipv6 === "0:0:0:0:0:0:0:0") {
97
+ return { safe: false, reason: "Blocked IPv6 loopback/reserved address" };
98
+ }
99
+ if (ipv6.startsWith("fe80:")) {
100
+ return { safe: false, reason: "Blocked IPv6 link-local address" };
101
+ }
102
+ if (ipv6.startsWith("fc") || ipv6.startsWith("fd")) {
103
+ return { safe: false, reason: "Blocked IPv6 unique-local address" };
104
+ }
105
+ }
106
+ return { safe: true };
107
+ }
29
108
  function parseBrowserCommand(browser) {
30
109
  const parts = browser.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
31
110
  return parts.map((item) => item.replace(/^"(.*)"$/, "$1"));
@@ -249,6 +328,17 @@ class InteractiveServer {
249
328
  res.status(400).json({ success: false, error: "URL is required" });
250
329
  return;
251
330
  }
331
+ if (typeof url !== "string") {
332
+ res.status(400).json({ success: false, error: "URL must be a string" });
333
+ return;
334
+ }
335
+ // Validate URL to prevent SSRF
336
+ const validation = isUrlSafeToOpen(url);
337
+ if (!validation.safe) {
338
+ (0, logger_js_1.warn)(`Blocked open-url request for unsafe URL: ${url} (${validation.reason})`);
339
+ res.status(403).json({ success: false, error: `URL not allowed: ${validation.reason}` });
340
+ return;
341
+ }
252
342
  (0, logger_js_1.info)("Received open URL request", { url });
253
343
  try {
254
344
  // Pass mcpServer directly - it's an ExtendedMcpServer instance with server property
@@ -427,8 +517,8 @@ class InteractiveServer {
427
517
  reject(err);
428
518
  }
429
519
  };
430
- // Host: default 0.0.0.0 so Cloud IDE / VSCode Remote port-forward can connect; set INTERACTIVE_SERVER_HOST=127.0.0.1 for local-only
431
- const host = process.env.INTERACTIVE_SERVER_HOST ?? "0.0.0.0";
520
+ // Host: default 127.0.0.1 for security (prevents SSRF from remote access); set INTERACTIVE_SERVER_HOST=0.0.0.0 to allow remote connections
521
+ const host = process.env.INTERACTIVE_SERVER_HOST ?? "127.0.0.1";
432
522
  // 设置成功监听处理
433
523
  const listeningHandler = () => {
434
524
  const address = this.server.address();
@@ -1429,6 +1429,85 @@ function isVSCodeEnvironment() {
1429
1429
  process.env.VSCODE_PID ||
1430
1430
  process.env.TERM_PROGRAM === "vscode");
1431
1431
  }
1432
+ /**
1433
+ * Validates that a URL is safe to open, blocking SSRF targets.
1434
+ * Only allows http/https schemes and rejects private/reserved IP ranges
1435
+ * and sensitive hostnames.
1436
+ */
1437
+ function isUrlSafeToOpen(url) {
1438
+ let parsed;
1439
+ try {
1440
+ parsed = new URL(url);
1441
+ }
1442
+ catch {
1443
+ return { safe: false, reason: "Invalid URL format" };
1444
+ }
1445
+ // Only allow http and https schemes
1446
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
1447
+ return { safe: false, reason: `Unsupported scheme: ${parsed.protocol}` };
1448
+ }
1449
+ const hostname = parsed.hostname.toLowerCase();
1450
+ // Block common sensitive / internal hostnames
1451
+ const blockedHostnames = [
1452
+ "localhost",
1453
+ "localhost.localdomain",
1454
+ "ip6-localhost",
1455
+ "ip6-loopback",
1456
+ "metadata.google.internal",
1457
+ "metadata.internal",
1458
+ ];
1459
+ if (blockedHostnames.includes(hostname)) {
1460
+ return { safe: false, reason: `Blocked hostname: ${hostname}` };
1461
+ }
1462
+ // Block hostnames ending with .internal or .local
1463
+ if (hostname.endsWith(".internal") || hostname.endsWith(".local")) {
1464
+ return { safe: false, reason: `Blocked internal hostname: ${hostname}` };
1465
+ }
1466
+ // Block IP addresses in private/reserved ranges (RFC 1918, loopback, link-local, etc.)
1467
+ const ipv4Regex = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/;
1468
+ const match = ipv4Regex.exec(hostname);
1469
+ if (match) {
1470
+ const octets = [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10), parseInt(match[4], 10)];
1471
+ // Loopback: 127.0.0.0/8
1472
+ if (octets[0] === 127) {
1473
+ return { safe: false, reason: "Blocked loopback IP address" };
1474
+ }
1475
+ // Link-local: 169.254.0.0/16
1476
+ if (octets[0] === 169 && octets[1] === 254) {
1477
+ return { safe: false, reason: "Blocked link-local IP address" };
1478
+ }
1479
+ // RFC 1918 private: 10.0.0.0/8
1480
+ if (octets[0] === 10) {
1481
+ return { safe: false, reason: "Blocked private IP address (10.x.x.x)" };
1482
+ }
1483
+ // RFC 1918 private: 172.16.0.0/12
1484
+ if (octets[0] === 172 && octets[1] >= 16 && octets[1] <= 31) {
1485
+ return { safe: false, reason: "Blocked private IP address (172.16-31.x.x)" };
1486
+ }
1487
+ // RFC 1918 private: 192.168.0.0/16
1488
+ if (octets[0] === 192 && octets[1] === 168) {
1489
+ return { safe: false, reason: "Blocked private IP address (192.168.x.x)" };
1490
+ }
1491
+ // Broadcast: 0.0.0.0
1492
+ if (octets[0] === 0 && octets[1] === 0 && octets[2] === 0 && octets[3] === 0) {
1493
+ return { safe: false, reason: "Blocked broadcast address" };
1494
+ }
1495
+ }
1496
+ // Block IPv6 loopback and link-local
1497
+ if (hostname.startsWith("[") && hostname.endsWith("]")) {
1498
+ const ipv6 = hostname.slice(1, -1).toLowerCase();
1499
+ if (ipv6 === "::1" || ipv6 === "0:0:0:0:0:0:0:1" || ipv6 === "::" || ipv6 === "0:0:0:0:0:0:0:0") {
1500
+ return { safe: false, reason: "Blocked IPv6 loopback/reserved address" };
1501
+ }
1502
+ if (ipv6.startsWith("fe80:")) {
1503
+ return { safe: false, reason: "Blocked IPv6 link-local address" };
1504
+ }
1505
+ if (ipv6.startsWith("fc") || ipv6.startsWith("fd")) {
1506
+ return { safe: false, reason: "Blocked IPv6 unique-local address" };
1507
+ }
1508
+ }
1509
+ return { safe: true };
1510
+ }
1432
1511
  function parseBrowserCommand(browser) {
1433
1512
  const parts = browser.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
1434
1513
  return parts.map((item) => item.replace(/^"(.*)"$/, "$1"));
@@ -1652,6 +1731,17 @@ class InteractiveServer {
1652
1731
  res.status(400).json({ success: false, error: "URL is required" });
1653
1732
  return;
1654
1733
  }
1734
+ if (typeof url !== "string") {
1735
+ res.status(400).json({ success: false, error: "URL must be a string" });
1736
+ return;
1737
+ }
1738
+ // Validate URL to prevent SSRF
1739
+ const validation = isUrlSafeToOpen(url);
1740
+ if (!validation.safe) {
1741
+ (0, logger_js_1.warn)(`Blocked open-url request for unsafe URL: ${url} (${validation.reason})`);
1742
+ res.status(403).json({ success: false, error: `URL not allowed: ${validation.reason}` });
1743
+ return;
1744
+ }
1655
1745
  (0, logger_js_1.info)("Received open URL request", { url });
1656
1746
  try {
1657
1747
  // Pass mcpServer directly - it's an ExtendedMcpServer instance with server property
@@ -1830,8 +1920,8 @@ class InteractiveServer {
1830
1920
  reject(err);
1831
1921
  }
1832
1922
  };
1833
- // Host: default 0.0.0.0 so Cloud IDE / VSCode Remote port-forward can connect; set INTERACTIVE_SERVER_HOST=127.0.0.1 for local-only
1834
- const host = process.env.INTERACTIVE_SERVER_HOST ?? "0.0.0.0";
1923
+ // Host: default 127.0.0.1 for security (prevents SSRF from remote access); set INTERACTIVE_SERVER_HOST=0.0.0.0 to allow remote connections
1924
+ const host = process.env.INTERACTIVE_SERVER_HOST ?? "127.0.0.1";
1835
1925
  // 设置成功监听处理
1836
1926
  const listeningHandler = () => {
1837
1927
  const address = this.server.address();