@cloudbase/cloudbase-mcp 2.23.10 → 2.24.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.cjs +3 -3
- package/dist/index.cjs +698 -329
- package/dist/index.js +698 -329
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2200,7 +2200,7 @@ ${envIdSection}
|
|
|
2200
2200
|
## 环境信息
|
|
2201
2201
|
- 操作系统: ${os_1.default.type()} ${os_1.default.release()}
|
|
2202
2202
|
- Node.js版本: ${process.version}
|
|
2203
|
-
- MCP 版本:${process.env.npm_package_version || "2.
|
|
2203
|
+
- MCP 版本:${process.env.npm_package_version || "2.24.0" || 0}
|
|
2204
2204
|
- 系统架构: ${os_1.default.arch()}
|
|
2205
2205
|
- 时间: ${new Date().toISOString()}
|
|
2206
2206
|
- 请求ID: ${requestId}
|
|
@@ -2281,7 +2281,7 @@ function createWrappedHandler(name, handler, server) {
|
|
|
2281
2281
|
requestId: (typeof error === 'object' && error && 'requestId' in error) ? error.requestId : '',
|
|
2282
2282
|
ide: server.ide || process.env.INTEGRATION_IDE || ''
|
|
2283
2283
|
});
|
|
2284
|
-
const mcpVersion = true ? "2.
|
|
2284
|
+
const mcpVersion = true ? "2.24.0" : 0;
|
|
2285
2285
|
const requestIdSuffix = requestId ? `\n🆔 RequestId: ${requestId}` : '';
|
|
2286
2286
|
const enhancedErrorMessage = `${errorMessage}${requestIdSuffix}\n\n📦 CloudBase MCP v${mcpVersion}\n🔗 遇到问题?请复制以下链接到浏览器打开\n即可自动携带错误详情快速创建 GitHub Issue:\n${issueLink}`;
|
|
2287
2287
|
// 创建新的错误对象,保持原有的错误类型但更新消息
|
|
@@ -7771,6 +7771,7 @@ const os = __importStar(__webpack_require__(8116));
|
|
|
7771
7771
|
const path = __importStar(__webpack_require__(2521));
|
|
7772
7772
|
const zod_1 = __webpack_require__(2971);
|
|
7773
7773
|
const cloudbase_manager_js_1 = __webpack_require__(3431);
|
|
7774
|
+
const tool_result_js_1 = __webpack_require__(9835);
|
|
7774
7775
|
const MAX_INLINE_TEXT_BYTES = 256 * 1024;
|
|
7775
7776
|
const STORAGE_READ_TEMP_PREFIX = 'cloudbase-mcp-storage-read-';
|
|
7776
7777
|
// Input schema for queryStorage tool
|
|
@@ -7853,7 +7854,7 @@ function registerStorageTools(server) {
|
|
|
7853
7854
|
// Tool 1: queryStorage - 查询存储信息(只读操作)
|
|
7854
7855
|
server.registerTool("queryStorage", {
|
|
7855
7856
|
title: "查询 CloudBase 存储信息",
|
|
7856
|
-
description: "查询 CloudBase 云存储信息,支持列出目录文件、获取文件信息、获取临时下载链接等只读操作。返回的文件信息包括文件名、大小、修改时间、下载链接等。注意:action=url 返回的 temporaryUrl 是临时签名链接,有效期由 maxAge 参数决定(默认1小时),不要当作永久公网地址使用。工具还会基于 DescribeEnvs 返回的 Storages[0].CdnDomain 推导 publicUrl,⚠️ 警告:publicUrl 仅在存储桶 ACL 为公有读(所有用户可读)时才能被匿名访问;默认私有读写存储桶返回的 publicUrl 会 403,此时请继续使用 temporaryUrl 或先通过控制台/SDK 将目标路径设置为公有读。\n\n💡 存储桶 ACL 权限管理请使用 permissions 工具:queryPermissions(action=\"getResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\") 查询,managePermissions(action=\"updateResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\", permission=\"READONLY\") 设置。\n\n📦 CloudBase PG / pgstore 环境:`DescribeEnvs.Storages[]` 列出的 bucket 是旧 NoSQL 后端的,不等于 pgstore bucket。本工具用于查看常规存储;为 PG 浏览器上传准备 bucket 时,请确认目标 bucket 是 pgstore 后端可用的,否则浏览器 `app.storage.from().upload(...)` 会得到 `STORAGE_BUCKET_NOT_FOUND` 并出现 `PUT https://undefined/`。",
|
|
7857
|
+
description: "⚠️ PG 模式环境请使用 queryPgStorage 而非本工具(pgstore 与旧 COS 是两套独立系统)。\n\n查询 CloudBase 云存储信息,支持列出目录文件、获取文件信息、获取临时下载链接等只读操作。返回的文件信息包括文件名、大小、修改时间、下载链接等。注意:action=url 返回的 temporaryUrl 是临时签名链接,有效期由 maxAge 参数决定(默认1小时),不要当作永久公网地址使用。工具还会基于 DescribeEnvs 返回的 Storages[0].CdnDomain 推导 publicUrl,⚠️ 警告:publicUrl 仅在存储桶 ACL 为公有读(所有用户可读)时才能被匿名访问;默认私有读写存储桶返回的 publicUrl 会 403,此时请继续使用 temporaryUrl 或先通过控制台/SDK 将目标路径设置为公有读。\n\n💡 存储桶 ACL 权限管理请使用 permissions 工具:queryPermissions(action=\"getResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\") 查询,managePermissions(action=\"updateResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\", permission=\"READONLY\") 设置。\n\n📦 CloudBase PG / pgstore 环境:`DescribeEnvs.Storages[]` 列出的 bucket 是旧 NoSQL 后端的,不等于 pgstore bucket。本工具用于查看常规存储;为 PG 浏览器上传准备 bucket 时,请确认目标 bucket 是 pgstore 后端可用的,否则浏览器 `app.storage.from().upload(...)` 会得到 `STORAGE_BUCKET_NOT_FOUND` 并出现 `PUT https://undefined/`。",
|
|
7857
7858
|
inputSchema: queryStorageInputSchema,
|
|
7858
7859
|
annotations: {
|
|
7859
7860
|
readOnlyHint: true,
|
|
@@ -7862,125 +7863,89 @@ function registerStorageTools(server) {
|
|
|
7862
7863
|
}
|
|
7863
7864
|
}, async (args) => {
|
|
7864
7865
|
const input = args;
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
7868
|
-
|
|
7869
|
-
const storageService = manager.storage;
|
|
7870
|
-
switch (input.action) {
|
|
7871
|
-
case 'list': {
|
|
7872
|
-
let files;
|
|
7873
|
-
if (storageOverrides?.listFiles) {
|
|
7874
|
-
files = await storageOverrides.listFiles({ cloudPath: input.cloudPath });
|
|
7875
|
-
}
|
|
7876
|
-
else {
|
|
7877
|
-
const result = await storageService.listDirectoryFiles(input.cloudPath);
|
|
7878
|
-
files = result || [];
|
|
7879
|
-
}
|
|
7880
|
-
return {
|
|
7881
|
-
content: [
|
|
7882
|
-
{
|
|
7883
|
-
type: "text",
|
|
7884
|
-
text: JSON.stringify({
|
|
7885
|
-
success: true,
|
|
7886
|
-
data: {
|
|
7887
|
-
action: 'list',
|
|
7888
|
-
cloudPath: input.cloudPath,
|
|
7889
|
-
files,
|
|
7890
|
-
totalCount: files.length
|
|
7891
|
-
},
|
|
7892
|
-
message: `Successfully listed ${files.length} files in directory '${input.cloudPath}'`
|
|
7893
|
-
}, null, 2)
|
|
7894
|
-
}
|
|
7895
|
-
]
|
|
7896
|
-
};
|
|
7897
|
-
}
|
|
7898
|
-
case 'info': {
|
|
7899
|
-
let fileInfo;
|
|
7900
|
-
if (storageOverrides?.getFileInfo) {
|
|
7901
|
-
fileInfo = await storageOverrides.getFileInfo({ cloudPath: input.cloudPath });
|
|
7902
|
-
}
|
|
7903
|
-
else {
|
|
7904
|
-
fileInfo = await storageService.getFileInfo(input.cloudPath);
|
|
7905
|
-
}
|
|
7906
|
-
return {
|
|
7907
|
-
content: [
|
|
7908
|
-
{
|
|
7909
|
-
type: "text",
|
|
7910
|
-
text: JSON.stringify({
|
|
7911
|
-
success: true,
|
|
7912
|
-
data: {
|
|
7913
|
-
action: 'info',
|
|
7914
|
-
cloudPath: input.cloudPath,
|
|
7915
|
-
fileInfo
|
|
7916
|
-
},
|
|
7917
|
-
message: `Successfully retrieved file info for '${input.cloudPath}'`
|
|
7918
|
-
}, null, 2)
|
|
7919
|
-
}
|
|
7920
|
-
]
|
|
7921
|
-
};
|
|
7866
|
+
try {
|
|
7867
|
+
const manager = await getManager();
|
|
7868
|
+
if (!manager) {
|
|
7869
|
+
throw new Error("Failed to initialize CloudBase manager. Please check your credentials and environment configuration.");
|
|
7922
7870
|
}
|
|
7923
|
-
|
|
7924
|
-
|
|
7925
|
-
|
|
7926
|
-
|
|
7927
|
-
|
|
7928
|
-
cloudPath: input.cloudPath
|
|
7929
|
-
|
|
7930
|
-
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
|
|
7934
|
-
|
|
7935
|
-
|
|
7936
|
-
|
|
7937
|
-
|
|
7938
|
-
|
|
7939
|
-
|
|
7940
|
-
|
|
7871
|
+
const storageService = manager.storage;
|
|
7872
|
+
switch (input.action) {
|
|
7873
|
+
case 'list': {
|
|
7874
|
+
let files;
|
|
7875
|
+
if (storageOverrides?.listFiles) {
|
|
7876
|
+
files = await storageOverrides.listFiles({ cloudPath: input.cloudPath });
|
|
7877
|
+
}
|
|
7878
|
+
else {
|
|
7879
|
+
const result = await storageService.listDirectoryFiles(input.cloudPath);
|
|
7880
|
+
files = result || [];
|
|
7881
|
+
}
|
|
7882
|
+
return {
|
|
7883
|
+
content: [
|
|
7884
|
+
{
|
|
7885
|
+
type: "text",
|
|
7886
|
+
text: JSON.stringify({
|
|
7887
|
+
success: true,
|
|
7888
|
+
data: {
|
|
7889
|
+
action: 'list',
|
|
7890
|
+
cloudPath: input.cloudPath,
|
|
7891
|
+
files,
|
|
7892
|
+
totalCount: files.length
|
|
7893
|
+
},
|
|
7894
|
+
message: `Successfully listed ${files.length} files in directory '${input.cloudPath}'`
|
|
7895
|
+
}, null, 2)
|
|
7896
|
+
}
|
|
7897
|
+
]
|
|
7898
|
+
};
|
|
7941
7899
|
}
|
|
7942
|
-
|
|
7943
|
-
|
|
7944
|
-
|
|
7945
|
-
|
|
7946
|
-
});
|
|
7947
|
-
return {
|
|
7948
|
-
content: [
|
|
7949
|
-
{
|
|
7950
|
-
type: "text",
|
|
7951
|
-
text: JSON.stringify({
|
|
7952
|
-
success: true,
|
|
7953
|
-
data: {
|
|
7954
|
-
action: 'url',
|
|
7955
|
-
cloudPath: input.cloudPath,
|
|
7956
|
-
temporaryUrl,
|
|
7957
|
-
expireTime: `${input.maxAge || 3600}秒`,
|
|
7958
|
-
fileId,
|
|
7959
|
-
storageCdnDomain: publicAccess.storageCdnDomain,
|
|
7960
|
-
publicUrl: publicAccess.publicUrl,
|
|
7961
|
-
note: "temporaryUrl 是临时签名链接,会按 expireTime 过期。publicUrl 基于 DescribeEnvs 返回的 Storages[0].CdnDomain 推导,⚠️ 仅在存储桶 ACL 为公有读(所有用户可读)时才能被匿名访问;默认私有读写存储桶返回的 publicUrl 会 403,此时请继续使用 temporaryUrl 或先将目标路径设置为公有读。"
|
|
7962
|
-
},
|
|
7963
|
-
message: `Successfully generated temporary URL for '${input.cloudPath}'`
|
|
7964
|
-
}, null, 2)
|
|
7965
|
-
}
|
|
7966
|
-
]
|
|
7967
|
-
};
|
|
7968
|
-
}
|
|
7969
|
-
case 'read': {
|
|
7970
|
-
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), STORAGE_READ_TEMP_PREFIX));
|
|
7971
|
-
const localPath = path.join(tempDir, getStorageTempFileName(input.cloudPath));
|
|
7972
|
-
try {
|
|
7973
|
-
if (storageOverrides?.downloadFile) {
|
|
7974
|
-
await storageOverrides.downloadFile({ cloudPath: input.cloudPath, localPath });
|
|
7900
|
+
case 'info': {
|
|
7901
|
+
let fileInfo;
|
|
7902
|
+
if (storageOverrides?.getFileInfo) {
|
|
7903
|
+
fileInfo = await storageOverrides.getFileInfo({ cloudPath: input.cloudPath });
|
|
7975
7904
|
}
|
|
7976
7905
|
else {
|
|
7977
|
-
await storageService.
|
|
7906
|
+
fileInfo = await storageService.getFileInfo(input.cloudPath);
|
|
7907
|
+
}
|
|
7908
|
+
return {
|
|
7909
|
+
content: [
|
|
7910
|
+
{
|
|
7911
|
+
type: "text",
|
|
7912
|
+
text: JSON.stringify({
|
|
7913
|
+
success: true,
|
|
7914
|
+
data: {
|
|
7915
|
+
action: 'info',
|
|
7916
|
+
cloudPath: input.cloudPath,
|
|
7917
|
+
fileInfo
|
|
7918
|
+
},
|
|
7919
|
+
message: `Successfully retrieved file info for '${input.cloudPath}'`
|
|
7920
|
+
}, null, 2)
|
|
7921
|
+
}
|
|
7922
|
+
]
|
|
7923
|
+
};
|
|
7924
|
+
}
|
|
7925
|
+
case 'url': {
|
|
7926
|
+
let temporaryUrl = "";
|
|
7927
|
+
let fileId = "";
|
|
7928
|
+
if (storageOverrides?.getFileUrl) {
|
|
7929
|
+
const urlResult = await storageOverrides.getFileUrl({
|
|
7978
7930
|
cloudPath: input.cloudPath,
|
|
7979
|
-
|
|
7931
|
+
maxAge: input.maxAge || 3600,
|
|
7980
7932
|
});
|
|
7933
|
+
temporaryUrl = urlResult.url;
|
|
7934
|
+
fileId = urlResult.fileId || "";
|
|
7981
7935
|
}
|
|
7982
|
-
|
|
7983
|
-
|
|
7936
|
+
else {
|
|
7937
|
+
const result = await storageService.getTemporaryUrl([{
|
|
7938
|
+
cloudPath: input.cloudPath,
|
|
7939
|
+
maxAge: input.maxAge || 3600
|
|
7940
|
+
}]);
|
|
7941
|
+
temporaryUrl = result[0]?.url || "";
|
|
7942
|
+
fileId = result[0]?.fileId || "";
|
|
7943
|
+
}
|
|
7944
|
+
const publicAccess = await resolveStoragePublicAccess({
|
|
7945
|
+
cloudPath: input.cloudPath,
|
|
7946
|
+
cloudBaseOptions,
|
|
7947
|
+
manager,
|
|
7948
|
+
});
|
|
7984
7949
|
return {
|
|
7985
7950
|
content: [
|
|
7986
7951
|
{
|
|
@@ -7988,33 +7953,80 @@ function registerStorageTools(server) {
|
|
|
7988
7953
|
text: JSON.stringify({
|
|
7989
7954
|
success: true,
|
|
7990
7955
|
data: {
|
|
7991
|
-
action: '
|
|
7956
|
+
action: 'url',
|
|
7992
7957
|
cloudPath: input.cloudPath,
|
|
7993
|
-
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7958
|
+
temporaryUrl,
|
|
7959
|
+
expireTime: `${input.maxAge || 3600}秒`,
|
|
7960
|
+
fileId,
|
|
7961
|
+
storageCdnDomain: publicAccess.storageCdnDomain,
|
|
7962
|
+
publicUrl: publicAccess.publicUrl,
|
|
7963
|
+
note: "temporaryUrl 是临时签名链接,会按 expireTime 过期。publicUrl 基于 DescribeEnvs 返回的 Storages[0].CdnDomain 推导,⚠️ 仅在存储桶 ACL 为公有读(所有用户可读)时才能被匿名访问;默认私有读写存储桶返回的 publicUrl 会 403,此时请继续使用 temporaryUrl 或先将目标路径设置为公有读。"
|
|
7997
7964
|
},
|
|
7998
|
-
message:
|
|
7999
|
-
? `Successfully read text content for '${input.cloudPath}' (truncated to ${MAX_INLINE_TEXT_BYTES} bytes)`
|
|
8000
|
-
: `Successfully read text content for '${input.cloudPath}'`
|
|
7965
|
+
message: `Successfully generated temporary URL for '${input.cloudPath}'`
|
|
8001
7966
|
}, null, 2)
|
|
8002
7967
|
}
|
|
8003
7968
|
]
|
|
8004
7969
|
};
|
|
8005
7970
|
}
|
|
8006
|
-
|
|
8007
|
-
await fs.
|
|
7971
|
+
case 'read': {
|
|
7972
|
+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), STORAGE_READ_TEMP_PREFIX));
|
|
7973
|
+
const localPath = path.join(tempDir, getStorageTempFileName(input.cloudPath));
|
|
7974
|
+
try {
|
|
7975
|
+
if (storageOverrides?.downloadFile) {
|
|
7976
|
+
await storageOverrides.downloadFile({ cloudPath: input.cloudPath, localPath });
|
|
7977
|
+
}
|
|
7978
|
+
else {
|
|
7979
|
+
await storageService.downloadFile({
|
|
7980
|
+
cloudPath: input.cloudPath,
|
|
7981
|
+
localPath
|
|
7982
|
+
});
|
|
7983
|
+
}
|
|
7984
|
+
const buffer = await fs.readFile(localPath);
|
|
7985
|
+
const decoded = decodeInlineTextContent(buffer);
|
|
7986
|
+
return {
|
|
7987
|
+
content: [
|
|
7988
|
+
{
|
|
7989
|
+
type: "text",
|
|
7990
|
+
text: JSON.stringify({
|
|
7991
|
+
success: true,
|
|
7992
|
+
data: {
|
|
7993
|
+
action: 'read',
|
|
7994
|
+
cloudPath: input.cloudPath,
|
|
7995
|
+
content: decoded.content,
|
|
7996
|
+
encoding: decoded.encoding,
|
|
7997
|
+
sizeBytes: decoded.sizeBytes,
|
|
7998
|
+
truncated: decoded.truncated
|
|
7999
|
+
},
|
|
8000
|
+
message: decoded.truncated
|
|
8001
|
+
? `Successfully read text content for '${input.cloudPath}' (truncated to ${MAX_INLINE_TEXT_BYTES} bytes)`
|
|
8002
|
+
: `Successfully read text content for '${input.cloudPath}'`
|
|
8003
|
+
}, null, 2)
|
|
8004
|
+
}
|
|
8005
|
+
]
|
|
8006
|
+
};
|
|
8007
|
+
}
|
|
8008
|
+
finally {
|
|
8009
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
8010
|
+
}
|
|
8008
8011
|
}
|
|
8012
|
+
default:
|
|
8013
|
+
throw new Error(`Unsupported action: ${input.action}`);
|
|
8009
8014
|
}
|
|
8010
|
-
|
|
8011
|
-
|
|
8015
|
+
}
|
|
8016
|
+
catch (error) {
|
|
8017
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8018
|
+
return (0, tool_result_js_1.buildJsonToolResult)({
|
|
8019
|
+
success: false,
|
|
8020
|
+
message: `queryStorage 操作失败: ${message}`,
|
|
8021
|
+
action: input.action,
|
|
8022
|
+
cloudPath: input.cloudPath,
|
|
8023
|
+
});
|
|
8012
8024
|
}
|
|
8013
8025
|
});
|
|
8014
8026
|
// Tool 2: manageStorage - 管理存储文件(写操作)
|
|
8015
8027
|
server.registerTool("manageStorage", {
|
|
8016
8028
|
title: "管理 CloudBase 存储文件",
|
|
8017
|
-
description: "管理 CloudBase 云存储文件,仅用于 COS/Storage 对象,不用于静态网站托管。支持上传文件/目录、下载文件/目录、删除文件/目录等操作。删除操作需要设置force=true进行确认,防止误删除重要文件。注意:上传后返回的 temporaryUrl 是临时签名链接,1小时后过期,不要当作永久公网地址写入配置或持久化存储。工具还会基于 DescribeEnvs 返回的 Storages[0].CdnDomain 推导 publicUrl,⚠️ 警告:publicUrl 仅在存储桶 ACL 为公有读(所有用户可读)时才能被匿名访问;默认私有读写存储桶返回的 publicUrl 会 403,此时请继续使用 temporaryUrl 或先通过控制台/SDK 将目标路径设置为公有读。\n\n💡 存储桶 ACL 权限管理请使用 permissions 工具:queryPermissions(action=\"getResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\") 查询,managePermissions(action=\"updateResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\", permission=\"READONLY\") 设置。\n\n📦 CloudBase PG / pgstore 桶必须先创建后使用(与 Supabase Storage 一致:upload 前 bucket 必须存在)。浏览器 SDK `app.storage.from().upload(path, file)` 不会自动建桶,且 `path` 的第一段就是 bucket 名(例如 `covers/foo.png` → bucket=`covers`);`from('covers')` 这个参数当前不会被拼到 path 里。如果上传时浏览器看到 `STORAGE_BUCKET_NOT_FOUND` 或 `PUT https://undefined/`(DevTools 表现为 `net::ERR_NAME_NOT_RESOLVED`),先用本工具或控制台确认 / 创建对应的 pgstore bucket,再让前端重试上传,不要让前端把上传失败静默吞掉。`DescribeEnvs.Storages[]` 返回的旧 NoSQL bucket(形如 `<hash>-<envId>-<appId>`)不是可用的 pgstore bucket,切勿当作默认目标使用。",
|
|
8029
|
+
description: "⚠️ PG 模式环境请使用 queryPgStorage 而非本工具(pgstore 与旧 COS 是两套独立系统)。\n\n管理 CloudBase 云存储文件,仅用于 COS/Storage 对象,不用于静态网站托管。支持上传文件/目录、下载文件/目录、删除文件/目录等操作。删除操作需要设置force=true进行确认,防止误删除重要文件。注意:上传后返回的 temporaryUrl 是临时签名链接,1小时后过期,不要当作永久公网地址写入配置或持久化存储。工具还会基于 DescribeEnvs 返回的 Storages[0].CdnDomain 推导 publicUrl,⚠️ 警告:publicUrl 仅在存储桶 ACL 为公有读(所有用户可读)时才能被匿名访问;默认私有读写存储桶返回的 publicUrl 会 403,此时请继续使用 temporaryUrl 或先通过控制台/SDK 将目标路径设置为公有读。\n\n💡 存储桶 ACL 权限管理请使用 permissions 工具:queryPermissions(action=\"getResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\") 查询,managePermissions(action=\"updateResourcePermission\", resourceType=\"storage\", resourceId=\"bucket-name\", permission=\"READONLY\") 设置。\n\n📦 CloudBase PG / pgstore 桶必须先创建后使用(与 Supabase Storage 一致:upload 前 bucket 必须存在)。浏览器 SDK `app.storage.from().upload(path, file)` 不会自动建桶,且 `path` 的第一段就是 bucket 名(例如 `covers/foo.png` → bucket=`covers`);`from('covers')` 这个参数当前不会被拼到 path 里。如果上传时浏览器看到 `STORAGE_BUCKET_NOT_FOUND` 或 `PUT https://undefined/`(DevTools 表现为 `net::ERR_NAME_NOT_RESOLVED`),先用本工具或控制台确认 / 创建对应的 pgstore bucket,再让前端重试上传,不要让前端把上传失败静默吞掉。`DescribeEnvs.Storages[]` 返回的旧 NoSQL bucket(形如 `<hash>-<envId>-<appId>`)不是可用的 pgstore bucket,切勿当作默认目标使用。",
|
|
8018
8030
|
inputSchema: manageStorageInputSchema,
|
|
8019
8031
|
annotations: {
|
|
8020
8032
|
readOnlyHint: false,
|
|
@@ -8025,196 +8037,207 @@ function registerStorageTools(server) {
|
|
|
8025
8037
|
}
|
|
8026
8038
|
}, async (args) => {
|
|
8027
8039
|
const input = args;
|
|
8028
|
-
|
|
8029
|
-
|
|
8030
|
-
|
|
8031
|
-
|
|
8032
|
-
|
|
8033
|
-
|
|
8034
|
-
|
|
8035
|
-
|
|
8036
|
-
|
|
8037
|
-
|
|
8038
|
-
if (
|
|
8039
|
-
|
|
8040
|
+
try {
|
|
8041
|
+
const manager = await getManager();
|
|
8042
|
+
if (!manager) {
|
|
8043
|
+
throw new Error("Failed to initialize CloudBase manager. Please check your credentials and environment configuration.");
|
|
8044
|
+
}
|
|
8045
|
+
const storageService = manager.storage;
|
|
8046
|
+
switch (input.action) {
|
|
8047
|
+
case 'upload': {
|
|
8048
|
+
const localPath = input.localPath;
|
|
8049
|
+
const cloudPath = input.cloudPath;
|
|
8050
|
+
if (input.isDirectory) {
|
|
8051
|
+
if (storageOverrides?.uploadDirectory) {
|
|
8052
|
+
await storageOverrides.uploadDirectory({ localPath, cloudPath });
|
|
8053
|
+
}
|
|
8054
|
+
else {
|
|
8055
|
+
await storageService.uploadDirectory({
|
|
8056
|
+
localPath,
|
|
8057
|
+
cloudPath,
|
|
8058
|
+
onProgress: (progressData) => {
|
|
8059
|
+
console.log("Upload directory progress:", progressData);
|
|
8060
|
+
}
|
|
8061
|
+
});
|
|
8062
|
+
}
|
|
8040
8063
|
}
|
|
8041
8064
|
else {
|
|
8042
|
-
|
|
8043
|
-
localPath,
|
|
8044
|
-
|
|
8045
|
-
|
|
8046
|
-
|
|
8047
|
-
|
|
8048
|
-
|
|
8065
|
+
if (storageOverrides?.uploadFile) {
|
|
8066
|
+
await storageOverrides.uploadFile({ localPath, cloudPath });
|
|
8067
|
+
}
|
|
8068
|
+
else {
|
|
8069
|
+
await storageService.uploadFile({
|
|
8070
|
+
localPath,
|
|
8071
|
+
cloudPath,
|
|
8072
|
+
onProgress: (progressData) => {
|
|
8073
|
+
console.log("Upload file progress:", progressData);
|
|
8074
|
+
}
|
|
8075
|
+
});
|
|
8076
|
+
}
|
|
8049
8077
|
}
|
|
8050
|
-
|
|
8051
|
-
|
|
8052
|
-
|
|
8053
|
-
|
|
8078
|
+
// 上传成功后获取临时 URL 和公网访问信息
|
|
8079
|
+
let temporaryUrl = "";
|
|
8080
|
+
let fileId = "";
|
|
8081
|
+
if (storageOverrides?.getFileUrl) {
|
|
8082
|
+
const urlResult = await storageOverrides.getFileUrl({
|
|
8083
|
+
cloudPath: input.cloudPath,
|
|
8084
|
+
maxAge: 3600,
|
|
8085
|
+
});
|
|
8086
|
+
temporaryUrl = urlResult.url;
|
|
8087
|
+
fileId = urlResult.fileId || "";
|
|
8054
8088
|
}
|
|
8055
8089
|
else {
|
|
8056
|
-
|
|
8057
|
-
|
|
8058
|
-
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
|
|
8062
|
-
|
|
8090
|
+
try {
|
|
8091
|
+
const fileUrls = await storageService.getTemporaryUrl([{
|
|
8092
|
+
cloudPath: input.cloudPath,
|
|
8093
|
+
maxAge: 3600
|
|
8094
|
+
}]);
|
|
8095
|
+
temporaryUrl = fileUrls[0]?.url || "";
|
|
8096
|
+
fileId = fileUrls[0]?.fileId || "";
|
|
8097
|
+
}
|
|
8098
|
+
catch {
|
|
8099
|
+
// StorageOverrides 下 getTemporaryUrl 可能不可用
|
|
8100
|
+
}
|
|
8063
8101
|
}
|
|
8064
|
-
|
|
8065
|
-
// 上传成功后获取临时 URL 和公网访问信息
|
|
8066
|
-
let temporaryUrl = "";
|
|
8067
|
-
let fileId = "";
|
|
8068
|
-
if (storageOverrides?.getFileUrl) {
|
|
8069
|
-
const urlResult = await storageOverrides.getFileUrl({
|
|
8070
|
-
cloudPath: input.cloudPath,
|
|
8071
|
-
maxAge: 3600,
|
|
8072
|
-
});
|
|
8073
|
-
temporaryUrl = urlResult.url;
|
|
8074
|
-
fileId = urlResult.fileId || "";
|
|
8075
|
-
}
|
|
8076
|
-
else {
|
|
8102
|
+
let publicAccess = {};
|
|
8077
8103
|
try {
|
|
8078
|
-
|
|
8079
|
-
|
|
8080
|
-
|
|
8081
|
-
|
|
8082
|
-
|
|
8083
|
-
fileId = fileUrls[0]?.fileId || "";
|
|
8104
|
+
publicAccess = await resolveStoragePublicAccess({
|
|
8105
|
+
cloudPath: input.cloudPath,
|
|
8106
|
+
cloudBaseOptions,
|
|
8107
|
+
manager,
|
|
8108
|
+
});
|
|
8084
8109
|
}
|
|
8085
8110
|
catch {
|
|
8086
|
-
//
|
|
8111
|
+
// DescribeEnvs 可能不支持(IDE 模式下会绕过)
|
|
8112
|
+
publicAccess = {};
|
|
8087
8113
|
}
|
|
8114
|
+
return {
|
|
8115
|
+
content: [
|
|
8116
|
+
{
|
|
8117
|
+
type: "text",
|
|
8118
|
+
text: JSON.stringify({
|
|
8119
|
+
success: true,
|
|
8120
|
+
data: {
|
|
8121
|
+
action: 'upload',
|
|
8122
|
+
localPath: input.localPath,
|
|
8123
|
+
cloudPath: input.cloudPath,
|
|
8124
|
+
isDirectory: input.isDirectory,
|
|
8125
|
+
temporaryUrl,
|
|
8126
|
+
expireTime: "1小时",
|
|
8127
|
+
storageCdnDomain: publicAccess.storageCdnDomain,
|
|
8128
|
+
publicUrl: publicAccess.publicUrl,
|
|
8129
|
+
note: "temporaryUrl 是临时签名链接,1小时后过期,不要当作永久公网地址写入配置或持久化存储。publicUrl 基于 DescribeEnvs 返回的 Storages[0].CdnDomain 推导,⚠️ 仅在存储桶 ACL 为公有读(所有用户可读)时才能被匿名访问;默认私有读写存储桶返回的 publicUrl 会 403,此时请继续使用 temporaryUrl 或先将目标路径设置为公有读。"
|
|
8130
|
+
},
|
|
8131
|
+
message: `Successfully uploaded ${input.isDirectory ? 'directory' : 'file'} from '${input.localPath}' to '${input.cloudPath}'`
|
|
8132
|
+
}, null, 2)
|
|
8133
|
+
}
|
|
8134
|
+
]
|
|
8135
|
+
};
|
|
8088
8136
|
}
|
|
8089
|
-
|
|
8090
|
-
|
|
8091
|
-
|
|
8092
|
-
|
|
8093
|
-
|
|
8094
|
-
|
|
8095
|
-
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
return {
|
|
8102
|
-
content: [
|
|
8103
|
-
{
|
|
8104
|
-
type: "text",
|
|
8105
|
-
text: JSON.stringify({
|
|
8106
|
-
success: true,
|
|
8107
|
-
data: {
|
|
8108
|
-
action: 'upload',
|
|
8109
|
-
localPath: input.localPath,
|
|
8110
|
-
cloudPath: input.cloudPath,
|
|
8111
|
-
isDirectory: input.isDirectory,
|
|
8112
|
-
temporaryUrl,
|
|
8113
|
-
expireTime: "1小时",
|
|
8114
|
-
storageCdnDomain: publicAccess.storageCdnDomain,
|
|
8115
|
-
publicUrl: publicAccess.publicUrl,
|
|
8116
|
-
note: "temporaryUrl 是临时签名链接,1小时后过期,不要当作永久公网地址写入配置或持久化存储。publicUrl 基于 DescribeEnvs 返回的 Storages[0].CdnDomain 推导,⚠️ 仅在存储桶 ACL 为公有读(所有用户可读)时才能被匿名访问;默认私有读写存储桶返回的 publicUrl 会 403,此时请继续使用 temporaryUrl 或先将目标路径设置为公有读。"
|
|
8117
|
-
},
|
|
8118
|
-
message: `Successfully uploaded ${input.isDirectory ? 'directory' : 'file'} from '${input.localPath}' to '${input.cloudPath}'`
|
|
8119
|
-
}, null, 2)
|
|
8137
|
+
case 'download': {
|
|
8138
|
+
const dlCloudPath = input.cloudPath;
|
|
8139
|
+
const dlLocalPath = input.localPath;
|
|
8140
|
+
if (input.isDirectory) {
|
|
8141
|
+
if (storageOverrides?.downloadDirectory) {
|
|
8142
|
+
await storageOverrides.downloadDirectory({ cloudPath: dlCloudPath, localPath: dlLocalPath });
|
|
8143
|
+
}
|
|
8144
|
+
else {
|
|
8145
|
+
await storageService.downloadDirectory({
|
|
8146
|
+
cloudPath: dlCloudPath,
|
|
8147
|
+
localPath: dlLocalPath
|
|
8148
|
+
});
|
|
8120
8149
|
}
|
|
8121
|
-
]
|
|
8122
|
-
};
|
|
8123
|
-
}
|
|
8124
|
-
case 'download': {
|
|
8125
|
-
const dlCloudPath = input.cloudPath;
|
|
8126
|
-
const dlLocalPath = input.localPath;
|
|
8127
|
-
if (input.isDirectory) {
|
|
8128
|
-
if (storageOverrides?.downloadDirectory) {
|
|
8129
|
-
await storageOverrides.downloadDirectory({ cloudPath: dlCloudPath, localPath: dlLocalPath });
|
|
8130
|
-
}
|
|
8131
|
-
else {
|
|
8132
|
-
await storageService.downloadDirectory({
|
|
8133
|
-
cloudPath: dlCloudPath,
|
|
8134
|
-
localPath: dlLocalPath
|
|
8135
|
-
});
|
|
8136
|
-
}
|
|
8137
|
-
}
|
|
8138
|
-
else {
|
|
8139
|
-
if (storageOverrides?.downloadFile) {
|
|
8140
|
-
await storageOverrides.downloadFile({ cloudPath: dlCloudPath, localPath: dlLocalPath });
|
|
8141
8150
|
}
|
|
8142
8151
|
else {
|
|
8143
|
-
|
|
8144
|
-
cloudPath: dlCloudPath,
|
|
8145
|
-
localPath: dlLocalPath
|
|
8146
|
-
});
|
|
8147
|
-
}
|
|
8148
|
-
}
|
|
8149
|
-
return {
|
|
8150
|
-
content: [
|
|
8151
|
-
{
|
|
8152
|
-
type: "text",
|
|
8153
|
-
text: JSON.stringify({
|
|
8154
|
-
success: true,
|
|
8155
|
-
data: {
|
|
8156
|
-
action: 'download',
|
|
8157
|
-
cloudPath: input.cloudPath,
|
|
8158
|
-
localPath: input.localPath,
|
|
8159
|
-
isDirectory: input.isDirectory
|
|
8160
|
-
},
|
|
8161
|
-
message: `Successfully downloaded ${input.isDirectory ? 'directory' : 'file'} from '${input.cloudPath}' to '${input.localPath}'`
|
|
8162
|
-
}, null, 2)
|
|
8152
|
+
if (storageOverrides?.downloadFile) {
|
|
8153
|
+
await storageOverrides.downloadFile({ cloudPath: dlCloudPath, localPath: dlLocalPath });
|
|
8163
8154
|
}
|
|
8164
|
-
|
|
8165
|
-
|
|
8166
|
-
|
|
8167
|
-
|
|
8168
|
-
|
|
8155
|
+
else {
|
|
8156
|
+
await storageService.downloadFile({
|
|
8157
|
+
cloudPath: dlCloudPath,
|
|
8158
|
+
localPath: dlLocalPath
|
|
8159
|
+
});
|
|
8160
|
+
}
|
|
8161
|
+
}
|
|
8169
8162
|
return {
|
|
8170
8163
|
content: [
|
|
8171
8164
|
{
|
|
8172
8165
|
type: "text",
|
|
8173
8166
|
text: JSON.stringify({
|
|
8174
|
-
success:
|
|
8175
|
-
|
|
8176
|
-
|
|
8167
|
+
success: true,
|
|
8168
|
+
data: {
|
|
8169
|
+
action: 'download',
|
|
8170
|
+
cloudPath: input.cloudPath,
|
|
8171
|
+
localPath: input.localPath,
|
|
8172
|
+
isDirectory: input.isDirectory
|
|
8173
|
+
},
|
|
8174
|
+
message: `Successfully downloaded ${input.isDirectory ? 'directory' : 'file'} from '${input.cloudPath}' to '${input.localPath}'`
|
|
8177
8175
|
}, null, 2)
|
|
8178
8176
|
}
|
|
8179
8177
|
]
|
|
8180
8178
|
};
|
|
8181
8179
|
}
|
|
8182
|
-
|
|
8183
|
-
if (
|
|
8184
|
-
|
|
8185
|
-
|
|
8186
|
-
|
|
8187
|
-
|
|
8180
|
+
case 'delete': {
|
|
8181
|
+
if (!input.force) {
|
|
8182
|
+
return {
|
|
8183
|
+
content: [
|
|
8184
|
+
{
|
|
8185
|
+
type: "text",
|
|
8186
|
+
text: JSON.stringify({
|
|
8187
|
+
success: false,
|
|
8188
|
+
error: "Delete operation requires confirmation",
|
|
8189
|
+
message: "Please set force: true to confirm deletion. This action cannot be undone."
|
|
8190
|
+
}, null, 2)
|
|
8191
|
+
}
|
|
8192
|
+
]
|
|
8193
|
+
};
|
|
8188
8194
|
}
|
|
8189
|
-
|
|
8190
|
-
|
|
8191
|
-
|
|
8192
|
-
|
|
8195
|
+
if (input.isDirectory) {
|
|
8196
|
+
if (storageOverrides?.deleteDirectory) {
|
|
8197
|
+
await storageOverrides.deleteDirectory({ cloudPath: input.cloudPath });
|
|
8198
|
+
}
|
|
8199
|
+
else {
|
|
8200
|
+
await storageService.deleteDirectory(input.cloudPath);
|
|
8201
|
+
}
|
|
8193
8202
|
}
|
|
8194
8203
|
else {
|
|
8195
|
-
|
|
8204
|
+
if (storageOverrides?.deleteFiles) {
|
|
8205
|
+
await storageOverrides.deleteFiles({ cloudPaths: [input.cloudPath] });
|
|
8206
|
+
}
|
|
8207
|
+
else {
|
|
8208
|
+
await storageService.deleteFile([input.cloudPath]);
|
|
8209
|
+
}
|
|
8196
8210
|
}
|
|
8211
|
+
return {
|
|
8212
|
+
content: [
|
|
8213
|
+
{
|
|
8214
|
+
type: "text",
|
|
8215
|
+
text: JSON.stringify({
|
|
8216
|
+
success: true,
|
|
8217
|
+
data: {
|
|
8218
|
+
action: 'delete',
|
|
8219
|
+
cloudPath: input.cloudPath,
|
|
8220
|
+
isDirectory: input.isDirectory,
|
|
8221
|
+
deleted: true
|
|
8222
|
+
},
|
|
8223
|
+
message: `Successfully deleted ${input.isDirectory ? 'directory' : 'file'} '${input.cloudPath}'`
|
|
8224
|
+
}, null, 2)
|
|
8225
|
+
}
|
|
8226
|
+
]
|
|
8227
|
+
};
|
|
8197
8228
|
}
|
|
8198
|
-
|
|
8199
|
-
|
|
8200
|
-
{
|
|
8201
|
-
type: "text",
|
|
8202
|
-
text: JSON.stringify({
|
|
8203
|
-
success: true,
|
|
8204
|
-
data: {
|
|
8205
|
-
action: 'delete',
|
|
8206
|
-
cloudPath: input.cloudPath,
|
|
8207
|
-
isDirectory: input.isDirectory,
|
|
8208
|
-
deleted: true
|
|
8209
|
-
},
|
|
8210
|
-
message: `Successfully deleted ${input.isDirectory ? 'directory' : 'file'} '${input.cloudPath}'`
|
|
8211
|
-
}, null, 2)
|
|
8212
|
-
}
|
|
8213
|
-
]
|
|
8214
|
-
};
|
|
8229
|
+
default:
|
|
8230
|
+
throw new Error(`Unsupported action: ${input.action}`);
|
|
8215
8231
|
}
|
|
8216
|
-
|
|
8217
|
-
|
|
8232
|
+
}
|
|
8233
|
+
catch (error) {
|
|
8234
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
8235
|
+
return (0, tool_result_js_1.buildJsonToolResult)({
|
|
8236
|
+
success: false,
|
|
8237
|
+
message: `manageStorage 操作失败: ${message}`,
|
|
8238
|
+
action: input.action,
|
|
8239
|
+
cloudPath: input.cloudPath,
|
|
8240
|
+
});
|
|
8218
8241
|
}
|
|
8219
8242
|
});
|
|
8220
8243
|
}
|
|
@@ -9914,7 +9937,7 @@ class TelemetryReporter {
|
|
|
9914
9937
|
const nodeVersion = process.version; // Node.js版本
|
|
9915
9938
|
const arch = os_1.default.arch(); // 系统架构
|
|
9916
9939
|
// 从构建时注入的版本号获取MCP版本信息
|
|
9917
|
-
const mcpVersion = process.env.npm_package_version || "2.
|
|
9940
|
+
const mcpVersion = process.env.npm_package_version || "2.24.0" || 0;
|
|
9918
9941
|
return {
|
|
9919
9942
|
userAgent: `${osType} ${osRelease} ${arch} ${nodeVersion} CloudBase-MCP/${mcpVersion}`,
|
|
9920
9943
|
deviceId: this.deviceId,
|
|
@@ -11611,6 +11634,7 @@ const MANAGE_ACTIONS = [
|
|
|
11611
11634
|
"listMigrations",
|
|
11612
11635
|
"migrationDetail",
|
|
11613
11636
|
"rollbackMigration",
|
|
11637
|
+
"repairMigration",
|
|
11614
11638
|
];
|
|
11615
11639
|
function buildPgToolResult(payload) {
|
|
11616
11640
|
return (0, tool_result_js_1.buildJsonToolResult)(payload);
|
|
@@ -11754,6 +11778,38 @@ function classifySqlRisk(sql) {
|
|
|
11754
11778
|
requiresConfirm: true,
|
|
11755
11779
|
};
|
|
11756
11780
|
}
|
|
11781
|
+
/**
|
|
11782
|
+
* Schema DDL that must go through applyMigration by default.
|
|
11783
|
+
* Includes CREATE/ALTER/COMMENT (schema_change) and DROP/TRUNCATE / destructive ALTER.
|
|
11784
|
+
* Excludes DML DELETE and security_change (GRANT/POLICY), which may still use execute.
|
|
11785
|
+
*/
|
|
11786
|
+
function isSchemaDdlRisk(risk, sql) {
|
|
11787
|
+
if (risk === "schema_change") {
|
|
11788
|
+
return true;
|
|
11789
|
+
}
|
|
11790
|
+
if (risk !== "destructive") {
|
|
11791
|
+
return false;
|
|
11792
|
+
}
|
|
11793
|
+
const verb = getSqlVerb(stripLeadingSqlComments(sql));
|
|
11794
|
+
return ["DROP", "TRUNCATE", "ALTER"].includes(verb);
|
|
11795
|
+
}
|
|
11796
|
+
function buildLocalMigrationFileHint(version, name) {
|
|
11797
|
+
return `migrations/${version}_${name}.sql`;
|
|
11798
|
+
}
|
|
11799
|
+
const MIGRATION_VERSION_REQUIRED_MESSAGE = "migrationVersion is required (14-digit UTC timestamp YYYYMMDDHHMMSS). " +
|
|
11800
|
+
"Decide the version first, write local file migrations/<version>_<migrationName>.sql, " +
|
|
11801
|
+
"then call planMigration/applyMigration with the same migrationVersion and migrationName.";
|
|
11802
|
+
function requireExplicitMigrationVersion(args, actionLabel) {
|
|
11803
|
+
const version = args.migrationVersion?.trim();
|
|
11804
|
+
if (!version) {
|
|
11805
|
+
return buildPgToolResult({
|
|
11806
|
+
success: false,
|
|
11807
|
+
errorCode: "MIGRATION_VERSION_REQUIRED",
|
|
11808
|
+
message: `Provide migrationVersion when action=${actionLabel}. ${MIGRATION_VERSION_REQUIRED_MESSAGE}`,
|
|
11809
|
+
});
|
|
11810
|
+
}
|
|
11811
|
+
return version;
|
|
11812
|
+
}
|
|
11757
11813
|
function quoteIdentifier(identifier) {
|
|
11758
11814
|
return `"${identifier.replace(/"/g, '""')}"`;
|
|
11759
11815
|
}
|
|
@@ -12360,6 +12416,34 @@ async function handleExecuteSql(args, context, deps) {
|
|
|
12360
12416
|
});
|
|
12361
12417
|
}
|
|
12362
12418
|
const classification = classifySqlRisk(args.sql);
|
|
12419
|
+
if (isSchemaDdlRisk(classification.risk, args.sql) && args.allowDdlViaExecute !== true) {
|
|
12420
|
+
return buildPgToolResult({
|
|
12421
|
+
success: false,
|
|
12422
|
+
errorCode: "DDL_USE_APPLY_MIGRATION",
|
|
12423
|
+
message: "Schema DDL (CREATE/ALTER/DROP/TRUNCATE/...) must use applyMigration with an explicit migrationVersion, " +
|
|
12424
|
+
"not execute. Write local migrations/<version>_<name>.sql first, then call applyMigration. " +
|
|
12425
|
+
"Set allowDdlViaExecute=true only for exceptional one-off ops that intentionally bypass migration history.",
|
|
12426
|
+
data: {
|
|
12427
|
+
classification,
|
|
12428
|
+
localFileHintPattern: "migrations/<migrationVersion>_<migrationName>.sql",
|
|
12429
|
+
},
|
|
12430
|
+
nextActions: [
|
|
12431
|
+
buildNextAction(MANAGE_PG_DATABASE, "planMigration", "Preview the schema change as a versioned migration (provide migrationVersion + migrationName).", {
|
|
12432
|
+
action: "planMigration",
|
|
12433
|
+
migrationName: "describe_your_change",
|
|
12434
|
+
migrationVersion: "YYYYMMDDHHMMSS",
|
|
12435
|
+
sql: args.sql,
|
|
12436
|
+
}),
|
|
12437
|
+
buildNextAction(MANAGE_PG_DATABASE, "applyMigration", "Apply the schema change via PushPGUserMigrations with the same explicit migrationVersion.", {
|
|
12438
|
+
action: "applyMigration",
|
|
12439
|
+
migrationName: "describe_your_change",
|
|
12440
|
+
migrationVersion: "YYYYMMDDHHMMSS",
|
|
12441
|
+
sql: args.sql,
|
|
12442
|
+
confirm: true,
|
|
12443
|
+
}),
|
|
12444
|
+
],
|
|
12445
|
+
});
|
|
12446
|
+
}
|
|
12363
12447
|
if (!classification.readOnly && !args.confirm) {
|
|
12364
12448
|
return buildPgToolResult({
|
|
12365
12449
|
success: false,
|
|
@@ -12369,7 +12453,12 @@ async function handleExecuteSql(args, context, deps) {
|
|
|
12369
12453
|
classification,
|
|
12370
12454
|
},
|
|
12371
12455
|
nextActions: [
|
|
12372
|
-
buildNextAction(MANAGE_PG_DATABASE, "execute", "Re-issue with confirm=true to actually run the SQL.", {
|
|
12456
|
+
buildNextAction(MANAGE_PG_DATABASE, "execute", "Re-issue with confirm=true to actually run the SQL.", {
|
|
12457
|
+
action: "execute",
|
|
12458
|
+
sql: args.sql,
|
|
12459
|
+
confirm: true,
|
|
12460
|
+
...(args.allowDdlViaExecute === true ? { allowDdlViaExecute: true } : {}),
|
|
12461
|
+
}),
|
|
12373
12462
|
buildNextAction(QUERY_PG_DATABASE, "metadata", "Inspect table size and shape before destructive changes.", { action: "metadata", limit: 20 }),
|
|
12374
12463
|
],
|
|
12375
12464
|
});
|
|
@@ -12386,8 +12475,15 @@ async function handleExecuteSql(args, context, deps) {
|
|
|
12386
12475
|
.slice(0, 5)
|
|
12387
12476
|
.map((row) => serializeValue(row)),
|
|
12388
12477
|
targetTable: targetTable ?? null,
|
|
12478
|
+
...(args.allowDdlViaExecute === true && isSchemaDdlRisk(classification.risk, args.sql)
|
|
12479
|
+
? {
|
|
12480
|
+
warning: "DDL executed via allowDdlViaExecute bypasses migration history. Prefer applyMigration for reproducible schema changes.",
|
|
12481
|
+
}
|
|
12482
|
+
: {}),
|
|
12389
12483
|
},
|
|
12390
|
-
message:
|
|
12484
|
+
message: args.allowDdlViaExecute === true && isSchemaDdlRisk(classification.risk, args.sql)
|
|
12485
|
+
? "Write SQL executed successfully (DDL via allowDdlViaExecute; migration history was bypassed)."
|
|
12486
|
+
: "Write SQL executed successfully.",
|
|
12391
12487
|
nextActions: classification.risk === "schema_change" && targetTable
|
|
12392
12488
|
? [
|
|
12393
12489
|
buildNextAction(QUERY_PG_DATABASE, "schema", "Inspect the table schema, columns, indexes, and RLS policies after schema changes.", {
|
|
@@ -12415,6 +12511,43 @@ async function handleDryRun(args) {
|
|
|
12415
12511
|
});
|
|
12416
12512
|
}
|
|
12417
12513
|
const classification = classifySqlRisk(args.sql);
|
|
12514
|
+
const schemaDdl = isSchemaDdlRisk(classification.risk, args.sql);
|
|
12515
|
+
if (classification.readOnly) {
|
|
12516
|
+
return buildPgToolResult({
|
|
12517
|
+
success: true,
|
|
12518
|
+
data: {
|
|
12519
|
+
classification,
|
|
12520
|
+
wouldExecute: false,
|
|
12521
|
+
sqlPreview: args.sql.trim().slice(0, 500),
|
|
12522
|
+
},
|
|
12523
|
+
message: "SQL dry run completed. This statement is read-only; use queryPgDatabase(action=sql) to execute it.",
|
|
12524
|
+
nextActions: [
|
|
12525
|
+
buildNextAction(QUERY_PG_DATABASE, "sql", "Execute the read-only SQL through queryPgDatabase.", { action: "sql", sql: args.sql, limit: 20 }),
|
|
12526
|
+
],
|
|
12527
|
+
});
|
|
12528
|
+
}
|
|
12529
|
+
if (schemaDdl) {
|
|
12530
|
+
return buildPgToolResult({
|
|
12531
|
+
success: true,
|
|
12532
|
+
data: {
|
|
12533
|
+
classification,
|
|
12534
|
+
wouldExecute: false,
|
|
12535
|
+
sqlPreview: args.sql.trim().slice(0, 500),
|
|
12536
|
+
preferredAction: "applyMigration",
|
|
12537
|
+
localFileHintPattern: "migrations/<migrationVersion>_<migrationName>.sql",
|
|
12538
|
+
},
|
|
12539
|
+
message: "SQL dry run completed. Schema DDL should use applyMigration with an explicit migrationVersion, not execute.",
|
|
12540
|
+
nextActions: [
|
|
12541
|
+
buildNextAction(MANAGE_PG_DATABASE, "applyMigration", "Apply schema DDL via versioned migration (provide migrationVersion + migrationName + confirm=true).", {
|
|
12542
|
+
action: "applyMigration",
|
|
12543
|
+
migrationName: "describe_your_change",
|
|
12544
|
+
migrationVersion: "YYYYMMDDHHMMSS",
|
|
12545
|
+
sql: args.sql,
|
|
12546
|
+
confirm: true,
|
|
12547
|
+
}),
|
|
12548
|
+
],
|
|
12549
|
+
});
|
|
12550
|
+
}
|
|
12418
12551
|
return buildPgToolResult({
|
|
12419
12552
|
success: true,
|
|
12420
12553
|
data: {
|
|
@@ -12422,15 +12555,9 @@ async function handleDryRun(args) {
|
|
|
12422
12555
|
wouldExecute: false,
|
|
12423
12556
|
sqlPreview: args.sql.trim().slice(0, 500),
|
|
12424
12557
|
},
|
|
12425
|
-
message:
|
|
12426
|
-
? "SQL dry run completed. This statement is read-only; use queryPgDatabase(action=sql) to execute it."
|
|
12427
|
-
: "SQL dry run completed. Write SQL requires managePgDatabase(action=execute, confirm=true).",
|
|
12558
|
+
message: "SQL dry run completed. Write SQL requires managePgDatabase(action=execute, confirm=true).",
|
|
12428
12559
|
nextActions: [
|
|
12429
|
-
buildNextAction(
|
|
12430
|
-
? "Execute the read-only SQL through queryPgDatabase."
|
|
12431
|
-
: "Execute the write SQL only after explicit confirmation.", classification.readOnly
|
|
12432
|
-
? { action: "sql", sql: args.sql, limit: 20 }
|
|
12433
|
-
: { action: "execute", sql: args.sql, confirm: true }),
|
|
12560
|
+
buildNextAction(MANAGE_PG_DATABASE, "execute", "Execute the write SQL only after explicit confirmation.", { action: "execute", sql: args.sql, confirm: true }),
|
|
12434
12561
|
],
|
|
12435
12562
|
});
|
|
12436
12563
|
}
|
|
@@ -12442,16 +12569,49 @@ async function handlePlanMigration(args, context, deps, cloudBaseOptions) {
|
|
|
12442
12569
|
message: "Provide migration SQL when action=planMigration.",
|
|
12443
12570
|
});
|
|
12444
12571
|
}
|
|
12572
|
+
if (!args.migrationName?.trim()) {
|
|
12573
|
+
return buildPgToolResult({
|
|
12574
|
+
success: false,
|
|
12575
|
+
errorCode: "MIGRATION_NAME_REQUIRED",
|
|
12576
|
+
message: "Provide migrationName (lowercase letters, digits and underscores, starting with a letter) when action=planMigration.",
|
|
12577
|
+
});
|
|
12578
|
+
}
|
|
12579
|
+
const versionOrError = requireExplicitMigrationVersion(args, "planMigration");
|
|
12580
|
+
if (typeof versionOrError !== "string") {
|
|
12581
|
+
return versionOrError;
|
|
12582
|
+
}
|
|
12583
|
+
const version = versionOrError;
|
|
12584
|
+
const localFileHint = buildLocalMigrationFileHint(version, args.migrationName);
|
|
12585
|
+
const migration = {
|
|
12586
|
+
Version: version,
|
|
12587
|
+
Name: args.migrationName,
|
|
12588
|
+
Query: args.sql,
|
|
12589
|
+
};
|
|
12590
|
+
if (args.rollbackSql?.trim()) {
|
|
12591
|
+
migration.Rollback = args.rollbackSql;
|
|
12592
|
+
}
|
|
12445
12593
|
try {
|
|
12446
12594
|
const result = await callPgMigrationApi(context, "PreviewPGUserMigrations", {
|
|
12447
|
-
|
|
12595
|
+
Migrations: [migration],
|
|
12448
12596
|
}, cloudBaseOptions);
|
|
12449
12597
|
return buildPgToolResult({
|
|
12450
12598
|
success: true,
|
|
12451
|
-
data:
|
|
12452
|
-
|
|
12599
|
+
data: {
|
|
12600
|
+
migrationVersion: version,
|
|
12601
|
+
migrationName: args.migrationName,
|
|
12602
|
+
localFileHint,
|
|
12603
|
+
apiResult: result,
|
|
12604
|
+
},
|
|
12605
|
+
message: `Migration plan generated via PreviewPGUserMigrations. Reuse migrationVersion=${version} on applyMigration. Ensure local file ${localFileHint} exists and matches.`,
|
|
12453
12606
|
nextActions: [
|
|
12454
|
-
buildNextAction(MANAGE_PG_DATABASE, "applyMigration", "Review the plan above. If it looks correct, call applyMigration
|
|
12607
|
+
buildNextAction(MANAGE_PG_DATABASE, "applyMigration", "Review the plan above. If it looks correct, call applyMigration with the same migrationVersion.", {
|
|
12608
|
+
action: "applyMigration",
|
|
12609
|
+
migrationName: args.migrationName,
|
|
12610
|
+
migrationVersion: version,
|
|
12611
|
+
sql: args.sql,
|
|
12612
|
+
confirm: true,
|
|
12613
|
+
...(args.rollbackSql ? { rollbackSql: args.rollbackSql } : {}),
|
|
12614
|
+
}),
|
|
12455
12615
|
],
|
|
12456
12616
|
});
|
|
12457
12617
|
}
|
|
@@ -12471,6 +12631,13 @@ async function handleApplyMigration(args, context, deps, cloudBaseOptions) {
|
|
|
12471
12631
|
message: "Provide migration SQL when action=applyMigration.",
|
|
12472
12632
|
});
|
|
12473
12633
|
}
|
|
12634
|
+
if (!args.migrationName?.trim()) {
|
|
12635
|
+
return buildPgToolResult({
|
|
12636
|
+
success: false,
|
|
12637
|
+
errorCode: "MIGRATION_NAME_REQUIRED",
|
|
12638
|
+
message: "Provide migrationName (lowercase letters, digits and underscores, starting with a letter) when action=applyMigration.",
|
|
12639
|
+
});
|
|
12640
|
+
}
|
|
12474
12641
|
if (args.confirm !== true) {
|
|
12475
12642
|
return buildPgToolResult({
|
|
12476
12643
|
success: false,
|
|
@@ -12478,14 +12645,41 @@ async function handleApplyMigration(args, context, deps, cloudBaseOptions) {
|
|
|
12478
12645
|
message: "PushPGUserMigrations requires confirm=true. Run with confirm=true to proceed.",
|
|
12479
12646
|
});
|
|
12480
12647
|
}
|
|
12648
|
+
const versionOrError = requireExplicitMigrationVersion(args, "applyMigration");
|
|
12649
|
+
if (typeof versionOrError !== "string") {
|
|
12650
|
+
return versionOrError;
|
|
12651
|
+
}
|
|
12652
|
+
const version = versionOrError;
|
|
12653
|
+
const localFileHint = buildLocalMigrationFileHint(version, args.migrationName);
|
|
12654
|
+
const migration = {
|
|
12655
|
+
Version: version,
|
|
12656
|
+
Name: args.migrationName,
|
|
12657
|
+
Query: args.sql,
|
|
12658
|
+
};
|
|
12659
|
+
if (args.rollbackSql?.trim()) {
|
|
12660
|
+
migration.Rollback = args.rollbackSql;
|
|
12661
|
+
}
|
|
12662
|
+
const params = { Migrations: [migration] };
|
|
12663
|
+
if (args.lockTimeoutMs !== undefined) {
|
|
12664
|
+
params.LockTimeoutMs = args.lockTimeoutMs;
|
|
12665
|
+
}
|
|
12666
|
+
if (args.statementTimeoutMs !== undefined) {
|
|
12667
|
+
params.StatementTimeoutMs = args.statementTimeoutMs;
|
|
12668
|
+
}
|
|
12481
12669
|
try {
|
|
12482
|
-
const result = await callPgMigrationApi(context, "PushPGUserMigrations",
|
|
12483
|
-
Sql: args.sql,
|
|
12484
|
-
}, cloudBaseOptions);
|
|
12670
|
+
const result = await callPgMigrationApi(context, "PushPGUserMigrations", params, cloudBaseOptions);
|
|
12485
12671
|
return buildPgToolResult({
|
|
12486
12672
|
success: true,
|
|
12487
|
-
data:
|
|
12488
|
-
|
|
12673
|
+
data: {
|
|
12674
|
+
migrationVersion: version,
|
|
12675
|
+
migrationName: args.migrationName,
|
|
12676
|
+
localFileHint,
|
|
12677
|
+
apiResult: result,
|
|
12678
|
+
},
|
|
12679
|
+
message: `Migrations applied via PushPGUserMigrations. Ensure local file ${localFileHint} exists and matches.`,
|
|
12680
|
+
nextActions: [
|
|
12681
|
+
buildNextAction(MANAGE_PG_DATABASE, "listMigrations", "Verify the remote migration history records the same migrationVersion.", { action: "listMigrations", limit: 20 }),
|
|
12682
|
+
],
|
|
12489
12683
|
});
|
|
12490
12684
|
}
|
|
12491
12685
|
catch (error) {
|
|
@@ -12497,8 +12691,15 @@ async function handleApplyMigration(args, context, deps, cloudBaseOptions) {
|
|
|
12497
12691
|
}
|
|
12498
12692
|
}
|
|
12499
12693
|
async function handleListMigrations(args, context, deps, cloudBaseOptions) {
|
|
12694
|
+
const params = {};
|
|
12695
|
+
if (args.limit !== undefined) {
|
|
12696
|
+
params.Limit = args.limit;
|
|
12697
|
+
}
|
|
12698
|
+
if (args.offset !== undefined) {
|
|
12699
|
+
params.Offset = args.offset;
|
|
12700
|
+
}
|
|
12500
12701
|
try {
|
|
12501
|
-
const result = await callPgMigrationApi(context, "ListPGUserMigrations",
|
|
12702
|
+
const result = await callPgMigrationApi(context, "ListPGUserMigrations", params, cloudBaseOptions);
|
|
12502
12703
|
return buildPgToolResult({
|
|
12503
12704
|
success: true,
|
|
12504
12705
|
data: result,
|
|
@@ -12514,16 +12715,16 @@ async function handleListMigrations(args, context, deps, cloudBaseOptions) {
|
|
|
12514
12715
|
}
|
|
12515
12716
|
}
|
|
12516
12717
|
async function handleMigrationDetail(args, context, deps, cloudBaseOptions) {
|
|
12517
|
-
if (!args.
|
|
12718
|
+
if (!args.migrationVersion?.trim()) {
|
|
12518
12719
|
return buildPgToolResult({
|
|
12519
12720
|
success: false,
|
|
12520
|
-
errorCode: "
|
|
12521
|
-
message: "Provide
|
|
12721
|
+
errorCode: "MIGRATION_VERSION_REQUIRED",
|
|
12722
|
+
message: "Provide migrationVersion (14-digit timestamp YYYYMMDDHHMMSS) when action=migrationDetail.",
|
|
12522
12723
|
});
|
|
12523
12724
|
}
|
|
12524
12725
|
try {
|
|
12525
12726
|
const result = await callPgMigrationApi(context, "DescribePGUserMigration", {
|
|
12526
|
-
|
|
12727
|
+
MigrationVersion: args.migrationVersion,
|
|
12527
12728
|
}, cloudBaseOptions);
|
|
12528
12729
|
return buildPgToolResult({
|
|
12529
12730
|
success: true,
|
|
@@ -12540,11 +12741,11 @@ async function handleMigrationDetail(args, context, deps, cloudBaseOptions) {
|
|
|
12540
12741
|
}
|
|
12541
12742
|
}
|
|
12542
12743
|
async function handleRollbackMigration(args, context, deps, cloudBaseOptions) {
|
|
12543
|
-
if (!args.
|
|
12744
|
+
if (args.lastN === undefined || args.lastN < 1 || !Number.isInteger(args.lastN)) {
|
|
12544
12745
|
return buildPgToolResult({
|
|
12545
12746
|
success: false,
|
|
12546
|
-
errorCode: "
|
|
12547
|
-
message: "Provide
|
|
12747
|
+
errorCode: "LAST_N_REQUIRED",
|
|
12748
|
+
message: "Provide lastN (positive integer) when action=rollbackMigration.",
|
|
12548
12749
|
});
|
|
12549
12750
|
}
|
|
12550
12751
|
if (args.confirm !== true) {
|
|
@@ -12556,7 +12757,7 @@ async function handleRollbackMigration(args, context, deps, cloudBaseOptions) {
|
|
|
12556
12757
|
}
|
|
12557
12758
|
try {
|
|
12558
12759
|
const result = await callPgMigrationApi(context, "RollbackPGUserMigrations", {
|
|
12559
|
-
|
|
12760
|
+
LastN: args.lastN,
|
|
12560
12761
|
}, cloudBaseOptions);
|
|
12561
12762
|
return buildPgToolResult({
|
|
12562
12763
|
success: true,
|
|
@@ -12572,6 +12773,67 @@ async function handleRollbackMigration(args, context, deps, cloudBaseOptions) {
|
|
|
12572
12773
|
});
|
|
12573
12774
|
}
|
|
12574
12775
|
}
|
|
12776
|
+
async function handleRepairMigration(args, context, deps, cloudBaseOptions) {
|
|
12777
|
+
if (!args.migrationVersion?.trim()) {
|
|
12778
|
+
return buildPgToolResult({
|
|
12779
|
+
success: false,
|
|
12780
|
+
errorCode: "MIGRATION_VERSION_REQUIRED",
|
|
12781
|
+
message: "Provide migrationVersion (14-digit timestamp YYYYMMDDHHMMSS) when action=repairMigration.",
|
|
12782
|
+
});
|
|
12783
|
+
}
|
|
12784
|
+
if (!args.migrationName?.trim()) {
|
|
12785
|
+
return buildPgToolResult({
|
|
12786
|
+
success: false,
|
|
12787
|
+
errorCode: "MIGRATION_NAME_REQUIRED",
|
|
12788
|
+
message: "Provide migrationName when action=repairMigration.",
|
|
12789
|
+
});
|
|
12790
|
+
}
|
|
12791
|
+
if (!args.repairStatus) {
|
|
12792
|
+
return buildPgToolResult({
|
|
12793
|
+
success: false,
|
|
12794
|
+
errorCode: "REPAIR_STATUS_REQUIRED",
|
|
12795
|
+
message: "Provide repairStatus (applied or reverted) when action=repairMigration.",
|
|
12796
|
+
});
|
|
12797
|
+
}
|
|
12798
|
+
if (!args.repairReason?.trim()) {
|
|
12799
|
+
return buildPgToolResult({
|
|
12800
|
+
success: false,
|
|
12801
|
+
errorCode: "REPAIR_REASON_REQUIRED",
|
|
12802
|
+
message: "Provide repairReason when action=repairMigration.",
|
|
12803
|
+
});
|
|
12804
|
+
}
|
|
12805
|
+
if (args.repairStatus === "applied" && !args.sql?.trim()) {
|
|
12806
|
+
return buildPgToolResult({
|
|
12807
|
+
success: false,
|
|
12808
|
+
errorCode: "SQL_REQUIRED",
|
|
12809
|
+
message: "Provide sql (Query) when action=repairMigration with repairStatus=applied.",
|
|
12810
|
+
});
|
|
12811
|
+
}
|
|
12812
|
+
const params = {
|
|
12813
|
+
MigrationVersion: args.migrationVersion,
|
|
12814
|
+
Name: args.migrationName,
|
|
12815
|
+
Status: args.repairStatus,
|
|
12816
|
+
Reason: args.repairReason,
|
|
12817
|
+
};
|
|
12818
|
+
if (args.repairStatus === "applied" && args.sql?.trim()) {
|
|
12819
|
+
params.Query = args.sql;
|
|
12820
|
+
}
|
|
12821
|
+
try {
|
|
12822
|
+
const result = await callPgMigrationApi(context, "RepairPGUserMigrationHistory", params, cloudBaseOptions);
|
|
12823
|
+
return buildPgToolResult({
|
|
12824
|
+
success: true,
|
|
12825
|
+
data: result,
|
|
12826
|
+
message: "Migration history repaired via RepairPGUserMigrationHistory.",
|
|
12827
|
+
});
|
|
12828
|
+
}
|
|
12829
|
+
catch (error) {
|
|
12830
|
+
return buildPgToolResult({
|
|
12831
|
+
success: false,
|
|
12832
|
+
errorCode: "MIGRATION_API_ERROR",
|
|
12833
|
+
message: `RepairPGUserMigrationHistory failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
12834
|
+
});
|
|
12835
|
+
}
|
|
12836
|
+
}
|
|
12575
12837
|
async function handleGetPgSchema(args, context, deps) {
|
|
12576
12838
|
const objectName = args.objectName ?? "";
|
|
12577
12839
|
const parsed = parseSchemaQualifiedName(objectName);
|
|
@@ -12692,15 +12954,15 @@ function registerPGDatabaseTools(server, providedDeps) {
|
|
|
12692
12954
|
});
|
|
12693
12955
|
server.registerTool?.(MANAGE_PG_DATABASE, {
|
|
12694
12956
|
title: "管理 PostgreSQL 上下文或执行写入 SQL",
|
|
12695
|
-
description: "管理 CloudBase PostgreSQL:执行已确认的写入 SQL
|
|
12957
|
+
description: "管理 CloudBase PostgreSQL:执行已确认的写入 SQL、SQL 风险预检、迁移管理。建表/ALTER/DROP 等 schema 变更必须使用 applyMigration(显式 migrationVersion + 本地 migrations/ 留档),不要默认用 execute。execute 主要用于 DML 与 GRANT/RLS 等运维 SQL。",
|
|
12696
12958
|
inputSchema: {
|
|
12697
12959
|
action: zod_1.z
|
|
12698
12960
|
.enum(MANAGE_ACTIONS)
|
|
12699
|
-
.describe("操作类型:execute=执行已确认的写入 SQL
|
|
12961
|
+
.describe("操作类型:execute=执行已确认的写入 SQL(DML/GRANT/RLS;schema DDL 默认拒绝,需 allowDdlViaExecute=true);dryRun=只分析 SQL 风险不执行;planMigration=预览迁移计划(需 migrationName + migrationVersion + sql);applyMigration=应用迁移,建表/改 schema 首选(需 migrationName + migrationVersion + sql + confirm=true);listMigrations=查询已应用的 Migration 列表(可传 limit/offset 分页);migrationDetail=查看单条 Migration 详情(需 migrationVersion);rollbackMigration=回滚最近 N 条 Migration(需 lastN + confirm=true);repairMigration=修复 Migration 历史记录(需 migrationVersion + migrationName + repairStatus + repairReason)"),
|
|
12700
12962
|
sql: zod_1.z
|
|
12701
12963
|
.string()
|
|
12702
12964
|
.optional()
|
|
12703
|
-
.describe("action=execute、dryRun、planMigration 或
|
|
12965
|
+
.describe("action=execute、dryRun、planMigration、applyMigration 或 repairMigration(applied) 使用的 SQL 语句"),
|
|
12704
12966
|
confirm: zod_1.z
|
|
12705
12967
|
.boolean()
|
|
12706
12968
|
.optional()
|
|
@@ -12724,7 +12986,62 @@ function registerPGDatabaseTools(server, providedDeps) {
|
|
|
12724
12986
|
objectName: zod_1.z
|
|
12725
12987
|
.string()
|
|
12726
12988
|
.optional()
|
|
12727
|
-
.describe("
|
|
12989
|
+
.describe("可选的对象名,当前仅用于非 migration 场景。migration 相关操作请使用 migrationName / migrationVersion / lastN。"),
|
|
12990
|
+
migrationName: zod_1.z
|
|
12991
|
+
.string()
|
|
12992
|
+
.regex(/^[a-z][a-z0-9_]*$/)
|
|
12993
|
+
.optional()
|
|
12994
|
+
.describe("plan/apply/repair 必填:migration 名称,小写字母开头,仅允许小写字母、数字和下划线。"),
|
|
12995
|
+
migrationVersion: zod_1.z
|
|
12996
|
+
.string()
|
|
12997
|
+
.regex(/^\d{14}$/)
|
|
12998
|
+
.optional()
|
|
12999
|
+
.describe("14 位时间戳 YYYYMMDDHHMMSS。plan/apply/detail/repair 必填;禁止由服务端静默生成,避免与本地 migrations/<version>_<name>.sql 分叉。"),
|
|
13000
|
+
rollbackSql: zod_1.z
|
|
13001
|
+
.string()
|
|
13002
|
+
.optional()
|
|
13003
|
+
.describe("plan/apply 可选:回滚 SQL 语句。"),
|
|
13004
|
+
lastN: zod_1.z
|
|
13005
|
+
.number()
|
|
13006
|
+
.int()
|
|
13007
|
+
.positive()
|
|
13008
|
+
.optional()
|
|
13009
|
+
.describe("rollback 必填:回滚最近 N 条已应用的 Migration,正整数。"),
|
|
13010
|
+
limit: zod_1.z
|
|
13011
|
+
.number()
|
|
13012
|
+
.int()
|
|
13013
|
+
.min(1)
|
|
13014
|
+
.max(500)
|
|
13015
|
+
.optional()
|
|
13016
|
+
.describe("list 可选:返回数量上限,1-500,默认 100。"),
|
|
13017
|
+
offset: zod_1.z
|
|
13018
|
+
.number()
|
|
13019
|
+
.int()
|
|
13020
|
+
.min(0)
|
|
13021
|
+
.optional()
|
|
13022
|
+
.describe("list 可选:分页偏移,默认 0。"),
|
|
13023
|
+
lockTimeoutMs: zod_1.z
|
|
13024
|
+
.number()
|
|
13025
|
+
.int()
|
|
13026
|
+
.optional()
|
|
13027
|
+
.describe("apply 可选:获取数据库锁的最长时间(毫秒),默认 5000。"),
|
|
13028
|
+
statementTimeoutMs: zod_1.z
|
|
13029
|
+
.number()
|
|
13030
|
+
.int()
|
|
13031
|
+
.optional()
|
|
13032
|
+
.describe("apply 可选:单条 SQL 执行最长时间(毫秒),默认 300000。"),
|
|
13033
|
+
repairStatus: zod_1.z
|
|
13034
|
+
.enum(["applied", "reverted"])
|
|
13035
|
+
.optional()
|
|
13036
|
+
.describe("repair 必填:applied=标记为已应用(可补录 Query),reverted=删除 history 记录。"),
|
|
13037
|
+
repairReason: zod_1.z
|
|
13038
|
+
.string()
|
|
13039
|
+
.optional()
|
|
13040
|
+
.describe("repair 必填:修复原因。"),
|
|
13041
|
+
allowDdlViaExecute: zod_1.z
|
|
13042
|
+
.boolean()
|
|
13043
|
+
.optional()
|
|
13044
|
+
.describe("可选,默认 false。仅当需要故意绕过 migration history 时设为 true,才允许 schema DDL 走 execute;正常建表/改 schema 必须用 applyMigration。"),
|
|
12728
13045
|
},
|
|
12729
13046
|
annotations: {
|
|
12730
13047
|
readOnlyHint: false,
|
|
@@ -12738,6 +13055,14 @@ function registerPGDatabaseTools(server, providedDeps) {
|
|
|
12738
13055
|
const cbOpts = server.cloudBaseOptions;
|
|
12739
13056
|
switch (args.action) {
|
|
12740
13057
|
case "execute": {
|
|
13058
|
+
// Soft-block schema DDL before readiness probe so agents get migration guidance immediately.
|
|
13059
|
+
if (args.sql?.trim()) {
|
|
13060
|
+
const classification = classifySqlRisk(args.sql);
|
|
13061
|
+
if (isSchemaDdlRisk(classification.risk, args.sql) &&
|
|
13062
|
+
args.allowDdlViaExecute !== true) {
|
|
13063
|
+
return handleExecuteSql(args, context, deps);
|
|
13064
|
+
}
|
|
13065
|
+
}
|
|
12741
13066
|
try {
|
|
12742
13067
|
await ensurePgReadyOnce(cbOpts, deps);
|
|
12743
13068
|
}
|
|
@@ -12760,13 +13085,15 @@ function registerPGDatabaseTools(server, providedDeps) {
|
|
|
12760
13085
|
case "applyMigration":
|
|
12761
13086
|
case "listMigrations":
|
|
12762
13087
|
case "migrationDetail":
|
|
12763
|
-
case "rollbackMigration":
|
|
13088
|
+
case "rollbackMigration":
|
|
13089
|
+
case "repairMigration": {
|
|
12764
13090
|
switch (args.action) {
|
|
12765
13091
|
case "planMigration": return handlePlanMigration(args, context, deps, cbOpts);
|
|
12766
13092
|
case "applyMigration": return handleApplyMigration(args, context, deps, cbOpts);
|
|
12767
13093
|
case "listMigrations": return handleListMigrations(args, context, deps, cbOpts);
|
|
12768
13094
|
case "migrationDetail": return handleMigrationDetail(args, context, deps, cbOpts);
|
|
12769
13095
|
case "rollbackMigration": return handleRollbackMigration(args, context, deps, cbOpts);
|
|
13096
|
+
case "repairMigration": return handleRepairMigration(args, context, deps, cbOpts);
|
|
12770
13097
|
default: return buildPgToolResult({ success: false, errorCode: "UNSUPPORTED_ACTION", message: `Unsupported action: ${args.action}` });
|
|
12771
13098
|
}
|
|
12772
13099
|
}
|
|
@@ -13478,7 +13805,7 @@ exports.IDE_TYPES = [
|
|
|
13478
13805
|
"openai-codex-cli", // OpenAI Codex CLI
|
|
13479
13806
|
"augment-code", // Augment Code
|
|
13480
13807
|
"github-copilot", // GitHub Copilot
|
|
13481
|
-
"roocode", // RooCode AI
|
|
13808
|
+
"roocode", // RooCode AI编辑器(已 deprecated 2026-05,可考虑引导转向 Cline/ZooCode)
|
|
13482
13809
|
"tongyi-lingma", // 通义灵码
|
|
13483
13810
|
"trae", // Trae AI编辑器
|
|
13484
13811
|
"qoder", // Qoder AI编辑器
|
|
@@ -13520,7 +13847,6 @@ exports.RAW_IDE_FILE_MAPPINGS = {
|
|
|
13520
13847
|
],
|
|
13521
13848
|
"baidu-comate": [
|
|
13522
13849
|
{ path: ".comate/rules/cloudbase-rules.mdr" },
|
|
13523
|
-
{ path: ".comate/rules/cloudbaase-rules.mdr" },
|
|
13524
13850
|
{ path: ".comate/mcp.json", isMcpConfig: true },
|
|
13525
13851
|
],
|
|
13526
13852
|
"openai-codex-cli": [
|
|
@@ -13530,10 +13856,10 @@ exports.RAW_IDE_FILE_MAPPINGS = {
|
|
|
13530
13856
|
"augment-code": [{ path: ".augment-guidelines" }],
|
|
13531
13857
|
"github-copilot": [{ path: ".github/copilot-instructions.md" }],
|
|
13532
13858
|
roocode: [
|
|
13533
|
-
{ path: ".roo/rules/
|
|
13859
|
+
{ path: ".roo/rules/cloudbase-rules.md" },
|
|
13534
13860
|
{ path: ".roo/mcp.json", isMcpConfig: true },
|
|
13535
13861
|
],
|
|
13536
|
-
"tongyi-lingma": [{ path: ".lingma/rules/
|
|
13862
|
+
"tongyi-lingma": [{ path: ".lingma/rules/cloudbase-rules.md" }],
|
|
13537
13863
|
trae: [{ path: ".trae/rules/" }],
|
|
13538
13864
|
qoder: [{ path: ".qoder/rules/" }],
|
|
13539
13865
|
antigravity: [{ path: ".agent/rules/" }],
|
|
@@ -13573,7 +13899,7 @@ const IDE_DESCRIPTIONS = {
|
|
|
13573
13899
|
"openai-codex-cli": "OpenAI Codex CLI",
|
|
13574
13900
|
"augment-code": "Augment Code",
|
|
13575
13901
|
"github-copilot": "GitHub Copilot",
|
|
13576
|
-
roocode: "RooCode AI
|
|
13902
|
+
roocode: "RooCode AI编辑器(deprecated)",
|
|
13577
13903
|
"tongyi-lingma": "通义灵码",
|
|
13578
13904
|
trae: "Trae AI编辑器",
|
|
13579
13905
|
qoder: "Qoder AI编辑器",
|
|
@@ -13868,7 +14194,7 @@ function registerSetupTools(server) {
|
|
|
13868
14194
|
server.registerTool("downloadTemplate", {
|
|
13869
14195
|
title: "下载项目模板",
|
|
13870
14196
|
description: `自动下载并部署CloudBase项目模板。
|
|
13871
|
-
**Note**: Call this tool when the user requests to create a new project using a CloudBase template.\n\n支持的模板:\n- react: React + CloudBase 全栈应用模板\n- vue: Vue + CloudBase 全栈应用模板\n- miniprogram: 微信小程序 + 云开发模板 \n- uniapp: UniApp + CloudBase 跨端应用模板\n- rules: 只包含AI编辑器配置文件(包含Cursor、WindSurf、CodeBuddy等所有主流编辑器配置),适合在已有项目中补充AI编辑器配置\n\n支持的IDE类型:\n- all: 下载所有IDE配置\n- cursor: Cursor AI编辑器\n- 其他IDE类型见下方列表\n\n注意:如果未传入 ide 参数且无法从环境变量检测到 IDE,将提示错误并要求传入 ide 参数\n- windsurf: WindSurf AI编辑器\n- codebuddy: CodeBuddy AI编辑器\n- claude-code: Claude Code AI编辑器\n- cline: Cline AI编辑器\n- gemini-cli: Gemini CLI\n- opencode: OpenCode AI编辑器\n- qwen-code: 通义灵码\n- baidu-comate: 百度Comate\n- openai-codex-cli: OpenAI Codex CLI\n- augment-code: Augment Code\n- github-copilot: GitHub Copilot\n- roocode: RooCode AI编辑器\n- tongyi-lingma: 通义灵码\n- trae: Trae AI编辑器\n- qoder: Qoder AI编辑器\n- antigravity: Google Antigravity AI编辑器\n- vscode: Visual Studio Code\n- kiro: Kiro AI编辑器\n- aider: Aider AI编辑器\n\n特别说明:\n- rules 模板会自动包含当前 mcp 版本号信息(版本号:${ true ? "2.
|
|
14197
|
+
**Note**: Call this tool when the user requests to create a new project using a CloudBase template.\n\n支持的模板:\n- react: React + CloudBase 全栈应用模板\n- vue: Vue + CloudBase 全栈应用模板\n- miniprogram: 微信小程序 + 云开发模板 \n- uniapp: UniApp + CloudBase 跨端应用模板\n- rules: 只包含AI编辑器配置文件(包含Cursor、WindSurf、CodeBuddy等所有主流编辑器配置),适合在已有项目中补充AI编辑器配置\n\n支持的IDE类型:\n- all: 下载所有IDE配置\n- cursor: Cursor AI编辑器\n- 其他IDE类型见下方列表\n\n注意:如果未传入 ide 参数且无法从环境变量检测到 IDE,将提示错误并要求传入 ide 参数\n- windsurf: WindSurf AI编辑器\n- codebuddy: CodeBuddy AI编辑器\n- claude-code: Claude Code AI编辑器\n- cline: Cline AI编辑器\n- gemini-cli: Gemini CLI\n- opencode: OpenCode AI编辑器\n- qwen-code: 通义灵码\n- baidu-comate: 百度Comate\n- openai-codex-cli: OpenAI Codex CLI\n- augment-code: Augment Code\n- github-copilot: GitHub Copilot\n- roocode: RooCode AI编辑器\n- tongyi-lingma: 通义灵码\n- trae: Trae AI编辑器\n- qoder: Qoder AI编辑器\n- antigravity: Google Antigravity AI编辑器\n- vscode: Visual Studio Code\n- kiro: Kiro AI编辑器\n- aider: Aider AI编辑器\n\n特别说明:\n- rules 模板会自动包含当前 mcp 版本号信息(版本号:${ true ? "2.24.0" : 0}),便于后续维护和版本追踪\n- 下载 rules 模板时,如果项目中已存在 README.md 文件,系统会自动保护该文件不被覆盖(除非设置 overwrite=true)`,
|
|
13872
14198
|
inputSchema: {
|
|
13873
14199
|
template: zod_1.z
|
|
13874
14200
|
.enum(["react", "vue", "miniprogram", "uniapp", "rules"])
|
|
@@ -16623,6 +16949,7 @@ const STORAGE_ACTIONS = [
|
|
|
16623
16949
|
"objectInfo",
|
|
16624
16950
|
"signUpload",
|
|
16625
16951
|
"signDownload",
|
|
16952
|
+
"createBucket",
|
|
16626
16953
|
];
|
|
16627
16954
|
function buildPgStorageResult(payload) {
|
|
16628
16955
|
return (0, tool_result_js_1.buildJsonToolResult)(payload);
|
|
@@ -16681,12 +17008,52 @@ async function handleBuckets(server) {
|
|
|
16681
17008
|
data: {
|
|
16682
17009
|
capability: "storageManagerSdkOrHttpApi",
|
|
16683
17010
|
envId: server.cloudBaseOptions?.envId ?? null,
|
|
16684
|
-
supportedActions: ["buckets", "config", "uploadPlan", "objectInfo"],
|
|
17011
|
+
supportedActions: ["buckets", "config", "uploadPlan", "objectInfo", "createBucket"],
|
|
16685
17012
|
note: "PG environment storage is exposed as metadata and implementation plans in MCP. Application data-plane upload/download should use SDK or HTTP API code.",
|
|
16686
17013
|
},
|
|
16687
17014
|
message: "Resolved PostgreSQL storage capability summary. Use uploadPlan for application-side upload implementation.",
|
|
16688
17015
|
});
|
|
16689
17016
|
}
|
|
17017
|
+
async function handleCreateBucket(args, server) {
|
|
17018
|
+
if (!args.bucket?.trim()) {
|
|
17019
|
+
return buildPgStorageResult({
|
|
17020
|
+
success: false,
|
|
17021
|
+
errorCode: "BUCKET_REQUIRED",
|
|
17022
|
+
message: "Provide bucket name to create a PG storage bucket.",
|
|
17023
|
+
});
|
|
17024
|
+
}
|
|
17025
|
+
const bucket = args.bucket.trim();
|
|
17026
|
+
const envId = server.cloudBaseOptions?.envId ?? "${envId}";
|
|
17027
|
+
return buildPgStorageResult({
|
|
17028
|
+
success: true,
|
|
17029
|
+
data: {
|
|
17030
|
+
action: "createBucket",
|
|
17031
|
+
bucket,
|
|
17032
|
+
description: `Creates PG storage bucket '${bucket}'`,
|
|
17033
|
+
plans: {
|
|
17034
|
+
sql: `INSERT INTO storage.buckets (id, name, public) VALUES ('${bucket}', '${bucket}', false);`,
|
|
17035
|
+
httpApi: {
|
|
17036
|
+
method: "POST",
|
|
17037
|
+
url: `https://${envId}.api.tcloudbasegateway.com/v1/storages/bucket/`,
|
|
17038
|
+
headers: {
|
|
17039
|
+
"Authorization": "Bearer {service_role_token}",
|
|
17040
|
+
"Content-Type": "application/json",
|
|
17041
|
+
},
|
|
17042
|
+
body: { name: bucket, public: false },
|
|
17043
|
+
},
|
|
17044
|
+
cli: `tcb db execute -e ${envId} --sql "INSERT INTO storage.buckets (id, name, public) VALUES ('${bucket}', '${bucket}', false);"`,
|
|
17045
|
+
},
|
|
17046
|
+
tokenRequired: "service_role",
|
|
17047
|
+
tokenGuide: "Use manageAppAuth(action='createApiKey', name='storage-server-key') to get a service_role level API key token.",
|
|
17048
|
+
nextSteps: [
|
|
17049
|
+
"Option A: Run the SQL via managePgDatabase(action='execute', confirm=true)",
|
|
17050
|
+
"Option B: Call the HTTP API with a service_role token",
|
|
17051
|
+
"Option C: Use CloudBase CLI: tcb db execute",
|
|
17052
|
+
],
|
|
17053
|
+
},
|
|
17054
|
+
message: `Generated bucket creation plan for '${bucket}'. Execute one of the provided plans to create the bucket, then configure RLS policies.`,
|
|
17055
|
+
});
|
|
17056
|
+
}
|
|
16690
17057
|
async function handleUploadPlan(args, server) {
|
|
16691
17058
|
const missingBucket = requireBucket(args);
|
|
16692
17059
|
if (missingBucket)
|
|
@@ -16757,7 +17124,7 @@ function registerPGStorageTools(server) {
|
|
|
16757
17124
|
inputSchema: {
|
|
16758
17125
|
action: zod_1.z
|
|
16759
17126
|
.enum(STORAGE_ACTIONS)
|
|
16760
|
-
.describe("操作类型:buckets/config=查询存储能力摘要;uploadPlan=生成 HTTP API/SDK 上传方案;objectInfo=生成对象元信息查询方案;signUpload/signDownload=显式的一次性签名 URL 请求占位"),
|
|
17127
|
+
.describe("操作类型:buckets/config=查询存储能力摘要;createBucket=生成 bucket 创建方案(SQL/HTTP API/CLI);uploadPlan=生成 HTTP API/SDK 上传方案;objectInfo=生成对象元信息查询方案;signUpload/signDownload=显式的一次性签名 URL 请求占位"),
|
|
16761
17128
|
bucket: zod_1.z.string().optional().describe("云存储 bucket 名称"),
|
|
16762
17129
|
objectKey: zod_1.z.string().optional().describe("单个对象 key"),
|
|
16763
17130
|
objectKeys: zod_1.z.array(zod_1.z.string()).optional().describe("多个对象 key,用于对象元信息查询规划"),
|
|
@@ -16781,6 +17148,8 @@ function registerPGStorageTools(server) {
|
|
|
16781
17148
|
case "buckets":
|
|
16782
17149
|
case "config":
|
|
16783
17150
|
return handleBuckets(server);
|
|
17151
|
+
case "createBucket":
|
|
17152
|
+
return handleCreateBucket(args, server);
|
|
16784
17153
|
case "uploadPlan":
|
|
16785
17154
|
return handleUploadPlan(args, server);
|
|
16786
17155
|
case "objectInfo":
|