Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 73 additions & 0 deletions mcp/src/tools/logs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,80 @@ describe("log tools", () => {
success: true,
data: {
action: "searchLogs",
aliasOf: "searchClsLog",
},
});
});

it("searchClsLog should expose the canonical log search entry", async () => {
const result = await tools.searchClsLog.handler({
queryString: 'request_id:"req-search-logs"',
startTime: "2026-04-01 00:00:00",
endTime: "2026-04-01 23:59:59",
limit: 5,
sort: "desc",
});
const payload = JSON.parse(result.content[0].text);

expect(mockSearchClsLog).toHaveBeenCalledWith({
queryString: 'request_id:"req-search-logs"',
StartTime: "2026-04-01 00:00:00",
EndTime: "2026-04-01 23:59:59",
Limit: 5,
Context: undefined,
Sort: "desc",
service: undefined,
});
expect(payload).toMatchObject({
success: true,
data: {
action: "searchClsLog",
aliasOf: "queryLogs(action=searchLogs)",
queryString: 'request_id:"req-search-logs"',
},
});
});

it("searchClsLog should register a read-only canonical entry", async () => {
expect(tools.searchClsLog).toBeDefined();
expect(tools.searchClsLog.meta.title).toBe("按 requestId 或 CLS 语句搜索日志");
expect(tools.searchClsLog.meta.annotations.readOnlyHint).toBe(true);

const result = await tools.searchClsLog.handler({
queryString: "level:error",
service: "tcbr",
startTime: "2026-04-01 00:00:00",
endTime: "2026-04-01 23:59:59",
limit: 5,
});
const payload = JSON.parse(result.content[0].text);

expect(mockSearchClsLog).toHaveBeenCalledWith({
queryString: "level:error",
StartTime: "2026-04-01 00:00:00",
EndTime: "2026-04-01 23:59:59",
Limit: 5,
Context: undefined,
Sort: undefined,
service: "tcbr",
});
expect(payload).toMatchObject({
success: true,
data: {
action: "searchClsLog",
aliasOf: "queryLogs(action=searchLogs)",
queryString: "level:error",
},
});
});

it("searchClsLog should error when queryString is missing", async () => {
const result = await tools.searchClsLog.handler({} as any);
const payload = JSON.parse(result.content[0].text);

expect(payload).toMatchObject({
success: false,
message: expect.stringContaining("queryString"),
});
});
});
145 changes: 123 additions & 22 deletions mcp/src/tools/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,24 @@ import { jsonContent } from "../utils/json-content.js";
const QUERY_LOG_ACTIONS = ["checkLogService", "searchLogs"] as const;

type QueryLogAction = (typeof QUERY_LOG_ACTIONS)[number];
type LogService = "tcb" | "tcbr";

type ToolEnvelope = {
success: boolean;
data: Record<string, unknown>;
message: string;
};

type SearchClsLogInput = {
queryString: string;
service?: LogService;
startTime?: string;
endTime?: string;
limit?: number;
context?: string;
sort?: "asc" | "desc";
};

function buildEnvelope(data: Record<string, unknown>, message: string): ToolEnvelope {
return {
success: true,
Expand All @@ -29,15 +40,58 @@ function buildErrorEnvelope(error: unknown): ToolEnvelope {
};
}

function buildSearchClsLogPayload({
queryString,
service,
startTime,
endTime,
limit,
context,
sort,
}: SearchClsLogInput) {
return {
queryString,
StartTime: startTime ?? "1970-01-01 00:00:00",
EndTime: endTime ?? "2099-12-31 23:59:59",
Limit: limit ?? 20,
Context: context,
Sort: sort,
service,
};
}

export function registerLogTools(server: ExtendedMcpServer) {
const cloudBaseOptions = server.cloudBaseOptions;
const getManager = () => getCloudBaseManager({ cloudBaseOptions });

const runSearchClsLog = async (
input: SearchClsLogInput,
action: "searchLogs" | "searchClsLog",
aliasOf?: string,
) => {
const cloudbase = await getManager();
const result = await cloudbase.log.searchClsLog(buildSearchClsLogPayload(input));
logCloudBaseResult(server.logger, result);
return jsonContent(
buildEnvelope(
{
action,
aliasOf,
queryString: input.queryString,
results: result.LogResults ?? null,
raw: result,
},
"日志检索成功",
),
);
};

server.registerTool?.(
"queryLogs",
{
title: "查询日志服务",
description: "日志域统一只读入口。支持检查日志服务状态并搜索 CLS 日志。",
description:
"日志域统一只读入口。支持检查日志服务状态,并通过 action=\"searchLogs\" 调用与 CloudBase Manager `searchClsLog` 对齐的 CLS 检索能力。(原工具名:searchClsLog,为兼容旧 AI 规则可继续使用该名称)",
inputSchema: {
action: z.enum(QUERY_LOG_ACTIONS),
queryString: z.string().optional(),
Expand Down Expand Up @@ -66,7 +120,7 @@ export function registerLogTools(server: ExtendedMcpServer) {
}: {
action: QueryLogAction;
queryString?: string;
service?: "tcb" | "tcbr";
service?: LogService;
startTime?: string;
endTime?: string;
limit?: number;
Expand All @@ -91,26 +145,73 @@ export function registerLogTools(server: ExtendedMcpServer) {
if (!queryString) {
throw new Error("action=searchLogs 时必须提供 queryString");
}
const result = await cloudbase.log.searchClsLog({
queryString,
StartTime: startTime ?? "1970-01-01 00:00:00",
EndTime: endTime ?? "2099-12-31 23:59:59",
Limit: limit ?? 20,
Context: context,
Sort: sort,
service,
});
logCloudBaseResult(server.logger, result);
return jsonContent(
buildEnvelope(
{
action,
queryString,
results: result.LogResults ?? null,
raw: result,
},
"日志检索成功",
),

return await runSearchClsLog(
{
queryString,
service,
startTime,
endTime,
limit,
context,
sort,
},
"searchLogs",
"searchClsLog",
);
} catch (error) {
return jsonContent(buildErrorEnvelope(error));
}
},
);

server.registerTool?.(
"searchClsLog",
{
title: "按 requestId 或 CLS 语句搜索日志",
description:
"直接暴露 CloudBase Manager `searchClsLog` 能力,适合按 requestId、关键词或完整 CLS 查询语句检索日志。与 queryLogs(action=\"searchLogs\") 等价,但名称更贴近底层能力。",
inputSchema: {
queryString: z.string().describe("CLS 查询语句,例如 request_id:\"<requestId>\" 或关键词条件"),
service: z.enum(["tcb", "tcbr"]).optional(),
startTime: z.string().optional(),
endTime: z.string().optional(),
limit: z.number().optional(),
context: z.string().optional(),
sort: z.enum(["asc", "desc"]).optional(),
},
annotations: {
readOnlyHint: true,
openWorldHint: true,
category: "logs",
},
},
async ({
queryString,
service,
startTime,
endTime,
limit,
context,
sort,
}: SearchClsLogInput) => {
try {
if (!queryString) {
throw new Error("必须提供 queryString");
}

return await runSearchClsLog(
{
queryString,
service,
startTime,
endTime,
limit,
context,
sort,
},
"searchClsLog",
"queryLogs(action=searchLogs)",
);
} catch (error) {
return jsonContent(buildErrorEnvelope(error));
Expand Down
Loading