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:
2026-04-21 18:00:41 +03:00
parent 06c662042b
commit b57b55f3e1
4 changed files with 756 additions and 0 deletions
+88
View File
@@ -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,
};
}
}