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
97 changes: 97 additions & 0 deletions mcp/src/tools/databaseSQL.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -575,4 +575,101 @@ describe("SQL database tools", () => {
},
});
});

it("manageSqlDatabase(provisionMySQL) proceeds when DescribeCreateMySQLResult throws ResourceNotFound", async () => {
mockCommonServiceCall.mockImplementation(async ({ Action }: { Action: string }) => {
if (Action === "DescribeCreateMySQLResult") {
throw Object.assign(new Error("ResourceNotFound"), {
code: "ResourceNotFound",
});
}
if (Action === "CreateMySQL") {
return {
RequestId: "req-provision",
Data: {
TaskId: "38662",
},
};
}
throw new Error(`unexpected action: ${Action}`);
});

const { tools } = createMockServer();
const result = await tools.manageSqlDatabase.handler({
action: "provisionMySQL",
confirm: true,
});
const payload = JSON.parse(result.content[0].text);

expect(mockCommonServiceCall).toHaveBeenCalledWith(
expect.objectContaining({
Action: "CreateMySQL",
Param: expect.objectContaining({
EnvId: "env-test",
DbInstanceType: "MYSQL",
}),
}),
);
expect(payload).toMatchObject({
success: true,
data: {
task: {
request: {
TaskId: "38662",
},
},
},
});
});

it("manageSqlDatabase(provisionMySQL) proceeds when DescribeMySQLClusterDetail throws unexpected not-found error", async () => {
mockCommonServiceCall.mockImplementation(async ({ Action }: { Action: string }) => {
if (Action === "DescribeCreateMySQLResult") {
return {
RequestId: "req-create",
};
}
if (Action === "DescribeMySQLClusterDetail") {
throw Object.assign(new Error("cluster not found"), {
code: "ClusterNotFound",
});
}
if (Action === "CreateMySQL") {
return {
RequestId: "req-provision",
Data: {
TaskId: "38663",
},
};
}
throw new Error(`unexpected action: ${Action}`);
});

const { tools } = createMockServer();
const result = await tools.manageSqlDatabase.handler({
action: "provisionMySQL",
confirm: true,
});
const payload = JSON.parse(result.content[0].text);

expect(mockCommonServiceCall).toHaveBeenCalledWith(
expect.objectContaining({
Action: "CreateMySQL",
Param: expect.objectContaining({
EnvId: "env-test",
DbInstanceType: "MYSQL",
}),
}),
);
expect(payload).toMatchObject({
success: true,
data: {
task: {
request: {
TaskId: "38663",
},
},
},
});
});
});
92 changes: 76 additions & 16 deletions mcp/src/tools/databaseSQL.ts
Original file line number Diff line number Diff line change
Expand Up @@ -390,21 +390,72 @@ async function callSqlControlPlane(
}) as Promise<TcbServiceResult>;
}

function isNotFoundErrorCode(errorCode: string | undefined): boolean {
if (!errorCode) return false;
const upper = errorCode.toUpperCase();
return (
upper.includes("DATASOURCENOTEXIST") ||
upper.includes("RESOURCENOTFOUND") ||
upper.includes("NOTFOUND") ||
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Narrow not-found code matching to CloudBase API errors

isNotFoundErrorCode now treats any code containing NOTFOUND as “MySQL not created”, which also matches transport errors like Node’s ENOTFOUND (DNS lookup failure). Because this helper is reused in getSqlInstanceInfo, handleRunQuery, handleRunStatement, and handleInitializeSchema, a temporary network/DNS outage can be misreported as MYSQL_NOT_CREATED and even trigger an unnecessary CreateMySQL attempt instead of surfacing the real infrastructure error.

Useful? React with 👍 / 👎.

upper.includes("NOT_EXIST") ||
upper.includes("NOTEXIST") ||
upper.includes("DOESNOTEXIST") ||
upper.includes("CLUSTERNOTFOUND") ||
upper.includes("INSTANCENOTFOUND")
);
}

function isNotFoundErrorMessage(message: string | undefined): boolean {
if (!message) return false;
const lower = message.toLowerCase();
return (
lower.includes("database instance not found") ||
lower.includes("not found") ||
lower.includes("not exist") ||
lower.includes("does not exist") ||
lower.includes("no such") ||
lower.includes("cluster not found") ||
lower.includes("instance not found")
);
}

