Skip to main content
Glama
DEVELOPER_GUIDE_MCP_AUTOTASK.md20 kB
# MCP Server Development with Autotask API - Complete Developer Guide ## Overview This guide documents patterns, gotchas, and best practices learned from developing MCP (Model Context Protocol) servers that integrate with Kaseya Autotask PSA. Use this as a reference for future MCP server projects. ## Table of Contents 1. [MCP Server Architecture](#mcp-server-architecture) 2. [Autotask API Integration](#autotask-api-integration) 3. [Common Gotchas & Solutions](#common-gotchas--solutions) 4. [Configuration Management](#configuration-management) 5. [Error Handling Patterns](#error-handling-patterns) 6. [Testing Strategies](#testing-strategies) 7. [Performance Optimization](#performance-optimization) 8. [Debugging Techniques](#debugging-techniques) 9. [Deployment Considerations](#deployment-considerations) 10. [Future Development Patterns](#future-development-patterns) --- ## MCP Server Architecture ### Core Components Structure ``` src/ ├── mcp/ │ ├── server.ts # Main MCP server implementation │ └── handlers/ # MCP request handlers ├── services/ │ ├── autotask.service.ts # Autotask API wrapper │ └── mapping.service.ts # ID-to-name mapping cache ├── handlers/ │ ├── tool.handler.ts # Tool execution logic │ └── resource.handler.ts # Resource access logic ├── types/ │ ├── mcp.ts # MCP-specific types │ ├── autotask.ts # Autotask entity types │ └── config.ts # Configuration types └── utils/ ├── logger.ts # Centralized logging ├── config.ts # Configuration management └── helpers.ts # Utility functions ``` ### Key Design Patterns #### 1. Service Layer Pattern ```typescript // Separate API concerns from MCP protocol concerns class AutotaskService { // Pure business logic, no MCP dependencies async searchTickets(params: SearchParams): Promise<TicketResult[]> } class AutotaskToolHandler { // MCP-specific logic, delegates to service async callTool(name: string, args: any): Promise<McpResult> } ``` #### 2. Handler Composition Pattern ```typescript class AutotaskMcpServer { constructor(config: McpServerConfig, logger: Logger) { this.autotaskService = new AutotaskService(config, logger); this.toolHandler = new ToolHandler(this.autotaskService, logger); this.resourceHandler = new ResourceHandler(this.autotaskService, logger); } } ``` #### 3. Configuration Injection Pattern ```typescript // Centralized config that flows down through all layers interface McpServerConfig { name: string; version: string; autotask: AutotaskConfig; logging: LoggingConfig; } ``` --- ## Autotask API Integration ### Authentication & Zone Discovery ```typescript // Autotask has distributed endpoints - zone discovery is critical const autotask = new AutotaskRestApi( username, // Must be API user, not regular user secret, // API secret key integrationCode // Required for all requests ); // Zone discovery happens automatically on first API call // No need to call zoneInformation separately in v2+ ``` ### Key API Patterns #### 1. Field Name Mapping ```typescript // CRITICAL: Always use official Autotask field names // Check documentation: https://autotask.net/help/developerhelp/ // ❌ WRONG - Common mistakes: 'type' // Should be 'projectType' for Projects 'name' // Should be 'companyName' for Companies 'description' // Context matters - could be multiple fields // ✅ CORRECT - Always verify field names: 'projectType' // Projects entity 'companyName' // Companies entity 'ticketNumber' // Tickets entity ``` #### 2. Essential Fields Strategy ```typescript // Always specify essential fields to avoid massive responses const essentialFields = [ 'id', // Always include ID 'companyName', // Primary display field 'status', // Status fields are usually important 'createDate', // Timestamps often needed 'isActive' // Active status common ]; const result = autotask.Companies.query({ filter: [...], includeFields: essentialFields // Reduces response size dramatically }); ``` #### 3. Parent-Child Entity Pattern ```typescript // Many Autotask entities have parent-child relationships // Query: Use child entity name (e.g., autotask.Contacts) // CUD Operations: Use parent-child syntax (e.g., autotask.CompanyContacts) // Query - direct access: await autotask.Contacts.query({...}); // Create - requires parent ID: await autotask.CompanyContacts.create(companyId, contactData); ``` ### Status and Picklist Values ```typescript // Status values are entity-specific integers // Common patterns: const CommonStatuses = { TICKETS: { NEW: 1, IN_PROGRESS: 2, COMPLETE: 5, // etc. }, PROJECTS: { NEW: 1, IN_PROGRESS: 2, COMPLETE: 5 } }; // Always use fieldInfo() to get valid picklist values: const fieldInfo = await autotask.Tickets.fieldInfo(); const statusValues = fieldInfo.fields .find(f => f.name === 'status') ?.picklistValues; ``` --- ## Common Gotchas & Solutions ### 1. HTTP 500 "Unable to find [field] in [Entity]" **Cause**: Using incorrect field names in `includeFields` or filters. **Solution**: - Always check official Autotask REST API documentation - Use `fieldInfo()` to verify available fields - Common wrong fields: `type` → `projectType`, `name` → `companyName` ```typescript // Debug field names: const info = await autotask.Projects.fieldInfo(); console.log(info.fields.map(f => f.name)); // Lists all valid fields ``` ### 2. HTTP 405 Method Not Allowed **Cause**: API permissions, not code issues. **Solutions**: - Verify API user permissions in Autotask admin - Check module access (Projects, CRM, etc.) - Ensure API user has appropriate security level - Some entities are read-only for certain users ### 3. HTTP 401/500 on Authentication **Cause**: Invalid credentials or integration code. **Debug**: ```typescript // Test basic connectivity: try { const company = await autotask.Companies.get(0); // Root company console.log('Auth successful:', company.item?.companyName); } catch (error) { console.error('Auth failed:', error.message); } ``` ### 4. Empty Results vs. Access Denied **Important**: Empty results != error. Many searches legitimately return no data. ```typescript // Handle both cases: const results = await autotask.Tickets.query({...}); if (results.items.length === 0) { logger.info('No tickets found matching criteria'); // Normal } else { logger.info(`Found ${results.items.length} tickets`); } ``` ### 5. Rate Limiting **Pattern**: Autotask has undocumented rate limits. ```typescript class AutotaskService { private async makeRequest<T>(operation: () => Promise<T>): Promise<T> { try { return await operation(); } catch (error) { if (error.status === 429) { await this.delay(1000); // Wait and retry return await operation(); } throw error; } } } ``` --- ## Configuration Management ### Environment Variables Pattern ```typescript // Use consistent naming: interface AutotaskConfig { username: string; // AUTOTASK_USERNAME secret: string; // AUTOTASK_SECRET integrationCode: string; // AUTOTASK_INTEGRATION_CODE timeout?: number; // AUTOTASK_TIMEOUT retryAttempts?: number; // AUTOTASK_RETRY_ATTEMPTS } // Validation pattern: function validateConfig(config: AutotaskConfig): void { const required = ['username', 'secret', 'integrationCode']; const missing = required.filter(key => !config[key]); if (missing.length > 0) { throw new Error(`Missing required config: ${missing.join(', ')}`); } } ``` ### Configuration Layering ```typescript // 1. Default config const defaultConfig = { timeout: 30000, retryAttempts: 3, logging: { level: 'info' } }; // 2. Environment override const envConfig = loadEnvironmentConfig(); // 3. MCP-specific overrides const mcpConfig = mergeWithMcpConfig(envConfig); // 4. Final config const finalConfig = { ...defaultConfig, ...mcpConfig }; ``` --- ## Error Handling Patterns ### Structured Error Response ```typescript interface ToolResult { content: Array<{ type: 'text'; text: string }>; isError: boolean; } class AutotaskToolHandler { async callTool(name: string, args: any): Promise<ToolResult> { try { const result = await this.executeToolLogic(name, args); return { content: [{ type: 'text', text: JSON.stringify(result) }], isError: false }; } catch (error) { return { content: [{ type: 'text', text: this.formatError(error) }], isError: true }; } } } ``` ### Error Categorization ```typescript enum ErrorType { AUTHENTICATION = 'authentication', PERMISSIONS = 'permissions', VALIDATION = 'validation', NOT_FOUND = 'not_found', RATE_LIMIT = 'rate_limit', SERVER_ERROR = 'server_error' } function categorizeAutotaskError(error: any): ErrorType { if (error.status === 401) return ErrorType.AUTHENTICATION; if (error.status === 403 || error.status === 405) return ErrorType.PERMISSIONS; if (error.status === 400) return ErrorType.VALIDATION; if (error.status === 404) return ErrorType.NOT_FOUND; if (error.status === 429) return ErrorType.RATE_LIMIT; return ErrorType.SERVER_ERROR; } ``` ### User-Friendly Error Messages ```typescript function formatErrorForUser(error: any): string { const type = categorizeAutotaskError(error); switch (type) { case ErrorType.PERMISSIONS: return `Access denied. Please check your API user permissions in Autotask admin.`; case ErrorType.VALIDATION: return `Invalid request: ${error.message}. Please check your input parameters.`; case ErrorType.NOT_FOUND: return `Resource not found. It may have been deleted or you may not have access.`; default: return `API error: ${error.message}`; } } ``` --- ## Testing Strategies ### Test Categories #### 1. Unit Tests - Service Layer ```typescript // Test business logic without MCP or API dependencies describe('AutotaskService', () => { test('should format search filters correctly', () => { const service = new AutotaskService(mockConfig, mockLogger); const filters = service.buildFilters({ searchTerm: 'test' }); expect(filters).toEqual([...expectedFilters]); }); }); ``` #### 2. Integration Tests - API Layer ```typescript // Test actual API calls with real credentials describe('Autotask API Integration', () => { test('should retrieve companies', async () => { const service = new AutotaskService(realConfig, logger); const result = await service.searchCompanies({ pageSize: 1 }); expect(result.companies).toBeDefined(); }); }); ``` #### 3. MCP Protocol Tests ```typescript // Test MCP server responses describe('MCP Server', () => { test('should list tools correctly', async () => { const server = new AutotaskMcpServer(config, logger); const tools = await server.toolHandler.listTools(); expect(tools.find(t => t.name === 'search_companies')).toBeDefined(); }); }); ``` ### Test Utilities ```typescript // Reusable test patterns: class TestHelpers { static createMockAutotaskResponse<T>(items: T[]) { return { items, pageDetails: { count: items.length, requestCount: 500, prevPageUrl: null, nextPageUrl: null } }; } static async waitForCondition( condition: () => boolean | Promise<boolean>, timeout = 5000 ): Promise<void> { // Utility for async testing } } ``` --- ## Performance Optimization ### Caching Strategy ```typescript class MappingService { private cache = new Map<string, { value: any; expiry: number }>(); private readonly TTL = 5 * 60 * 1000; // 5 minutes async getCompanyName(id: number): Promise<string> { const key = `company:${id}`; const cached = this.cache.get(key); if (cached && cached.expiry > Date.now()) { return cached.value; } const company = await this.autotask.Companies.get(id); const name = company.item?.companyName || `Company ${id}`; this.cache.set(key, { value: name, expiry: Date.now() + this.TTL }); return name; } } ``` ### Pagination Handling ```typescript async function getAllResults<T>( queryFn: (page: number) => Promise<{ items: T[]; pageDetails: any }> ): Promise<T[]> { const allItems: T[] = []; let page = 1; let hasMore = true; while (hasMore) { const result = await queryFn(page); allItems.push(...result.items); hasMore = result.pageDetails.nextPageUrl !== null; page++; // Safety valve if (page > 100) { logger.warn('Stopping pagination at 100 pages'); break; } } return allItems; } ``` ### Essential Fields Optimization ```typescript // Create field sets for different use cases: const FieldSets = { LIST_VIEW: ['id', 'companyName', 'status', 'createDate'], DETAIL_VIEW: ['id', 'companyName', 'status', 'createDate', 'address1', 'phone'], EXPORT_VIEW: [...ALL_ESSENTIAL_FIELDS] }; // Use appropriate field set based on context: const companies = await autotask.Companies.query({ includeFields: FieldSets.LIST_VIEW, // Much faster filter: filters }); ``` --- ## Debugging Techniques ### Logging Strategy ```typescript class Logger { debug(message: string, meta?: any): void { if (this.level === 'debug') { console.log(`[DEBUG] ${message}`, meta ? JSON.stringify(meta, null, 2) : ''); } } logApiCall(method: string, url: string, params?: any, duration?: number): void { this.debug(`API Call: ${method} ${url}`, { params, duration: duration ? `${duration}ms` : undefined }); } } ``` ### API Call Instrumentation ```typescript class AutotaskService { private async instrumentedCall<T>( operation: string, fn: () => Promise<T> ): Promise<T> { const start = Date.now(); this.logger.debug(`Starting ${operation}`); try { const result = await fn(); const duration = Date.now() - start; this.logger.debug(`Completed ${operation}`, { duration: `${duration}ms` }); return result; } catch (error) { const duration = Date.now() - start; this.logger.error(`Failed ${operation}`, { error: error.message, duration: `${duration}ms` }); throw error; } } } ``` ### Field Validation Debugging ```typescript async function debugEntityFields(entityName: string): Promise<void> { const autotask = new AutotaskRestApi(/* credentials */); const entity = autotask[entityName]; try { const fieldInfo = await entity.fieldInfo(); console.log(`\n=== ${entityName} Fields ===`); fieldInfo.fields.forEach(field => { console.log(`${field.name}: ${field.dataType} (required: ${field.isRequired})`); if (field.isPickList && field.picklistValues) { console.log(` Values: ${field.picklistValues.map(v => `${v.value}=${v.label}`).join(', ')}`); } }); } catch (error) { console.error(`Failed to get field info for ${entityName}:`, error.message); } } ``` --- ## Deployment Considerations ### Environment Setup ```bash # Production environment checklist: export AUTOTASK_USERNAME="prod-api-user@company.com" # Dedicated API user export AUTOTASK_SECRET="secure-secret-key" # Rotate regularly export AUTOTASK_INTEGRATION_CODE="prod-integration" # Production integration export LOG_LEVEL="info" # Reduce log verbosity export NODE_ENV="production" # Enable optimizations ``` ### Health Checks ```typescript class HealthCheckHandler { async checkHealth(): Promise<{ status: string; details: any }> { try { // Test basic connectivity const company = await this.autotask.Companies.get(0); return { status: 'healthy', details: { autotask_connection: 'ok', root_company: company.item?.companyName || 'unknown', timestamp: new Date().toISOString() } }; } catch (error) { return { status: 'unhealthy', details: { autotask_connection: 'failed', error: error.message, timestamp: new Date().toISOString() } }; } } } ``` ### Process Management ```typescript // Graceful shutdown handling: class AutotaskMcpServer { async stop(): Promise<void> { this.logger.info('Stopping MCP Server...'); // Close connections await this.server.close(); // Clear caches this.mappingService.clearCache(); // Final log this.logger.info('MCP Server stopped gracefully'); } } // Signal handling: process.on('SIGINT', async () => { await server.stop(); process.exit(0); }); ``` --- ## Future Development Patterns ### Extensible Tool Architecture ```typescript interface ToolDefinition { name: string; description: string; inputSchema: any; handler: (args: any) => Promise<any>; } class ExtensibleToolHandler { private tools = new Map<string, ToolDefinition>(); registerTool(tool: ToolDefinition): void { this.tools.set(tool.name, tool); } async callTool(name: string, args: any): Promise<ToolResult> { const tool = this.tools.get(name); if (!tool) { throw new Error(`Tool ${name} not found`); } return await tool.handler(args); } } ``` ### Plugin System ```typescript interface McpPlugin { name: string; version: string; tools?: ToolDefinition[]; resources?: ResourceDefinition[]; initialize(server: AutotaskMcpServer): Promise<void>; } class PluginManager { private plugins: McpPlugin[] = []; async loadPlugin(plugin: McpPlugin): Promise<void> { await plugin.initialize(this.server); this.plugins.push(plugin); } } ``` ### Configuration Schema Evolution ```typescript // Versioned configuration for backward compatibility: interface ConfigV1 { version: '1.0'; autotask: AutotaskConfig; } interface ConfigV2 extends ConfigV1 { version: '2.0'; features: FeatureFlags; plugins: PluginConfig[]; } function migrateConfig(config: any): ConfigV2 { if (config.version === '1.0') { return { ...config, version: '2.0', features: getDefaultFeatures(), plugins: [] }; } return config; } ``` --- ## Key Takeaways ### Do's ✅ - Always verify Autotask field names against official documentation - Use essential fields to optimize performance - Implement comprehensive error handling with user-friendly messages - Cache frequently accessed data (company names, resource names) - Test with real API credentials, not just mocks - Handle both empty results and actual errors gracefully - Use structured logging for debugging ### Don'ts ❌ - Don't assume field names match intuitive expectations - Don't ignore 405 errors - they indicate permission issues - Don't fetch all fields if you only need a few - Don't rely solely on unit tests - integration tests are crucial - Don't hardcode status values - use fieldInfo() to get valid options - Don't forget parent-child entity relationships for CUD operations ### Common Field Name Gotchas ```typescript // Entity-specific field mappings to remember: const FieldMappings = { Projects: { wrong: 'type', correct: 'projectType' }, Companies: { wrong: 'name', correct: 'companyName' }, Contacts: { wrong: 'name', correct: 'firstName + lastName' // Two separate fields }, Tickets: { wrong: 'number', correct: 'ticketNumber' } }; ``` This guide should serve as a comprehensive reference for building MCP servers with Autotask API integration. Keep it updated as new patterns and gotchas are discovered.

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/asachs01/autotask-mcp'

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