dbx-mcp-server
Version:
A Model Context Protocol server for Dropbox integration with AI-powered PDF analysis, multi-directory indexing, and parallel processing
171 lines • 7.13 kB
JavaScript
import { McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js';
import { listFiles, downloadFile, getFileMetadata } from './dbx-api.js';
// Helper function to determine MIME type from file extension
function getMimeType(filename) {
const ext = filename.split('.').pop()?.toLowerCase();
if (!ext)
return 'application/octet-stream';
const mimeTypes = {
'txt': 'text/plain',
'pdf': 'application/pdf',
'jpg': 'image/jpeg',
'jpeg': 'image/jpeg',
'png': 'image/png',
'gif': 'image/gif',
'doc': 'application/msword',
'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'xls': 'application/vnd.ms-excel',
'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
'json': 'application/json',
'md': 'text/markdown',
'html': 'text/html',
'css': 'text/css',
'js': 'application/javascript',
'zip': 'application/zip',
'mp3': 'audio/mpeg',
'mp4': 'video/mp4',
};
return mimeTypes[ext] || 'application/octet-stream';
}
// Helper function to get resource metadata
function getResourceMetadata(item, parentItems) {
const metadata = {
id: item.id,
path: item.path_display,
modified: item.client_modified,
size: item.size || 0,
parentFolder: item.path_display.split('/').slice(0, -1).join('/'),
sharing_info: item.sharing_info,
hasChildren: item['.tag'] === 'folder' && (parentItems?.some((child) => child.path_display.startsWith(item.path_display + '/')) || false)
};
if (item['.tag'] === 'folder' && parentItems) {
metadata.childCount = parentItems.filter((child) => child.path_display.startsWith(item.path_display + '/')).length;
}
return metadata;
}
// Helper function to normalize Dropbox path
function normalizePath(path) {
// Remove leading slash if present
return path.replace(/^\/+/, '');
}
async function handleListResources(request) {
try {
const args = request.params?.arguments;
const path = args?.path || '';
const dropboxFiles = await listFiles(normalizePath(path));
const items = JSON.parse(dropboxFiles.content[0].text);
const resources = items.map((item) => {
const mimeType = item['.tag'] === 'folder' ?
'folder' :
getMimeType(item.name);
// Get children info for folders
const children = item['.tag'] === 'folder' ?
items
.filter(child => child.path_display.startsWith(item.path_display + '/'))
.map(child => child.name)
.join(', ') :
undefined;
return {
uri: `dbx://${normalizePath(item.path_display)}`,
name: item.name,
type: item['.tag'] === 'file' ? 'dbx-file' : 'dbx-folder',
mimeType,
description: item['.tag'] === 'folder' ?
`Folder containing: ${children || 'empty'} (integrates with Dropbox)` :
`File (${mimeType}) (integrates with Dropbox)`,
metadata: getResourceMetadata(item, items)
};
});
return {
resources: resources
};
}
catch (error) {
console.error("Error listing resources:", error);
if (error.message.includes('path not found')) {
throw new McpError(ErrorCode.InvalidParams, `Path not found: ${request.params?.arguments?.path || '/'}`);
}
throw new McpError(ErrorCode.InternalError, `Failed to list resources: ${error.message}`);
}
}
async function handleReadResource(request) {
try {
const uri = request.params.uri;
// Validate and parse URI
if (!uri.startsWith('dbx://')) {
throw new McpError(ErrorCode.InvalidRequest, `Invalid URI format: ${uri}`);
}
// Handle URI templates
let path = '';
if (uri.startsWith('dbx:///shared/')) {
const shareId = uri.replace('dbx:///shared/', '');
// Here you would implement share ID resolution logic
throw new McpError(ErrorCode.MethodNotFound, 'Shared item access not yet implemented');
}
else {
path = normalizePath(uri.replace('dbx://', ''));
}
// Try to get the file directly first
try {
const result = await downloadFile(path);
const mimeType = getMimeType(path);
const metadata = await getFileMetadata(path);
const metadataObj = JSON.parse(metadata.content[0].text);
return {
contents: [
{
uri: uri,
mimeType,
text: result.content[0].text,
encoding: result.content[0].encoding,
metadata: {
accessed: new Date().toISOString(),
size: metadataObj.size,
path,
modified: metadataObj.client_modified,
rev: metadataObj.rev
}
}
]
};
}
catch (error) {
// If file download fails, try listing as a folder
if (error.message.includes('path not found')) {
const folderContents = await listFiles(path);
const items = JSON.parse(folderContents.content[0].text);
return {
contents: [
{
uri: uri,
mimeType: 'application/json',
text: JSON.stringify({
path,
items: items.map((item) => ({
name: item.name,
path: item.path_display,
type: item['.tag'],
modified: item.client_modified,
metadata: getResourceMetadata(item, items)
}))
}, null, 2)
}
]
};
}
throw error;
}
}
catch (error) {
console.error("Error reading resource:", error);
if (error.message.includes('path not found')) {
throw new McpError(ErrorCode.InvalidParams, `Resource not found: ${request.params.uri}`);
}
if (error.message.includes('insufficient permissions')) {
throw new McpError(ErrorCode.InvalidRequest, `Access denied to resource: ${request.params.uri}`);
}
throw new McpError(ErrorCode.InternalError, `Failed to read resource: ${error.message}`);
}
}
export { handleListResources, handleReadResource };
//# sourceMappingURL=resource-handler.js.map