Skip to main content
Glama
canny.ts7.57 kB
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'; import { CONFIG } from '../config/config.js'; import { CannyApiResponse, CannyPost, CannyBoard, CannyCategory, CannyComment, CannyUser, CannyTag, } from './types.js'; /** * Canny API Client * Implements Customer-Centric approach by providing reliable API access * Following Efficiency principle by implementing proper error handling and retries */ export class CannyClient { private client: AxiosInstance; private rateLimitTracker: Map<string, number[]> = new Map(); constructor(private apiKey: string, private baseUrl: string = CONFIG.baseUrl) { this.client = axios.create({ baseURL: this.baseUrl, timeout: CONFIG.timeout, headers: { 'Content-Type': 'application/json', }, }); // Add request interceptor for API key and rate limiting this.client.interceptors.request.use( (config) => { // Add API key to request if (config.method === 'get') { config.params = { ...config.params, apiKey: this.apiKey }; } else { config.data = { ...config.data, apiKey: this.apiKey }; } // Check rate limiting this.checkRateLimit(config.url || ''); return config; }, (error) => Promise.reject(error) ); } private checkRateLimit(endpoint: string): void { const now = Date.now(); const windowMs = 60 * 1000; // 1 minute window if (!this.rateLimitTracker.has(endpoint)) { this.rateLimitTracker.set(endpoint, []); } const requests = this.rateLimitTracker.get(endpoint)!; // Remove old requests outside the window const validRequests = requests.filter(time => now - time < windowMs); if (validRequests.length >= CONFIG.rateLimit.requestsPerMinute) { throw new Error(`Rate limit exceeded for endpoint: ${endpoint}`); } validRequests.push(now); this.rateLimitTracker.set(endpoint, validRequests); } private async makeRequest<T>(config: AxiosRequestConfig): Promise<CannyApiResponse<T>> { try { const response = await this.client.request<T>(config); return { data: response.data, status: response.status, }; } catch (error) { if (axios.isAxiosError(error)) { return { error: error.response?.data?.error || error.message, status: error.response?.status || 500, }; } return { error: error instanceof Error ? error.message : 'Unknown error', status: 500, }; } } /** * Get all boards accessible to the API key */ async getBoards(): Promise<CannyApiResponse<CannyBoard[]>> { const response = await this.makeRequest<{ boards: CannyBoard[] }>({ method: 'GET', url: '/boards/list', }); // Handle the nested response structure if (response.data && response.data.boards) { return { data: response.data.boards, status: response.status, }; } return { data: [], status: response.status, error: response.error, }; } /** * Get posts from a specific board */ async getPosts(boardId: string, options?: { limit?: number; skip?: number; status?: string; search?: string; sort?: 'newest' | 'oldest' | 'relevance' | 'trending'; }): Promise<CannyApiResponse<{ posts: CannyPost[]; hasMore: boolean }>> { return this.makeRequest<{ posts: CannyPost[]; hasMore: boolean }>({ method: 'GET', url: '/posts/list', params: { boardID: boardId, limit: options?.limit || 10, skip: options?.skip || 0, ...(options?.status && { status: options.status }), ...(options?.search && { search: options.search }), ...(options?.sort && { sort: options.sort }), }, }); } /** * Get a specific post by ID */ async getPost(postId: string): Promise<CannyApiResponse<CannyPost>> { return this.makeRequest<CannyPost>({ method: 'GET', url: '/posts/retrieve', params: { id: postId }, }); } /** * Create a new post */ async createPost(data: { authorID: string; boardID: string; title: string; details?: string; categoryID?: string; customFields?: Record<string, any>; }): Promise<CannyApiResponse<CannyPost>> { return this.makeRequest<CannyPost>({ method: 'POST', url: '/posts/create', data, }); } /** * Update an existing post */ async updatePost(postId: string, data: { title?: string; details?: string; categoryID?: string; customFields?: Record<string, any>; status?: string; }): Promise<CannyApiResponse<CannyPost>> { return this.makeRequest<CannyPost>({ method: 'POST', url: '/posts/change_status', data: { postID: postId, ...data }, }); } /** * Search posts across all accessible boards */ async searchPosts(query: string, options?: { boardIDs?: string[]; limit?: number; status?: string; }): Promise<CannyApiResponse<{ posts: CannyPost[]; hasMore: boolean }>> { return this.makeRequest<{ posts: CannyPost[]; hasMore: boolean }>({ method: 'GET', url: '/posts/list', params: { search: query, limit: options?.limit || 20, ...(options?.boardIDs && { boardIDs: options.boardIDs.join(',') }), ...(options?.status && { status: options.status }), }, }); } /** * Get categories from a specific board */ async getCategories(boardId: string): Promise<CannyApiResponse<CannyCategory[]>> { const response = await this.makeRequest<{ categories: CannyCategory[] }>({ method: 'GET', url: '/categories/list', params: { boardID: boardId, }, }); // Handle the nested response structure if (response.data && response.data.categories) { return { data: response.data.categories, status: response.status, }; } return { data: [], status: response.status, error: response.error, }; } /** * Get comments from a specific post */ async getComments(postId: string, options?: { limit?: number; skip?: number; }): Promise<CannyApiResponse<{ comments: CannyComment[]; hasMore: boolean }>> { return this.makeRequest<{ comments: CannyComment[]; hasMore: boolean }>({ method: 'GET', url: '/comments/list', params: { postID: postId, limit: options?.limit || 10, skip: options?.skip || 0, }, }); } /** * Get users from your Canny instance */ async getUsers(options?: { limit?: number; skip?: number; search?: string; }): Promise<CannyApiResponse<{ users: CannyUser[]; hasMore: boolean }>> { return this.makeRequest<{ users: CannyUser[]; hasMore: boolean }>({ method: 'GET', url: '/users/list', params: { limit: options?.limit || 10, skip: options?.skip || 0, ...(options?.search && { search: options.search }), }, }); } /** * Get tags from boards */ async getTags(options?: { boardId?: string; limit?: number; }): Promise<CannyApiResponse<{ tags: CannyTag[]; hasMore?: boolean }>> { return this.makeRequest<{ tags: CannyTag[]; hasMore?: boolean }>({ method: 'GET', url: '/tags/list', params: { limit: options?.limit || 20, ...(options?.boardId && { boardID: options.boardId }), }, }); } }

Implementation Reference

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/itsocialist/canny-mcp-server'

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