import { z } from 'zod';
import { pocketBaseService, PocketBaseError } from '../pocketbase-service.js';
import { logger } from '../utils/logger.js';
import { GetFileUrlParamsSchema, } from '../types/mcp.js';
// Helper function to create tool results
function createResult(text, isError = false, meta) {
const result = {
content: [{ type: 'text', text }],
isError,
};
if (meta !== undefined) {
result._meta = meta;
}
return result;
}
// Helper function to validate parameters
function validateParams(schema, params) {
try {
return schema.parse(params);
}
catch (error) {
if (error instanceof z.ZodError) {
const errors = error.errors.map((err) => `${err.path.join('.')}: ${err.message}`);
throw new Error(`Invalid parameters:\n${errors.join('\n')}`);
}
throw error;
}
}
// Get file URL tool
export const getFileUrlTool = {
name: 'pb_files_get_url',
description: 'Get the URL for a file attached to a record',
inputSchema: {
type: 'object',
properties: {
collection: {
type: 'string',
description: 'Collection name or ID',
},
recordId: {
type: 'string',
description: 'Record ID that owns the file',
},
filename: {
type: 'string',
description: 'File name',
},
thumb: {
type: 'string',
description: 'Thumbnail size (e.g., "100x100", "0x100")',
},
},
required: ['collection', 'recordId', 'filename'],
},
};
export async function handleGetFileUrl(params) {
try {
const { collection, recordId, filename, thumb } = validateParams(GetFileUrlParamsSchema, params);
logger.info('Processing get file URL request', {
collection,
recordId,
filename,
thumb,
});
// First get the record to pass to the file URL function
const record = await pocketBaseService.getRecord(collection, recordId);
const fileUrl = pocketBaseService.getFileUrl(record, filename, { thumb });
const result = {
success: true,
collection,
recordId,
filename,
thumb,
url: fileUrl,
};
return createResult(`File URL generated for '${filename}' in record ${recordId}`, false, result);
}
catch (error) {
logger.error('Get file URL failed', error);
if (error instanceof PocketBaseError) {
return createResult(`Get file URL failed: ${error.message}`, true, { error: error.message, status: error.status });
}
const message = error instanceof Error ? error.message : 'Unknown error occurred';
return createResult(`Get file URL failed: ${message}`, true);
}
}
// Upload file tool
export const uploadFileTool = {
name: 'pb_files_upload',
description: 'Upload a file to a record field',
inputSchema: {
type: 'object',
properties: {
collection: {
type: 'string',
description: 'Collection name or ID',
},
recordId: {
type: 'string',
description: 'Record ID to attach the file to',
},
fieldName: {
type: 'string',
description: 'Field name to store the file in',
},
fileData: {
type: 'string',
description: 'Base64 encoded file data',
},
fileName: {
type: 'string',
description: 'Original file name',
},
mimeType: {
type: 'string',
description: 'File MIME type',
},
},
required: ['collection', 'recordId', 'fieldName', 'fileData', 'fileName'],
},
};
export async function handleUploadFile(params) {
try {
const { collection, recordId, fieldName, fileData, fileName, mimeType } = params;
logger.info('Processing file upload request', {
collection,
recordId,
fieldName,
fileName,
mimeType,
});
// Convert base64 to blob
const binaryData = atob(fileData);
const bytes = new Uint8Array(binaryData.length);
for (let i = 0; i < binaryData.length; i++) {
bytes[i] = binaryData.charCodeAt(i);
}
const blob = new Blob([bytes], { type: mimeType || 'application/octet-stream' });
const file = new File([blob], fileName, { type: mimeType });
// Create FormData for the update
const formData = new FormData();
formData.append(fieldName, file);
// Update the record with the file
const updatedRecord = await pocketBaseService.updateRecord(collection, recordId, formData);
// Get the file URL for the uploaded file
const fileUrl = pocketBaseService.getFileUrl(updatedRecord, fileName);
const result = {
success: true,
collection,
recordId,
fieldName,
fileName,
fileUrl,
record: updatedRecord,
};
return createResult(`File '${fileName}' uploaded successfully to field '${fieldName}' in record ${recordId}`, false, result);
}
catch (error) {
logger.error('File upload failed', error);
if (error instanceof PocketBaseError) {
return createResult(`File upload failed: ${error.message}`, true, { error: error.message, status: error.status });
}
const message = error instanceof Error ? error.message : 'Unknown error occurred';
return createResult(`File upload failed: ${message}`, true);
}
}
// Delete file tool
export const deleteFileTool = {
name: 'pb_files_delete',
description: 'Delete a file from a record field',
inputSchema: {
type: 'object',
properties: {
collection: {
type: 'string',
description: 'Collection name or ID',
},
recordId: {
type: 'string',
description: 'Record ID that owns the file',
},
fieldName: {
type: 'string',
description: 'Field name that contains the file',
},
filename: {
type: 'string',
description: 'File name to delete',
},
},
required: ['collection', 'recordId', 'fieldName', 'filename'],
},
};
export async function handleDeleteFile(params) {
try {
const { collection, recordId, fieldName, filename } = params;
logger.info('Processing delete file request', {
collection,
recordId,
fieldName,
filename,
});
// Get the current record
const record = await pocketBaseService.getRecord(collection, recordId);
// Get current field value
const currentFiles = record[fieldName];
let updatedFiles = [];
if (Array.isArray(currentFiles)) {
// Multiple files field - remove the specific file
updatedFiles = currentFiles.filter((file) => file !== filename);
}
else if (currentFiles === filename) {
// Single file field - clear it
updatedFiles = [];
}
else {
return createResult(`File '${filename}' not found in field '${fieldName}'`, true);
}
// Update the record with the new file list
const updateData = {};
updateData[fieldName] = updatedFiles.length === 0 ? null : updatedFiles;
const updatedRecord = await pocketBaseService.updateRecord(collection, recordId, updateData);
const result = {
success: true,
collection,
recordId,
fieldName,
filename,
record: updatedRecord,
};
return createResult(`File '${filename}' deleted successfully from field '${fieldName}' in record ${recordId}`, false, result);
}
catch (error) {
logger.error('Delete file failed', error);
if (error instanceof PocketBaseError) {
return createResult(`Delete file failed: ${error.message}`, true, { error: error.message, status: error.status });
}
const message = error instanceof Error ? error.message : 'Unknown error occurred';
return createResult(`Delete file failed: ${message}`, true);
}
}
// Get file token tool
export const getFileTokenTool = {
name: 'pb_files_get_token',
description: 'Get a file access token for private files',
inputSchema: {
type: 'object',
properties: {},
},
};
export async function handleGetFileToken() {
try {
logger.info('Processing get file token request');
const token = await pocketBaseService.getFileToken();
const result = {
success: true,
token,
expiresIn: '2 hours', // PocketBase default file token duration
};
return createResult('File access token generated successfully', false, result);
}
catch (error) {
logger.error('Get file token failed', error);
if (error instanceof PocketBaseError) {
return createResult(`Get file token failed: ${error.message}`, true, { error: error.message, status: error.status });
}
const message = error instanceof Error ? error.message : 'Unknown error occurred';
return createResult(`Get file token failed: ${message}`, true);
}
}
// List files in record tool
export const listRecordFilesTool = {
name: 'pb_files_list_record_files',
description: 'List all files attached to a record',
inputSchema: {
type: 'object',
properties: {
collection: {
type: 'string',
description: 'Collection name or ID',
},
recordId: {
type: 'string',
description: 'Record ID to list files for',
},
},
required: ['collection', 'recordId'],
},
};
export async function handleListRecordFiles(params) {
try {
const { collection, recordId } = params;
logger.info('Processing list record files request', { collection, recordId });
// Get the record and extract file fields
const record = await pocketBaseService.getRecord(collection, recordId);
// Get collection schema to identify file fields
const collectionInfo = await pocketBaseService.getCollection(collection);
const fileFields = collectionInfo.schema.filter((field) => field.type === 'file');
const files = [];
for (const field of fileFields) {
const fieldValue = record[field.name];
const fieldFiles = [];
if (fieldValue) {
const fileNames = Array.isArray(fieldValue) ? fieldValue : [fieldValue];
for (const fileName of fileNames) {
if (fileName) {
const fileUrl = pocketBaseService.getFileUrl(record, fileName);
fieldFiles.push({
name: fileName,
url: fileUrl,
});
}
}
}
files.push({
fieldName: field.name,
files: fieldFiles,
});
}
const totalFiles = files.reduce((sum, field) => sum + field.files.length, 0);
const result = {
success: true,
collection,
recordId,
totalFiles,
fileFields: files,
};
return createResult(`Found ${totalFiles} files in record ${recordId} across ${files.length} file fields`, false, result);
}
catch (error) {
logger.error('List record files failed', error);
if (error instanceof PocketBaseError) {
return createResult(`List record files failed: ${error.message}`, true, { error: error.message, status: error.status });
}
const message = error instanceof Error ? error.message : 'Unknown error occurred';
return createResult(`List record files failed: ${message}`, true);
}
}
// Export all file tools and handlers
export const fileTools = [
getFileUrlTool,
uploadFileTool,
deleteFileTool,
getFileTokenTool,
listRecordFilesTool,
];
export const fileHandlers = {
pb_files_get_url: handleGetFileUrl,
pb_files_upload: handleUploadFile,
pb_files_delete: handleDeleteFile,
pb_files_get_token: handleGetFileToken,
pb_files_list_record_files: handleListRecordFiles,
};
//# sourceMappingURL=files.js.map