-
Notifications
You must be signed in to change notification settings - Fork 126
Expand file tree
/
Copy pathcapi.ts
More file actions
310 lines (274 loc) · 15 KB
/
capi.ts
File metadata and controls
310 lines (274 loc) · 15 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
import { z } from "zod";
import { getCloudBaseManager, getEnvId, logCloudBaseResult } from "../cloudbase-manager.js";
import { TCB_ACTION_INDEX_MAP } from "../generated/tcb-action-index.js";
import { ExtendedMcpServer } from "../server.js";
const CATEGORY = "cloud-api";
const CLOUDBASE_CONTROL_PLANE_DOC_URL = "https://cloud.tencent.com/document/product/876/34809";
const CLOUDBASE_DEPENDENCY_API_DOC_URL = "https://cloud.tencent.com/document/product/876/34808";
const ALLOWED_SERVICES = [
"tcb",
"scf",
"sts",
"cam",
"lowcode",
"cdn",
"vpc",
] as const;
type AllowedService = (typeof ALLOWED_SERVICES)[number];
function levenshteinDistance(left: string, right: string) {
const rows = left.length + 1;
const cols = right.length + 1;
const matrix = Array.from({ length: rows }, () => Array(cols).fill(0));
for (let row = 0; row < rows; row += 1) {
matrix[row][0] = row;
}
for (let col = 0; col < cols; col += 1) {
matrix[0][col] = col;
}
for (let row = 1; row < rows; row += 1) {
for (let col = 1; col < cols; col += 1) {
const substitutionCost = left[row - 1] === right[col - 1] ? 0 : 1;
matrix[row][col] = Math.min(
matrix[row - 1][col] + 1,
matrix[row][col - 1] + 1,
matrix[row - 1][col - 1] + substitutionCost,
);
}
}
return matrix[left.length][right.length];
}
function findTcbActionEntry(action: string) {
if (TCB_ACTION_INDEX_MAP[action]) {
return TCB_ACTION_INDEX_MAP[action];
}
const normalizedAction = action.toLowerCase();
return Object.values(TCB_ACTION_INDEX_MAP).find(
(entry) => entry.action.toLowerCase() === normalizedAction,
);
}
function suggestTcbActions(action: string, limit = 3) {
const normalizedAction = action.toLowerCase();
return Object.values(TCB_ACTION_INDEX_MAP)
.map((entry) => {
const normalizedCandidate = entry.action.toLowerCase();
let score = levenshteinDistance(normalizedAction, normalizedCandidate);
if (normalizedCandidate.startsWith(normalizedAction)) {
score -= 3;
}
if (normalizedCandidate.includes(normalizedAction)) {
score -= 2;
}
if (normalizedAction.startsWith(normalizedCandidate)) {
score -= 1;
}
return { entry, score };
})
.sort((left, right) => {
if (left.score !== right.score) {
return left.score - right.score;
}
return left.entry.action.localeCompare(right.entry.action);
})
.slice(0, limit)
.map(({ entry }) => entry.action);
}
function formatTcbParamKeys(keys: string[]) {
return keys.map((item: string) => `\`${item}\``).join("、");
}
function formatTcbParamsTypeHint(action: string) {
const entry = findTcbActionEntry(action);
if (!entry) {
return undefined;
}
return `参数类型参考:\n\`\`\`ts\n${entry.paramsType}\n\`\`\``;
}
function buildCapiDocGuidance(service: AllowedService) {
if (service === "tcb" || service === "lowcode" || service === "scf") {
return `优先查阅 CloudBase API 概览 ${CLOUDBASE_CONTROL_PLANE_DOC_URL} 与云开发依赖资源接口指引 ${CLOUDBASE_DEPENDENCY_API_DOC_URL}。`;
}
return `请优先核对对应官方云 API 文档;若你的场景其实是通过 HTTP 协议直接集成 auth/functions/cloudrun/storage/mysqldb 等 CloudBase 业务 API,请优先使用 OpenAPI / Swagger 或 searchKnowledgeBase(mode="openapi"),不要继续猜测管控面 Action。`;
}
export function buildCapiErrorMessage(service: AllowedService, action: string, error: unknown): string {
const baseMessage = error instanceof Error ? error.message : String(error);
const suggestions: string[] = [];
const tcbEntry = service === "tcb" ? findTcbActionEntry(action) : undefined;
const hasInvalidActionError = /invalid or not found|does not exist|not recognized/i.test(baseMessage);
const hasParameterError = /parameter\s+`?.+?`?\s+is not recognized|MissingParameter|missing parameter|missing required/i.test(baseMessage);
if (hasInvalidActionError) {
suggestions.push(
`Action \`${action}\` 可能不存在或不对外开放。请不要继续猜测 Action 名称,先确认 service=\`${service}\` 下该 Action 在当前 API 版本是否真实存在。`,
);
if (service === "tcb") {
const candidates = suggestTcbActions(action);
if (candidates.length > 0) {
suggestions.push(`可能的 tcb Action:${candidates.map((item) => `\`${item}\``).join("、")}。`);
}
}
suggestions.push(buildCapiDocGuidance(service));
}
if (hasParameterError) {
suggestions.push("请求参数名与 API 定义不一致,请核对参数字段(区分大小写)并移除未支持字段。");
if (service === "tcb" && tcbEntry) {
const paramHint = [
tcbEntry.paramKeys.length > 0
? `常见参数键:${formatTcbParamKeys(tcbEntry.paramKeys)}`
: "",
tcbEntry.requiredKeys.length > 0
? `必填参数:${formatTcbParamKeys(tcbEntry.requiredKeys)}`
: "",
].filter(Boolean);
if (paramHint.length > 0) {
suggestions.push(`\`${tcbEntry.action}\` ${paramHint.join(";")}。`);
}
const paramsTypeHint = formatTcbParamsTypeHint(tcbEntry.action);
if (paramsTypeHint) {
suggestions.push(paramsTypeHint);
}
}
}
if (/ECONNRESET|socket hang up|ETIMEDOUT|ENOTFOUND/i.test(baseMessage)) {
suggestions.push("网络请求异常,建议稍后重试,并检查本地网络/代理设置。");
}
if (suggestions.length === 0) {
suggestions.push(`请检查 service/action/params 是否与官方 API 文档一致后重试。${buildCapiDocGuidance(service)}`);
if (service === "tcb" && tcbEntry && tcbEntry.paramKeys.length > 0) {
suggestions.push(`\`${tcbEntry.action}\` 常见参数键:${formatTcbParamKeys(tcbEntry.paramKeys)}。`);
const paramsTypeHint = formatTcbParamsTypeHint(tcbEntry.action);
if (paramsTypeHint) {
suggestions.push(paramsTypeHint);
}
}
}
return `[${service}/${action}] 调用失败: ${baseMessage}\n建议:${suggestions.join(" ")}\n参考文档:CloudBase API 概览 ${CLOUDBASE_CONTROL_PLANE_DOC_URL}\n云开发依赖资源接口指引 ${CLOUDBASE_DEPENDENCY_API_DOC_URL}`;
}
/**
* Register Common Service based Cloud API tool.
* The tool is intentionally generic; callers must read project rules or
* skills to ensure correct API usage before invoking.
*/
export function registerCapiTools(server: ExtendedMcpServer) {
const cloudBaseOptions = server.cloudBaseOptions;
const logger = server.logger;
const getManager = () => getCloudBaseManager({ cloudBaseOptions });
server.registerTool?.(
"callCloudApi",
{
title: "调用云API",
description:
`通用的云 API 调用工具,主要用于 CloudBase / 腾讯云管控面与依赖资源相关 API 调用。调用前请先确认 service、Action 与 Param,避免猜测 Action 名称。如果你的目标是通过 HTTP 协议直接集成 auth/functions/cloudrun/storage/mysqldb 等 CloudBase 业务 API,请不要优先使用 callCloudApi,而应优先查看对应 OpenAPI / Swagger。现有 OpenAPI / Swagger 能力不是通用的管控面 Action 集合;管控面 API 请优先参考 CloudBase API 概览 ${CLOUDBASE_CONTROL_PLANE_DOC_URL} 与云开发依赖资源接口指引 ${CLOUDBASE_DEPENDENCY_API_DOC_URL}。对于 tcb service,常用 Action 分类如下:
**环境管理**: \`CreateEnv\`、\`ModifyEnv\`、\`DescribeEnvs\`、\`DestroyEnv\`
**用户管理**: \`CreateUser\`、\`ModifyUser\`、\`DescribeUserList\`、\`DeleteUsers\`
**认证配置**: \`EditAuthConfig\`、\`DescribeAuthDomains\`
**云函数**: \`DescribeFunctions\`、\`CreateFunction\`、\`UpdateFunctionCode\`、\`DeleteFunction\`
**数据库**: \`CreateMySQLInstance\`、\`DescribeMySQLInstances\`、\`DestroyMySQLInstance\`
重要参数约定:
1. **EnvId 自动注入**:tcb 的绝大多数 Action 都要求 \`EnvId\` 作为必填参数。如果调用时 params 中未传 \`EnvId\`,工具会自动从当前环境注入,无需手动填写。
2. **参数必须扁平传递**:params 中的字段必须是 API 定义的顶层参数,不要嵌套在子对象中。例如 CreateUser 的参数应直接放在 params 里:\`{ "Name": "zhangsan", "NickName": "张三", "Type": "internalUser", "UserStatus": "ACTIVE" }\`,而不是 \`{ "User": { ... } }\`。
3. **参数名严格区分大小写**:必须使用 API 官方定义的参数名,例如用户名用 \`Name\`(不是 \`UserName\`),用户类型用 \`Type\`(不是 \`UserType\`),用户状态用 \`UserStatus\`(不是 \`Status\`)。如不确定参数名,请先查官方文档。
销毁环境时,常见做法是至少带上 \`EnvId\` 和 \`BypassCheck: true\`,如果环境已经处于隔离期再按文档补 \`IsForce: true\`。`,
inputSchema: {
service: z
.enum(ALLOWED_SERVICES)
.describe(
"选择要访问的服务。可选:tcb、scf、sts、cam、lowcode、cdn、vpc。对于 tcb / scf / lowcode 等 CloudBase 管控面 Action,请优先查官方文档,不要直接猜测 Action。",
),
action: z
.string()
.min(1)
.describe("具体 Action 名称,需符合对应服务的官方 API 定义。若不确定正确 Action,请先查官方文档;不要用近义词或历史命名进行猜测。tcb 常用 Action:环境管理 CreateEnv/ModifyEnv/DescribeEnvs/DestroyEnv、用户管理 CreateUser/ModifyUser/DescribeUserList/DeleteUsers、认证配置 EditAuthConfig、云函数 DescribeFunctions/CreateFunction、数据库 CreateMySQLInstance 等。"),
params: z
.record(z.any())
.optional()
.describe(
"Action 对应的参数对象,键名需与官方 API 定义一致(区分大小写)。参数必须扁平传递,不要嵌套在子对象中。tcb 的绝大多数 Action 都要求 EnvId,如未传则自动从当前环境注入。tcb 示例:`{ \"service\": \"tcb\", \"action\": \"CreateUser\", \"params\": { \"Name\": \"zhangsan\", \"NickName\": \"张三\", \"Type\": \"internalUser\", \"Phone\": \"13800138000\", \"Email\": \"zhangsan@example.com\", \"UserStatus\": \"ACTIVE\" } }`(注意:用户名用 Name 不是 UserName,类型用 Type 不是 UserType,状态用 UserStatus 不是 Status,且不要嵌套在 User 对象中);销毁环境:`{ \"service\": \"tcb\", \"action\": \"DestroyEnv\", \"params\": { \"EnvId\": \"env-xxx\", \"BypassCheck\": true } }`。若你的场景是通过 HTTP 协议直接集成 auth/functions/cloudrun/storage/mysqldb 等 CloudBase 业务 API,请优先使用 OpenAPI / Swagger 或 searchKnowledgeBase(mode=\"openapi\"),而不是优先使用 callCloudApi。",
),
},
annotations: {
readOnlyHint: false,
destructiveHint: true,
idempotentHint: false,
openWorldHint: true,
category: CATEGORY,
},
},
async ({
service,
action,
params,
}: {
service: AllowedService;
action: string;
params?: Record<string, any>;
}) => {
if (!ALLOWED_SERVICES.includes(service)) {
throw new Error(
`Service ${service} is not allowed. Allowed services: ${ALLOWED_SERVICES.join(", ")}`,
);
}
const cloudbase = await getManager();
// Auto-inject EnvId for tcb actions that require it but caller omitted it.
// Most tcb actions require EnvId; models frequently forget to include it,
// causing a wasted round-trip. We resolve the current environment ID from
// the auth context and inject it automatically.
let finalParams = params ?? {};
if (service === 'tcb' && !finalParams.EnvId) {
const tcbEntry = findTcbActionEntry(action);
if (!tcbEntry || tcbEntry.requiredKeys.includes('EnvId')) {
try {
const resolvedEnvId = await getEnvId(cloudBaseOptions);
finalParams = { ...finalParams, EnvId: resolvedEnvId };
} catch {
// If env resolution fails, let the API call proceed and
// produce its own "missing EnvId" error with guidance.
}
}
}
if (['1', 'true'].includes(process.env.CLOUDBASE_EVALUATE_MODE ?? '')) {
if (service === 'lowcode') {
throw new Error(`${service}/${action} Cloud API is not exposed or does not exist. Please use another API.`);
}
if (service === 'tcb') {
const tcbCapiForbidList = [
// 未明确对外的云API
'DescribeStorageACL', 'ModifyStorageACL', 'DescribeSecurityRule',
// 要下线的云API
"ListTables",
"DescribeCloudBaseGWAPI",
"DescribeCloudBaseGWService",
"CreateCloudBaseGWAPI",
"DeleteCloudBaseGWAPI",
"ModifyCloudBaseGWAPI",
"DeleteCloudBaseGWDomain",
"BindCloudBaseGWDomain",
"BindCloudBaseAccessDomain"
];
if (tcbCapiForbidList.includes(action)) {
throw new Error(`${service}/${action} Cloud API is not exposed or does not exist. Please use another API.`);
}
}
}
let result: unknown;
try {
result = await cloudbase.commonService(service).call({
Action: action,
Param: finalParams,
});
} catch (error) {
throw new Error(buildCapiErrorMessage(service, action, error));
}
logCloudBaseResult(logger, result);
return {
content: [
{
type: "text",
text: JSON.stringify(
result,
null,
2,
),
},
],
};
},
);
}