import type { Tool } from '@modelcontextprotocol/sdk/types.js';
import { z } from 'zod';
import { pocketBaseService, PocketBaseError } from '../pocketbase-service.js';
import { logger } from '../utils/logger.js';
import type {
ToolResult,
} from '../types/mcp.js';
import {
GetFileUrlParamsSchema,
} from '../types/mcp.js';
// Helper function to create tool results
function createResult<T = unknown>(
text: string,
isError = false,
meta?: T,
): ToolResult<T> {
const result: ToolResult<T> = {
content: [{ type: 'text', text }],
isError,
};
if (meta !== undefined) {
(result as any)._meta = meta;
}
return result;
}
// Helper function to validate parameters
function validateParams<T>(schema: z.ZodSchema<T>, params: unknown): T {
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: Tool = {
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: unknown): Promise<ToolResult> {
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: Tool = {
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: unknown): Promise<ToolResult> {
try {
const { collection, recordId, fieldName, fileData, fileName, mimeType } = params as {
collection: string;
recordId: string;
fieldName: string;
fileData: string;
fileName: string;
mimeType?: string;
};
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 as any,
);
// 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: Tool = {
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: unknown): Promise<ToolResult> {
try {
const { collection, recordId, fieldName, filename } = params as {
collection: string;
recordId: string;
fieldName: string;
filename: string;
};
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 as any)[fieldName];
let updatedFiles: string[] = [];
if (Array.isArray(currentFiles)) {
// Multiple files field - remove the specific file
updatedFiles = currentFiles.filter((file: string) => 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: Record<string, unknown> = {};
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: Tool = {
name: 'pb_files_get_token',
description: 'Get a file access token for private files',
inputSchema: {
type: 'object',
properties: {},
},
};
export async function handleGetFileToken(): Promise<ToolResult> {
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: Tool = {
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: unknown): Promise<ToolResult> {
try {
const { collection, recordId } = params as {
collection: string;
recordId: string;
};
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: Array<{
fieldName: string;
files: Array<{
name: string;
url: string;
}>;
}> = [];
for (const field of fileFields) {
const fieldValue = (record as any)[field.name];
const fieldFiles: Array<{ name: string; url: string }> = [];
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,
};