UNPKG

@cloudbase/cloudbase-mcp

Version:

CloudBase MCP Server — operate Tencent CloudBase (database, auth, functions, storage, hosting) from AI coding tools via the Model Context Protocol. Part of CloudBase AI Toolkit.

1,202 lines (1,198 loc) 1.04 MB
import * as __WEBPACK_EXTERNAL_MODULE_lockfile__ from "lockfile"; import * as __WEBPACK_EXTERNAL_MODULE_open__ from "open"; import * as __WEBPACK_EXTERNAL_MODULE__cloudbase_toolbox_034e5bd0__ from "@cloudbase/toolbox"; import * as __WEBPACK_EXTERNAL_MODULE__modelcontextprotocol_sdk_types_js_70eb1363__ from "@modelcontextprotocol/sdk/types.js"; import * as __WEBPACK_EXTERNAL_MODULE__modelcontextprotocol_sdk_server_stdio_js_25848778__ from "@modelcontextprotocol/sdk/server/stdio.js"; import * as __WEBPACK_EXTERNAL_MODULE_path__ from "path"; import * as __WEBPACK_EXTERNAL_MODULE_express__ from "express"; import * as __WEBPACK_EXTERNAL_MODULE_adm_zip_aa3527d5__ from "adm-zip"; import * as __WEBPACK_EXTERNAL_MODULE_zod__ from "zod"; import * as __WEBPACK_EXTERNAL_MODULE_winston_daily_rotate_file_69928d76__ from "winston-daily-rotate-file"; import * as __WEBPACK_EXTERNAL_MODULE_http__ from "http"; import * as __WEBPACK_EXTERNAL_MODULE_https__ from "https"; import * as __WEBPACK_EXTERNAL_MODULE_net__ from "net"; import * as __WEBPACK_EXTERNAL_MODULE_fs__ from "fs"; import * as __WEBPACK_EXTERNAL_MODULE__modelcontextprotocol_sdk_server_mcp_js_45c326f0__ from "@modelcontextprotocol/sdk/server/mcp.js"; import * as __WEBPACK_EXTERNAL_MODULE_child_process__ from "child_process"; import * as __WEBPACK_EXTERNAL_MODULE_ws__ from "ws"; import * as __WEBPACK_EXTERNAL_MODULE__cloudbase_manager_node_6a52ea40__ from "@cloudbase/manager-node"; import * as __WEBPACK_EXTERNAL_MODULE_url__ from "url"; import * as __WEBPACK_EXTERNAL_MODULE__cloudbase_cals_lib_cjs_utils_mermaid_datasource_mermaid_json_transform_04ea0f14__ from "@cloudbase/cals/lib/cjs/utils/mermaid-datasource/mermaid-json-transform"; import * as __WEBPACK_EXTERNAL_MODULE_dns__ from "dns"; import * as __WEBPACK_EXTERNAL_MODULE_crypto__ from "crypto"; import * as __WEBPACK_EXTERNAL_MODULE_fs_promises_f8dae9d1__ from "fs/promises"; import * as __WEBPACK_EXTERNAL_MODULE_os__ from "os"; import * as __WEBPACK_EXTERNAL_MODULE_winston__ from "winston"; /******/ var __webpack_modules__ = ({ /***/ 622: /***/ ((__unused_webpack_module, exports, __webpack_require__) => { Object.defineProperty(exports, "__esModule", ({ value: true })); exports.DEFAULT_CREATE_ENV_RESOURCES = exports.CREATE_ENV_RESOURCE_VALUES = void 0; exports.resolveCreateEnvResources = resolveCreateEnvResources; exports.simplifyEnvList = simplifyEnvList; exports.isUsableNoSqlDatabaseEntry = isUsableNoSqlDatabaseEntry; exports.registerEnvTools = registerEnvTools; const toolbox_1 = __webpack_require__(2090); const zod_1 = __webpack_require__(2971); const auth_js_1 = __webpack_require__(7291); const cloudbase_manager_js_1 = __webpack_require__(3431); const logger_js_1 = __webpack_require__(3039); const tool_result_js_1 = __webpack_require__(9835); const env_setup_js_1 = __webpack_require__(6978); /** * Resources accepted by CreateEnv. Matches Cloud API / Manager SDK contract. * `postgresql` enables CloudBase PostgreSQL (PG mode) when supported by the package. */ exports.CREATE_ENV_RESOURCE_VALUES = [ "flexdb", "storage", "function", "postgresql", ]; /** Default Resources when callers omit the field (schema promises "all four"). */ exports.DEFAULT_CREATE_ENV_RESOURCES = [ ...exports.CREATE_ENV_RESOURCE_VALUES, ]; function resolveCreateEnvResources(resources) { if (Array.isArray(resources) && resources.length > 0) { return [...resources]; } return [...exports.DEFAULT_CREATE_ENV_RESOURCES]; } /** * Simplify environment list data by keeping only essential fields for AI assistant * This reduces token consumption when returning environment lists via MCP tools * @param envList - Full environment list from API * @returns Simplified environment list with only essential fields */ function simplifyEnvList(envList) { if (!Array.isArray(envList)) { return envList; } return envList.map((env) => { // Only keep essential fields that are useful for AI assistant const simplified = {}; if (env.EnvId !== undefined) simplified.EnvId = env.EnvId; if (env.Alias !== undefined) simplified.Alias = env.Alias; if (env.Status !== undefined) simplified.Status = env.Status; if (env.EnvType !== undefined) simplified.EnvType = env.EnvType; if (env.Region !== undefined) simplified.Region = env.Region; if (env.PackageId !== undefined) simplified.PackageId = env.PackageId; if (env.PackageName !== undefined) simplified.PackageName = env.PackageName; if (env.IsDefault !== undefined) simplified.IsDefault = env.IsDefault; return simplified; }); } const DEFAULT_ENV_CANDIDATE_LIMIT = 20; const DEFAULT_ENV_FIELDS = [ "EnvId", "Alias", "Status", "EnvType", "Region", "PackageId", "PackageName", "IsDefault", ]; function selectEnvFields(env, fields) { const selectedFields = fields && fields.length > 0 ? fields : DEFAULT_ENV_FIELDS; const simplified = {}; for (const field of selectedFields) { if (env[field] !== undefined) { simplified[field] = env[field]; } } return simplified; } function filterEnvList(envList, filters) { const alias = filters.alias?.trim().toLowerCase(); const aliasExact = filters.aliasExact === true; const envId = filters.envId?.trim().toLowerCase(); return envList.filter((env) => { const normalizedAlias = String(env.Alias ?? "").toLowerCase(); const matchesAlias = alias ? aliasExact ? normalizedAlias === alias : normalizedAlias.includes(alias) : true; const matchesEnvId = envId ? String(env.EnvId ?? "").toLowerCase() === envId : true; return matchesAlias && matchesEnvId; }); } function paginateEnvList(envList, offset, limit) { const safeOffset = Math.max(0, Math.floor(offset ?? 0)); const safeLimit = limit === undefined ? undefined : Math.max(1, Math.floor(limit)); const items = safeLimit === undefined ? envList.slice(safeOffset) : envList.slice(safeOffset, safeOffset + safeLimit); return { total: envList.length, offset: safeOffset, limit: safeLimit ?? envList.length, items, }; } function buildEnvCandidatePayload(envCandidates, limit = DEFAULT_ENV_CANDIDATE_LIMIT) { const env_candidates = envCandidates.slice(0, limit); return { env_candidates, env_candidates_summary: { total: envCandidates.length, returned: env_candidates.length, truncated: envCandidates.length > env_candidates.length, }, }; } function buildLocalDevDomainHint() { return { format: "host:port", useActualOrigin: true, requiredValue: "当前浏览器实际访问 origin 对应的 host:port", deriveFrom: ["浏览器地址栏中的当前 origin", "本地 dev server 实际启动输出"], note: "如果你的前端运行在自定义域名或本地开发端口上,请把当前浏览器实际访问地址对应的 host:port 加入安全域名。不要依赖一组固定默认端口,也不要假设已有 localhost/127.0.0.1 条目已经覆盖当前运行端口。", }; } function summarizeConfiguredLocalDevEntries(domains) { const localEntries = domains .map((domain) => String(domain?.Domain ?? "").trim()) .filter((domain) => domain.startsWith("127.0.0.1:") || domain.startsWith("localhost:")); return { hasAnyConfiguredLocalEntry: localEntries.length > 0, configuredEntries: localEntries, }; } function simplifyEnvDomains(domains) { if (!Array.isArray(domains)) { return domains; } return domains.map((domain) => { if (!domain || typeof domain !== "object") { return domain; } const source = domain; return { ...(source.Id !== undefined ? { Id: source.Id } : {}), ...(source.Domain !== undefined ? { Domain: source.Domain } : {}), ...(source.CreateTime !== undefined ? { CreateTime: source.CreateTime } : {}), ...(source.UpdateTime !== undefined ? { UpdateTime: source.UpdateTime } : {}), ...(source.Status !== undefined ? { Status: source.Status } : {}), ...(source.Type !== undefined ? { Type: source.Type } : {}), }; }); } function buildEnvDomainManagementResult(params) { const { action, domains, result } = params; const rawResult = result && typeof result === "object" && !Array.isArray(result) ? result : { result }; if (action === "create") { return { ...rawResult, ok: true, code: "DOMAIN_UPDATE_PENDING", operation: action, targetDomains: domains, asyncState: "PENDING", message: '安全域名已提交添加请求。该变更通常需要数分钟传播(平台侧);请每 10 秒轮询 queryEnv(action="domains") 直到 Status 为 ENABLE,勿一次 sleep 满 10 分钟。', propagation: { requiresPolling: true, pollTool: "queryEnv", pollAction: "domains", pollIntervalSuggestionSeconds: 10, timeoutSuggestionSeconds: 300, successCondition: '目标域名出现在 queryEnv(action="domains") 返回中,且 Status 为 ENABLE。', }, next_step: { tool: "queryEnv", action: "domains", suggested_args: { action: "domains", }, }, }; } return { ...rawResult, ok: true, code: "DOMAIN_DELETE_PENDING", operation: action, targetDomains: domains, asyncState: "PENDING", message: '安全域名已提交删除请求。该变更通常需要数分钟传播;请每 10 秒轮询 queryEnv(action="domains") 直到目标域名不再出现,勿一次 sleep 满数分钟。', propagation: { requiresPolling: true, pollTool: "queryEnv", pollAction: "domains", pollIntervalSuggestionSeconds: 10, timeoutSuggestionSeconds: 300, successCondition: '目标域名不再出现在 queryEnv(action="domains") 返回中。', }, next_step: { tool: "queryEnv", action: "domains", suggested_args: { action: "domains", }, }, }; } function formatDeviceAuthHint(deviceAuthInfo) { if (!deviceAuthInfo) { return ""; } const verificationUriComplete = (0, auth_js_1.buildVerificationUriComplete)(deviceAuthInfo); const lines = [ "", "### Device Flow 授权信息", `- user_code: ${deviceAuthInfo.user_code}`, ]; if (deviceAuthInfo.verification_uri) { lines.push(`- verification_uri: ${deviceAuthInfo.verification_uri}`); } if (verificationUriComplete) { lines.push(`- verification_uri_complete: ${verificationUriComplete}`); } lines.push(`- expires_in: ${deviceAuthInfo.expires_in}s`); lines.push("", "请优先向用户展示完整的 `verification_uri_complete`,不要截断或改写 URL。"); return lines.join("\n"); } function emitDeviceAuthNotice(server, deviceAuthInfo) { // Temporarily disabled: avoid sending logging notifications for device auth } async function fetchAvailableEnvCandidates(cloudBaseOptions, server) { try { return await (0, cloudbase_manager_js_1.listAvailableEnvCandidates)({ cloudBaseOptions, }); } catch { return []; } } const CODEBUDDY_AUTH_ACTIONS = ["status", "set_env", "login_by_api_key"]; const DEFAULT_AUTH_ACTIONS = [ "status", "start_auth", "set_env", "logout", "get_temp_credentials", "login_by_api_key", ]; function maskSensitiveValue(value) { if (value.length <= 4) { return "*".repeat(value.length); } return `${value.slice(0, 2)}******${value.slice(-2)}`; } function isTemporaryCredentialLoginState(loginState) { const refreshToken = normalizeOptionalToolString(loginState.refreshToken); const token = normalizeOptionalToolString(loginState.token); const accessTokenExpired = typeof loginState.accessTokenExpired === "number" || typeof loginState.accessTokenExpired === "string"; return Boolean(token && (refreshToken || accessTokenExpired)); } function getCurrentIde(server) { return server.ide || process.env.INTEGRATION_IDE || ""; } function isCodeBuddyIde(server) { return getCurrentIde(server) === "CodeBuddy"; } function getSupportedAuthActions(server) { return isCodeBuddyIde(server) ? CODEBUDDY_AUTH_ACTIONS : DEFAULT_AUTH_ACTIONS; } function buildAuthRequiredNextStep(server) { if (isCodeBuddyIde(server)) { return (0, tool_result_js_1.buildAuthNextStep)("status", { suggestedArgs: { action: "status" }, }); } return (0, tool_result_js_1.buildAuthNextStep)("start_auth", { suggestedArgs: { action: "start_auth", authMode: "device" }, }); } function buildSetEnvNextStep(envCandidates) { const singleEnvId = envCandidates.length === 1 ? envCandidates[0].envId : undefined; return (0, tool_result_js_1.buildAuthNextStep)("set_env", { requiredParams: singleEnvId ? undefined : ["envId"], suggestedArgs: singleEnvId ? { action: "set_env", envId: singleEnvId } : { action: "set_env" }, }); } function dedupeActions(actions) { return actions.filter((action, index) => actions.indexOf(action) === index); } function buildAuthEnvSetupFailure(params) { return { reason: params.reason, error_code: params.errorCode, message: params.message, help_url: params.helpUrl, need_real_name_auth: params.needRealNameAuth, need_cam_auth: params.needCamAuth, }; } function buildAuthEnvSetupPayload(preparation) { return { current_env_id: preparation.currentEnvId, env_status: preparation.envStatus, env_setup_status: preparation.envSetupStatus, env_setup_actions: preparation.envSetupActions, ...(preparation.envSetupFailure ? { env_setup_failure: preparation.envSetupFailure, } : {}), ...buildEnvCandidatePayload(preparation.envCandidates), }; } async function prepareAuthEnvironment(params) { const { server, cloudBaseOptions, loginState } = params; const currentEnvId = (0, cloudbase_manager_js_1.getCachedEnvId)() || process.env.CLOUDBASE_ENV_ID || (typeof loginState?.envId === "string" && loginState.envId.length > 0 ? loginState.envId : null); if (currentEnvId) { return { currentEnvId, envStatus: "READY", envCandidates: [], envSetupStatus: "NOT_NEEDED", envSetupActions: [], message: `当前已登录,环境: ${currentEnvId}`, nextStep: (0, tool_result_js_1.buildAuthNextStep)("status", { suggestedArgs: { action: "status" }, }), }; } const envSetupActions = ["list_envs"]; const envCandidates = await fetchAvailableEnvCandidates(cloudBaseOptions, server); if (envCandidates.length === 1) { const singleEnvId = envCandidates[0].envId; await cloudbase_manager_js_1.envManager.setEnvId(singleEnvId); return { currentEnvId: singleEnvId, envStatus: "READY", envCandidates, envSetupStatus: "AUTO_BOUND", envSetupActions: dedupeActions(envSetupActions), message: `当前已登录,已自动绑定唯一环境: ${singleEnvId}`, nextStep: (0, tool_result_js_1.buildAuthNextStep)("status", { suggestedArgs: { action: "status" }, }), }; } if (envCandidates.length > 1) { return { currentEnvId: null, envStatus: "MULTIPLE", envCandidates, envSetupStatus: "SELECTION_REQUIRED", envSetupActions: dedupeActions(envSetupActions), message: "当前已登录,但存在多个可用环境,请先选择环境。", nextStep: buildSetEnvNextStep(envCandidates), }; } let setupContext = {}; const manager = await (0, cloudbase_manager_js_1.getCloudBaseManager)({ requireEnvId: false, cloudBaseOptions: cloudBaseOptions ? { ...cloudBaseOptions, envId: undefined, } : undefined, mcpServer: server, }); setupContext = await (0, env_setup_js_1.checkAndInitTcbService)(manager, setupContext); if (setupContext.checkTcbServiceAttempted) { envSetupActions.push("check_tcb_service"); } if (setupContext.initTcbAttempted) { envSetupActions.push("init_tcb"); } if (setupContext.initTcbError || !setupContext.tcbServiceInitialized) { const failure = setupContext.initTcbError ? buildAuthEnvSetupFailure({ reason: "tcb_init_failed", errorCode: setupContext.initTcbError.code || "TCB_INIT_FAILED", message: setupContext.initTcbError.message, helpUrl: setupContext.initTcbError.helpUrl, needRealNameAuth: setupContext.initTcbError.needRealNameAuth, needCamAuth: setupContext.initTcbError.needCamAuth, }) : buildAuthEnvSetupFailure({ reason: "tcb_init_failed", errorCode: "TCB_INIT_FAILED", message: "CloudBase 服务初始化失败,请稍后重试。", helpUrl: "https://buy.cloud.tencent.com/lowcode?buyType=tcb&channel=mcp", }); return { currentEnvId: null, envStatus: "NONE", envCandidates: [], envSetupStatus: "ACTION_REQUIRED", envSetupActions: dedupeActions(envSetupActions), envSetupFailure: failure, message: failure.message, nextStep: (0, tool_result_js_1.buildAuthNextStep)("status", { suggestedArgs: { action: "status" }, }), }; } const createResult = await (0, env_setup_js_1.checkAndCreateFreeEnv)(manager, setupContext); setupContext = createResult.context; if (setupContext.promotionalActivitiesChecked) { envSetupActions.push("check_promotional_activity"); } if (setupContext.createFreeEnvAttempted) { envSetupActions.push("create_free_env"); } // Surface the user-facing notice emitted at checkAndCreateFreeEnv entry. // Informational only — no confirm required. Prepend to the final message so // the user knows an automatic free-env creation was attempted. const userNotice = createResult.userNotice?.trim() || ""; if (createResult.success && createResult.envId) { await cloudbase_manager_js_1.envManager.setEnvId(createResult.envId); const successMessage = `当前已登录,已自动创建并绑定环境: ${createResult.envId}`; return { currentEnvId: createResult.envId, envStatus: "READY", envCandidates: [], envSetupStatus: "AUTO_CREATED", envSetupActions: dedupeActions(envSetupActions), message: userNotice ? `${userNotice}\n${successMessage}` : successMessage, nextStep: (0, tool_result_js_1.buildAuthNextStep)("status", { suggestedArgs: { action: "status" }, }), }; } const createFailure = setupContext.createEnvError ? buildAuthEnvSetupFailure({ reason: "env_creation_failed", errorCode: setupContext.createEnvError.code || "ENV_CREATION_FAILED", message: setupContext.createEnvError.message, helpUrl: setupContext.createEnvError.helpUrl, }) : buildAuthEnvSetupFailure({ reason: "env_creation_failed", errorCode: "ENV_CREATION_FAILED", message: "环境创建失败,请稍后重试或手动创建环境。", helpUrl: "https://buy.cloud.tencent.com/lowcode?buyType=tcb&channel=mcp", }); const failureMessage = userNotice ? `${userNotice}\n${createFailure.message}` : createFailure.message; return { currentEnvId: null, envStatus: "NONE", envCandidates: [], envSetupStatus: "ACTION_REQUIRED", envSetupActions: dedupeActions(envSetupActions), envSetupFailure: createFailure, message: failureMessage, nextStep: (0, tool_result_js_1.buildAuthNextStep)("status", { suggestedArgs: { action: "status" }, }), }; } function buildEnvQueryListResult(params) { const envList = Array.isArray(params.result?.EnvList) ? params.result.EnvList : []; const shouldRestrictToCurrentEnv = params.hasEnvId && !params.filters.alias && !params.filters.envId; const baseList = shouldRestrictToCurrentEnv ? envList.filter((env) => env.EnvId === params.cloudBaseOptions?.envId) : envList; const filteredList = filterEnvList(baseList, { alias: params.filters.alias, aliasExact: params.filters.aliasExact, envId: params.filters.envId, }); const paginated = paginateEnvList(filteredList, params.filters.offset, params.filters.limit); const exactEnvIdSummaryHint = params.filters.envId ? { tool: "queryEnv", action: "info", reason: "action=list with envId only returns a concise summary. Use action=info to fetch detailed environment information such as full resource metadata and additional environment details.", } : undefined; return { EnvList: paginated.items.map((env) => selectEnvFields(env, params.filters.fields)), TotalCount: paginated.total, Offset: paginated.offset, Limit: paginated.limit, HasMore: paginated.offset + paginated.items.length < paginated.total, AppliedFilters: { alias: params.filters.alias ?? null, aliasExact: params.filters.aliasExact ?? null, envId: params.filters.envId ?? null, fields: params.filters.fields ?? [...DEFAULT_ENV_FIELDS], currentEnvOnly: shouldRestrictToCurrentEnv, }, ...(exactEnvIdSummaryHint ? { RecommendedNextAction: exactEnvIdSummaryHint, } : {}), }; } async function enrichEnvInfoWithBilling(params) { const targetEnvId = params.envId || params.result?.EnvInfo?.EnvId; if (!targetEnvId || !params.result?.EnvInfo) { return params.result; } try { const billingResult = await params.manager.commonService("tcb", "2018-06-08").call({ Action: "DescribeBillingInfo", Param: { EnvId: targetEnvId, }, }); (0, cloudbase_manager_js_1.logCloudBaseResult)(params.logger, billingResult); const billingList = billingResult?.EnvBillingInfoList || billingResult?.Response?.EnvBillingInfoList || billingResult?.Data?.EnvBillingInfoList || []; const matchedBillingInfo = Array.isArray(billingList) ? billingList.find((item) => item?.EnvId === targetEnvId) ?? billingList[0] : undefined; if (!matchedBillingInfo) { return params.result; } return { ...params.result, EnvInfo: { ...params.result.EnvInfo, BillingInfo: matchedBillingInfo, }, }; } catch (billingError) { (0, logger_js_1.debug)("DescribeBillingInfo enrichment failed, continuing without billing info", { error: billingError, envId: targetEnvId, }); return params.result; } } /** * Derive a RuntimeMode hint from the EnvInfo payload so the AI agent can * immediately tell which CloudBase data backends are actually present in * this environment. * * Reality model (revised after observing a real PG environment): a single * CloudBase environment can have any combination of: * - PostgreSQL (CloudBase PG / pgstore): signaled by `EnvInfo.PostgreSQL[]` * non-empty and/or `EnvInfo.Meta` containing `postgresql=enable`. * - NoSQL document database (flexdb): signaled ONLY by a usable entry in * `EnvInfo.Databases[]`. The usable signal is a non-empty `InstanceId` * (FlexDB Tag, typically `tnt-...` — same value `getDatabaseInstanceId` * / ListTables `Tag` consume) with `Status` missing or `RUNNING`. * Cloud storage (`EnvInfo.Storages[]` / CreateEnv `storage`) is unrelated * and must never flip `RuntimeBackends.nosql`. * - MySQL: signaled by a non-empty `EnvInfo.MysqlInstances[]` (or similar * field, name varies). In a pure PG environment this is absent. * * In particular: a CloudBase PG environment commonly STILL has flexdb * provisioned in parallel (`Databases[].InstanceId = tnt-...`). NoSQL is * therefore "co-present" — its collection APIs and NoSQL `securityRule`s * remain valid for collections that already live there. The thing that is * NOT a substitute for the new PG surface is using NoSQL collections to * model a brand-new business table that the task explicitly puts in PG. * * What this enrichment does: * - Adds `EnvInfo.RuntimeMode = "postgresql" | "nosql"` based on whether * PG is present. This is the recommended primary backend for new * business data when set to "postgresql". * - Adds `EnvInfo.RuntimeBackends`, a structured snapshot of which * backends are actually available (postgresql / nosql / mysql), so the * agent does not have to re-read `Databases`/`PostgreSQL`. * - Adds `EnvInfo.RuntimeModeHints` summarizing which API/tool to prefer * for new code, including an explicit `MysqlNotAvailable` line when * MySQL is absent — that one IS a hard "do not use" signal. */ /** Exported for unit tests / live-fixture checks against FlexDB InstanceId. */ function isUsableNoSqlDatabaseEntry(db) { if (!db || typeof db !== "object") { return false; } const entry = db; const instanceId = typeof entry.InstanceId === "string" ? entry.InstanceId.trim() : ""; // Empty InstanceId means no flexdb tenant — same failure mode as // getDatabaseInstanceId() throwing "无法获取数据库实例ID". if (!instanceId) { return false; } // Some API versions omit Status; when present, only RUNNING counts as usable. if (entry.Status != null && String(entry.Status).toUpperCase() !== "RUNNING") { return false; } return true; } async function enrichEnvInfoWithRuntimeMode(result, manager) { const envInfo = result?.EnvInfo; if (!envInfo || typeof envInfo !== "object") { return result; } const pgList = Array.isArray(envInfo.PostgreSQL) ? envInfo.PostgreSQL : []; const metaList = Array.isArray(envInfo.Meta) ? envInfo.Meta : []; const metaPostgresEnabled = metaList.some((item) => item && typeof item.Key === "string" && /^postgre[_]?sql$/i.test(item.Key) && String(item.Value).toLowerCase() === "enable"); const hasPostgresql = pgList.length > 0 || metaPostgresEnabled; // NoSQL = flexdb tenant id on Databases[].InstanceId (typically tnt-...). // Aligns with getDatabaseInstanceId() / ListTables Tag. Storage is unrelated. const databasesList = Array.isArray(envInfo.Databases) ? envInfo.Databases : []; const hasNoSql = databasesList.some(isUsableNoSqlDatabaseEntry); // MySQL field name has been seen as MysqlInstances / MySQLInstances / // MySQL across API versions; check any of them. const mysqlList = (() => { for (const k of ["MysqlInstances", "MySQLInstances", "MySQL"]) { const v = envInfo[k]; if (Array.isArray(v) && v.length > 0) return v; } return []; })(); let hasMysql = mysqlList.length > 0; // Fallback: if MySQL not detected from DescribeEnvInfo fields, probe via // DescribeMySQLClusterDetail (dedicated MySQL API). This covers cases where // the SDK strips MySQL fields from the DescribeEnvInfo response. if (!hasMysql && manager?.commonService) { try { const probeResult = await manager .commonService("tcb", "2018-06-08") .call({ Action: "DescribeMySQLClusterDetail", Param: { EnvId: envInfo.EnvId }, }); const clusterId = probeResult?.DbClusterId || probeResult?.Response?.DbClusterId || probeResult?.Data?.DbClusterId; if (clusterId) { hasMysql = true; } } catch { // Probe failure means MySQL is not provisioned — keep hasMysql = false } } // Primary mode: prefer PG when it is provisioned, otherwise fall back to // legacy NoSQL labeling. This drives "what skill to read first / what API // to reach for when starting new business code". const runtimeMode = hasPostgresql ? "postgresql" : "nosql"; const hints = hasPostgresql ? { PrimaryBackend: "PostgreSQL (CloudBase PG) is provisioned — prefer it for any NEW business data the task introduces.", BusinessDataAPI: "For NEW business data on PG: use CloudBase JS SDK v3 `app.rdb()` (Supabase-style chained query). Existing NoSQL collections in this env keep working through `app.database()`; do not migrate them unless the task asks.", Permissions: "PG table permissions use Row-Level Security. Run `ALTER TABLE ... ENABLE ROW LEVEL SECURITY` and `CREATE POLICY` via `managePgDatabase(action=\"execute\", confirm=true)`. `managePermissions(resourceType=\"noSqlDatabase\", securityRule=...)` only governs NoSQL collection rules and has NO effect on PG tables — keep using it for the NoSQL collections that already exist here.", Storage: "PG-mode browser uploads should use `app.storage.from().upload(<bucket>/<key>, file)` against an explicitly-created `pgstore` bucket (same model as Supabase Storage; the v3 SDK does not auto-create one). `EnvInfo.Storages[]` here is the legacy NoSQL bucket — it is still usable for the legacy `app.uploadFile()` flow but is NOT a valid pgstore target.", CoexistingNoSQL: hasNoSql ? "This env also has a usable NoSQL (flexdb) InstanceId in EnvInfo.Databases[] (FlexDB Tag, typically tnt-...). Existing collections and `managePermissions(resourceType=\"noSqlDatabase\")` rules remain valid for that data." : "No usable NoSQL (flexdb) InstanceId in EnvInfo.Databases[] — do not assume app.database() / NoSQL MCP tools work. Cloud storage does not imply flexdb.", MysqlNotAvailable: hasMysql ? "MySQL instance(s) detected — see EnvInfo.MysqlInstances." : "No MySQL instance is provisioned for this env. Do NOT use `manageMysqlDatabase` / `queryMysqlDatabase` (those are MySQL-specific) and do NOT read the `relational-database-mcp-cloudbase` skill — that family targets MySQL, not CloudBase PG.", RecommendedSkills: "Read `postgresql-development-cloudbase` first for new PG code. `cloudbase-document-database-web-sdk` is still applicable for existing NoSQL collections in this env. Skip `relational-database-mcp-cloudbase` (MySQL-only) entirely.", } : { PrimaryBackend: "PostgreSQL is NOT provisioned in this env — this is a legacy NoSQL CloudBase backend.", BusinessDataAPI: "Use `app.database()` collections via `@cloudbase/js-sdk` for browser business data. Do not switch to `app.rdb()` here — PG is not available.", Permissions: "Use `managePermissions(resourceType=\"noSqlDatabase\", securityRule=...)` for collection rules. PG-only RLS guidance (e.g. `auth.uid()` SQL policies) does not apply here.", Storage: "Browser uploads use `app.uploadFile()` against the bucket exposed in `EnvInfo.Storages[].Bucket`.", MysqlNotAvailable: hasMysql ? "MySQL instance(s) detected — see EnvInfo.MysqlInstances." : "No MySQL instance in this env. `manageMysqlDatabase` / `queryMysqlDatabase` and the `relational-database-mcp-cloudbase` skill are not applicable.", RecommendedSkills: "Read `cloudbase-document-database-web-sdk` (and `cloud-storage-web` for uploads). `postgresql-development-cloudbase` and `relational-database-mcp-cloudbase` are not applicable to this env.", }; return { ...result, EnvInfo: { ...envInfo, RuntimeMode: runtimeMode, RuntimeBackends: { postgresql: hasPostgresql, nosql: hasNoSql, mysql: hasMysql, }, RuntimeModeHints: hints, }, }; } /** * 补充 SDK getEnvInfo() 遗漏的字段。 * * @cloudbase/manager-node 的 getEnvInfo() 从 DescribeEnvInfo CAPI 响应的 * EnvBaseInfo 中手写白名单映射时,漏掉了 PostgreSQL、Meta、StaticStorages 等字段。 * 这导致 enrichEnvInfoWithRuntimeMode 无法正确判断环境是否支持 PostgreSQL。 * * 此函数通过 commonService 额外调用 DescribeEnvInfo CAPI,从原始响应中提取 * 缺失字段补到 EnvInfo 上。 */ async function enrichEnvInfoWithMissingFields(manager, result, envId) { const envInfo = result?.EnvInfo; if (!envInfo || typeof envInfo !== "object") { return result; } // 如果 PostgreSQL 字段已存在且非空,说明 SDK 已经透传了,不需要补充 if (Array.isArray(envInfo.PostgreSQL) && envInfo.PostgreSQL.length > 0 && Array.isArray(envInfo.Meta)) { return result; } try { const capiResult = await manager.commonService("tcb", "2018-06-08").call({ Action: "DescribeEnvInfo", Param: { EnvId: envId }, }); const envBaseInfo = capiResult?.EnvInfo?.EnvBaseInfo || capiResult?.Response?.EnvInfo?.EnvBaseInfo; if (!envBaseInfo || typeof envBaseInfo !== "object") { return result; } return { ...result, EnvInfo: { ...envInfo, // 补充 PostgreSQL 字段(SDK 白名单映射遗漏) ...(Array.isArray(envBaseInfo.PostgreSQL) && { PostgreSQL: envBaseInfo.PostgreSQL, }), // 补充 Meta 字段(SDK 白名单映射遗漏) ...(Array.isArray(envBaseInfo.Meta) && { Meta: envBaseInfo.Meta, }), // 补充 StaticStorages 字段(SDK 白名单映射遗漏) ...(Array.isArray(envBaseInfo.StaticStorages) && { StaticStorages: envBaseInfo.StaticStorages, }), }, }; } catch { // CAPI 调用失败不影响已有数据,静默忽略 return result; } } function normalizeOptionalToolString(value) { return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined; } /** * Build enhanced error message for queryEnv tool errors * Provides actionable guidance based on error patterns */ function buildEnvQueryErrorMessage(error, action) { const baseMessage = error instanceof Error ? error.message : String(error); // Check for common error patterns and provide specific guidance const hasInvalidParameterError = /400|invalid parameter|invalid argument|parameter value/i.test(baseMessage); const hasAuthError = /未登录|auth required|unauthorized|authentication|credential|token|secret/i.test(baseMessage); const hasNetworkError = /ECONNRESET|socket hang up|ETIMEDOUT|ENOTFOUND|timeout/i.test(baseMessage); const hasPermissionError = /permission|denied|forbidden|无权|拒绝/i.test(baseMessage); const hasEnvNotFoundError = /env|environment|环境.*不存在|not found/i.test(baseMessage); const suggestions = []; if (hasInvalidParameterError) { suggestions.push("参数错误:可能是认证信息无效或已过期,请尝试以下步骤:"); suggestions.push("1. 先调用 auth(action=\"status\") 检查当前登录状态"); suggestions.push("2. 如果未登录,调用 auth(action=\"start_auth\", authMode=\"device\") 完成登录"); suggestions.push("3. 登录完成后再次调用 queryEnv(action=\"list\")"); } if (hasAuthError) { suggestions.push("认证错误:当前未登录或认证已过期。"); suggestions.push("建议先执行 auth(action=\"status\") 查看状态,然后按提示完成登录。"); } if (hasPermissionError) { suggestions.push("权限错误:当前账号可能没有访问该资源的权限。"); suggestions.push("请确认:1) 已选择正确的环境 2) 账号有对应权限"); } if (hasEnvNotFoundError) { suggestions.push("环境错误:指定的环境不存在或无法访问。"); suggestions.push("请使用 queryEnv(action=\"list\") 查看可用的环境列表。"); } if (hasNetworkError) { suggestions.push("网络错误:请检查网络连接,稍后重试。"); } // If no specific pattern matched, provide general guidance if (suggestions.length === 0) { suggestions.push("查询环境信息时出错,建议:"); suggestions.push("1. 先调用 auth(action=\"status\") 确认登录状态"); suggestions.push("2. 如未登录,执行 auth(action=\"start_auth\") 完成认证"); suggestions.push("3. 确认环境 ID 正确且可访问"); } return `[queryEnv/${action}] 调用失败: ${baseMessage}\n\n解决建议:\n${suggestions.join("\n")}`; } function normalizeOptionalToolBoolean(value) { return typeof value === "boolean" ? value : undefined; } /** * 解析用于询价的 region。与 cloudbase-manager.ts 一致: * cloudBaseOptions.region → TCB_REGION → 'ap-shanghai'。 */ function resolvePricingRegion(cloudBaseOptions) { return ((typeof cloudBaseOptions?.region === "string" && cloudBaseOptions.region) || process.env.TCB_REGION || "ap-shanghai"); } /** * 从 describeBaasPackageList 查询某个 packageId 的人类可读套餐名。 * 失败时返回 undefined,调用方降级为只显示 packageId。 */ async function fetchPackageTitle(manager, packageId) { try { const result = await manager.env.describeBaasPackageList({ TargetAction: "new", Source: "qcloud", }); const packageList = result?.PackageList || []; const matched = packageList.find((item) => item?.BillTags === packageId || item?.PackageName === packageId); if (matched) { return (matched.PackageTitle || matched.PackageName || matched.BillTags); } return undefined; } catch (error) { (0, logger_js_1.debug)("fetchPackageTitle failed", { packageId, error }); return undefined; } } /** * 查询环境的当前计费/套餐信息(PackageName、PackageId、Region、到期时间、PayMode)。 * 失败时返回 undefined。 */ async function fetchEnvBillingSummary(manager, envId) { try { // describeBillingInfo 返回 EnvBillingInfoList;从中挑出 envId 对应项 const billingResult = await manager.env.describeBillingInfo({ EnvId: envId }); const billingList = billingResult?.EnvBillingInfoList || billingResult?.Response?.EnvBillingInfoList || billingResult?.Data?.EnvBillingInfoList || []; const matched = Array.isArray(billingList) ? billingList.find((item) => item?.EnvId === envId) ?? billingList[0] : undefined; if (!matched) { return undefined; } return { packageName: matched.PackageName, packageId: matched.PackageId, region: matched.Region, expireTime: matched.ExpireTime, payMode: matched.PayMode, isAutoRenew: matched.IsAutoRenew, }; } catch (error) { (0, logger_js_1.debug)("fetchEnvBillingSummary failed", { envId, error }); return undefined; } } /** * 询价新购价格。失败时返回 { error }。 */ async function calculateCreatePrice(manager, params) { try { const priceResult = await manager.env.calculatePackageCreatePrice({ packageId: params.packageId, region: params.region, period: params.period, }); return { priceResult }; } catch (error) { const message = error instanceof Error ? error.message : String(error); (0, logger_js_1.debug)("calculatePackageCreatePrice failed", { params, error }); return { error: message }; } } /** * 询价续费价格。失败时返回 { error }。 */ async function calculateRenewPrice(manager, envId, period) { try { const priceResult = await manager.env.calculatePackageRenewPrice({ envId, period, }); return { priceResult }; } catch (error) { const message = error instanceof Error ? error.message : String(error); (0, logger_js_1.debug)("calculatePackageRenewPrice failed", { envId, period, error }); return { error: message }; } } /** * 询价变配价格(含退款)。失败时返回 { error }。 */ async function calculateModifyPrice(manager, envId, newPackageId) { try { const priceResult = await manager.env.calculatePackageModifyPrice({ envId, newPackageId, }); return { priceResult }; } catch (error) { const message = error instanceof Error ? error.message : String(error); (0, logger_js_1.debug)("calculatePackageModifyPrice failed", { envId, newPackageId, error }); return { error: message }; } } /** * 把 PriceResult 格式化为人类可读的价格摘要。 * 返回 { summary, detail }:summary 是一行短语,detail 是多行明细。 */ function formatPriceSection(priceResult) { if (!priceResult || typeof priceResult !== "object") { return { summary: "询价返回为空", detail: "" }; } const currency = priceResult.Currency === "USD" ? "$" : "¥"; const realTotalCost = Number(priceResult.RealTotalCost); const totalCost = Number(priceResult.TotalCost); const unitPrice = Number(priceResult.Price); const timeSpan = priceResult.TimeSpan; const TimeUnit = priceResult.TimeUnit || ""; const refund = priceResult.Refund ? Number(priceResult.Refund) : undefined; const lines = []; let summary = ""; if (Number.isFinite(realTotalCost) && realTotalCost > 0) { summary = `预计实付 ${currency}${realTotalCost}`; lines.push(`- 实付总价: ${currency}${realTotalCost}`); } else if (Number.isFinite(totalCost) && totalCost > 0) { summary = `预计费用 ${currency}${totalCost}`; lines.push(`- 总价: ${currency}${totalCost}`); } if (Number.isFinite(totalCost) && totalCost > 0 && totalCost !== realTotalCost) { lines.push(`- 原价: ${currency}${totalCost}`); } if (Number.isFinite(unitPrice) && unitPrice > 0) { lines.push(`- 单价: ${currency}${unitPrice}`); } if (timeSpan !== undefined && TimeUnit) { lines.push(`- 时长: ${timeSpan} ${TimeUnit}`); } if (refund !== undefined && Number.isFinite(refund) && refund > 0) { lines.push(`- 退款: ${currency}${refund}(变配降级时退回差价)`); if (summary) { summary += `,退款 ${currency}${refund}`; } else { summary = `退款 ${currency}${refund}`; } } if (priceResult.Formula && typeof priceResult.Formula === "string") { lines.push(`- 计价公式: ${priceResult.Formula}`); } return { summary: summary || "询价返回缺少总价字段", detail: lines.join("\n"), }; } /** * 构建释放方式说明。manageEnv 当前未提供 destroy action, * 指向控制台让用户手动销毁。 */ function buildReleaseMethodHint() { return { method: "手动销毁", consoleUrl: "https://console.cloud.tencent.com/tcb", note: '环境创建后可随时销毁以停止计费。当前 manageEnv 未提供 destroy action,请前往控制台手动销毁,或调用 manageEnv(action="listPackages") 查看其他套餐。', }; } /** * manageEnv 涉及计费的官方文档链接。来源:腾讯云开发 CloudBase 公开文档。 * 任何 confirm 消息展示这些链接前不需要再次校验 URL 真实性。 */ const MANAGE_ENV_DOC_LINKS = { // 包年包月套餐说明(个人版 / 标准版 / 企业版 / 企业高级版) package: "https://cloud.tencent.com/document/product/876/39093", // 计费能力项说明 billingItems: "https://cloud.tencent.com/document/product/876/120713", // 资源点价格文档 resourcePointPrice: "https://cloud.tencent.com/document/product/876/127357", // 预付费计费与到期释放(费用中心通用) prepayExpiry: "https://cloud.tencent.com/document/product/555/9618", }; /** * 把文档链接渲染为多行 markdown 列表文本。 * filter 不传时全部展示。 */ function renderDocLinksBlock(filter) { const labels = { package: "包年包月套餐说明", billingItems: "计费能力项说明", resourcePointPrice: "资源点价格文档", prepayExpiry: "预付费计费与到期释放(费用中心通用)", }; const entries = (filter ?? Object.keys(MANAGE_ENV_DOC_LINKS)) .map((k) => `- [${labels[k]}](${MANAGE_ENV_DOC_LINKS[k]})`) .join("\n"); return `参考文档:\n${entries}`; } /** * 资源清单详细描述(对应控制台购买页"资源清单"段)。 */ function buildResourceListText() { return "资源清单:\n- 云开发环境 ×1(含 云数据库 / 云函数 / 云存储 / 静态托管 / 身份认证 等基础资源)"; } /** * 计费项披露(对应控制台购买页"计费项"段)。 * 此处为静态披露,调用方在 confirm 消息中拼接即可。 */ function buildBillingItemsText() { return "计费项:\n- 数据库容量/调用 · 云函数调用/资源/流量 · 存储读写/CDN · 网关/认证/API 调用 · QPS 超限按量 · 日志"; } /** * 计费方式披露(对应控制台购买页"计费方式"段)。 * 针对用户传入的 packageId 判断套餐类型: * - 含 free/activity/trial/试用 等关键字视为免费体验版 * - 其余视为付费套餐 */ function buildBillingModeText(packageId) { const id = (packageId || "").toLowerCase(); const isFree = id.includes("free") || id.includes("activity") || id.includes("trial") || id.includes("试用") || id.includes("体验"); if (isFree) { return ("计费方式:\n- 免费体验版(每月赠送约 3000 资源点 ≈ 3 元,0 元开通)\n" + "- 付费套餐:个人版 / 标准版 / 企业版 / 企业高级版"); } return ("计费方式:\n- 付费套餐:个人版 / 标准版 / 企业版 / 企业高级版\n" + "- 免费体验版通过 auth 工具自动创建;本次 manageEnv(create) 不会创建免费版"); } /** * 资源释放方式详细说明(对应控制台购买页"资源释放方式"段)。 * - 免费版:1 个月有效期 + 免费续期 + 停服(保留数据) + 1~7天回收站 + 释放(数据不可恢复) * - 付费版:到期未续费 → 停服(保留数据) + 1~7天可回收站找回 + 释放(数据不可恢复) * 任何时候都可在控制台主动销毁、关闭按量、退订加购资源。 */ function buildReleaseMethodDetailText(packageId) { const id = (packageId || "").toLowerCase(); const isFree = id.includes("free") || id.includes("activity") || id.includes("trial") || id.includes("试用") || id.includes("体验"); const lines = ["资源释放方式:\n- 可随时在控制台销毁环境、关闭按量、退订加购资源"]; if (isFree) { lines.push("- 免费体验版:有效期 1 个月,到期可免费续期 1 个月;未续期则停服(保留数据)→1~7天回收站→释放(数据不可恢复)"); } else { lines.push("- 付费套餐到期未续费:停服(保留数据)→1~7天可回收站找回→释放(数据不可恢复)"); lines.push("- 主动销毁:随时生效;销毁前请确保已迁移或备份数据"); } return lines.join("\n"); } /** * 预计费用披露段:把询价结果 + 周期说明 + 超额按量说明组合成人类可读文本。 */ function buildPricingDisclosureText(packageId, priceSection, priceError, period) { const id = (packageId || "").toLowerCase(); const isFree = id.includes("free") || id.includes("activity") || id.includes("trial") || id.includes("试用") || id.includes("体验"); const lines = ["预计费用:"]; if (isFree) { lines.push(`- 免费体验版:本次开通 0 元;超免费额度后需升级为付费套餐(免费版不支持开按量)`); lines.push("- 实际单价以资源点价格文档为准(3000 资源点 ≈ 3 元)"); } else { lines.push(`- 付费套餐:${priceSection?.summary ?? (priceError ? `⚠️ 询价失败(${priceError}),请前往控制台确认` : "询价返回为空")}`); if (priceSection?.detail) { lines.push(priceSection.detail); } if (priceError && priceSection) { lines.push(`- ⚠️ 部分明细缺失:${priceError}`); } lines.push(`- 计费周期:约 ${period} 个月(包年包月),到期可续费或变配`); lines.push("- 超额可另开按量(次日结算),详细计费项以官方文档为准"); } return lines.join("\n"); } function resolveToolAuthOptions(server, overrides) { return (0, auth_js_1.resolveAuthOptions)({ ...overrides, serverAuthOptions: server.authOptions, }); } function registerEnvTools(server) { // 获取 cloudBaseOptions,如果没有则为 undefined const cloudBaseOptions = server.cloudBaseOptions; // Default: env-scoped tools need a bound envId (domains / domain management use SDK this.envId). // manageEnv create/listPackages are account-level — use requireEnvId: false there only. const getManager = (options) => (0, cloudbase_manager_js_1.getCloudBaseManager)({ cloudBaseOptions, requireEnvId: options?.requireEnvId ?? true, mcpServer: server, }); const getManagerForEnvQuery = (targetEnvId, requireEnvId = true) => (0, cloudbase_manager_js_1.getCloudBaseManager)({ cloudBaseOptions: targetEnvId && targetEnvId !== cloudBaseOptions?.envId ? { ...cloudBaseOptions, envId: targetEnvId, } : cloudBaseOptions, requireEnvId, mcpServer: server, }); const hasEnvId = typeof cloudBaseOptions?.envId === 'string' && cloudBaseOptions?.envId.length > 0; const supportedAuthActions = getSupportedAuthActions(server); const authActionEnum = [...supportedAuthActions]; // auth - CloudBase (云开发) 开发阶段登录与环境绑定 // 微信 IDE 使用票据认证,不需要登录工具 if (server.ide !== 'wxide') { server.registerTool?.("auth", { title: "CloudBase 开发阶段登录与环境", description: "CloudBase(腾讯云开发)开发阶段登录与环境绑定。登录后即可访问云资源;环境(env)是云函数、数据库、静态托管等资源的隔离单元,绑定环境后其他 MCP 工具才能操作该环境。支持:查询状态、发起登录、API Key登录、绑定环境(set_env)、退出登录。", inputSchema: { action: zod_1.z .enum(authActionEnum) .optional() .describe("动作:status=查询状态,start_auth=发起登录,login_by_api_key=API Key登录,set_env=绑定环境(传envId),logout=退出登录"), ...(supportedAuthActions.includes("start_auth") ? { authMode: zod_1.z .enum(["device", "web"]) .optional() .describe("认证模式:device=设备码授权,web=浏览器回调授权"), oauthEndpoint: zod_1.z .string() .optional() .describe("高级可选:自定义 device-code 登录 endpoint。配置后 oauthCustom 默认按 true 处理"), clientId: zod_1.z .string() .optional() .describe("高级可选:自定义 device-code 登录 client_id,不传则使用默认值"), oauthCustom: zod_1.z .boolean() .optional() .describe("高级可选:自定义 endpoint 返回格式开关。未配置 endpoint 时默认 false;配置 endpoint 后默认 true,且不能设为 false"), } : {}), envId: zod_1.z .string() .optional() .describe("环境ID(CloudBase 环境唯一标识),绑定后工具将操作该环境。action=set_env 时必填"),