Skip to main content
Glama
secure-client.ts11.3 kB
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; import { validateApiToken, sanitizeInput, rateLimiter, DEFAULT_RATE_LIMITS } from '../utils/security.js'; import { handleClickUpApiError, RetryManager, generateRequestId } from '../utils/error-handling.js'; // ClickUp API base URL const API_BASE_URL = 'https://api.clickup.com/api/v2'; export interface SecureClickUpClientConfig { apiToken: string; baseUrl?: string; timeout?: number; maxRetries?: number; enableRateLimit?: boolean; userAgent?: string; } export class SecureClickUpClient { private axiosInstance: AxiosInstance; private retryManager: RetryManager; private enableRateLimit: boolean; private userAgent: string; constructor(config: SecureClickUpClientConfig) { // Validate API token const tokenValidation = validateApiToken(config.apiToken); if (!tokenValidation.isValid) { throw new Error(`Invalid API token: ${tokenValidation.error}`); } this.enableRateLimit = config.enableRateLimit ?? true; this.userAgent = config.userAgent || 'ClickUp-MCP-Server/3.0.0'; this.retryManager = new RetryManager(config.maxRetries || 3); this.axiosInstance = axios.create({ baseURL: config.baseUrl || API_BASE_URL, timeout: config.timeout || 30000, // 30 second timeout headers: { 'Authorization': config.apiToken, 'Content-Type': 'application/json', 'User-Agent': this.userAgent, 'Accept': 'application/json', 'X-Requested-With': 'XMLHttpRequest' } }); // Add request interceptor for security and rate limiting this.axiosInstance.interceptors.request.use( (config) => { // Generate request ID for tracking const requestId = generateRequestId(); config.headers['X-Request-ID'] = requestId; // Rate limiting if (this.enableRateLimit) { const rateLimitKey = `api_${config.url}`; if (!rateLimiter.isAllowed(rateLimitKey, DEFAULT_RATE_LIMITS.api)) { throw new Error('Rate limit exceeded. Please slow down your requests.'); } } // Sanitize request data if (config.data) { config.data = sanitizeInput(config.data); } // Sanitize query parameters if (config.params) { config.params = sanitizeInput(config.params); } return config; }, (error) => Promise.reject(error) ); // Add response interceptor for error handling this.axiosInstance.interceptors.response.use( (response) => { // Log successful requests in debug mode if (process.env.NODE_ENV === 'development') { console.debug(`[${response.config.headers['X-Request-ID']}] ${response.config.method?.toUpperCase()} ${response.config.url} - ${response.status}`); } return response; }, (error) => { const requestId = error.config?.headers?.['X-Request-ID']; const structuredError = handleClickUpApiError(error, { operation: `${error.config?.method?.toUpperCase()} ${error.config?.url}`, requestId }); // Log error console.error(`[${requestId}] API Error:`, structuredError.message); return Promise.reject(structuredError); } ); } /** * Secure GET request with retry logic */ async get<T = any>(endpoint: string, params?: any, config?: AxiosRequestConfig): Promise<T> { return this.retryManager.executeWithRetry(async () => { const response = await this.axiosInstance.get(endpoint, { params: sanitizeInput(params), ...config }); return response.data; }, { operationName: `GET ${endpoint}` }); } /** * Secure POST request with retry logic */ async post<T = any>(endpoint: string, data?: any, config?: AxiosRequestConfig): Promise<T> { return this.retryManager.executeWithRetry(async () => { const response = await this.axiosInstance.post(endpoint, sanitizeInput(data), config); return response.data; }, { operationName: `POST ${endpoint}` }); } /** * Secure PUT request with retry logic */ async put<T = any>(endpoint: string, data?: any, config?: AxiosRequestConfig): Promise<T> { return this.retryManager.executeWithRetry(async () => { const response = await this.axiosInstance.put(endpoint, sanitizeInput(data), config); return response.data; }, { operationName: `PUT ${endpoint}` }); } /** * Secure DELETE request with retry logic */ async delete<T = any>(endpoint: string, config?: AxiosRequestConfig): Promise<T> { return this.retryManager.executeWithRetry(async () => { const response = await this.axiosInstance.delete(endpoint, config); return response.data; }, { operationName: `DELETE ${endpoint}` }); } /** * Secure PATCH request with retry logic */ async patch<T = any>(endpoint: string, data?: any, config?: AxiosRequestConfig): Promise<T> { return this.retryManager.executeWithRetry(async () => { const response = await this.axiosInstance.patch(endpoint, sanitizeInput(data), config); return response.data; }, { operationName: `PATCH ${endpoint}` }); } /** * Upload file securely with validation */ async uploadFile<T = any>( endpoint: string, file: { filename: string; data: Buffer | string; mimetype?: string; }, additionalData?: Record<string, any> ): Promise<T> { // Validate file upload const { validateFileUpload } = await import('../utils/security.js'); const validation = validateFileUpload(file.filename, file.mimetype, Buffer.isBuffer(file.data) ? file.data.length : file.data.length); if (!validation.isValid) { throw new Error(`File upload validation failed: ${validation.errors.join(', ')}`); } // Rate limit file uploads more strictly if (this.enableRateLimit) { const rateLimitKey = `upload_${endpoint}`; if (!rateLimiter.isAllowed(rateLimitKey, DEFAULT_RATE_LIMITS.upload)) { throw new Error('Upload rate limit exceeded. Please wait before uploading more files.'); } } const formData = new FormData(); // Add file const blob = new Blob([file.data], { type: file.mimetype }); formData.append('file', blob, file.filename); // Add additional data if (additionalData) { Object.entries(sanitizeInput(additionalData)).forEach(([key, value]) => { formData.append(key, String(value)); }); } return this.retryManager.executeWithRetry(async () => { const response = await this.axiosInstance.post(endpoint, formData, { headers: { 'Content-Type': 'multipart/form-data' }, timeout: 120000 // 2 minute timeout for uploads }); return response.data; }, { operationName: `UPLOAD ${endpoint}` }); } /** * Batch requests with concurrency control */ async batchRequests<T>( requests: Array<{ method: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH'; endpoint: string; data?: any; params?: any; }>, options: { concurrency?: number; failFast?: boolean; } = {} ): Promise<Array<{ success: boolean; data?: T; error?: string }>> { const { concurrency = 5, failFast = false } = options; const results: Array<{ success: boolean; data?: T; error?: string }> = []; // Process requests in batches for (let i = 0; i < requests.length; i += concurrency) { const batch = requests.slice(i, i + concurrency); const batchPromises = batch.map(async (request) => { try { let data: T; switch (request.method) { case 'GET': data = await this.get<T>(request.endpoint, request.params); break; case 'POST': data = await this.post<T>(request.endpoint, request.data); break; case 'PUT': data = await this.put<T>(request.endpoint, request.data); break; case 'DELETE': data = await this.delete<T>(request.endpoint); break; case 'PATCH': data = await this.patch<T>(request.endpoint, request.data); break; default: throw new Error(`Unsupported method: ${request.method}`); } return { success: true, data }; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error'; if (failFast) { throw error; } return { success: false, error: errorMessage }; } }); const batchResults = await Promise.all(batchPromises); results.push(...batchResults); } return results; } /** * Health check endpoint */ async healthCheck(): Promise<{ status: 'healthy' | 'unhealthy'; latency: number; timestamp: string; }> { const startTime = Date.now(); try { // Simple API call to check connectivity await this.get('/user'); const latency = Date.now() - startTime; return { status: 'healthy', latency, timestamp: new Date().toISOString() }; } catch (error) { return { status: 'unhealthy', latency: Date.now() - startTime, timestamp: new Date().toISOString() }; } } /** * Get client statistics */ getStats(): { totalRequests: number; rateLimitEnabled: boolean; userAgent: string; baseUrl: string; } { return { totalRequests: 0, // Would need to implement request counting rateLimitEnabled: this.enableRateLimit, userAgent: this.userAgent, baseUrl: this.axiosInstance.defaults.baseURL || API_BASE_URL }; } /** * Update configuration */ updateConfig(config: Partial<SecureClickUpClientConfig>): void { if (config.apiToken) { const tokenValidation = validateApiToken(config.apiToken); if (!tokenValidation.isValid) { throw new Error(`Invalid API token: ${tokenValidation.error}`); } this.axiosInstance.defaults.headers['Authorization'] = config.apiToken; } if (config.timeout) { this.axiosInstance.defaults.timeout = config.timeout; } if (config.userAgent) { this.userAgent = config.userAgent; this.axiosInstance.defaults.headers['User-Agent'] = config.userAgent; } if (config.enableRateLimit !== undefined) { this.enableRateLimit = config.enableRateLimit; } } /** * Cleanup resources */ destroy(): void { // Clear any pending requests this.axiosInstance.defaults.timeout = 1; // Reset rate limiter rateLimiter.reset(); } } // Create secure client factory export const createSecureClickUpClient = (config?: Partial<SecureClickUpClientConfig>): SecureClickUpClient => { const apiToken = config?.apiToken || process.env.CLICKUP_API_TOKEN; if (!apiToken) { throw new Error('CLICKUP_API_TOKEN environment variable is required'); } return new SecureClickUpClient({ apiToken, ...config }); };

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/Chykalophia/ClickUp-MCP-Server---Enhanced'

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