Skip to main content
Glama
clickup.service.ts8.2 kB
import axios, { AxiosInstance, AxiosError, AxiosHeaders, RawAxiosRequestHeaders, AxiosRequestConfig, InternalAxiosRequestConfig, } from "axios"; // Encryption is optional now, only if we want to encrypt the token at rest (less critical than OAuth tokens) // import { encrypt, decrypt } from "../security.js"; import { logger } from "../logger.js"; // Import the refactored config import { config } from "../config/app.config.js"; import { ClickUpTask, ClickUpList, ClickUpBoard, ClickUpTeam, GetSpacesParams, ClickUpSpace, CreateSpaceParams, UpdateSpaceParams, ClickUpSuccessResponse, // Folder Types GetFoldersParams, ClickUpFolder, CreateFolderParams, UpdateFolderParams, // DeleteFolderParams is not directly used by method signature but good to have for completeness // Custom Field Types ClickUpCustomField, SetTaskCustomFieldValueParams, RemoveTaskCustomFieldValueParams, // Doc Types ClickUpDoc, SearchDocsParams, CreateDocParams, ClickUpDocPage, GetDocPagesParams, CreateDocPageParams, GetDocPageContentParams, EditDocPageContentParams, // View Types ClickUpView, GetViewsParams, ClickUpViewParentType, } from "../types.js"; // Keep ClickUpTeam for API v2 responses // Import new resource services import { TaskService } from "./resources/task.service.js"; import { SpaceService } from "./resources/space.service.js"; import { FolderService } from "./resources/folder.service.js"; import { CustomFieldService } from "./resources/custom-field.service.js"; import { DocService } from "./resources/doc.service.js"; import { ViewService } from "./resources/view.service.js"; import { ListService } from "./resources/list.service.js"; // Remove TokenData interface if not used elsewhere (it was removed from types.ts) export class ClickUpService { private client: AxiosInstance; // Remove tokenStore // private tokenStore: Map<string, string>; private personalToken: string; // Store the personal token // Resource service instances private _taskService: TaskService; private _spaceService: SpaceService; private _folderService: FolderService; private _customFieldService: CustomFieldService; private _docService: DocService; private _viewService: ViewService; private _listService: ListService; // Add other resource services here later constructor() { // Remove tokenStore initialization // this.tokenStore = new Map(); // Store the personal token from config this.personalToken = config.clickUpPersonalToken; if (!this.personalToken) { // This should be caught by config validation, but double-check throw new Error( "ClickUp Personal API Token is missing in configuration.", ); } this.client = axios.create({ // Use the specific API URL from the refactored config baseURL: config.clickUpApiUrl, headers: { "Content-Type": "application/json", }, }); // Add REQUEST interceptor for Authorization header this.client.interceptors.request.use( (axiosConfig) => { // Add Authorization header using the stored personal token axiosConfig.headers.Authorization = `${this.personalToken}`; // It seems ClickUp API uses the token directly, not "Bearer " prefix for personal tokens // axiosConfig.headers.Authorization = `Bearer ${this.personalToken}`; logger.debug("Added Authorization header to ClickUp request."); return axiosConfig; }, (error) => { logger.error("Error adding Authorization header:", error); return Promise.reject(error); }, ); // Keep existing response interceptor for rate limiting this.client.interceptors.response.use( (response) => { // ... rate limit logging ... const remaining = parseInt( response.headers["x-ratelimit-remaining"] || "100", ); const reset = parseInt(response.headers["x-ratelimit-reset"] || "0"); logger.debug( `Rate limit: ${remaining} requests remaining, reset in ${reset}s`, ); return response; }, (error: AxiosError) => { // ... rate limit error handling ... if (error.response?.status === 429) { logger.warn("Rate limit exceeded"); } return Promise.reject(error); }, ); // Instantiate resource services this._taskService = new TaskService(this.client); this._spaceService = new SpaceService(this.client); this._folderService = new FolderService(this.client); this._customFieldService = new CustomFieldService(this.client); this._docService = new DocService(this.client); this._viewService = new ViewService(this.client); this._listService = new ListService(this.client); // Instantiate other services here later logger.info("ClickUpService initialized with all resource services."); } // Public accessors for resource services public get taskService(): TaskService { return this._taskService; } public get spaceService(): SpaceService { return this._spaceService; } public get folderService(): FolderService { return this._folderService; } public get customFieldService(): CustomFieldService { return this._customFieldService; } public get docService(): DocService { return this._docService; } public get viewService(): ViewService { return this._viewService; } public get listService(): ListService { return this._listService; } // Add other accessors here later // Remove getToken method // private async getToken(...) { ... } // Remove refreshToken method // private async refreshToken(...) { ... } // Remove setToken method // setToken(...) { ... } // Keep getRequestConfig (or simplify if only headers needed) private async getRequestConfig(): Promise<AxiosRequestConfig> { // No longer need to set Content-Type here if set in defaults // and Authorization is handled by interceptor return {}; // Return empty config, interceptor handles auth } // Remove createTask method (delegated) // async createTask(taskData: ClickUpTask): Promise<ClickUpTask> { ... } // Remove updateTask method (delegated) // async updateTask(...) : Promise<ClickUpTask> { ... } // Get current user info which includes workspace information async getTeams(): Promise<ClickUpTeam[]> { try { // Use the correct endpoint to get workspaces/teams const response = await this.client.get("/v2/team", {}); // Return teams directly from response if (!response.data || !response.data.teams) { logger.warn("No teams found in response"); return []; } return response.data.teams; } catch (error) { if (error instanceof Error) { logger.error(`Failed to get teams: ${error.message}`); } throw new Error("Failed to retrieve teams from ClickUp"); } } // Remove userId parameter async getLists(folderId: string): Promise<ClickUpList[]> { try { // Interceptor will add auth header const response = await this.client.get( `/v2/folder/${folderId}/list`, {}, ); return response.data.lists; } catch (error) { // ... existing error handling ... if (error instanceof Error) { logger.error(`Failed to get lists: ${error.message}`); } throw new Error("Failed to retrieve lists from ClickUp"); } } // Remove userId parameter async createBoard(boardData: ClickUpBoard): Promise<ClickUpBoard> { try { // Interceptor will add auth header const response = await this.client.post( // Ensure space_id is present `/v2/space/${boardData.space_id}/board`, boardData, {}, ); return response.data; } catch (error) { // ... existing error handling ... if (error instanceof Error) { logger.error(`Failed to create board: ${error.message}`); } throw new Error("Failed to create board in ClickUp"); } } // REMOVE View Methods if they were here, or ensure class ends correctly // async getViews(...) { ... } }

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/Nazruden/clickup-mcp-server'

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