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
JavaScript
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