Skip to main content
Glama
index_full.ts45.7 kB
import 'dotenv/config' import { Server } from '@modelcontextprotocol/sdk/server/index.js'; import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; import { CallToolRequestSchema, ListToolsRequestSchema, Tool, } from '@modelcontextprotocol/sdk/types.js'; import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { Client } from "@modelcontextprotocol/sdk/client/index.js"; import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); interface DatasetItem { id?: string; [key: string]: any; } interface QueryParams { filter?: Record<string, any>; search?: string; limit?: number; offset?: number; sortBy?: string; sortOrder?: 'asc' | 'desc'; } interface GetDataParams { dataId: string; params?: QueryParams; } interface CrossReferenceParams { queryType: string; inputId: string; } interface ModifyDataBatchParams { operations: { dataId: string; op: 'insert' | 'update'; record: Record<string, any>; }[]; } interface DeleteDataParams { dataId: string; filter: Record<string, any>; } interface CreateDatasetParams { dataId: string; data: any[]; description?: string; } interface DeleteDatasetParams { dataId: string; confirm?: boolean; } interface TriggerZapierToolParams { toolName: string; arguments: Record<string, any>; } interface TriggerGoogleSheetsToolParams { toolName: string; arguments: Record<string, any>; } interface BulkOperationParams { operations: { type: 'create_dataset' | 'modify_data' | 'delete_data' | 'delete_dataset' | 'trigger_zapier_tool' | 'trigger_google_sheets_tool'; params: any; }[]; } interface ServerStats { totalDatasets: number; totalRecords: number; zapierConnected: boolean; googleSheetsConnected: boolean; uptime: number; lastActivity: string; } class OrganizationalDataServer { private server: Server; private datasets: Map<string, DatasetItem[]> = new Map(); private zapierClient: Client | null = null; private googleSheetsClient: Client | null = null; private startTime: number = Date.now(); private lastActivity: number = Date.now(); private toolCache: Map<string, Tool[]> = new Map(); private cacheExpiry: Map<string, number> = new Map(); private readonly CACHE_DURATION = 5 * 60 * 1000; // 5 minutes private datasetPaths: Record<string, string> = { employees: '../../data/employees.json', companies: '../../data/companies.json', departments: '../../data/departments.json', domains: '../../data/domains.json', projects: '../../data/projects.json', customers: '../../data/customers.json', assets: '../../data/assets.json', policies: '../../data/policies.json', roles: '../../data/roles.json', accesslogs: '../../data/accesslogs.json', training: '../../data/training.json', performance: '../../data/performance.json', tickets: '../../data/tickets.json', vendors: '../../data/vendors.json', locations: '../../data/locations.json', }; constructor() { this.server = new Server( { name: 'organizational-data-server', version: '1.2.0', }, { capabilities: { tools: {}, }, } ); this.setupRequestHandlers(); } async initialize(): Promise<void> { await this.loadDatasets(); await this.initializeZapierClient(); await this.initializeGoogleSheetsClient(); } private async initializeZapierClient(): Promise<void> { try { if (!process.env.ZAPIER_MCP_URL) { console.error(' ZAPIER_MCP_URL not configured, Zapier integration disabled'); return; } this.zapierClient = new Client( { name: "organizational-zapier-client", version: "1.0.0", }, { capabilities: {}, } ); const transport = new StreamableHTTPClientTransport(new URL(process.env.ZAPIER_MCP_URL)); await this.zapierClient.connect(transport); console.error('✓ Connected to Zapier MCP server'); // Cache Zapier tools await this.cacheExternalTools('zapier'); } catch (error) { console.error('✗ Failed to connect to Zapier MCP server:', error); this.zapierClient = null; } } private async initializeGoogleSheetsClient(): Promise<void> { try { if (!process.env.GOOGLE_SHEETS_MCP_URL) { console.error(' GOOGLE_SHEETS_MCP_URL not configured, Google Sheets integration disabled'); return; } this.googleSheetsClient = new Client( { name: "organizational-sheets-client", version: "1.0.0", }, { capabilities: {}, } ); const transport = new StreamableHTTPClientTransport(new URL(process.env.GOOGLE_SHEETS_MCP_URL)); await this.googleSheetsClient.connect(transport); console.error('✓ Connected to Google Sheets MCP server'); // Cache Google Sheets tools await this.cacheExternalTools('google_sheets'); } catch (error) { console.error('✗ Failed to connect to Google Sheets MCP server:', error); this.googleSheetsClient = null; } } private async cacheExternalTools(service: 'zapier' | 'google_sheets'): Promise<void> { try { const client = service === 'zapier' ? this.zapierClient : this.googleSheetsClient; if (!client) return; const tools = await client.listTools(); this.toolCache.set(service, tools.tools); this.cacheExpiry.set(service, Date.now() + this.CACHE_DURATION); console.error(`✓ Cached ${tools.tools.length} ${service} tools`); } catch (error) { console.error(`✗ Failed to cache ${service} tools:`, error); } } private async getExternalTools(service: 'zapier' | 'google_sheets'): Promise<Tool[]> { const cacheKey = service; const cached = this.toolCache.get(cacheKey); const expiry = this.cacheExpiry.get(cacheKey); // Return cached tools if still valid if (cached && expiry && Date.now() < expiry) { return cached; } // Refresh cache await this.cacheExternalTools(service); return this.toolCache.get(cacheKey) || []; } private async loadDatasets(): Promise<void> { const dataDir = path.resolve(__dirname, '../../data'); try { await fs.access(dataDir); } catch { await fs.mkdir(dataDir, { recursive: true }); console.error('✓ Created data directory'); } // Load predefined datasets for (const [dataId, relativePath] of Object.entries(this.datasetPaths)) { try { const fullPath = path.resolve(__dirname, relativePath); const data = await fs.readFile(fullPath, 'utf-8'); const jsonData = JSON.parse(data); const dataset = Array.isArray(jsonData) ? jsonData : [jsonData]; this.datasets.set(dataId, dataset); console.error(`✓ Loaded ${dataset.length} items for ${dataId}`); } catch (error) { console.error(`✗ Failed to load ${dataId}: ${error}`); this.datasets.set(dataId, []); try { const fullPath = path.resolve(__dirname, relativePath); await fs.writeFile(fullPath, '[]', 'utf-8'); console.error(`✓ Created empty ${dataId}.json file`); } catch (writeError) { console.error(`✗ Failed to create ${dataId}.json: ${writeError}`); } } } // Load custom datasets try { const files = await fs.readdir(dataDir); const jsonFiles = files.filter(file => file.endsWith('.json')); for (const file of jsonFiles) { const dataId = file.replace('.json', ''); if (this.datasets.has(dataId)) continue; try { const fullPath = path.join(dataDir, file); const data = await fs.readFile(fullPath, 'utf-8'); const jsonData = JSON.parse(data); const dataset = Array.isArray(jsonData) ? jsonData : [jsonData]; this.datasets.set(dataId, dataset); this.datasetPaths[dataId] = `../../data/${file}`; console.error(`✓ Loaded custom dataset ${dataId} with ${dataset.length} items`); } catch (error) { console.error(`✗ Failed to load custom dataset ${dataId}: ${error}`); } } } catch (error) { console.error('✗ Error scanning data directory:', error); } } private setupRequestHandlers(): void { this.server.setRequestHandler(ListToolsRequestSchema, async () => { const tools: Tool[] = [ // Core dataset operations { name: 'get_data', description: 'Query organizational data with advanced filtering, search, and pagination', inputSchema: { type: 'object', properties: { dataId: { type: 'string', description: 'Dataset to query (employees, companies, etc.) or custom dataset' }, params: { type: 'object', properties: { filter: { type: 'object', description: 'Advanced filtering with operators like $regex, $gt, $lt, $in, etc.' }, search: { type: 'string', description: 'Text search across all fields' }, limit: { type: 'number', description: 'Maximum number of results (default: 100)' }, offset: { type: 'number', description: 'Number of results to skip (default: 0)' }, sortBy: { type: 'string', description: 'Field to sort by' }, sortOrder: { type: 'string', enum: ['asc', 'desc'], description: 'Sort order (default: asc)' }, }, }, }, required: ['dataId'], }, }, { name: 'resolve_cross_reference', description: 'Resolve relationships between datasets (employee-company, customer-employee, etc.)', inputSchema: { type: 'object', properties: { queryType: { type: 'string', enum: ['customer_to_employee_info', 'employee_to_company_info', 'asset_to_employee_info'], description: 'Type of cross-reference to resolve' }, inputId: { type: 'string', description: 'ID to resolve from' }, }, required: ['queryType', 'inputId'], }, }, { name: 'modify_data', description: 'Insert or update records in datasets with batch operations and deduplication', inputSchema: { type: 'object', properties: { operations: { type: 'array', items: { type: 'object', properties: { dataId: { type: 'string' }, op: { type: 'string', enum: ['insert', 'update'] }, record: { type: 'object' } }, required: ['dataId', 'op', 'record'] } } }, required: ['operations'] }, }, { name: 'delete_data', description: 'Delete records from datasets using advanced filters', inputSchema: { type: 'object', properties: { dataId: { type: 'string' }, filter: { type: 'object', description: 'Filter criteria for records to delete' }, }, required: ['dataId', 'filter'] }, }, { name: 'create_dataset', description: 'Create new JSON dataset with validation and automatic file management', inputSchema: { type: 'object', properties: { dataId: { type: 'string', description: 'Dataset name (alphanumeric, underscore, hyphen only)' }, data: { type: 'array', description: 'Array of records to store' }, description: { type: 'string', description: 'Optional dataset description' } }, required: ['dataId', 'data'] }, }, { name: 'delete_dataset', description: 'Delete entire dataset with automatic backup creation', inputSchema: { type: 'object', properties: { dataId: { type: 'string', description: 'Dataset to delete (custom datasets only)' }, confirm: { type: 'boolean', description: 'Must be true to confirm deletion' } }, required: ['dataId', 'confirm'] }, }, { name: 'list_datasets', description: 'List all datasets with metadata and sample records', inputSchema: { type: 'object', properties: {}, }, }, { name: 'backup_dataset', description: 'Create timestamped backup of dataset', inputSchema: { type: 'object', properties: { dataId: { type: 'string' }, }, required: ['dataId'] }, }, { name: 'get_server_stats', description: 'Get server statistics and connection status', inputSchema: { type: 'object', properties: {}, }, }, { name: 'bulk_operations', description: 'Execute multiple operations atomically with transaction-like behavior', inputSchema: { type: 'object', properties: { operations: { type: 'array', items: { type: 'object', properties: { type: { type: 'string', enum: ['create_dataset', 'modify_data', 'delete_data', 'delete_dataset', 'trigger_zapier_tool', 'trigger_google_sheets_tool'] }, params: { type: 'object', description: 'Operation-specific parameters' } }, required: ['type', 'params'] } } }, required: ['operations'] }, }, // External integrations { name: 'trigger_zapier_tool', description: 'Execute Zapier tools for Google Drive, Slack, and other integrations', inputSchema: { type: 'object', properties: { toolName: { type: 'string', description: 'Zapier tool name (e.g., google_drive_retrieve_files_from_google_drive)' }, arguments: { type: 'object', description: 'Tool-specific arguments', properties: { instructions: { type: 'string', description: 'Clear instructions for the operation' }, maxResults: { type: 'number', description: 'Maximum results to return' }, orderBy: { type: 'string', description: 'Result ordering (e.g., modifiedTime desc)' }, fileId: { type: 'string', description: 'Google Drive file ID' }, folderId: { type: 'string', description: 'Google Drive folder ID' }, query: { type: 'string', description: 'Search query' } } } }, required: ['toolName'] }, }, { name: 'list_zapier_tools', description: 'List all available Zapier tools with caching', inputSchema: { type: 'object', properties: { refresh: { type: 'boolean', description: 'Force refresh of tool cache' } }, }, }, { name: 'trigger_google_sheets_tool', description: 'Execute Google Sheets operations with enhanced error handling', inputSchema: { type: 'object', properties: { toolName: { type: 'string', description: 'Google Sheets tool name' }, arguments: { type: 'object', description: 'Tool-specific arguments for sheets operations' } }, required: ['toolName'] }, }, { name: 'list_google_sheets_tools', description: 'List all available Google Sheets tools with caching', inputSchema: { type: 'object', properties: { refresh: { type: 'boolean', description: 'Force refresh of tool cache' } }, }, }, // Data analysis and reporting { name: 'analyze_dataset', description: 'Perform statistical analysis on dataset fields', inputSchema: { type: 'object', properties: { dataId: { type: 'string' }, fields: { type: 'array', items: { type: 'string' }, description: 'Fields to analyze' }, analysisType: { type: 'string', enum: ['summary', 'distribution', 'correlations', 'trends'], description: 'Type of analysis to perform' } }, required: ['dataId', 'fields', 'analysisType'] }, }, { name: 'export_dataset', description: 'Export dataset in various formats (CSV, Excel, JSON)', inputSchema: { type: 'object', properties: { dataId: { type: 'string' }, format: { type: 'string', enum: ['csv', 'excel', 'json'], description: 'Export format' }, filter: { type: 'object', description: 'Optional filter for export' } }, required: ['dataId', 'format'] }, } ]; return { tools }; }); this.server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args } = request.params; this.lastActivity = Date.now(); try { switch (name) { case 'get_data': return await this.handleGetData(args as unknown as GetDataParams); case 'resolve_cross_reference': return await this.handleCrossReference(args as unknown as CrossReferenceParams); case 'modify_data': return await this.handleModifyData(args as unknown as ModifyDataBatchParams); case 'delete_data': return await this.handleDeleteData(args as unknown as DeleteDataParams); case 'create_dataset': return await this.handleCreateDataset(args as unknown as CreateDatasetParams); case 'delete_dataset': return await this.handleDeleteDataset(args as unknown as DeleteDatasetParams); case 'list_datasets': return await this.handleListDatasets(); case 'backup_dataset': return await this.handleBackupDataset(args as unknown as { dataId: string }); case 'get_server_stats': return await this.handleGetServerStats(); case 'bulk_operations': return await this.handleBulkOperations(args as unknown as BulkOperationParams); case 'trigger_zapier_tool': return await this.handleTriggerZapierTool(args as unknown as TriggerZapierToolParams); case 'list_zapier_tools': return await this.handleListZapierTools(args as unknown as { refresh?: boolean }); case 'trigger_google_sheets_tool': return await this.handleTriggerGoogleSheetsTools(args as unknown as TriggerGoogleSheetsToolParams); case 'list_google_sheets_tools': return await this.handleListGoogleSheetsTools(args as unknown as { refresh?: boolean }); case 'analyze_dataset': return await this.handleAnalyzeDataset(args as unknown as any); case 'export_dataset': return await this.handleExportDataset(args as unknown as any); default: throw new Error(`Unknown tool: ${name}`); } } catch (error) { console.error(`Error in ${name}:`, error); return { content: [ { type: 'text', text: `Error: ${error instanceof Error ? error.message : 'Unknown error'}`, }, ], }; } }); } // Enhanced handlers with improved error handling and features public async handleGetServerStats() { const stats: ServerStats = { totalDatasets: this.datasets.size, totalRecords: Array.from(this.datasets.values()).reduce((sum, dataset) => sum + dataset.length, 0), zapierConnected: this.zapierClient !== null, googleSheetsConnected: this.googleSheetsClient !== null, uptime: Date.now() - this.startTime, lastActivity: new Date(this.lastActivity).toISOString() }; return { content: [{ type: 'text', text: JSON.stringify(stats, null, 2) }], }; } public async handleTriggerZapierTool(params: TriggerZapierToolParams) { if (!this.zapierClient) { throw new Error('Zapier client not initialized. Please check ZAPIER_MCP_URL environment variable.'); } try { const { toolName, arguments: toolArgs = {} } = params; if (!toolName) { throw new Error('toolName is required'); } // Enhanced argument processing for common operations const safeArgs = { ...toolArgs }; // Auto-add instructions for Google Drive operations if (toolName.includes('google_drive') && !safeArgs.instructions) { safeArgs.instructions = this.generateGoogleDriveInstructions(toolName, safeArgs); } // Add default limits for list operations if (toolName.includes('list') || toolName.includes('retrieve')) { safeArgs.maxResults = safeArgs.maxResults || 50; } console.error(`🔧 Calling Zapier tool: ${toolName}`); const result = await this.zapierClient.callTool({ name: toolName, arguments: safeArgs }); return { content: [{ type: "text", text: `✅ Zapier Tool "${toolName}" executed successfully:\n${JSON.stringify(result.content, null, 2)}` }] }; } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; console.error(`❌ Error calling Zapier tool ${params.toolName}:`, err); return { content: [{ type: "text", text: `❌ Error calling Zapier tool "${params.toolName}": ${errorMessage}` }] }; } } public async handleListZapierTools(params: { refresh?: boolean } = {}) { if (!this.zapierClient) { return { content: [{ type: "text", text: "❌ Zapier client not initialized. Please check ZAPIER_MCP_URL environment variable." }] }; } try { if (params.refresh) { await this.cacheExternalTools('zapier'); } const tools = await this.getExternalTools('zapier'); const toolsList = tools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })); return { content: [{ type: "text", text: `📋 Available Zapier Tools (${toolsList.length}):\n${JSON.stringify(toolsList, null, 2)}` }] }; } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; console.error('❌ Error listing Zapier tools:', err); return { content: [{ type: "text", text: `❌ Error listing Zapier tools: ${errorMessage}` }] }; } } public async handleTriggerGoogleSheetsTools(params: TriggerGoogleSheetsToolParams) { if (!this.googleSheetsClient) { throw new Error('Google Sheets client not initialized. Please check GOOGLE_SHEETS_MCP_URL environment variable.'); } try { const { toolName, arguments: toolArgs = {} } = params; if (!toolName) { throw new Error('toolName is required'); } console.error(`📊 Calling Google Sheets tool: ${toolName}`); const result = await this.googleSheetsClient.callTool({ name: toolName, arguments: toolArgs }); return { content: [{ type: "text", text: `✅ Google Sheets Tool "${toolName}" executed successfully:\n${JSON.stringify(result.content, null, 2)}` }] }; } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; console.error(`❌ Error calling Google Sheets tool ${params.toolName}:`, err); return { content: [{ type: "text", text: `❌ Error calling Google Sheets tool "${params.toolName}": ${errorMessage}` }] }; } } public async handleListGoogleSheetsTools(params: { refresh?: boolean } = {}) { if (!this.googleSheetsClient) { return { content: [{ type: "text", text: "❌ Google Sheets client not initialized. Please check GOOGLE_SHEETS_MCP_URL environment variable." }] }; } try { if (params.refresh) { await this.cacheExternalTools('google_sheets'); } const tools = await this.getExternalTools('google_sheets'); const toolsList = tools.map(tool => ({ name: tool.name, description: tool.description, inputSchema: tool.inputSchema })); return { content: [{ type: "text", text: `📊 Available Google Sheets Tools (${toolsList.length}):\n${JSON.stringify(toolsList, null, 2)}` }] }; } catch (err) { const errorMessage = err instanceof Error ? err.message : 'Unknown error'; console.error('❌ Error listing Google Sheets tools:', err); return { content: [{ type: "text", text: `❌ Error listing Google Sheets tools: ${errorMessage}` }] }; } } public async handleAnalyzeDataset(params: any) { const { dataId, fields, analysisType } = params; const dataset = this.datasets.get(dataId); if (!dataset) { throw new Error(`Dataset '${dataId}' not found`); } const analysis = this.performAnalysis(dataset, fields, analysisType); return { content: [{ type: 'text', text: JSON.stringify(analysis, null, 2) }], }; } // Complete the handleExportDataset method public async handleExportDataset(params: any) { const { dataId, format, filter } = params; let dataset = this.datasets.get(dataId); if (!dataset) { throw new Error(`Dataset '${dataId}' not found`); } // Apply filter if provided if (filter) { dataset = this.applyFilter(dataset, filter); } let exportContent: string; let mimeType: string; switch (format) { case 'csv': exportContent = this.convertToCSV(dataset); mimeType = 'text/csv'; break; case 'excel': // For Excel, we'll return instructions since we can't generate actual Excel files exportContent = JSON.stringify({ message: 'Excel export requested', data: dataset, instructions: 'Use the data to create an Excel file in your application' }, null, 2); mimeType = 'application/json'; break; case 'json': exportContent = JSON.stringify(dataset, null, 2); mimeType = 'application/json'; break; default: throw new Error(`Unsupported format: ${format}`); } return { content: [{ type: 'text', text: `📤 Dataset '${dataId}' exported as ${format}:\n\n${exportContent}` }], }; } // Missing handler methods public async handleGetData(params: GetDataParams) { const { dataId, params: queryParams = {} } = params; let dataset = this.datasets.get(dataId); if (!dataset) { throw new Error(`Dataset '${dataId}' not found`); } // Apply filtering if (queryParams.filter) { dataset = this.applyFilter(dataset, queryParams.filter); } // Apply search if (queryParams.search) { dataset = this.applySearch(dataset, queryParams.search); } // Apply sorting if (queryParams.sortBy) { dataset = this.applySort(dataset, queryParams.sortBy, queryParams.sortOrder || 'asc'); } // Apply pagination const offset = queryParams.offset || 0; const limit = queryParams.limit || 100; const paginatedDataset = dataset.slice(offset, offset + limit); const result = { data: paginatedDataset, total: dataset.length, offset, limit, hasMore: offset + limit < dataset.length }; return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } public async handleCrossReference(params: CrossReferenceParams) { const { queryType, inputId } = params; try { let result: any; switch (queryType) { case 'customer_to_employee_info': result = await this.resolveCrossReference('customers', 'employees', inputId, 'assignedEmployeeId'); break; case 'employee_to_company_info': result = await this.resolveCrossReference('employees', 'companies', inputId, 'companyId'); break; case 'asset_to_employee_info': result = await this.resolveCrossReference('assets', 'employees', inputId, 'assignedTo'); break; default: throw new Error(`Unsupported query type: ${queryType}`); } return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], }; } catch (error) { throw new Error(`Cross-reference failed: ${error instanceof Error ? error.message : 'Unknown error'}`); } } public async handleModifyData(params: ModifyDataBatchParams) { const results: any[] = []; for (const operation of params.operations) { try { const { dataId, op, record } = operation; let dataset = this.datasets.get(dataId); if (!dataset) { throw new Error(`Dataset '${dataId}' not found`); } if (op === 'insert') { // Generate ID if not provided if (!record.id) { record.id = this.generateId(); } // Check for duplicates const existingIndex = dataset.findIndex(item => item.id === record.id); if (existingIndex !== -1) { throw new Error(`Record with ID '${record.id}' already exists`); } dataset.push(record); results.push({ operation: 'insert', dataId, id: record.id, status: 'success' }); } else if (op === 'update') { if (!record.id) { throw new Error('ID is required for update operations'); } const existingIndex = dataset.findIndex(item => item.id === record.id); if (existingIndex === -1) { throw new Error(`Record with ID '${record.id}' not found`); } dataset[existingIndex] = { ...dataset[existingIndex], ...record }; results.push({ operation: 'update', dataId, id: record.id, status: 'success' }); } // Save to file await this.saveDataset(dataId); } catch (error) { results.push({ operation: operation.op, dataId: operation.dataId, status: 'error', error: error instanceof Error ? error.message : 'Unknown error' }); } } return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }], }; } public async handleDeleteData(params: DeleteDataParams) { const { dataId, filter } = params; const dataset = this.datasets.get(dataId); if (!dataset) { throw new Error(`Dataset '${dataId}' not found`); } const initialCount = dataset.length; const filteredDataset = dataset.filter(item => !this.matchesFilter(item, filter)); const deletedCount = initialCount - filteredDataset.length; this.datasets.set(dataId, filteredDataset); await this.saveDataset(dataId); return { content: [{ type: 'text', text: JSON.stringify({ dataId, deletedCount, remainingCount: filteredDataset.length }, null, 2) }], }; } public async handleCreateDataset(params: CreateDatasetParams) { const { dataId, data, description } = params; // Validate dataset name if (!/^[a-zA-Z0-9_-]+$/.test(dataId)) { throw new Error('Dataset name must contain only alphanumeric characters, underscores, and hyphens'); } if (this.datasets.has(dataId)) { throw new Error(`Dataset '${dataId}' already exists`); } // Validate data if (!Array.isArray(data)) { throw new Error('Data must be an array'); } // Add IDs to records that don't have them const processedData = data.map(record => ({ ...record, id: record.id || this.generateId() })); this.datasets.set(dataId, processedData); // Create file path const fileName = `${dataId}.json`; const filePath = `../../data/${fileName}`; this.datasetPaths[dataId] = filePath; await this.saveDataset(dataId); return { content: [{ type: 'text', text: JSON.stringify({ dataId, recordCount: processedData.length, description, status: 'created' }, null, 2) }], }; } public async handleDeleteDataset(params: DeleteDatasetParams) { const { dataId, confirm } = params; if (!confirm) { throw new Error('Confirmation required to delete dataset'); } // Don't allow deletion of predefined datasets const predefinedDatasets = ['employees', 'companies', 'departments', 'domains', 'projects', 'customers', 'assets', 'policies', 'roles', 'accesslogs', 'training', 'performance', 'tickets', 'vendors', 'locations']; if (predefinedDatasets.includes(dataId)) { throw new Error('Cannot delete predefined datasets'); } if (!this.datasets.has(dataId)) { throw new Error(`Dataset '${dataId}' not found`); } // Create backup before deletion await this.handleBackupDataset({ dataId }); // Delete from memory this.datasets.delete(dataId); // Delete file const relativePath = this.datasetPaths[dataId]; if (relativePath) { try { const fullPath = path.resolve(__dirname, relativePath); await fs.unlink(fullPath); } catch (error) { console.error(`Failed to delete file for dataset ${dataId}:`, error); } } delete this.datasetPaths[dataId]; return { content: [{ type: 'text', text: JSON.stringify({ dataId, status: 'deleted', backup: 'created' }, null, 2) }], }; } public async handleListDatasets() { const datasets = Array.from(this.datasets.entries()).map(([dataId, data]) => ({ dataId, recordCount: data.length, sampleRecord: data.length > 0 ? data[0] : null, fields: data.length > 0 ? Object.keys(data[0]) : [], type: this.datasetPaths[dataId]?.includes('../../data/') ? 'predefined' : 'custom' })); return { content: [{ type: 'text', text: JSON.stringify(datasets, null, 2) }], }; } public async handleBackupDataset(params: { dataId: string }) { const { dataId } = params; const dataset = this.datasets.get(dataId); if (!dataset) { throw new Error(`Dataset '${dataId}' not found`); } const timestamp = new Date().toISOString().replace(/[:.]/g, '-'); const backupFileName = `${dataId}_backup_${timestamp}.json`; const backupPath = path.resolve(__dirname, '../../backups', backupFileName); // Ensure backup directory exists const backupDir = path.dirname(backupPath); try { await fs.access(backupDir); } catch { await fs.mkdir(backupDir, { recursive: true }); } await fs.writeFile(backupPath, JSON.stringify(dataset, null, 2)); return { content: [{ type: 'text', text: JSON.stringify({ dataId, backupFile: backupFileName, timestamp, recordCount: dataset.length }, null, 2) }], }; } public async handleBulkOperations(params: BulkOperationParams) { const results: any[] = []; for (const operation of params.operations) { try { let result: any; switch (operation.type) { case 'create_dataset': result = await this.handleCreateDataset(operation.params); break; case 'modify_data': result = await this.handleModifyData(operation.params); break; case 'delete_data': result = await this.handleDeleteData(operation.params); break; case 'delete_dataset': result = await this.handleDeleteDataset(operation.params); break; case 'trigger_zapier_tool': result = await this.handleTriggerZapierTool(operation.params); break; case 'trigger_google_sheets_tool': result = await this.handleTriggerGoogleSheetsTools(operation.params); break; default: throw new Error(`Unsupported operation type: ${operation.type}`); } results.push({ operation: operation.type, status: 'success', result: result.content[0].text }); } catch (error) { results.push({ operation: operation.type, status: 'error', error: error instanceof Error ? error.message : 'Unknown error' }); } } return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }], }; } // Utility methods private generateGoogleDriveInstructions(toolName: string, args: any): string { if (toolName.includes('retrieve_files')) { return `List files in Google Drive${args.folderId ? ` from folder ${args.folderId}` : ''}${args.query ? ` matching "${args.query}"` : ''}`; } if (toolName.includes('create_folder')) { return `Create a new folder in Google Drive${args.parentId ? ` inside folder ${args.parentId}` : ''}`; } if (toolName.includes('upload')) { return `Upload file to Google Drive${args.folderId ? ` to folder ${args.folderId}` : ''}`; } return `Execute Google Drive operation: ${toolName}`; } private applyFilter(dataset: DatasetItem[], filter: Record<string, any>): DatasetItem[] { return dataset.filter(item => this.matchesFilter(item, filter)); } private matchesFilter(item: DatasetItem, filter: Record<string, any>): boolean { return Object.entries(filter).every(([key, value]) => { if (typeof value === 'object' && value !== null) { // Handle operators like $regex, $gt, $lt, $in, etc. if (value.$regex) { const regex = new RegExp(value.$regex, value.$options || 'i'); return regex.test(String(item[key] || '')); } if (value.$gt !== undefined) { return (item[key] || 0) > value.$gt; } if (value.$lt !== undefined) { return (item[key] || 0) < value.$lt; } if (value.$in) { return value.$in.includes(item[key]); } } return item[key] === value; }); } private applySearch(dataset: DatasetItem[], search: string): DatasetItem[] { const searchLower = search.toLowerCase(); return dataset.filter(item => Object.values(item).some(value => String(value || '').toLowerCase().includes(searchLower) ) ); } private applySort(dataset: DatasetItem[], sortBy: string, sortOrder: 'asc' | 'desc'): DatasetItem[] { return [...dataset].sort((a, b) => { const aVal = a[sortBy]; const bVal = b[sortBy]; if (aVal < bVal) return sortOrder === 'asc' ? -1 : 1; if (aVal > bVal) return sortOrder === 'asc' ? 1 : -1; return 0; }); } private async resolveCrossReference( sourceDataset: string, targetDataset: string, inputId: string, foreignKey: string ): Promise<any> { const sourceData = this.datasets.get(sourceDataset); const targetData = this.datasets.get(targetDataset); if (!sourceData || !targetData) { throw new Error(`Source or target dataset not found`); } const sourceRecord = sourceData.find(item => item.id === inputId); if (!sourceRecord) { throw new Error(`Record with ID '${inputId}' not found in ${sourceDataset}`); } const foreignId = sourceRecord[foreignKey]; if (!foreignId) { return { sourceRecord, targetRecord: null }; } const targetRecord = targetData.find(item => item.id === foreignId); return { sourceRecord, targetRecord }; } private generateId(): string { return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); } private async saveDataset(dataId: string): Promise<void> { const dataset = this.datasets.get(dataId); const relativePath = this.datasetPaths[dataId]; if (!dataset || !relativePath) { throw new Error(`Dataset '${dataId}' not found or path not configured`); } const fullPath = path.resolve(__dirname, relativePath); await fs.writeFile(fullPath, JSON.stringify(dataset, null, 2)); } private convertToCSV(data: DatasetItem[]): string { if (data.length === 0) return ''; const headers = Object.keys(data[0]); const csvHeaders = headers.join(','); const csvRows = data.map(item => headers.map(header => { const value = item[header]; return typeof value === 'string' && value.includes(',') ? `"${value}"` : String(value || ''); }).join(',') ); return [csvHeaders, ...csvRows].join('\n'); } private performAnalysis(dataset: DatasetItem[], fields: string[], analysisType: string): any { const result: any = { dataset: dataset.length, fields: fields.length, type: analysisType }; switch (analysisType) { case 'summary': result.summary = fields.map(field => ({ field, count: dataset.filter(item => item[field] !== undefined).length, unique: new Set(dataset.map(item => item[field])).size, sample: dataset.slice(0, 3).map(item => item[field]) })); break; case 'distribution': result.distribution = fields.map(field => { const values = dataset.map(item => item[field]).filter(v => v !== undefined); const counts: Record<string, number> = {}; values.forEach(v => counts[String(v)] = (counts[String(v)] || 0) + 1); return { field, distribution: counts }; }); break; case 'trends': result.trends = { message: 'Trend analysis requires time-series data' }; break; default: result.analysis = { message: `Analysis type '${analysisType}' not fully implemented` }; } return result; } async run(): Promise<void> { const transport = new StdioServerTransport(); await this.server.connect(transport); console.error('🚀 Organizational Data Server running on stdio'); } } // Main execution async function main() { const server = new OrganizationalDataServer(); await server.initialize(); await server.run(); } main().catch(console.error);

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/sujalpat1810/Orgflow_MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server