Skip to content

Commit 4fd81ea

Browse files
authored
Merge pull request #564 from TencentCloudBase/automation/attribution-issue-mnrqrzmm-i0zigr-querystorage-mcp-agent-result-json
fix: queryStorage MCP 工具缺少文件内容读取能力,导致 agent 绕行验证内容后 RESULT.json 字段命名偏离预期
2 parents 65c585d + 9934d06 commit 4fd81ea

1 file changed

Lines changed: 66 additions & 2 deletions

File tree

mcp/src/tools/storage.ts

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
import * as fs from "fs/promises";
2+
import * as os from "os";
3+
import * as path from "path";
14
import { z } from "zod";
25
import { getCloudBaseManager } from '../cloudbase-manager.js';
36
import { ExtendedMcpServer } from '../server.js';
47

8+
const MAX_INLINE_TEXT_BYTES = 256 * 1024;
9+
const STORAGE_READ_TEMP_PREFIX = 'cloudbase-mcp-storage-read-';
10+
511
// Input schema for queryStorage tool
612
const queryStorageInputSchema = {
7-
action: z.enum(['list', 'info', 'url']).describe('查询操作类型:list=列出目录下的所有文件,info=获取指定文件的详细信息,url=获取文件的临时下载链接'),
13+
action: z.enum(['list', 'info', 'url', 'read']).describe('查询操作类型:list=列出目录下的所有文件,info=获取指定文件的详细信息,url=获取文件的临时下载链接,read=读取文本文件内容'),
814
cloudPath: z.string().describe('云端文件路径,例如 files/data.txt 或 files/(目录)'),
915
maxAge: z.number().min(1).max(86400).optional().default(3600).describe('临时链接有效期,单位为秒,取值范围:1-86400,默认值:3600(1小时)')
1016
};
@@ -19,11 +25,30 @@ const manageStorageInputSchema = {
1925
};
2026

2127
type QueryStorageInput = {
22-
action: 'list' | 'info' | 'url';
28+
action: 'list' | 'info' | 'url' | 'read';
2329
cloudPath: string;
2430
maxAge?: number;
2531
};
2632

33+
function getStorageTempFileName(cloudPath: string) {
34+
const baseName = path.posix.basename(cloudPath);
35+
return baseName || 'storage-file';
36+
}
37+
38+
function decodeInlineTextContent(buffer: Buffer) {
39+
const inlineBuffer = buffer.subarray(0, MAX_INLINE_TEXT_BYTES);
40+
if (inlineBuffer.includes(0)) {
41+
throw new Error('queryStorage action=read 仅支持读取文本文件内容;二进制文件请改用 action=url 获取下载链接,或使用 manageStorage(action="download") 下载到本地。');
42+
}
43+
44+
return {
45+
content: inlineBuffer.toString('utf8'),
46+
truncated: buffer.length > MAX_INLINE_TEXT_BYTES,
47+
sizeBytes: buffer.length,
48+
encoding: 'utf8' as const,
49+
};
50+
}
51+
2752
type ManageStorageInput = {
2853
action: 'upload' | 'download' | 'delete';
2954
localPath: string;
@@ -132,6 +157,45 @@ export function registerStorageTools(server: ExtendedMcpServer) {
132157
};
133158
}
134159

160+
case 'read': {
161+
const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), STORAGE_READ_TEMP_PREFIX));
162+
const localPath = path.join(tempDir, getStorageTempFileName(input.cloudPath));
163+
164+
try {
165+
await storageService.downloadFile({
166+
cloudPath: input.cloudPath,
167+
localPath
168+
});
169+
170+
const buffer = await fs.readFile(localPath);
171+
const decoded = decodeInlineTextContent(buffer);
172+
173+
return {
174+
content: [
175+
{
176+
type: "text",
177+
text: JSON.stringify({
178+
success: true,
179+
data: {
180+
action: 'read',
181+
cloudPath: input.cloudPath,
182+
content: decoded.content,
183+
encoding: decoded.encoding,
184+
sizeBytes: decoded.sizeBytes,
185+
truncated: decoded.truncated
186+
},
187+
message: decoded.truncated
188+
? `Successfully read text content for '${input.cloudPath}' (truncated to ${MAX_INLINE_TEXT_BYTES} bytes)`
189+
: `Successfully read text content for '${input.cloudPath}'`
190+
}, null, 2)
191+
}
192+
]
193+
};
194+
} finally {
195+
await fs.rm(tempDir, { recursive: true, force: true });
196+
}
197+
}
198+
135199
default:
136200
throw new Error(`Unsupported action: ${input.action}`);
137201
}

0 commit comments

Comments
 (0)