UNPKG

dbx-mcp-server

Version:

A Model Context Protocol server for Dropbox integration with AI-powered PDF analysis, multi-directory indexing, and parallel processing

417 lines 18.6 kB
import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListResourcesRequestSchema, ListResourceTemplatesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, McpError, ErrorCode } from '@modelcontextprotocol/sdk/types.js'; import { getValidAccessToken } from './auth.js'; import { handleListPrompts, handleGetPrompt } from './prompt-handler.js'; import { handleListResources, handleReadResource } from './resource-handler.js'; import { toolDefinitions } from './tool-definitions.js'; import { config, log } from './config.js'; import * as dbxApi from './dbx-api.js'; import { analyzePDF } from './pdf-analyzer.js'; import { indexFile, indexFolder, searchIndexed, getDocumentStats } from './document-indexer.js'; import { schemaManager } from './schema-manager.js'; // Define resource templates const resourceTemplates = [ { uriTemplate: 'dbx://{path}', name: 'Dbx Item', description: 'Access any file or folder in Dropbox by path', parameters: { path: { description: 'Path to the file or folder', required: true, type: 'string' } } }, { uriTemplate: 'dbx:///shared/{share_id}', name: 'Shared Dbx Item', description: 'Access a shared Dropbox item by its share ID', parameters: { share_id: { description: 'Shared item identifier', required: true, type: 'string' } } } ]; class DbxServer { constructor() { this.server = new Server({ name: 'dbx-mcp-server', version: '0.1.0', }, { capabilities: { resources: { types: ["dbx-file", "dbx-folder"], }, tools: { tools: toolDefinitions }, prompts: { listChanged: true } }, }); this.setupHandlers(); this.setupPromptHandlers(); // Error handling this.server.onerror = (error) => { log.error('Server error:', { error: error.message, code: error.code, stack: error.stack }); }; } setupPromptHandlers() { this.server.setRequestHandler(ListPromptsRequestSchema, handleListPrompts); this.server.setRequestHandler(GetPromptRequestSchema, handleGetPrompt); } setupHandlers() { // Resource handlers this.server.setRequestHandler(ListResourcesRequestSchema, handleListResources); this.server.setRequestHandler(ReadResourceRequestSchema, handleReadResource); this.server.setRequestHandler(ListResourceTemplatesRequestSchema, async () => ({ resourceTemplates })); // Tool handlers this.server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: toolDefinitions })); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { // Verify authentication if (!config.dropbox.accessToken && !await getValidAccessToken()) { throw new McpError(ErrorCode.InvalidRequest, 'No valid access token available. Please authenticate first.'); } // Log request (without sensitive data) log.info('Tool request:', { tool: request.params.name, args: this.sanitizeArgs(request.params.arguments) }); // Handle tool requests const result = await (async () => { switch (request.params.name) { case 'list_files': return await dbxApi.listFiles(String(request.params.arguments?.path || '')); case 'upload_file': return await dbxApi.uploadFile(String(request.params.arguments?.path), String(request.params.arguments?.content)); case 'download_file': return await dbxApi.downloadFile(String(request.params.arguments?.path)); case 'safe_delete_item': return await dbxApi.safeDeleteItem(String(request.params.arguments?.path), String(request.params.arguments?.userId), Boolean(request.params.arguments?.skipConfirmation), Number(request.params.arguments?.retentionDays || config.safety.retentionDays), String(request.params.arguments?.reason || ''), Boolean(request.params.arguments?.permanent)); case 'delete_item': // Legacy delete operation - logs a warning and uses safe delete with default settings log.warn('Legacy delete operation used', { path: request.params.arguments?.path }); return await dbxApi.safeDeleteItem(String(request.params.arguments?.path), 'legacy_user', true, // skipConfirmation undefined, // retentionDays 'Legacy delete operation', // reason true // permanent ); case 'create_folder': return await dbxApi.createFolder(String(request.params.arguments?.path)); case 'copy_item': return await dbxApi.copyItem(String(request.params.arguments?.from_path), String(request.params.arguments?.to_path)); case 'move_item': return await dbxApi.moveItem(String(request.params.arguments?.from_path), String(request.params.arguments?.to_path)); case 'get_file_metadata': return await dbxApi.getFileMetadata(String(request.params.arguments?.path)); case 'search_file_db': { const searchOptions = { query: String(request.params.arguments?.query), path: String(request.params.arguments?.path || ''), maxResults: Number(request.params.arguments?.max_results || 20), fileExtensions: request.params.arguments?.file_extensions, fileCategories: request.params.arguments?.file_categories, dateRange: request.params.arguments?.date_range, includeContentMatch: Boolean(request.params.arguments?.include_content_match), sortBy: request.params.arguments?.sort_by || 'relevance', order: request.params.arguments?.order || 'desc' }; return await dbxApi.searchFiles(searchOptions); } case 'get_sharing_link': return await dbxApi.getSharingLink(String(request.params.arguments?.path)); case 'get_account_info': return await dbxApi.getAccountInfo(); case 'get_file_content': return await dbxApi.getFileContent(String(request.params.arguments?.path)); case 'dropbox_analyze_pdf': return await analyzePDF({ path: String(request.params.arguments?.path), prompt: String(request.params.arguments?.prompt), useThinking: Boolean(request.params.arguments?.useThinking), maxReasoningTokens: Number(request.params.arguments?.maxReasoningTokens) || 5000 }); case 'dropbox_index_file': return await indexFile({ path: String(request.params.arguments?.path) }); case 'dropbox_index_folder': return await indexFolder({ path: String(request.params.arguments?.path), recursive: Boolean(request.params.arguments?.recursive ?? true), batchSize: Number(request.params.arguments?.batchSize) || 200, excludePaths: request.params.arguments?.excludePaths, excludePatterns: request.params.arguments?.excludePatterns, respectGlobalExclusions: Boolean(request.params.arguments?.respectGlobalExclusions ?? true) }); case 'dropbox_search_indexed': return await searchIndexed({ query: String(request.params.arguments?.query), docType: request.params.arguments?.docType, limit: Number(request.params.arguments?.limit) || 20 }); case 'dropbox_document_stats': return await getDocumentStats(); case 'dropbox_team_info': return await dbxApi.getTeamInfo(); // Schema management tools case 'get_schema_config': return this.handleGetSchemaConfig(request.params.arguments?.documentType); case 'update_document_type': return this.handleUpdateDocumentType(request.params.arguments); case 'add_document_type': return this.handleAddDocumentType(request.params.arguments); case 'remove_document_type': return this.handleRemoveDocumentType(request.params.arguments); case 'update_global_prompt': return this.handleUpdateGlobalPrompt(request.params.arguments); case 'test_schema_prompt': return this.handleTestSchemaPrompt(); default: throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`); } })(); return { content: result.content, _meta: {} }; }); } sanitizeArgs(args) { if (!args) return args; const sanitized = { ...args }; // Remove sensitive data from logs if (sanitized.content) sanitized.content = '[CONTENT]'; return sanitized; } // Schema management handler methods handleGetSchemaConfig(documentType) { try { const config = schemaManager.getConfiguration(); if (documentType) { const typeConfig = schemaManager.getDocumentTypeConfig(documentType); if (!typeConfig) { return { content: [{ type: "text", text: `Document type '${documentType}' not found` }], isError: true }; } return { content: [{ type: "text", text: `**Document Type: ${documentType}**\n\n${JSON.stringify(typeConfig, null, 2)}` }], isError: false }; } let resultText = `**Schema Configuration**\n\n`; resultText += `**Version:** ${config.version}\n\n`; resultText += `**Global Prompt Configuration:**\n`; resultText += `- Base Prompt: ${config.globalPrompt.basePrompt}\n`; resultText += `- Classification: ${config.globalPrompt.classification}\n`; resultText += `- Guidelines: ${config.globalPrompt.guidelines.length} items\n\n`; resultText += `**Document Types (${Object.keys(config.documentTypes).length}):**\n`; for (const [typeName, typeConfig] of Object.entries(config.documentTypes)) { const status = typeConfig.enabled ? '✅' : '❌'; const fieldCount = Object.keys(typeConfig.fields).length; resultText += `${status} **${typeName}**: ${typeConfig.description} (${fieldCount} fields)\n`; } return { content: [{ type: "text", text: resultText }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error getting schema configuration: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } handleUpdateDocumentType(args) { try { const { typeName, ...updates } = args; if (!typeName) { return { content: [{ type: "text", text: "Error: typeName is required" }], isError: true }; } schemaManager.updateDocumentType(typeName, updates); return { content: [{ type: "text", text: `Successfully updated document type '${typeName}'` }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error updating document type: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } handleAddDocumentType(args) { try { const { name, description, enabled = true, fields, prompt } = args; if (!name || !description || !fields) { return { content: [{ type: "text", text: "Error: name, description, and fields are required" }], isError: true }; } const newType = { name, description, enabled, fields, prompt }; schemaManager.addDocumentType(newType); return { content: [{ type: "text", text: `Successfully added document type '${name}' with ${Object.keys(fields).length} fields` }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error adding document type: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } handleRemoveDocumentType(args) { try { const { typeName, confirm } = args; if (!typeName) { return { content: [{ type: "text", text: "Error: typeName is required" }], isError: true }; } if (!confirm) { return { content: [{ type: "text", text: "Error: Please set confirm=true to confirm document type removal" }], isError: true }; } schemaManager.removeDocumentType(typeName); return { content: [{ type: "text", text: `Successfully removed document type '${typeName}'` }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error removing document type: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } handleUpdateGlobalPrompt(args) { try { schemaManager.updateGlobalPrompt(args); return { content: [{ type: "text", text: "Successfully updated global prompt configuration" }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error updating global prompt: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } handleTestSchemaPrompt() { try { const prompt = schemaManager.generateAnalysisPrompt(); const enabledTypes = schemaManager.getDocumentTypes(); let resultText = `**Current Analysis Prompt Preview**\n\n`; resultText += `**Enabled Document Types:** ${enabledTypes.join(', ')}\n\n`; resultText += `**Generated Prompt:**\n\n`; resultText += prompt; return { content: [{ type: "text", text: resultText }], isError: false }; } catch (error) { return { content: [{ type: "text", text: `Error generating prompt preview: ${error instanceof Error ? error.message : String(error)}` }], isError: true }; } } async run() { try { const transport = new StdioServerTransport(); log.info('Connecting to transport...'); await this.server.connect(transport); log.info('Dbx MCP server running on stdio'); } catch (error) { log.error('Error connecting server:', { error: error instanceof Error ? error.message : 'Unknown error', stack: error instanceof Error ? error.stack : undefined }); throw error; } } } export default DbxServer; //# sourceMappingURL=dbx-server.js.map