feat(mcp): add file tools for scenarios and runs
- expose scenario file upload, listing, and content retrieval over MCP\n- expose run artifact listing and content retrieval with end-to-end coverage
This commit is contained in:
@@ -5,6 +5,7 @@ import {
|
||||
NotFoundException,
|
||||
} from "@nestjs/common";
|
||||
import { InjectRepository } from "@nestjs/typeorm";
|
||||
import * as fs from "fs/promises";
|
||||
import * as path from "path";
|
||||
import { Like, Repository } from "typeorm";
|
||||
import {
|
||||
@@ -768,6 +769,47 @@ export class ScenarioService {
|
||||
res.sendFile(fullPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get scenario file content as a buffer (for MCP use).
|
||||
* Returns metadata plus raw bytes without streaming to Response.
|
||||
*/
|
||||
async getScenarioFileContentAsBuffer(
|
||||
scenarioId: string,
|
||||
fileId: string,
|
||||
): Promise<{ file: { id: string; name: string; mimeType: string; size: number; sha256: string; expiresAt: Date | null; createdAt: Date }; contentBuffer: Buffer }> {
|
||||
// Verify scenario exists
|
||||
await this.findOne(scenarioId);
|
||||
|
||||
// Check if file is linked to this scenario
|
||||
const link = await this.scenarioFileRepo.findOne({
|
||||
where: { scenarioId, fileId },
|
||||
relations: ["file"],
|
||||
});
|
||||
|
||||
if (!link) {
|
||||
throw new NotFoundException(`File ${fileId} not found in scenario ${scenarioId}`);
|
||||
}
|
||||
|
||||
const file = link.file;
|
||||
const fullPath = path.resolve(this.fileStorageService.getAbsolutePath(file.filePath));
|
||||
|
||||
// Read file content as buffer
|
||||
const contentBuffer = await fs.readFile(fullPath);
|
||||
|
||||
return {
|
||||
file: {
|
||||
id: file.id,
|
||||
name: file.originalName,
|
||||
mimeType: file.mimeType,
|
||||
size: file.size,
|
||||
sha256: file.sha256,
|
||||
expiresAt: file.expiresAt,
|
||||
createdAt: file.createdAt,
|
||||
},
|
||||
contentBuffer,
|
||||
};
|
||||
}
|
||||
|
||||
async listRunFiles(
|
||||
scenarioId: string,
|
||||
runId: string,
|
||||
@@ -833,4 +875,50 @@ export class ScenarioService {
|
||||
|
||||
res.sendFile(fullPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get run artifact file content as a buffer (for MCP use).
|
||||
* Returns metadata plus raw bytes without streaming to Response.
|
||||
*/
|
||||
async getRunFileContentAsBuffer(
|
||||
scenarioId: string,
|
||||
runId: string,
|
||||
fileId: string,
|
||||
): Promise<{ file: { id: string; name: string; mimeType: string; size: number; sha256: string; expiresAt: Date | null; createdAt: Date }; contentBuffer: Buffer }> {
|
||||
// Verify scenario and run exist
|
||||
await this.findOne(scenarioId);
|
||||
const run = await this.runRepo.findOneBy({ id: runId, scenarioId });
|
||||
if (!run) {
|
||||
throw new NotFoundException(`Run ${runId} not found in scenario ${scenarioId}`);
|
||||
}
|
||||
|
||||
// Check if file is linked to this run
|
||||
const link = await this.scenarioRunFileRepo.findOne({
|
||||
where: { runId, fileId },
|
||||
relations: ["file"],
|
||||
});
|
||||
|
||||
if (!link) {
|
||||
throw new NotFoundException(`File ${fileId} not found in run ${runId}`);
|
||||
}
|
||||
|
||||
const file = link.file;
|
||||
const fullPath = path.resolve(this.fileStorageService.getAbsolutePath(file.filePath));
|
||||
|
||||
// Read file content as buffer
|
||||
const contentBuffer = await fs.readFile(fullPath);
|
||||
|
||||
return {
|
||||
file: {
|
||||
id: file.id,
|
||||
name: file.originalName,
|
||||
mimeType: file.mimeType,
|
||||
size: file.size,
|
||||
sha256: file.sha256,
|
||||
expiresAt: file.expiresAt,
|
||||
createdAt: file.createdAt,
|
||||
},
|
||||
contentBuffer,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user