@cloudbase/cloudbase-mcp 2.25.10 → 2.26.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/dist/index.js CHANGED
@@ -41,6 +41,7 @@ const auth_js_1 = __webpack_require__(7291);
41
41
  const cloudbase_manager_js_1 = __webpack_require__(3431);
42
42
  const logger_js_1 = __webpack_require__(3039);
43
43
  const tool_result_js_1 = __webpack_require__(9835);
44
+ const gateway_access_urls_js_1 = __webpack_require__(2435);
44
45
  const env_setup_js_1 = __webpack_require__(6978);
45
46
  /**
46
47
  * Resources accepted by CreateEnv. Matches Cloud API / Manager SDK contract.
@@ -793,6 +794,89 @@ async function enrichEnvInfoWithMissingFields(manager, result, envId) {
793
794
  return result;
794
795
  }
795
796
  }
797
+ /**
798
+ * Project gateway Route.Enable onto envQuery(info) without mutating StaticDomain.
799
+ *
800
+ * StaticStorages[].StaticDomain stays the cloud-API nominal hostname.
801
+ * Sibling field staticDomainRouteEnabled (and EnvInfo-level mirrors for the
802
+ * primary store) tell callers whether the default hosting domain route is
803
+ * actually reachable (Enable !== false). Matches queryHosting websiteConfig.
804
+ */
805
+ async function enrichEnvInfoWithStaticDomainRouteEnabled(manager, result, envId) {
806
+ const envInfo = result?.EnvInfo;
807
+ if (!envInfo || typeof envInfo !== "object") {
808
+ return result;
809
+ }
810
+ const staticStorages = Array.isArray(envInfo.StaticStorages)
811
+ ? envInfo.StaticStorages
812
+ : null;
813
+ if (!staticStorages || staticStorages.length === 0) {
814
+ return result;
815
+ }
816
+ if (typeof manager?.env?.describeHttpServiceRoute !== "function") {
817
+ return result;
818
+ }
819
+ const resolvedEnvId = (typeof envId === "string" && envId.trim()) ||
820
+ (typeof envInfo.EnvId === "string" && envInfo.EnvId.trim()) ||
821
+ undefined;
822
+ if (!resolvedEnvId) {
823
+ return result;
824
+ }
825
+ try {
826
+ const routeResult = await manager.env.describeHttpServiceRoute({
827
+ EnvId: resolvedEnvId,
828
+ Limit: 1000,
829
+ });
830
+ const routes = (0, gateway_access_urls_js_1.flattenHttpServiceRoutes)(routeResult);
831
+ let primaryRouteEnabled = null;
832
+ const enrichedStorages = staticStorages.map((store, index) => {
833
+ if (!store || typeof store !== "object" || Array.isArray(store)) {
834
+ return store;
835
+ }
836
+ const record = store;
837
+ const domain = typeof record.StaticDomain === "string" ? record.StaticDomain : "";
838
+ if (!domain) {
839
+ return store;
840
+ }
841
+ const reachable = (0, gateway_access_urls_js_1.isDomainPathReachableViaGateway)(routes, domain, "/");
842
+ if (index === 0) {
843
+ primaryRouteEnabled = reachable;
844
+ }
845
+ return {
846
+ ...record,
847
+ staticDomainRouteEnabled: reachable,
848
+ };
849
+ });
850
+ const enrichment = {
851
+ StaticStorages: enrichedStorages,
852
+ };
853
+ if (primaryRouteEnabled !== null) {
854
+ enrichment.staticDomainRouteEnabled = primaryRouteEnabled;
855
+ enrichment.accessUrlReachable = primaryRouteEnabled !== false;
856
+ if (primaryRouteEnabled === false) {
857
+ const primaryDomain = typeof enrichedStorages[0]
858
+ ?.StaticDomain === "string"
859
+ ? String(enrichedStorages[0].StaticDomain)
860
+ : "";
861
+ enrichment.routeDisabled = true;
862
+ if (primaryDomain) {
863
+ enrichment.disabledAccessUrls = [`https://${primaryDomain}/`];
864
+ }
865
+ }
866
+ }
867
+ return {
868
+ ...result,
869
+ EnvInfo: {
870
+ ...envInfo,
871
+ ...enrichment,
872
+ },
873
+ };
874
+ }
875
+ catch {
876
+ // Gateway route lookup is best-effort enrichment only.
877
+ return result;
878
+ }
879
+ }
796
880
  function normalizeOptionalToolString(value) {
797
881
  return typeof value === "string" && value.trim().length > 0
798
882
  ? value.trim()
@@ -1274,8 +1358,9 @@ function registerEnvTools(server) {
1274
1358
  if (action === "status") {
1275
1359
  const loginState = await (0, auth_js_1.peekLoginState)();
1276
1360
  const authFlowState = await (0, auth_js_1.getAuthProgressState)();
1277
- // 判断是否为 API Key 模式
1278
- const isApiKeyMode = !!(process.env.CLOUDBASE_API_KEY && process.env.CLOUDBASE_ENV_ID);
1361
+ // Detect API Key mode (CLOUDBASE_API_KEY preferred, CLOUDBASE_APIKEY fallback)
1362
+ const apiKeyFromEnv = (0, auth_js_1.getCloudBaseApiKeyFromEnv)();
1363
+ const isApiKeyMode = !!(apiKeyFromEnv && process.env.CLOUDBASE_ENV_ID);
1279
1364
  const authStatus = loginState
1280
1365
  ? "READY"
1281
1366
  : authFlowState.status === "PENDING"
@@ -1323,8 +1408,9 @@ function registerEnvTools(server) {
1323
1408
  });
1324
1409
  }
1325
1410
  if (action === "start_auth") {
1326
- // API Key 模式:尝试换取临时密钥
1327
- if (process.env.CLOUDBASE_API_KEY && process.env.CLOUDBASE_ENV_ID) {
1411
+ // API Key mode: exchange for temporary credentials
1412
+ const apiKeyFromEnv = (0, auth_js_1.getCloudBaseApiKeyFromEnv)();
1413
+ if (apiKeyFromEnv && process.env.CLOUDBASE_ENV_ID) {
1328
1414
  try {
1329
1415
  const existingLoginState = await (0, auth_js_1.peekLoginState)();
1330
1416
  if (existingLoginState) {
@@ -1338,22 +1424,21 @@ function registerEnvTools(server) {
1338
1424
  }
1339
1425
  }
1340
1426
  catch (e) {
1341
- // peekLoginState 内部异常,记录后 fall through
1427
+ // peekLoginState threw; log and fall through to diagnostic
1342
1428
  (0, logger_js_1.debug)("start_auth: peekLoginState threw", { error: e instanceof Error ? e.message : String(e) });
1343
1429
  }
1344
- // API Key 换取失败:返回详细诊断信息
1345
- // 尝试获取更详细的错误信息
1430
+ // API Key exchange failed: return diagnostic details
1346
1431
  let diagMessage = "当前配置了 API Key 认证模式,但换取临时密钥失败。";
1347
1432
  const endpoint = process.env.CLOUDBASE_API_ENDPOINT || `https://${process.env.CLOUDBASE_ENV_ID}.ap-shanghai.tcb-api.tencentcloudapi.com`;
1348
1433
  diagMessage += `\n\n诊断信息:`;
1349
1434
  diagMessage += `\n- CLOUDBASE_ENV_ID: ${process.env.CLOUDBASE_ENV_ID}`;
1350
- diagMessage += `\n- CLOUDBASE_API_KEY: ${process.env.CLOUDBASE_API_KEY.slice(0, 20)}...(已截断)`;
1435
+ diagMessage += `\n- CLOUDBASE_API_KEY: ${apiKeyFromEnv.slice(0, 20)}...(已截断)`;
1351
1436
  diagMessage += `\n- Endpoint: ${endpoint}`;
1352
1437
  diagMessage += `\n\n可能原因:`;
1353
1438
  diagMessage += `\n1. API Key 已过期或被删除`;
1354
1439
  diagMessage += `\n2. Endpoint 不可达(网络/DNS 问题)`;
1355
1440
  diagMessage += `\n3. CLOUDBASE_ENV_ID 与 API Key 所属环境不匹配`;
1356
- diagMessage += `\n\n建议:检查 MCP 配置中的 CLOUDBASE_API_KEY 和 CLOUDBASE_ENV_ID 环境变量是否正确。`;
1441
+ diagMessage += `\n\n建议:检查 MCP 配置中的 CLOUDBASE_API_KEY(或兼容的 CLOUDBASE_APIKEY)和 CLOUDBASE_ENV_ID 环境变量是否正确。`;
1357
1442
  return (0, tool_result_js_1.buildJsonToolResult)({
1358
1443
  ok: false,
1359
1444
  code: "API_KEY_AUTH_FAILED",
@@ -1639,12 +1724,12 @@ function registerEnvTools(server) {
1639
1724
  });
1640
1725
  }
1641
1726
  if (action === "logout") {
1642
- // API Key 模式下不允许 logout
1643
- if (process.env.CLOUDBASE_API_KEY && process.env.CLOUDBASE_ENV_ID) {
1727
+ // Disallow logout in API Key mode
1728
+ if ((0, auth_js_1.getCloudBaseApiKeyFromEnv)() && process.env.CLOUDBASE_ENV_ID) {
1644
1729
  return (0, tool_result_js_1.buildJsonToolResult)({
1645
1730
  ok: false,
1646
1731
  code: "LOGOUT_NOT_ALLOWED",
1647
- message: "当前使用 API Key 认证模式,不支持退出登录。如需切换认证方式,请移除 CLOUDBASE_API_KEY 环境变量后重启。",
1732
+ message: "当前使用 API Key 认证模式,不支持退出登录。如需切换认证方式,请移除 CLOUDBASE_API_KEY(或兼容的 CLOUDBASE_APIKEY)环境变量后重启。",
1648
1733
  auth_mode: "api_key",
1649
1734
  });
1650
1735
  }
@@ -1866,6 +1951,8 @@ function registerEnvTools(server) {
1866
1951
  if (envId) {
1867
1952
  result = await enrichEnvInfoWithMissingFields(cloudbaseInfo, result, envId);
1868
1953
  }
1954
+ // Project gateway Route.Enable next to StaticDomain (do not rewrite StaticDomain).
1955
+ result = await enrichEnvInfoWithStaticDomainRouteEnabled(cloudbaseInfo, result, envId);
1869
1956
  result = await enrichEnvInfoWithRuntimeMode(result, cloudbaseInfo);
1870
1957
  break;
1871
1958
  case "domains":
@@ -1934,7 +2021,7 @@ function registerEnvTools(server) {
1934
2021
  // Register primary tool name (queryEnv)
1935
2022
  const queryEnvToolSchema = {
1936
2023
  title: "CloudBase 环境查询",
1937
- description: "查询 CloudBase 环境相关信息,支持查询环境列表、指定环境详情和安全域名。(曾用名:envQuery、listEnvs、getEnvInfo、getEnvAuthDomains)当 action=list 时,会按 DescribeEnvs 语义做列表/筛选,标准返回字段为 EnvId、Alias、Status、EnvType、Region、PackageId、PackageName、IsDefault,并支持通过 fields 白名单裁剪这些字段;aliasExact=true 时会按别名精确筛选,避免把前缀相近的环境误当作候选;即使传入 envId,action=list 也只返回摘要,不会返回完整资源明细或 expiry。如需查询某个已知 EnvId 对应环境的详细信息(包括资源字段和计费信息),必须使用 action=info 并传入目标环境的 envId 参数。action=info 会在可用时补充 BillingInfo(如 ExpireTime、PayMode、IsAutoRenew 等计费字段)。\n\n🔍 action=info 还会派生三个用于后端选型的字段:\n- `EnvInfo.RuntimeMode`:'postgresql' 或 'nosql',表示新业务建议默认使用的后端(PG 已开通时为 postgresql,否则为 nosql)。\n- `EnvInfo.RuntimeBackends`:`{postgresql, nosql, mysql}` 三个布尔值,描述当前环境实际并存的后端。\n- `EnvInfo.RuntimeModeHints`:每个后端对应的 API/工具/skill 提示。\n\nAI 在写业务/权限/存储代码前必须先看这三项:PG 模式下新业务推荐 `app.rdb()` + RLS(`managePgDatabase action=execute` 跑 `CREATE POLICY`)+ pgstore;已存在的 NoSQL 集合 / 旧 storage / `managePermissions(resourceType=\"noSqlDatabase\")` 在 PG 环境下仍然有效。真正不适用的是 MySQL:当 `RuntimeBackends.mysql === false` 时,`manageMysqlDatabase` / `queryMysqlDatabase` / `relational-database-mcp-cloudbase` skill 都不该使用。",
2024
+ description: "查询 CloudBase 环境相关信息,支持查询环境列表、指定环境详情和安全域名。(曾用名:envQuery、listEnvs、getEnvInfo、getEnvAuthDomains)当 action=list 时,会按 DescribeEnvs 语义做列表/筛选,标准返回字段为 EnvId、Alias、Status、EnvType、Region、PackageId、PackageName、IsDefault,并支持通过 fields 白名单裁剪这些字段;aliasExact=true 时会按别名精确筛选,避免把前缀相近的环境误当作候选;即使传入 envId,action=list 也只返回摘要,不会返回完整资源明细或 expiry。如需查询某个已知 EnvId 对应环境的详细信息(包括资源字段和计费信息),必须使用 action=info 并传入目标环境的 envId 参数。action=info 会在可用时补充 BillingInfo(如 ExpireTime、PayMode、IsAutoRenew 等计费字段)。\n\n🔍 action=info 还会派生三个用于后端选型的字段:\n- `EnvInfo.RuntimeMode`:'postgresql' 或 'nosql',表示新业务建议默认使用的后端(PG 已开通时为 postgresql,否则为 nosql)。\n- `EnvInfo.RuntimeBackends`:`{postgresql, nosql, mysql}` 三个布尔值,描述当前环境实际并存的后端。\n- `EnvInfo.RuntimeModeHints`:每个后端对应的 API/工具/skill 提示。\n\n🌐 action=info 还会在不改写 `StaticStorages[].StaticDomain`(云 API 名义域名)的前提下,投影网关路由 Enable 状态:`StaticStorages[].staticDomainRouteEnabled` 与 `EnvInfo.staticDomainRouteEnabled`(与 queryHosting websiteConfig 同源)。`false` 表示默认静态域名根路由已禁用(访问会返回 GATEWAY_ROUTE_DISABLED),勿把名义域名当成可达 URL。\n\nAI 在写业务/权限/存储代码前必须先看这三项:PG 模式下新业务推荐 `app.rdb()` + RLS(`managePgDatabase action=execute` 跑 `CREATE POLICY`)+ pgstore;已存在的 NoSQL 集合 / 旧 storage / `managePermissions(resourceType=\"noSqlDatabase\")` 在 PG 环境下仍然有效。真正不适用的是 MySQL:当 `RuntimeBackends.mysql === false` 时,`manageMysqlDatabase` / `queryMysqlDatabase` / `relational-database-mcp-cloudbase` skill 都不该使用。",
1938
2025
  inputSchema: {
1939
2026
  action: zod_1.z
1940
2027
  .enum(["list", "info", "domains"])
@@ -2816,7 +2903,7 @@ ${envIdSection}
2816
2903
  ## 环境信息
2817
2904
  - 操作系统: ${os_1.default.type()} ${os_1.default.release()}
2818
2905
  - Node.js版本: ${process.version}
2819
- - MCP 版本:${process.env.npm_package_version || ( true ? "2.25.10" : 0)}
2906
+ - MCP 版本:${process.env.npm_package_version || ( true ? "2.26.0" : 0)}
2820
2907
  - 系统架构: ${os_1.default.arch()}
2821
2908
  - 时间: ${new Date().toISOString()}
2822
2909
  - 请求ID: ${requestId}
@@ -2897,7 +2984,7 @@ function createWrappedHandler(name, handler, server) {
2897
2984
  requestId: (typeof error === 'object' && error && 'requestId' in error) ? error.requestId : '',
2898
2985
  ide: server.ide || process.env.INTEGRATION_IDE || ''
2899
2986
  });
2900
- const mcpVersion = true ? "2.25.10" : 0;
2987
+ const mcpVersion = true ? "2.26.0" : 0;
2901
2988
  const requestIdSuffix = requestId ? `\n🆔 RequestId: ${requestId}` : '';
2902
2989
  const enhancedErrorMessage = `${errorMessage}${requestIdSuffix}\n\n📦 CloudBase MCP v${mcpVersion}\n🔗 遇到问题?请复制以下链接到浏览器打开\n即可自动携带错误详情快速创建 GitHub Issue:\n${issueLink}`;
2903
2990
  // 创建新的错误对象,保持原有的错误类型但更新消息
@@ -3499,12 +3586,19 @@ module.exports = __WEBPACK_EXTERNAL_MODULE__modelcontextprotocol_sdk_types_js_70
3499
3586
 
3500
3587
 
3501
3588
  Object.defineProperty(exports, "__esModule", ({ value: true }));
3589
+ exports.isGatewayRouteEnabled = isGatewayRouteEnabled;
3590
+ exports.isDomainPathReachableViaGateway = isDomainPathReachableViaGateway;
3502
3591
  exports.rankGatewayAccessUrls = rankGatewayAccessUrls;
3503
3592
  exports.flattenHttpServiceRoutes = flattenHttpServiceRoutes;
3504
3593
  exports.filterRoutesByUpstream = filterRoutesByUpstream;
3505
3594
  exports.toAccessUrlEnvelope = toAccessUrlEnvelope;
3506
3595
  exports.resolveGatewayAccessUrls = resolveGatewayAccessUrls;
3596
+ exports.resolveAllGatewayRoutes = resolveAllGatewayRoutes;
3507
3597
  exports.preferGatewayOrFallback = preferGatewayOrFallback;
3598
+ /** Route is reachable unless Enable is explicitly false. */
3599
+ function isGatewayRouteEnabled(route) {
3600
+ return route.Enable !== false;
3601
+ }
3508
3602
  function normalizeAccessPath(path) {
3509
3603
  if (!path) {
3510
3604
  return "/";
@@ -3514,13 +3608,63 @@ function normalizeAccessPath(path) {
3514
3608
  function buildHttpsUrl(domain, path) {
3515
3609
  return `https://${domain}${normalizeAccessPath(path)}`;
3516
3610
  }
3611
+ function extractHostname(urlOrHost) {
3612
+ const trimmed = urlOrHost.trim();
3613
+ if (!trimmed) {
3614
+ return "";
3615
+ }
3616
+ try {
3617
+ if (trimmed.includes("://")) {
3618
+ return new URL(trimmed).hostname.toLowerCase();
3619
+ }
3620
+ }
3621
+ catch {
3622
+ // Fall through to treat as bare hostname.
3623
+ }
3624
+ return trimmed.replace(/\/.*$/, "").toLowerCase();
3625
+ }
3626
+ /**
3627
+ * Whether a hostname has an enabled covering gateway route for the given path.
3628
+ * Longest matching Path prefix wins. Missing route info → unknown (null).
3629
+ */
3630
+ function isDomainPathReachableViaGateway(routes, domainOrUrl, requestPath) {
3631
+ const hostname = extractHostname(domainOrUrl);
3632
+ if (!hostname) {
3633
+ return null;
3634
+ }
3635
+ const normalizedRequest = normalizeAccessPath(requestPath);
3636
+ const domainRoutes = routes.filter((route) => Boolean(route.Domain) &&
3637
+ route.Domain.toLowerCase() === hostname);
3638
+ if (domainRoutes.length === 0) {
3639
+ return null;
3640
+ }
3641
+ const covering = domainRoutes
3642
+ .map((route) => ({
3643
+ route,
3644
+ path: normalizeAccessPath(route.Path),
3645
+ }))
3646
+ .filter(({ path }) => {
3647
+ if (path === "/") {
3648
+ return true;
3649
+ }
3650
+ return (normalizedRequest === path ||
3651
+ normalizedRequest.startsWith(path.endsWith("/") ? path : `${path}/`));
3652
+ })
3653
+ .sort((a, b) => b.path.length - a.path.length);
3654
+ if (covering.length === 0) {
3655
+ return null;
3656
+ }
3657
+ return isGatewayRouteEnabled(covering[0].route);
3658
+ }
3517
3659
  /**
3518
3660
  * Rank gateway route URLs: custom domains (IsDefault !== true) before default domain.
3519
- * Stable within each group; dedupe by full URL.
3661
+ * By default skips Enable === false routes. Stable within each group; dedupe by full URL.
3520
3662
  */
3521
- function rankGatewayAccessUrls(routes) {
3663
+ function rankGatewayAccessUrls(routes, options) {
3664
+ const includeDisabled = options?.includeDisabled === true;
3522
3665
  const mapped = routes
3523
3666
  .filter((route) => Boolean(route.Domain))
3667
+ .filter((route) => includeDisabled || isGatewayRouteEnabled(route))
3524
3668
  .map((route) => {
3525
3669
  const path = normalizeAccessPath(route.Path);
3526
3670
  const isDefault = route.IsDefault === true;
@@ -3529,6 +3673,7 @@ function rankGatewayAccessUrls(routes) {
3529
3673
  domain: route.Domain,
3530
3674
  path,
3531
3675
  isDefault,
3676
+ enabled: isGatewayRouteEnabled(route),
3532
3677
  source: (isDefault
3533
3678
  ? "gateway.default"
3534
3679
  : "gateway.custom"),
@@ -3556,6 +3701,7 @@ function flattenHttpServiceRoutes(result) {
3556
3701
  Domain: domainItem.Domain ?? "",
3557
3702
  IsDefault: domainItem.IsDefault,
3558
3703
  Path: typeof route.Path === "string" ? route.Path : undefined,
3704
+ Enable: typeof route.Enable === "boolean" ? route.Enable : undefined,
3559
3705
  UpstreamResourceType: typeof route.UpstreamResourceType === "string"
3560
3706
  ? route.UpstreamResourceType
3561
3707
  : undefined,
@@ -3578,64 +3724,116 @@ function filterRoutesByUpstream(routes, input) {
3578
3724
  types.includes(String(route.UpstreamResourceType)));
3579
3725
  });
3580
3726
  }
3581
- function toAccessUrlEnvelope(ranked) {
3582
- if (ranked.length === 0) {
3583
- return { accessUrls: [], routes: [] };
3727
+ function toAccessUrlEnvelope(ranked, disabledAccessUrls = []) {
3728
+ const enabled = ranked.filter((item) => item.enabled);
3729
+ if (enabled.length === 0) {
3730
+ return {
3731
+ accessUrls: [],
3732
+ routes: [],
3733
+ disabledAccessUrls,
3734
+ };
3584
3735
  }
3585
3736
  return {
3586
- accessUrl: ranked[0].url,
3587
- accessUrls: ranked.map((item) => item.url),
3588
- accessUrlSource: ranked[0].source,
3589
- routes: ranked,
3737
+ accessUrl: enabled[0].url,
3738
+ accessUrls: enabled.map((item) => item.url),
3739
+ accessUrlSource: enabled[0].source,
3740
+ routes: enabled,
3741
+ disabledAccessUrls,
3590
3742
  };
3591
3743
  }
3744
+ async function loadHttpServiceRoutes(getManager, envId) {
3745
+ const manager = await getManager();
3746
+ const result = await manager.env.describeHttpServiceRoute({
3747
+ EnvId: envId,
3748
+ Limit: 1000,
3749
+ });
3750
+ return flattenHttpServiceRoutes(result);
3751
+ }
3592
3752
  /**
3593
3753
  * Best-effort lookup of gateway access URLs for an upstream resource.
3594
3754
  * Never throws — returns empty lists on failure.
3755
+ * Disabled routes (Enable === false) are excluded from accessUrl(s).
3595
3756
  */
3596
3757
  async function resolveGatewayAccessUrls(input) {
3597
3758
  try {
3598
- const manager = await input.getManager();
3599
- const result = await manager.env.describeHttpServiceRoute({
3600
- EnvId: input.envId,
3601
- Limit: 1000,
3602
- });
3603
- const flattened = flattenHttpServiceRoutes(result);
3759
+ const flattened = await loadHttpServiceRoutes(input.getManager, input.envId);
3604
3760
  const matched = filterRoutesByUpstream(flattened, {
3605
3761
  upstreamResourceName: input.upstreamResourceName,
3606
3762
  upstreamResourceTypes: input.upstreamResourceTypes,
3607
3763
  });
3608
- return toAccessUrlEnvelope(rankGatewayAccessUrls(matched));
3764
+ const disabledAccessUrls = rankGatewayAccessUrls(matched, {
3765
+ includeDisabled: true,
3766
+ })
3767
+ .filter((item) => !item.enabled)
3768
+ .map((item) => item.url);
3769
+ return toAccessUrlEnvelope(rankGatewayAccessUrls(matched), disabledAccessUrls);
3770
+ }
3771
+ catch {
3772
+ return { accessUrls: [], routes: [], disabledAccessUrls: [] };
3773
+ }
3774
+ }
3775
+ /**
3776
+ * Load all flattened HTTP service routes (best-effort). Used by hosting to
3777
+ * judge whether the static-domain fallback itself is gateway-disabled.
3778
+ */
3779
+ async function resolveAllGatewayRoutes(input) {
3780
+ try {
3781
+ return await loadHttpServiceRoutes(input.getManager, input.envId);
3609
3782
  }
3610
3783
  catch {
3611
- return { accessUrls: [], routes: [] };
3784
+ return [];
3612
3785
  }
3613
3786
  }
3614
3787
  /**
3615
3788
  * Merge ranked gateway URLs with a resource-native fallback URL.
3616
3789
  * Gateway custom/default always win over the fallback when present.
3790
+ * Unreachable fallbacks are omitted from accessUrl(s) and listed under
3791
+ * disabledAccessUrls instead.
3617
3792
  */
3618
3793
  function preferGatewayOrFallback(input) {
3794
+ const disabledAccessUrls = [
3795
+ ...(input.gateway.disabledAccessUrls ?? []),
3796
+ ];
3797
+ const fallbackReachable = input.fallbackReachable !== false;
3619
3798
  if (input.gateway.accessUrl) {
3620
3799
  const urls = [...input.gateway.accessUrls];
3621
3800
  if (input.fallbackUrl &&
3801
+ fallbackReachable &&
3622
3802
  !urls.includes(input.fallbackUrl)) {
3623
3803
  urls.push(input.fallbackUrl);
3624
3804
  }
3805
+ else if (input.fallbackUrl &&
3806
+ !fallbackReachable &&
3807
+ !disabledAccessUrls.includes(input.fallbackUrl)) {
3808
+ disabledAccessUrls.push(input.fallbackUrl);
3809
+ }
3625
3810
  return {
3626
3811
  accessUrl: input.gateway.accessUrl,
3627
3812
  accessUrls: urls,
3628
3813
  accessUrlSource: input.gateway.accessUrlSource,
3814
+ accessUrlReachable: true,
3815
+ disabledAccessUrls,
3629
3816
  };
3630
3817
  }
3631
- if (input.fallbackUrl) {
3818
+ if (input.fallbackUrl && fallbackReachable) {
3632
3819
  return {
3633
3820
  accessUrl: input.fallbackUrl,
3634
3821
  accessUrls: [input.fallbackUrl],
3635
3822
  accessUrlSource: input.fallbackSource,
3823
+ accessUrlReachable: true,
3824
+ disabledAccessUrls,
3636
3825
  };
3637
3826
  }
3638
- return { accessUrls: [] };
3827
+ if (input.fallbackUrl && !fallbackReachable) {
3828
+ if (!disabledAccessUrls.includes(input.fallbackUrl)) {
3829
+ disabledAccessUrls.push(input.fallbackUrl);
3830
+ }
3831
+ }
3832
+ return {
3833
+ accessUrls: [],
3834
+ accessUrlReachable: false,
3835
+ disabledAccessUrls,
3836
+ };
3639
3837
  }
3640
3838
 
3641
3839
 
@@ -7892,6 +8090,8 @@ const MANAGE_GATEWAY_ACTIONS = [
7892
8090
  "createRoute",
7893
8091
  "updateRoute",
7894
8092
  "deleteRoute",
8093
+ "enableRoute",
8094
+ "disableRoute",
7895
8095
  "bindCustomDomain",
7896
8096
  "deleteCustomDomain",
7897
8097
  "enableService",
@@ -7964,6 +8164,7 @@ function registerGatewayTools(server) {
7964
8164
  Domain: route.Domain,
7965
8165
  Path: route.Path,
7966
8166
  IsDefault: route.IsDefault,
8167
+ Enable: typeof route.Enable === "boolean" ? route.Enable : undefined,
7967
8168
  UpstreamResourceType: route.UpstreamResourceType,
7968
8169
  UpstreamResourceName: route.UpstreamResourceName,
7969
8170
  }));
@@ -7974,16 +8175,26 @@ function registerGatewayTools(server) {
7974
8175
  Domain: route.Domain,
7975
8176
  Path: route.Path,
7976
8177
  IsDefault: route.IsDefault,
8178
+ Enable: typeof route.Enable === "boolean" ? route.Enable : undefined,
7977
8179
  UpstreamResourceType: route.UpstreamResourceType,
7978
8180
  UpstreamResourceName: route.UpstreamResourceName,
7979
8181
  }));
7980
- const envelope = (0, gateway_access_urls_js_1.toAccessUrlEnvelope)((0, gateway_access_urls_js_1.rankGatewayAccessUrls)(candidates));
8182
+ const disabledAccessUrls = (0, gateway_access_urls_js_1.rankGatewayAccessUrls)(candidates, {
8183
+ includeDisabled: true,
8184
+ })
8185
+ .filter((item) => !item.enabled)
8186
+ .map((item) => item.url);
8187
+ const envelope = (0, gateway_access_urls_js_1.toAccessUrlEnvelope)((0, gateway_access_urls_js_1.rankGatewayAccessUrls)(candidates), disabledAccessUrls);
7981
8188
  return {
7982
8189
  ...(envelope.accessUrl ? { accessUrl: envelope.accessUrl } : {}),
7983
8190
  accessUrls: envelope.accessUrls,
7984
8191
  ...(envelope.accessUrlSource
7985
8192
  ? { accessUrlSource: envelope.accessUrlSource }
7986
8193
  : {}),
8194
+ accessUrlReachable: Boolean(envelope.accessUrl),
8195
+ ...(disabledAccessUrls.length > 0
8196
+ ? { disabledAccessUrls, routeDisabled: true }
8197
+ : {}),
7987
8198
  };
7988
8199
  };
7989
8200
  const pickUsableDefaultDomain = (domains) => {
@@ -8073,6 +8284,15 @@ function registerGatewayTools(server) {
8073
8284
  : input.enablePathTransmission !== undefined
8074
8285
  ? input.enablePathTransmission
8075
8286
  : undefined;
8287
+ // Route enable: prefer route.enable. For updateRoute only, top-level
8288
+ // enable also maps to Route.Enable (enableService/authSwitch use the
8289
+ // same field name but different actions).
8290
+ const routeEnable = input.route?.enable !== undefined
8291
+ ? input.route.enable
8292
+ : input.action === "updateRoute" &&
8293
+ typeof input.enable === "boolean"
8294
+ ? input.enable
8295
+ : undefined;
8076
8296
  const route = {
8077
8297
  Path: path,
8078
8298
  UpstreamResourceType: upstreamResourceType,
@@ -8082,6 +8302,9 @@ function registerGatewayTools(server) {
8082
8302
  if (enablePathTransmission !== undefined) {
8083
8303
  route.EnablePathTransmission = enablePathTransmission;
8084
8304
  }
8305
+ if (routeEnable !== undefined) {
8306
+ route.Enable = routeEnable;
8307
+ }
8085
8308
  return {
8086
8309
  EnvId: await resolveEnvId(),
8087
8310
  Domain: {
@@ -8095,9 +8318,46 @@ function registerGatewayTools(server) {
8095
8318
  upstreamResourceName,
8096
8319
  enableAuth,
8097
8320
  enablePathTransmission,
8321
+ enable: routeEnable,
8098
8322
  },
8099
8323
  };
8100
8324
  };
8325
+ const resolveExistingRouteForToggle = async (input) => {
8326
+ const routePath = input.routePath ?? input.path;
8327
+ if (!routePath) {
8328
+ throw new Error("action=enableRoute/disableRoute 时必须提供 path 或 route.path(例如 \"/\" 或 \"/api\")");
8329
+ }
8330
+ const normalizedPath = normalizeAccessPath(routePath);
8331
+ const preferredDomain = input.domain;
8332
+ const result = await listHttpServiceRoutes(preferredDomain ? { domain: preferredDomain } : undefined);
8333
+ const upstreamName = input.routeServiceName ?? input.targetName;
8334
+ const matches = flattenRoutes(result).filter((item) => {
8335
+ if (preferredDomain &&
8336
+ item.Domain !== preferredDomain) {
8337
+ return false;
8338
+ }
8339
+ if (normalizeAccessPath(String(item.Path ?? "")) !== normalizedPath) {
8340
+ return false;
8341
+ }
8342
+ if (upstreamName && item.UpstreamResourceName !== upstreamName) {
8343
+ return false;
8344
+ }
8345
+ return true;
8346
+ });
8347
+ if (matches.length === 0) {
8348
+ throw new Error(`未找到路径 ${normalizedPath} 的路由` +
8349
+ (preferredDomain ? `(域名 ${preferredDomain})` : "") +
8350
+ (upstreamName ? `(上游 ${upstreamName})` : "") +
8351
+ "。请先用 queryGateway(action=\"listRoutes\") 确认 Domain / Path,再调用 enableRoute/disableRoute。" +
8352
+ "关闭静态托管默认域名(*.tcloudbaseapp.com)时,请显式传 domain=该 STATIC_STORE IsDefault 域名,并通常 path=\"/\"。");
8353
+ }
8354
+ if (matches.length > 1) {
8355
+ const domains = [...new Set(matches.map((item) => item.Domain))];
8356
+ throw new Error(`路径 ${normalizedPath} 匹配到 ${matches.length} 条路由(域名:${domains.join(", ")})。` +
8357
+ "请补充 domain(必要时再加 targetName/route.serviceName)精确定位后再 enableRoute/disableRoute。");
8358
+ }
8359
+ return matches[0];
8360
+ };
8101
8361
  const routeMutationNextActions = (targetName) => [
8102
8362
  {
8103
8363
  tool: "queryGateway",
@@ -8301,6 +8561,10 @@ function registerGatewayTools(server) {
8301
8561
  Domain: payload.Domain,
8302
8562
  });
8303
8563
  (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, result);
8564
+ const routeEnabled = payload.resolved.enable !== false;
8565
+ const accessUrl = routeEnabled
8566
+ ? `https://${payload.resolved.domain}${payload.resolved.path}`
8567
+ : undefined;
8304
8568
  return buildEnvelope({
8305
8569
  action: input.action,
8306
8570
  model: "httpServiceRoute",
@@ -8310,13 +8574,31 @@ function registerGatewayTools(server) {
8310
8574
  upstreamResourceName: payload.resolved.upstreamResourceName,
8311
8575
  auth: payload.resolved.enableAuth ?? null,
8312
8576
  enablePathTransmission: payload.resolved.enablePathTransmission ?? null,
8313
- accessUrl: `https://${payload.resolved.domain}${payload.resolved.path}`,
8314
- accessUrls: [`https://${payload.resolved.domain}${payload.resolved.path}`],
8577
+ enable: payload.resolved.enable ?? null,
8578
+ ...(accessUrl
8579
+ ? {
8580
+ accessUrl,
8581
+ accessUrls: [accessUrl],
8582
+ accessUrlReachable: true,
8583
+ }
8584
+ : {
8585
+ accessUrls: [],
8586
+ accessUrlReachable: false,
8587
+ routeDisabled: true,
8588
+ disabledAccessUrls: [
8589
+ `https://${payload.resolved.domain}${payload.resolved.path}`,
8590
+ ],
8591
+ }),
8315
8592
  accessUrlSource: input.domain && input.domain === payload.resolved.domain
8316
8593
  ? "gateway.custom"
8317
8594
  : "gateway.default",
8318
8595
  raw: result,
8319
8596
  }, `HTTP 路由更新成功(${payload.resolved.domain}${payload.resolved.path}` +
8597
+ (payload.resolved.enable === false
8598
+ ? ",路由已禁用 Enable=false"
8599
+ : payload.resolved.enable === true
8600
+ ? ",路由已启用 Enable=true"
8601
+ : "") +
8320
8602
  (payload.resolved.enablePathTransmission === true
8321
8603
  ? ",路径透传=开启"
8322
8604
  : payload.resolved.enablePathTransmission === false
@@ -8324,6 +8606,102 @@ function registerGatewayTools(server) {
8324
8606
  : "") +
8325
8607
  `)`, routeMutationNextActions(payload.resolved.upstreamResourceName));
8326
8608
  }
8609
+ case "enableRoute":
8610
+ case "disableRoute": {
8611
+ const enable = input.action === "enableRoute";
8612
+ const existing = await resolveExistingRouteForToggle({
8613
+ path: input.path,
8614
+ routePath: input.route?.path,
8615
+ domain: input.domain,
8616
+ targetName: input.targetName,
8617
+ routeServiceName: input.route?.serviceName,
8618
+ });
8619
+ const upstreamResourceType = resolveUpstreamResourceType({
8620
+ upstreamResourceType: input.upstreamResourceType ??
8621
+ existing.UpstreamResourceType,
8622
+ routeUpstreamResourceType: input.route?.upstreamResourceType,
8623
+ });
8624
+ const upstreamResourceName = input.route?.serviceName ??
8625
+ input.targetName ??
8626
+ String(existing.UpstreamResourceName ?? "");
8627
+ if (!upstreamResourceName) {
8628
+ throw new Error(`action=${input.action} 无法解析上游资源名;请用 queryGateway(listRoutes) 确认后传入 targetName 或 route.serviceName。`);
8629
+ }
8630
+ const normalizedPath = normalizeAccessPath(String(existing.Path ?? input.route?.path ?? input.path ?? "/"));
8631
+ const domain = existing.Domain;
8632
+ const enableAuth = input.route?.auth !== undefined
8633
+ ? input.route.auth
8634
+ : input.auth !== undefined
8635
+ ? input.auth
8636
+ : existing.EnableAuth;
8637
+ const enablePathTransmission = input.route?.enablePathTransmission !== undefined
8638
+ ? input.route.enablePathTransmission
8639
+ : input.enablePathTransmission !== undefined
8640
+ ? input.enablePathTransmission
8641
+ : existing.EnablePathTransmission;
8642
+ const route = {
8643
+ Path: normalizedPath,
8644
+ UpstreamResourceType: upstreamResourceType,
8645
+ UpstreamResourceName: upstreamResourceName,
8646
+ Enable: enable,
8647
+ };
8648
+ if (enableAuth !== undefined) {
8649
+ route.EnableAuth = enableAuth;
8650
+ }
8651
+ if (enablePathTransmission !== undefined) {
8652
+ route.EnablePathTransmission = enablePathTransmission;
8653
+ }
8654
+ const cloudbase = await getManager();
8655
+ const result = await cloudbase.env.modifyHttpServiceRoute({
8656
+ EnvId: await resolveEnvId(),
8657
+ Domain: {
8658
+ Domain: domain,
8659
+ Routes: [route],
8660
+ },
8661
+ });
8662
+ (0, cloudbase_manager_js_1.logCloudBaseResult)(server.logger, result);
8663
+ const verb = enable ? "启用" : "禁用";
8664
+ const accessUrl = enable
8665
+ ? `https://${domain}${normalizedPath}`
8666
+ : undefined;
8667
+ return buildEnvelope({
8668
+ action: input.action,
8669
+ model: "httpServiceRoute",
8670
+ domain,
8671
+ path: normalizedPath,
8672
+ upstreamResourceType,
8673
+ upstreamResourceName,
8674
+ enable,
8675
+ auth: enableAuth ?? null,
8676
+ enablePathTransmission: enablePathTransmission ?? null,
8677
+ ...(accessUrl
8678
+ ? {
8679
+ accessUrl,
8680
+ accessUrls: [accessUrl],
8681
+ accessUrlReachable: true,
8682
+ }
8683
+ : {
8684
+ accessUrls: [],
8685
+ accessUrlReachable: false,
8686
+ routeDisabled: true,
8687
+ disabledAccessUrls: [`https://${domain}${normalizedPath}`],
8688
+ }),
8689
+ accessUrlSource: existing.IsDefault
8690
+ ? "gateway.default"
8691
+ : "gateway.custom",
8692
+ raw: result,
8693
+ }, `HTTP 路由已${verb}(${domain}${normalizedPath},Enable=${enable})。` +
8694
+ (enable
8695
+ ? "启用后通常数秒到约 30 秒内可访问;请用 queryGateway(getRoute) 或探测 accessUrl 确认。"
8696
+ : "禁用后该路径将不可公网访问(GATEWAY_ROUTE_DISABLED);关闭静态托管默认域名(*.tcloudbaseapp.com)时,请确认 DomainType=STATIC_STORE 且 IsDefault=true。") +
8697
+ " 底层对应 tcb ModifyHTTPServiceRoute(不是 ModifyGatewayRoute)。", [
8698
+ {
8699
+ tool: "queryGateway",
8700
+ action: "getRoute",
8701
+ reason: `复核路由 Enable=${enable} 是否已生效`,
8702
+ },
8703
+ ]);
8704
+ }
8327
8705
  case "deleteRoute": {
8328
8706
  const routePath = input.route?.path ?? input.path;
8329
8707
  if (!routePath) {
@@ -8531,8 +8909,11 @@ function registerGatewayTools(server) {
8531
8909
  }, async (input) => withEnvelope(() => handleQueryGateway(input)));
8532
8910
  server.registerTool?.("manageGateway", {
8533
8911
  title: "管理 CloudBase 网关",
8534
- description: "CloudBase HTTP 网关统一写入口(Domain/Route)。createRoute/updateRoute/deleteRoute 把域名下的 path 转到上游;未传 domain 时用 DomainType=HTTPSERVICE 的 IsDefault 默认 HTTP 域名(形如 *.{region}.app.tcloudbase.com),不会使用静态托管 CDN 域名(*.tcloudbaseapp.com,DomainType=STATIC_STORE)。" +
8912
+ description: "CloudBase HTTP 网关统一写入口(Domain/Route)。createRoute/updateRoute/deleteRoute 把域名下的 path 转到上游;" +
8913
+ "enableRoute/disableRoute 启用或禁用已有路由(底层 ModifyHTTPServiceRoute 的 Routes[].Enable,不是 ModifyGatewayRoute)。" +
8914
+ "未传 domain 时用 DomainType=HTTPSERVICE 的 IsDefault 默认 HTTP 域名(形如 *.{region}.app.tcloudbase.com),不会使用静态托管 CDN 域名(*.tcloudbaseapp.com,DomainType=STATIC_STORE)。" +
8535
8915
  "这是网关默认域上的路径路由,不是 STATIC_STORE 上游绑定;STATIC_STORE 上游必须显式传 upstreamResourceType=STATIC_STORE。" +
8916
+ "关闭静态托管默认域名(*.tcloudbaseapp.com):先 queryGateway(listRoutes) 找到 DomainType=STATIC_STORE 且 IsDefault=true 的 domain,再 manageGateway(action=\"disableRoute\", domain=该域名, path=\"/\");勿用 manageHosting。" +
8536
8917
  "创建后可用 queryGateway(action=\"listRoutes\") 核对 Domain / DomainType / Path / UpstreamResourceType。" +
8537
8918
  "上游类型只用一个参数 upstreamResourceType(也可写在 route.upstreamResourceType,route 优先):" +
8538
8919
  "WEB_SCF=HTTP云函数,SCF=Event云函数,CBR=云托管,STATIC_STORE=静态托管,LH=轻量应用服务器;" +
@@ -8545,9 +8926,12 @@ function registerGatewayTools(server) {
8545
8926
  inputSchema: {
8546
8927
  action: zod_1.z
8547
8928
  .enum(MANAGE_GATEWAY_ACTIONS)
8548
- .describe("写操作:createRoute/updateRoute/deleteRoute 管理路由;bindCustomDomain/deleteCustomDomain 管理自定义域名;" +
8929
+ .describe("写操作:createRoute/updateRoute/deleteRoute 管理路由;enableRoute/disableRoute 启用或禁用已有路由(需 path,建议显式传 domain);" +
8930
+ "bindCustomDomain/deleteCustomDomain 管理自定义域名;" +
8549
8931
  "enableService/authSwitch 开关 HTTP 网关总开关与访问鉴权(需配合 enable 参数)。" +
8550
- "createRoute/updateRoute 必须提供 upstreamResourceType。" +
8932
+ "createRoute/updateRoute 必须提供 upstreamResourceType;enableRoute/disableRoute 会先 listRoutes 定位已有路由,通常不必重填上游。" +
8933
+ "updateRoute 也可传 enable/route.enable 直接改 Routes[].Enable。" +
8934
+ "关闭 *.tcloudbaseapp.com 默认静态托管域:disableRoute + domain=该 STATIC_STORE IsDefault 域名 + path=\"/\"。" +
8551
8935
  "已有自定义域名时优先 createRoute(domain=已有域名) 实现访问,不必再次 bindCustomDomain / 传入 certificateId;" +
8552
8936
  "bindCustomDomain 仅用于首次绑定新域名(需 certificateId;可选 accessType=DIRECT|CDN|CUSTOM,CUSTOM 需 customCname;普通场景用默认 DIRECT)。" +
8553
8937
  "接入说明:https://docs.cloudbase.net/service/custom-domain"),
@@ -8596,16 +8980,25 @@ function registerGatewayTools(server) {
8596
8980
  .boolean()
8597
8981
  .optional()
8598
8982
  .describe("同顶层 enablePathTransmission。route 内设置时优先于顶层。"),
8983
+ enable: zod_1.z
8984
+ .boolean()
8985
+ .optional()
8986
+ .describe("路由级开关(Routes[].Enable / Route.Enable)。createRoute/updateRoute 可用:" +
8987
+ "enable=false 禁用该 Domain+Path(访问返回 GATEWAY_ROUTE_DISABLED);" +
8988
+ "enable=true 重新启用。updateRoute 也可用顶层 enable 表达同一语义;" +
8989
+ "也可用专用 action enableRoute/disableRoute。route.enable 优先于顶层 enable。"),
8599
8990
  })
8600
8991
  .optional()
8601
8992
  .describe("路由对象(可选写法)。例:云函数 {upstreamResourceType:\"WEB_SCF\",serviceName:\"fn\",path:\"/api\"};" +
8602
8993
  "云托管 {upstreamResourceType:\"CBR\",serviceName:\"svc\",path:\"/api\"};" +
8603
- "静态托管 {upstreamResourceType:\"STATIC_STORE\",serviceName:\"staticstore\",path:\"/\"}。"),
8994
+ "静态托管 {upstreamResourceType:\"STATIC_STORE\",serviceName:\"staticstore\",path:\"/\"};" +
8995
+ "禁用路由 {path:\"/\",enable:false}(配合 updateRoute,或直接用 disableRoute)。"),
8604
8996
  domain: zod_1.z
8605
8997
  .string()
8606
8998
  .optional()
8607
8999
  .describe("域名。省略时自动使用环境 DomainType=HTTPSERVICE 的 IsDefault 默认 HTTP 域名(*.{region}.app.tcloudbase.com),不会回退到静态托管 CDN 域名(*.tcloudbaseapp.com,DomainType=STATIC_STORE);也不是 STATIC_STORE 上游绑定。" +
8608
9000
  "可用 queryGateway(action=\"listRoutes\") 核对实际 Domain / DomainType。" +
9001
+ "enableRoute/disableRoute 操作 *.tcloudbaseapp.com 时必须显式传入该域名。" +
8609
9002
  "已有自定义域名时请显式传入该域名并 createRoute/updateRoute/deleteRoute,即可实现自定义域名访问且无需证书 ID;" +
8610
9003
  "仅 bindCustomDomain 时表示要新绑定的域名。"),
8611
9004
  certificateId: zod_1.z
@@ -8628,9 +9021,11 @@ function registerGatewayTools(server) {
8628
9021
  enable: zod_1.z
8629
9022
  .boolean()
8630
9023
  .optional()
8631
- .describe("开关目标状态(enableService / authSwitch 必填):enable=true 开启,enable=false 关闭。" +
8632
- "bindCustomDomain 可选:enable=false 表示绑定后禁用域名(默认启用)。" +
8633
- "省略或非布尔值会返回参数错误。"),
9024
+ .describe("开关目标状态:enableService / authSwitch 必填(true 开启 / false 关闭);" +
9025
+ "bindCustomDomain 可选:enable=false 表示绑定后禁用域名(默认启用);" +
9026
+ "updateRoute 可选:映射到 Routes[].Enable(也可用 route.enable,route 优先;" +
9027
+ "也可用专用 action enableRoute/disableRoute)。" +
9028
+ "省略或非布尔值在 enableService/authSwitch 会返回参数错误。"),
8634
9029
  },
8635
9030
  annotations: {
8636
9031
  readOnlyHint: false,
@@ -11255,7 +11650,7 @@ class TelemetryReporter {
11255
11650
  const arch = os_1.default.arch(); // 系统架构
11256
11651
  // Build-time injected version; guard for vitest / non-webpack runtimes.
11257
11652
  const mcpVersion = process.env.npm_package_version ||
11258
- ( true ? "2.25.10" : 0);
11653
+ ( true ? "2.26.0" : 0);
11259
11654
  return {
11260
11655
  userAgent: `${osType} ${osRelease} ${arch} ${nodeVersion} CloudBase-MCP/${mcpVersion}`,
11261
11656
  deviceId: this.deviceId,
@@ -16790,7 +17185,7 @@ function registerSetupTools(server) {
16790
17185
  server.registerTool("downloadTemplate", {
16791
17186
  title: "下载项目模板",
16792
17187
  description: `自动下载并部署CloudBase项目模板。
16793
- **Note**: Call this tool when the user requests to create a new project using a CloudBase template.\n\n支持的模板:\n- react: React + CloudBase 全栈应用模板\n- vue: Vue + CloudBase 全栈应用模板\n- miniprogram: 微信小程序 + 云开发模板 \n- uniapp: UniApp + CloudBase 跨端应用模板\n- rules: 只包含AI编辑器配置文件(包含Cursor、WindSurf、CodeBuddy等所有主流编辑器配置),适合在已有项目中补充AI编辑器配置\n\n支持的IDE类型:\n- all: 下载所有IDE配置\n- cursor: Cursor AI编辑器\n- 其他IDE类型见下方列表\n\n注意:如果未传入 ide 参数且无法从环境变量检测到 IDE,将提示错误并要求传入 ide 参数\n- windsurf: WindSurf AI编辑器\n- codebuddy: CodeBuddy AI编辑器\n- claude-code: Claude Code AI编辑器\n- cline: Cline AI编辑器\n- gemini-cli: Gemini CLI\n- opencode: OpenCode AI编辑器\n- qwen-code: 通义灵码\n- baidu-comate: 百度Comate\n- openai-codex-cli: OpenAI Codex CLI\n- augment-code: Augment Code\n- github-copilot: GitHub Copilot\n- roocode: RooCode AI编辑器\n- tongyi-lingma: 通义灵码\n- trae: Trae AI编辑器\n- qoder: Qoder AI编辑器\n- antigravity: Google Antigravity AI编辑器\n- vscode: Visual Studio Code\n- kiro: Kiro AI编辑器\n- aider: Aider AI编辑器\n\n特别说明:\n- rules 模板会自动包含当前 mcp 版本号信息(版本号:${ true ? "2.25.10" : 0}),便于后续维护和版本追踪\n- 下载 rules 模板时,如果项目中已存在 README.md 文件,系统会自动保护该文件不被覆盖(除非设置 overwrite=true)`,
17188
+ **Note**: Call this tool when the user requests to create a new project using a CloudBase template.\n\n支持的模板:\n- react: React + CloudBase 全栈应用模板\n- vue: Vue + CloudBase 全栈应用模板\n- miniprogram: 微信小程序 + 云开发模板 \n- uniapp: UniApp + CloudBase 跨端应用模板\n- rules: 只包含AI编辑器配置文件(包含Cursor、WindSurf、CodeBuddy等所有主流编辑器配置),适合在已有项目中补充AI编辑器配置\n\n支持的IDE类型:\n- all: 下载所有IDE配置\n- cursor: Cursor AI编辑器\n- 其他IDE类型见下方列表\n\n注意:如果未传入 ide 参数且无法从环境变量检测到 IDE,将提示错误并要求传入 ide 参数\n- windsurf: WindSurf AI编辑器\n- codebuddy: CodeBuddy AI编辑器\n- claude-code: Claude Code AI编辑器\n- cline: Cline AI编辑器\n- gemini-cli: Gemini CLI\n- opencode: OpenCode AI编辑器\n- qwen-code: 通义灵码\n- baidu-comate: 百度Comate\n- openai-codex-cli: OpenAI Codex CLI\n- augment-code: Augment Code\n- github-copilot: GitHub Copilot\n- roocode: RooCode AI编辑器\n- tongyi-lingma: 通义灵码\n- trae: Trae AI编辑器\n- qoder: Qoder AI编辑器\n- antigravity: Google Antigravity AI编辑器\n- vscode: Visual Studio Code\n- kiro: Kiro AI编辑器\n- aider: Aider AI编辑器\n\n特别说明:\n- rules 模板会自动包含当前 mcp 版本号信息(版本号:${ true ? "2.26.0" : 0}),便于后续维护和版本追踪\n- 下载 rules 模板时,如果项目中已存在 README.md 文件,系统会自动保护该文件不被覆盖(除非设置 overwrite=true)`,
16794
17189
  inputSchema: {
16795
17190
  template: zod_1.z
16796
17191
  .enum(["react", "vue", "miniprogram", "uniapp", "rules"])
@@ -18064,16 +18459,19 @@ function isDirectoryUploadTarget(localPath, cloudPath) {
18064
18459
  return true;
18065
18460
  return path_1.default.posix.extname(normalizedCloudPath) === '';
18066
18461
  }
18067
- function buildHostingAccessUrl(staticDomain, cloudPath, localPath) {
18068
- if (!staticDomain)
18069
- return '';
18462
+ function hostingAccessPathname(cloudPath, localPath) {
18070
18463
  const normalizedCloudPath = (cloudPath ?? '').trim().replace(/^\/+|\/+$/g, '');
18071
18464
  const isDirectory = isDirectoryUploadTarget(localPath, cloudPath);
18072
18465
  if (!normalizedCloudPath) {
18073
- return `https://${staticDomain}/`;
18466
+ return '/';
18074
18467
  }
18075
18468
  const pathname = isDirectory ? `${normalizedCloudPath}/` : normalizedCloudPath;
18076
- return `https://${staticDomain}/${pathname}`;
18469
+ return `/${pathname}`;
18470
+ }
18471
+ function buildHostingAccessUrl(staticDomain, cloudPath, localPath) {
18472
+ if (!staticDomain)
18473
+ return '';
18474
+ return `https://${staticDomain}${hostingAccessPathname(cloudPath, localPath)}`;
18077
18475
  }
18078
18476
  function buildUploadErrorMessage(error, localPath) {
18079
18477
  const baseMessage = error instanceof Error ? error.message : String(error);
@@ -18254,7 +18652,7 @@ function buildDomainMutationResult(params) {
18254
18652
  message: `静态托管域名${actionLabel}请求已提交。域名配置、证书校验和边缘侧传播通常需要 30 秒到 10 分钟,请继续调用 queryHosting(action="domainStatus") 确认最终结果。`,
18255
18653
  };
18256
18654
  }
18257
- async function getHostingWebsiteConfig(cloudbase, logger) {
18655
+ async function getHostingWebsiteConfig(cloudbase, logger, cloudBaseOptions) {
18258
18656
  const websiteConfig = await cloudbase.hosting.getWebsiteConfig();
18259
18657
  (0, cloudbase_manager_js_1.logCloudBaseResult)(logger, websiteConfig);
18260
18658
  const hostingResult = {
@@ -18271,6 +18669,27 @@ async function getHostingWebsiteConfig(cloudbase, logger) {
18271
18669
  catch {
18272
18670
  // Ignore enrichment failures and return the website config as-is.
18273
18671
  }
18672
+ try {
18673
+ const cdnDomain = typeof hostingResult.CdnDomain === 'string' ? hostingResult.CdnDomain : '';
18674
+ if (cdnDomain && typeof cloudbase.env?.describeHttpServiceRoute === 'function') {
18675
+ const envId = await (0, cloudbase_manager_js_1.getEnvId)(cloudBaseOptions);
18676
+ const routeResult = await cloudbase.env.describeHttpServiceRoute({
18677
+ EnvId: envId,
18678
+ Limit: 1000,
18679
+ });
18680
+ const routes = (0, gateway_access_urls_js_1.flattenHttpServiceRoutes)(routeResult);
18681
+ const reachable = (0, gateway_access_urls_js_1.isDomainPathReachableViaGateway)(routes, cdnDomain, '/');
18682
+ hostingResult.staticDomainRouteEnabled = reachable;
18683
+ hostingResult.accessUrlReachable = reachable !== false;
18684
+ if (reachable === false) {
18685
+ hostingResult.routeDisabled = true;
18686
+ hostingResult.disabledAccessUrls = [`https://${cdnDomain}/`];
18687
+ }
18688
+ }
18689
+ }
18690
+ catch {
18691
+ // Gateway route lookup is best-effort enrichment only.
18692
+ }
18274
18693
  return hostingResult;
18275
18694
  }
18276
18695
  async function resolveHostingStaticDomain(cloudbase, logger) {
@@ -18354,7 +18773,7 @@ function registerHostingTools(server) {
18354
18773
  const cloudbase = await getManager();
18355
18774
  switch (input.action) {
18356
18775
  case 'websiteConfig': {
18357
- const websiteConfig = await getHostingWebsiteConfig(cloudbase, server.logger);
18776
+ const websiteConfig = await getHostingWebsiteConfig(cloudbase, server.logger, cloudBaseOptions);
18358
18777
  return (0, tool_result_js_1.buildJsonToolResult)({
18359
18778
  success: true,
18360
18779
  data: {
@@ -18480,7 +18899,7 @@ function registerHostingTools(server) {
18480
18899
  });
18481
18900
  server.registerTool('manageHosting', {
18482
18901
  title: '管理 CloudBase 静态托管',
18483
- description: '管理 CloudBase 静态托管的变更操作。action=upload 上传本地构建产物到共享域名(域名格式:<envId>-<appId>.tcloudbaseapp.com/<cloudPath>);action=delete 删除托管文件或目录(必须 confirm=true);action=setWebsiteDocument 设置首页/错误页/路由规则;action=enableService 开通静态托管;action=bindDomain / unbindDomain / updateDomain 管理自定义域名;action=downloadFile / downloadDirectory 下载托管内容到本地。⚠️ 新项目部署优先使用 manageApps(部署到独立子域名),本工具适合已有老项目继续使用或作为 manageApps 的 fallback。manageApps 与 manageHosting 域名不同,切换会导致老链接失效。若任务只是查看配置、文件或域名状态,请改用 queryHosting。',
18902
+ description: '管理 CloudBase 静态托管的变更操作。action=upload 上传本地构建产物到共享域名(域名格式:<envId>-<appId>.tcloudbaseapp.com/<cloudPath>);action=delete 删除托管文件或目录(必须 confirm=true);action=setWebsiteDocument 设置首页/错误页/路由规则;action=enableService 开通静态托管;action=bindDomain / unbindDomain / updateDomain 管理自定义域名;action=downloadFile / downloadDirectory 下载托管内容到本地。⚠️ 本工具没有关闭默认域名(*.tcloudbaseapp.com)的 action;要禁用该默认公网域名,请用 manageGateway(action="disableRoute", domain=该 STATIC_STORE IsDefault 域名, path="/")(底层 ModifyHTTPServiceRoute,不是 ModifyGatewayRoute)。⚠️ 新项目部署优先使用 manageApps(部署到独立子域名),本工具适合已有老项目继续使用或作为 manageApps 的 fallback。manageApps 与 manageHosting 域名不同,切换会导致老链接失效。若任务只是查看配置、文件或域名状态,请改用 queryHosting。',
18484
18903
  inputSchema: manageHostingInputSchema,
18485
18904
  annotations: {
18486
18905
  readOnlyHint: false,
@@ -18520,7 +18939,24 @@ function registerHostingTools(server) {
18520
18939
  const cdnFromStore = (store.CdnDomain ?? store.StaticDomain);
18521
18940
  const staticDomain = cdnFromStore || await resolveHostingStaticDomain(cloudbase, server.logger);
18522
18941
  const fallbackAccessUrl = buildHostingAccessUrl(staticDomain, input.cloudPath, input.localPath);
18942
+ const accessPathname = hostingAccessPathname(input.cloudPath, input.localPath);
18523
18943
  const envId = await (0, cloudbase_manager_js_1.getEnvId)(cloudBaseOptions);
18944
+ const getGatewayManager = async () => {
18945
+ const manager = await getManager();
18946
+ if (!manager) {
18947
+ throw new Error("cloudbase manager unavailable");
18948
+ }
18949
+ return manager;
18950
+ };
18951
+ const allGatewayRoutes = await (0, gateway_access_urls_js_1.resolveAllGatewayRoutes)({
18952
+ envId,
18953
+ getManager: getGatewayManager,
18954
+ });
18955
+ const staticDomainRouteEnabled = staticDomain
18956
+ ? (0, gateway_access_urls_js_1.isDomainPathReachableViaGateway)(allGatewayRoutes, staticDomain, accessPathname)
18957
+ : null;
18958
+ // null = no matching gateway route info → keep legacy fallback.
18959
+ const fallbackReachable = staticDomainRouteEnabled !== false;
18524
18960
  const gatewayCandidates = Array.from(new Set([
18525
18961
  store.StaticStoreName,
18526
18962
  store.Name,
@@ -18530,34 +18966,38 @@ function registerHostingTools(server) {
18530
18966
  ]
18531
18967
  .map((item) => (typeof item === "string" ? item.trim() : ""))
18532
18968
  .filter(Boolean)));
18533
- let accessUrl = fallbackAccessUrl;
18969
+ let accessUrl = fallbackReachable ? fallbackAccessUrl : "";
18534
18970
  let accessUrls = accessUrl ? [accessUrl] : [];
18535
18971
  let accessUrlSource = accessUrl
18536
18972
  ? "hosting.staticDomain"
18537
18973
  : undefined;
18974
+ let accessUrlReachable = Boolean(accessUrl);
18975
+ let disabledAccessUrls = [];
18976
+ if (fallbackAccessUrl && !fallbackReachable) {
18977
+ disabledAccessUrls.push(fallbackAccessUrl);
18978
+ }
18538
18979
  for (const upstreamName of gatewayCandidates) {
18539
18980
  const gateway = await (0, gateway_access_urls_js_1.resolveGatewayAccessUrls)({
18540
18981
  envId,
18541
18982
  upstreamResourceName: upstreamName,
18542
18983
  upstreamResourceTypes: ["STATIC_STORE"],
18543
- getManager: async () => {
18544
- const manager = await getManager();
18545
- if (!manager) {
18546
- throw new Error("cloudbase manager unavailable");
18547
- }
18548
- return manager;
18549
- },
18984
+ getManager: getGatewayManager,
18550
18985
  });
18551
18986
  const preferred = (0, gateway_access_urls_js_1.preferGatewayOrFallback)({
18552
18987
  gateway,
18553
18988
  fallbackUrl: fallbackAccessUrl || undefined,
18554
18989
  fallbackSource: "hosting.staticDomain",
18990
+ fallbackReachable,
18555
18991
  });
18556
- if (preferred.accessUrl) {
18557
- accessUrl = preferred.accessUrl;
18992
+ if (preferred.accessUrl || preferred.disabledAccessUrls.length > 0) {
18993
+ accessUrl = preferred.accessUrl ?? "";
18558
18994
  accessUrls = preferred.accessUrls;
18559
18995
  accessUrlSource = preferred.accessUrlSource;
18560
- break;
18996
+ accessUrlReachable = preferred.accessUrlReachable;
18997
+ disabledAccessUrls = preferred.disabledAccessUrls;
18998
+ if (preferred.accessUrl) {
18999
+ break;
19000
+ }
18561
19001
  }
18562
19002
  }
18563
19003
  try {
@@ -18573,18 +19013,31 @@ function registerHostingTools(server) {
18573
19013
  projectName = path_1.default.basename(input.localPath);
18574
19014
  }
18575
19015
  }
18576
- await (0, notification_js_1.sendDeployNotification)(server, {
18577
- deployType: 'hosting',
18578
- url: accessUrl,
18579
- projectId: envId,
18580
- projectName,
18581
- consoleUrl: `https://tcb.cloud.tencent.com/dev?envId=${envId}#/static-hosting`,
18582
- });
19016
+ // sendDeployNotification requires a concrete url; skip when no reachable accessUrl
19017
+ if (accessUrl) {
19018
+ await (0, notification_js_1.sendDeployNotification)(server, {
19019
+ deployType: 'hosting',
19020
+ url: accessUrl,
19021
+ projectId: envId,
19022
+ projectName,
19023
+ consoleUrl: `https://tcb.cloud.tencent.com/dev?envId=${envId}#/static-hosting`,
19024
+ });
19025
+ }
18583
19026
  }
18584
19027
  catch {
18585
19028
  // Notification failure should not block uploads.
18586
19029
  }
18587
19030
  const uploadPrefix = input.cloudPath ?? input.files?.[0]?.cloudPath ?? '';
19031
+ const routeDisabled = staticDomainRouteEnabled === false;
19032
+ let message = '静态托管文件上传成功。若需要校验上传结果,请继续调用 queryHosting(action="findFiles") 或 queryHosting(action="listFiles")。';
19033
+ if (routeDisabled && accessUrlReachable) {
19034
+ message =
19035
+ '静态托管文件上传成功。默认静态托管域名的网关路由已禁用(访问会返回 GATEWAY_ROUTE_DISABLED),已改用其他可达域名作为 accessUrl。可用 manageGateway(action="updateRoute", route.enable=true) 重新启用默认域路由。';
19036
+ }
19037
+ else if (routeDisabled && !accessUrlReachable) {
19038
+ message =
19039
+ '静态托管文件上传成功,但默认静态托管域名的网关路由已禁用(GATEWAY_ROUTE_DISABLED),当前没有可达的 accessUrl。请用 manageGateway(action="updateRoute", domain="<静态域名>", path="/", upstreamResourceType="STATIC_STORE", targetName="staticstore", route.enable=true) 启用路由,或改用已启用的自定义域名 / CloudBase Sites 域名访问。';
19040
+ }
18588
19041
  return (0, tool_result_js_1.buildJsonToolResult)({
18589
19042
  success: true,
18590
19043
  data: {
@@ -18594,13 +19047,19 @@ function registerHostingTools(server) {
18594
19047
  files: input.files ?? [],
18595
19048
  ignore: input.ignore,
18596
19049
  staticDomain,
18597
- accessUrl,
19050
+ staticDomainRouteEnabled,
19051
+ accessUrl: accessUrl || undefined,
18598
19052
  accessUrls,
18599
19053
  accessUrlSource,
19054
+ accessUrlReachable,
19055
+ ...(disabledAccessUrls.length > 0
19056
+ ? { disabledAccessUrls }
19057
+ : {}),
19058
+ ...(routeDisabled ? { routeDisabled: true } : {}),
18600
19059
  result,
18601
19060
  nextActions: uploadPrefix ? [buildFindFilesNextStep(uploadPrefix.replace(/^\/+/, ''))] : undefined,
18602
19061
  },
18603
- message: '静态托管文件上传成功。若需要校验上传结果,请继续调用 queryHosting(action="findFiles") 或 queryHosting(action="listFiles")。',
19062
+ message,
18604
19063
  });
18605
19064
  }
18606
19065
  case 'delete': {
@@ -18803,6 +19262,7 @@ function registerHostingTools(server) {
18803
19262
  Object.defineProperty(exports, "__esModule", ({ value: true }));
18804
19263
  exports.buildVerificationUriComplete = buildVerificationUriComplete;
18805
19264
  exports.buildDeviceAuthChallengePayload = buildDeviceAuthChallengePayload;
19265
+ exports.getCloudBaseApiKeyFromEnv = getCloudBaseApiKeyFromEnv;
18806
19266
  exports.getAuthProgressStateSync = getAuthProgressStateSync;
18807
19267
  exports.getAuthProgressState = getAuthProgressState;
18808
19268
  exports.resolveAuthOptions = resolveAuthOptions;
@@ -18904,16 +19364,24 @@ function updateAuthProgressState(partial) {
18904
19364
  });
18905
19365
  return getAuthProgressStateSync();
18906
19366
  }
19367
+ /**
19368
+ * Resolve CloudBase API Key from env.
19369
+ * Prefer CLOUDBASE_API_KEY; fall back to CLOUDBASE_APIKEY (js-sdk / node-sdk convention).
19370
+ */
19371
+ function getCloudBaseApiKeyFromEnv() {
19372
+ return process.env.CLOUDBASE_API_KEY || process.env.CLOUDBASE_APIKEY || undefined;
19373
+ }
18907
19374
  function normalizeLoginStateFromEnvVars(options) {
18908
19375
  if (options?.ignoreEnvVars) {
18909
19376
  return null;
18910
19377
  }
18911
- // 优先检查 CloudBase API Key(优先级高于 TENCENTCLOUD_*)
18912
- const { CLOUDBASE_API_KEY, CLOUDBASE_ENV_ID: CLOUDBASE_ENV, } = process.env;
18913
- if (CLOUDBASE_API_KEY && CLOUDBASE_ENV) {
19378
+ // Prefer CloudBase API Key over TENCENTCLOUD_*
19379
+ const apiKey = getCloudBaseApiKeyFromEnv();
19380
+ const CLOUDBASE_ENV = process.env.CLOUDBASE_ENV_ID;
19381
+ if (apiKey && CLOUDBASE_ENV) {
18914
19382
  return {
18915
19383
  _type: 'api_key',
18916
- apiKey: CLOUDBASE_API_KEY,
19384
+ apiKey,
18917
19385
  envId: CLOUDBASE_ENV,
18918
19386
  };
18919
19387
  }
@@ -19085,7 +19553,7 @@ async function peekLoginState(options) {
19085
19553
  }
19086
19554
  async function ensureLogin(options) {
19087
19555
  (0, logger_js_1.debug)("TENCENTCLOUD_SECRETID", { hasSecretId: !!process.env.TENCENTCLOUD_SECRETID });
19088
- (0, logger_js_1.debug)("CLOUDBASE_API_KEY", { hasApiKey: !!process.env.CLOUDBASE_API_KEY });
19556
+ (0, logger_js_1.debug)("CLOUDBASE_API_KEY", { hasApiKey: !!getCloudBaseApiKeyFromEnv() });
19089
19557
  const loginState = await peekLoginState({
19090
19558
  ignoreEnvVars: options?.ignoreEnvVars,
19091
19559
  });