@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 +13 -6
- package/dist/341.index.js +92 -2
- package/dist/602.index.cjs +92 -2
- package/dist/cli.cjs +3 -3
- package/dist/index.cjs +257 -432
- package/dist/index.d.ts +3 -0
- package/dist/index.js +256 -430
- package/package.json +1 -1
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`
|
|
123
|
+
在 `url` 里可通过 query 参数控制插件启用范围:
|
|
124
|
+
|
|
125
|
+
- `enable_plugins`:仅启用指定插件,多个插件使用逗号分隔,例如只启用 `env` 和 `database`
|
|
126
|
+
- `disable_plugins`:从默认插件集合中禁用指定插件,多个插件使用逗号分隔,例如禁用 `rag` 和 `env`
|
|
124
127
|
|
|
125
128
|
```
|
|
126
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
441
|
+
## 项目活跃度
|
|
435
442
|
|
|
436
|
-
|
|
443
|
+

|
|
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
|
|
431
|
-
const host = process.env.INTERACTIVE_SERVER_HOST ?? "
|
|
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();
|
package/dist/602.index.cjs
CHANGED
|
@@ -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
|
|
1834
|
-
const host = process.env.INTERACTIVE_SERVER_HOST ?? "
|
|
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();
|