Skip to content

Commit e147051

Browse files
author
CodeBuddy Attribution Bot
committed
fix(attribution): CloudBase MCP 工具参数设计不清晰且错误提示不具体 (issue_mo8yrp43_f3zmbp)
1 parent e2ca73f commit e147051

2 files changed

Lines changed: 199 additions & 55 deletions

File tree

mcp/src/tools/functions.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { beforeEach, describe, expect, it, vi } from "vitest";
22
import {
33
buildFunctionOperationErrorMessage,
4+
buildFunctionQueryErrorMessage,
45
DEFAULT_RUNTIME,
56
registerFunctionTools,
67
resolveEventFunctionRuntime,
@@ -153,6 +154,40 @@ describe("functions tool helpers", () => {
153154
expect(() => resolveEventFunctionRuntime("Ruby3.2")).toThrow(/Python3.9/);
154155
});
155156

157+
it("provides specific parameter guidance for invalid parameter errors", () => {
158+
const message = buildFunctionQueryErrorMessage(
159+
"getFunctionDetail",
160+
{ action: "getFunctionDetail" },
161+
new Error("400 invalid parameter value"),
162+
);
163+
164+
expect(message).toContain("getFunctionDetail");
165+
expect(message).toContain("functionName");
166+
expect(message).toContain("必填");
167+
});
168+
169+
it("shows current input parameters when reporting errors", () => {
170+
const message = buildFunctionQueryErrorMessage(
171+
"getFunctionDetail",
172+
{ action: "getFunctionDetail", functionName: "testFunc" },
173+
new Error("invalid parameter"),
174+
);
175+
176+
expect(message).toContain("testFunc");
177+
expect(message).toContain("当前传入的参数");
178+
});
179+
180+
it("suggests listFunctions when function is not found", () => {
181+
const message = buildFunctionQueryErrorMessage(
182+
"getFunctionDetail",
183+
{ action: "getFunctionDetail", functionName: "nonExistentFunc" },
184+
new Error("Function not found"),
185+
);
186+
187+
expect(message).toContain("listFunctions");
188+
expect(message).toContain("nonExistentFunc");
189+
});
190+
156191
it("guides HTTP functions through anonymous-access follow-up without auto-creating gateway access", async () => {
157192
const result = await tools.manageFunctions.handler({
158193
action: "createFunction",

mcp/src/tools/functions.ts

Lines changed: 164 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,80 @@ function wrapFunctionOperationError(
436436
return wrappedError;
437437
}
438438

439+
/**
440+
* Build a helpful error message for function query operations
441+
* when the API returns generic errors like "invalid parameter value"
442+
*/
443+
export function buildFunctionQueryErrorMessage(
444+
action: string,
445+
input: QueryFunctionsInput,
446+
error: unknown,
447+
): string {
448+
const baseMessage = error instanceof Error ? error.message : String(error);
449+
const suggestions: string[] = [];
450+
451+
// Check for generic parameter errors from CloudBase API
452+
if (/invalid parameter|invalid param|||400/i.test(baseMessage)) {
453+
suggestions.push(`API 返回参数错误,请检查以下${action}必需的参数:`);
454+
455+
// Add specific parameter guidance based on action
456+
switch (action) {
457+
case "getFunctionDetail":
458+
suggestions.push("- functionName: 函数名称(必填,例如 'getUserInfo')");
459+
suggestions.push("- codeSecret: 代码保护密钥(可选,如果函数设置了代码保护则需要)");
460+
break;
461+
case "listFunctionLogs":
462+
suggestions.push("- functionName: 函数名称(必填)");
463+
suggestions.push("- startTime/endTime: 日志查询时间范围(可选,格式如 '2024-01-01 00:00:00')");
464+
suggestions.push("- offset/limit: 分页参数(可选)");
465+
break;
466+
case "listFunctionLayers":
467+
case "listFunctionTriggers":
468+
suggestions.push("- functionName: 函数名称(必填)");
469+
break;
470+
case "getFunctionLogDetail":
471+
suggestions.push("- requestId: 日志请求 ID(必填)");
472+
suggestions.push("- startTime/endTime: 查询时间范围(可选)");
473+
break;
474+
case "listLayerVersions":
475+
suggestions.push("- layerName: 层名称(必填)");
476+
break;
477+
case "getLayerVersionDetail":
478+
suggestions.push("- layerName: 层名称(必填)");
479+
suggestions.push("- layerVersion: 层版本号(必填,数字类型)");
480+
break;
481+
default:
482+
suggestions.push("- functionName: 函数名称(函数相关操作必填)");
483+
}
484+
485+
// Add the actual input received for debugging
486+
const relevantParams = Object.entries(input)
487+
.filter(([key, value]) => value !== undefined && key !== "action")
488+
.map(([key, value]) => `${key}=${JSON.stringify(value)}`);
489+
490+
if (relevantParams.length > 0) {
491+
suggestions.push(`\n当前传入的参数:${relevantParams.join(", ")}`);
492+
} else {
493+
suggestions.push("\n当前没有传入任何参数。");
494+
}
495+
}
496+
497+
// Check for function not found errors
498+
if (/function.*not found|.*|/i.test(baseMessage)) {
499+
suggestions.push("\n提示:函数可能不存在或名称拼写错误。");
500+
suggestions.push("请先使用 listFunctions 查看环境中所有函数。");
501+
if (input.functionName) {
502+
suggestions.push(`当前查询的函数名:'${input.functionName}'`);
503+
}
504+
}
505+
506+
if (suggestions.length === 0) {
507+
return `[${action}] ${baseMessage}`;
508+
}
509+
510+
return `[${action}] ${baseMessage}\n\n${suggestions.join("\n")}`;
511+
}
512+
439513
export function registerFunctionTools(server: ExtendedMcpServer) {
440514
const cloudBaseOptions = server.cloudBaseOptions;
441515
const getManager = () => getCloudBaseManager({ cloudBaseOptions });
@@ -451,15 +525,28 @@ export function registerFunctionTools(server: ExtendedMcpServer) {
451525
...(nextActions?.length ? { nextActions } : {}),
452526
});
453527

454-
const buildErrorEnvelope = (
455-
error: unknown,
456-
errorCode?: string,
457-
): Record<string, unknown> => ({
458-
success: false,
459-
data: {},
460-
message: error instanceof Error ? error.message : String(error),
461-
...(errorCode ? { errorCode } : {}),
462-
});
528+
const buildErrorEnvelope = (
529+
error: unknown,
530+
errorCode?: string,
531+
): Record<string, unknown> => ({
532+
success: false,
533+
data: {},
534+
message: error instanceof Error ? error.message : String(error),
535+
...(errorCode ? { errorCode } : {}),
536+
});
537+
538+
const withQueryEnvelope = async (
539+
action: string,
540+
input: QueryFunctionsInput,
541+
handler: () => Promise<FunctionToolEnvelope>,
542+
) => {
543+
try {
544+
return jsonContent(await handler());
545+
} catch (error) {
546+
const enhancedMessage = buildFunctionQueryErrorMessage(action, input, error);
547+
return jsonContent(buildErrorEnvelope(new Error(enhancedMessage)));
548+
}
549+
};
463550

464551
const withEnvelope = async (handler: () => Promise<FunctionToolEnvelope>) => {
465552
try {
@@ -561,43 +648,51 @@ export function registerFunctionTools(server: ExtendedMcpServer) {
561648
}
562649
case "getFunctionDetail": {
563650
if (!input.functionName) {
564-
throw new Error("getFunctionDetail 操作时,functionName 参数是必需的");
651+
throw new Error(
652+
"getFunctionDetail 操作时,functionName 参数是必需的\n\n" +
653+
"正确示例:{ action: 'getFunctionDetail', functionName: 'getUserInfo' }\n" +
654+
"请提供环境中已存在的函数名称,可使用 listFunctions 查看所有函数。"
655+
);
565656
}
566657
const cloudbase = await getManager();
567-
const result = await cloudbase.functions.getFunctionDetail(
568-
input.functionName,
569-
input.codeSecret,
570-
);
571-
logCloudBaseResult(server.logger, result);
572-
return buildEnvelope(
573-
{
574-
action: input.action,
575-
functionName: input.functionName,
576-
functionDetail: result,
577-
layers: normalizeFunctionLayers(result.Layers),
578-
triggers: result.Triggers || [],
579-
requestId: result.RequestId,
580-
raw: result,
581-
},
582-
`已获取函数 ${input.functionName} 的详情`,
583-
[
584-
{
585-
tool: "queryFunctions",
586-
action: "listFunctionLogs",
587-
reason: "查看该函数的执行日志",
588-
},
589-
{
590-
tool: "manageFunctions",
591-
action: "updateFunctionConfig",
592-
reason: "更新该函数配置",
593-
},
658+
try {
659+
const result = await cloudbase.functions.getFunctionDetail(
660+
input.functionName,
661+
input.codeSecret,
662+
);
663+
logCloudBaseResult(server.logger, result);
664+
return buildEnvelope(
594665
{
595-
tool: "queryGateway",
596-
action: "getAccess",
597-
reason: "查看该函数是否已暴露网关访问入口",
666+
action: input.action,
667+
functionName: input.functionName,
668+
functionDetail: result,
669+
layers: normalizeFunctionLayers(result.Layers),
670+
triggers: result.Triggers || [],
671+
requestId: result.RequestId,
672+
raw: result,
598673
},
599-
],
600-
);
674+
`已获取函数 ${input.functionName} 的详情`,
675+
[
676+
{
677+
tool: "queryFunctions",
678+
action: "listFunctionLogs",
679+
reason: "查看该函数的执行日志",
680+
},
681+
{
682+
tool: "manageFunctions",
683+
action: "updateFunctionConfig",
684+
reason: "更新该函数配置",
685+
},
686+
{
687+
tool: "queryGateway",
688+
action: "getAccess",
689+
reason: "查看该函数是否已暴露网关访问入口",
690+
},
691+
],
692+
);
693+
} catch (apiError) {
694+
throw new Error(buildFunctionQueryErrorMessage(input.action, input, apiError));
695+
}
601696
}
602697
case "listFunctionLogs": {
603698
if (!input.functionName) {
@@ -1416,19 +1511,33 @@ export function registerFunctionTools(server: ExtendedMcpServer) {
14161511
inputSchema: {
14171512
action: z
14181513
.enum(QUERY_FUNCTION_ACTIONS)
1419-
.describe("只读操作类型,例如 listFunctions、getFunctionDetail、listFunctionLogs"),
1420-
functionName: z.string().optional().describe("函数名称。函数相关 action 必填"),
1421-
limit: z.number().optional().describe("分页数量。列表类 action 可选"),
1422-
offset: z.number().optional().describe("分页偏移。列表类 action 可选"),
1423-
codeSecret: z.string().optional().describe("代码保护密钥"),
1424-
startTime: z.string().optional().describe("日志查询开始时间"),
1425-
endTime: z.string().optional().describe("日志查询结束时间"),
1426-
requestId: z.string().optional().describe("日志 requestId。获取日志详情时必填"),
1427-
qualifier: z.string().optional().describe("函数版本,日志查询时可选"),
1428-
runtime: z.string().optional().describe("层查询的运行时筛选"),
1429-
searchKey: z.string().optional().describe("层名称搜索关键字"),
1430-
layerName: z.string().optional().describe("层名称。层相关 action 必填"),
1431-
layerVersion: z.number().optional().describe("层版本号。获取层版本详情时必填"),
1514+
.describe(
1515+
"只读操作类型。\n" +
1516+
"- listFunctions: 列出所有函数(无需 functionName)\n" +
1517+
"- getFunctionDetail: 获取函数详情(必须提供 functionName)\n" +
1518+
"- listFunctionLogs: 获取函数日志(必须提供 functionName)\n" +
1519+
"- getFunctionLogDetail: 获取单条日志详情(必须提供 requestId)\n" +
1520+
"- listFunctionLayers: 获取函数绑定的层(必须提供 functionName)\n" +
1521+
"- listFunctionTriggers: 获取函数触发器(必须提供 functionName)\n" +
1522+
"- listLayers: 列出所有层(无需额外参数)\n" +
1523+
"- listLayerVersions: 获取层版本列表(必须提供 layerName)\n" +
1524+
"- getLayerVersionDetail: 获取层版本详情(必须提供 layerName 和 layerVersion)\n" +
1525+
"- getFunctionDownloadUrl: 获取函数代码下载链接(必须提供 functionName)"
1526+
),
1527+
functionName: z.string().optional().describe(
1528+
"函数名称。getFunctionDetail、listFunctionLogs、listFunctionLayers、listFunctionTriggers、getFunctionDownloadUrl 时必填"
1529+
),
1530+
limit: z.number().optional().describe("分页数量。listFunctions、listLayers 等列表类 action 可选,默认返回全部"),
1531+
offset: z.number().optional().describe("分页偏移。listFunctions、listLayers 等列表类 action 可选"),
1532+
codeSecret: z.string().optional().describe("代码保护密钥。如果函数设置了代码保护,获取详情或下载代码时需要提供"),
1533+
startTime: z.string().optional().describe("日志查询开始时间。格式如 '2024-01-15 10:00:00',listFunctionLogs/getFunctionLogDetail 可选"),
1534+
endTime: z.string().optional().describe("日志查询结束时间。格式如 '2024-01-15 11:00:00',与 startTime 间隔不能超过一天"),
1535+
requestId: z.string().optional().describe("日志请求 ID。getFunctionLogDetail 时必填,可从 listFunctionLogs 返回的日志中获取"),
1536+
qualifier: z.string().optional().describe("函数版本,如 $LATEST。listFunctionLogs 时可选"),
1537+
runtime: z.string().optional().describe("层查询的运行时筛选。listLayers 时可选,例如 Nodejs18.15、Python3.9"),
1538+
searchKey: z.string().optional().describe("层名称搜索关键字。listLayers 时可选,用于模糊搜索层名称"),
1539+
layerName: z.string().optional().describe("层名称。listLayerVersions、getLayerVersionDetail 时必填"),
1540+
layerVersion: z.number().optional().describe("层版本号。getLayerVersionDetail 时必填,正整数如 1、2、3"),
14321541
},
14331542
annotations: {
14341543
readOnlyHint: true,

0 commit comments

Comments
 (0)