async function getSqlInstanceInfo({
getManager,
cloudBaseOptions,
server,
}: QueryManageContext): Promise<InstanceInfoResult> {
const cloudbase = await getManager();
const envId = await getEnvId(cloudBaseOptions);
const createResult = await callSqlControlPlane(cloudbase, "DescribeCreateMySQLResult", {
EnvId: envId,
});
logCloudBaseResult(server.logger, createResult);

const createData = pickDataPayload(createResult);
const createRawStatus = createData?.Status ?? pickLifecycleSource(createResult);
const createStatus = normalizeCreateResultStatus(createRawStatus);
let createResult: TcbServiceResult;
let createData: Record<string, unknown> | undefined;
let createRawStatus: unknown;
let createStatus: SqlLifecycleStatus;

try {
createResult = await callSqlControlPlane(cloudbase, "DescribeCreateMySQLResult", {
EnvId: envId,
});
logCloudBaseResult(server.logger, createResult);

createData = pickDataPayload(createResult);
createRawStatus = createData?.Status ?? pickLifecycleSource(createResult);
createStatus = normalizeCreateResultStatus(createRawStatus);
} catch (error) {
const errorCode = extractErrorCode(error);
if (isNotFoundErrorCode(errorCode) || isNotFoundErrorMessage((error as Error)?.message)) {
return {
exists: false,
envId,
instanceId: "default",
schema: envId,
rawStatus: null,
status: "NOT_CREATED",
createResult: undefined,
};
}
throw error;
}

if (createStatus === "NOT_CREATED") {
return {
Expand Down Expand Up @@ -471,20 +522,27 @@ async function getSqlInstanceInfo({
};
} catch (error) {
const errorCode = extractErrorCode(error);
const isNotFound = errorCode === "FailedOperation.DataSourceNotExist";
const isNotFound =
isNotFoundErrorCode(errorCode) ||
isNotFoundErrorMessage((error as Error)?.message);

if (!isNotFound) {
throw error;
}

const hasExplicitPendingStatus =
typeof createRawStatus === "string" &&
createRawStatus.trim().length > 0 &&
createStatus === "PENDING";

return {
exists: createStatus === "PENDING" || createStatus === "RUNNING",
exists: hasExplicitPendingStatus || createStatus === "RUNNING",
envId,
instanceId: "default",
schema: envId,
rawStatus:
typeof createRawStatus === "string" ? createRawStatus : null,
status: createStatus,
status: hasExplicitPendingStatus ? createStatus : "NOT_CREATED",
createResult: createData ?? createResult,
};
}
Expand Down Expand Up @@ -622,7 +680,7 @@ async function handleRunQuery(
logCloudBaseResult(context.server.logger, result);
} catch (error: any) {
const errorCode = typeof error === "object" && error && "code" in error ? (error as any).code : "";
if (errorCode === "FailedOperation.DataSourceNotExist" || error.message?.includes("Database instance not found")) {
if (isNotFoundErrorCode(errorCode) || isNotFoundErrorMessage(error.message)) {
return buildSqlToolResult({
success: false,
errorCode: "MYSQL_NOT_CREATED",
Expand Down Expand Up @@ -784,9 +842,11 @@ async function handleProvisionMySQL(

const existing = await getSqlInstanceInfo(context);
if (
existing.status === "READY" ||
existing.status === "PENDING" ||
existing.status === "RUNNING"
existing.exists && (
existing.status === "READY" ||
existing.status === "PENDING" ||
existing.status === "RUNNING"
)
) {
return buildSqlToolResult({
success: true,
Expand Down Expand Up @@ -991,7 +1051,7 @@ async function handleRunStatement(
logCloudBaseResult(context.server.logger, result);
} catch (error: any) {
const errorCode = typeof error === "object" && error && "code" in error ? (error as any).code : "";
if (errorCode === "FailedOperation.DataSourceNotExist" || error.message?.includes("Database instance not found")) {
if (isNotFoundErrorCode(errorCode) || isNotFoundErrorMessage(error.message)) {
return buildSqlToolResult({
success: false,
errorCode: "MYSQL_NOT_CREATED",
Expand Down Expand Up @@ -1156,7 +1216,7 @@ async function handleInitializeSchema(
logCloudBaseResult(context.server.logger, result);
} catch (error: any) {
const errorCode = typeof error === "object" && error && "code" in error ? (error as any).code : "";
if (errorCode === "FailedOperation.DataSourceNotExist" || error.message?.includes("Database instance not found")) {
if (isNotFoundErrorCode(errorCode) || isNotFoundErrorMessage(error.message)) {
return buildSqlToolResult({
success: false,
errorCode: "MYSQL_NOT_CREATED",
Expand Down
Loading