@taazkareem/clickup-mcp-server 0.8.5 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,211 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Simple Authentication Middleware
6
+ *
7
+ * Handles authentication for both OAuth2 and API key methods:
8
+ * - Request authentication validation
9
+ * - Session management (simplified)
10
+ * - Backward compatibility with API keys
11
+ * - Pass-through to ClickUp API (no custom user management)
12
+ */
13
+ import { Logger } from '../logger.js';
14
+ import config from '../config.js';
15
+ /**
16
+ * Simple authentication middleware class
17
+ */
18
+ export class AuthMiddleware {
19
+ constructor(sessionService) {
20
+ this.logger = new Logger('AuthMiddleware');
21
+ this.sessionService = sessionService;
22
+ }
23
+ /**
24
+ * Main authentication middleware (simplified)
25
+ * @param options Middleware options
26
+ * @returns Express middleware function
27
+ */
28
+ authenticate(options = {}) {
29
+ return async (req, res, next) => {
30
+ try {
31
+ const { required = true, allowApiKey = true } = options;
32
+ this.logger.debug('Authenticating request', {
33
+ path: req.path,
34
+ method: req.method,
35
+ required,
36
+ allowApiKey
37
+ });
38
+ // Try OAuth2 authentication first
39
+ const oauth2Result = await this.tryOAuth2Authentication(req);
40
+ if (oauth2Result.success) {
41
+ req.userSession = oauth2Result.session;
42
+ req.authMethod = 'oauth2';
43
+ req.clickupApiKey = oauth2Result.session?.accessToken;
44
+ req.clickupTeamId = config.clickupTeamId;
45
+ req.clickupUserId = oauth2Result.session?.clickupUserId;
46
+ this.logger.info('OAuth2 authentication successful', {
47
+ clickupUserId: req.clickupUserId,
48
+ sessionId: req.userSession?.id
49
+ });
50
+ return next();
51
+ }
52
+ // Fall back to API key authentication if allowed
53
+ if (allowApiKey) {
54
+ const apiKeyResult = await this.tryApiKeyAuthentication(req);
55
+ if (apiKeyResult.success) {
56
+ req.authMethod = 'api_key';
57
+ req.clickupApiKey = apiKeyResult.apiKey;
58
+ req.clickupTeamId = apiKeyResult.teamId;
59
+ this.logger.info('API key authentication successful');
60
+ return next();
61
+ }
62
+ }
63
+ // If authentication is required but failed
64
+ if (required) {
65
+ this.logger.warn('Authentication required but failed', {
66
+ path: req.path,
67
+ oauth2Error: oauth2Result.error,
68
+ apiKeyAllowed: allowApiKey
69
+ });
70
+ return res.status(401).json({
71
+ error: 'Authentication required',
72
+ message: 'Please provide valid OAuth2 session or API key',
73
+ code: 'AUTHENTICATION_REQUIRED'
74
+ });
75
+ }
76
+ // Authentication not required, continue without user context
77
+ this.logger.debug('Authentication not required, continuing without user context');
78
+ next();
79
+ }
80
+ catch (error) {
81
+ this.logger.error('Authentication middleware error', {
82
+ error: error.message,
83
+ path: req.path
84
+ });
85
+ res.status(500).json({
86
+ error: 'Authentication error',
87
+ message: 'Internal authentication error',
88
+ code: 'AUTHENTICATION_ERROR'
89
+ });
90
+ }
91
+ };
92
+ }
93
+ /**
94
+ * Try OAuth2 authentication using session (simplified)
95
+ * @private
96
+ */
97
+ async tryOAuth2Authentication(req) {
98
+ try {
99
+ // Check for session ID in various places
100
+ let sessionId;
101
+ // 1. Authorization header (Bearer token)
102
+ const authHeader = req.headers.authorization;
103
+ if (authHeader && authHeader.startsWith('Bearer ')) {
104
+ sessionId = authHeader.substring(7);
105
+ }
106
+ // 2. Session cookie
107
+ if (!sessionId && req.cookies?.sessionId) {
108
+ sessionId = req.cookies.sessionId;
109
+ }
110
+ // 3. Query parameter (for testing)
111
+ if (!sessionId && req.query.sessionId) {
112
+ sessionId = req.query.sessionId;
113
+ }
114
+ if (!sessionId) {
115
+ return {
116
+ success: false,
117
+ error: 'No session ID provided'
118
+ };
119
+ }
120
+ // Get session from session service
121
+ const sessionResult = await this.sessionService.getSession(sessionId);
122
+ if (!sessionResult.success || !sessionResult.data) {
123
+ return {
124
+ success: false,
125
+ error: 'Invalid or expired session'
126
+ };
127
+ }
128
+ const session = sessionResult.data;
129
+ // No need to get user info - ClickUp API will handle permissions
130
+ return {
131
+ success: true,
132
+ session
133
+ };
134
+ }
135
+ catch (error) {
136
+ this.logger.error('OAuth2 authentication error', { error: error.message });
137
+ return {
138
+ success: false,
139
+ error: error.message
140
+ };
141
+ }
142
+ }
143
+ /**
144
+ * Try API key authentication (backward compatibility)
145
+ * @private
146
+ */
147
+ async tryApiKeyAuthentication(req) {
148
+ try {
149
+ // Check for API key in various places
150
+ let apiKey;
151
+ let teamId;
152
+ // 1. Authorization header
153
+ const authHeader = req.headers.authorization;
154
+ if (authHeader && !authHeader.startsWith('Bearer ')) {
155
+ apiKey = authHeader;
156
+ }
157
+ // 2. Custom headers
158
+ if (!apiKey && req.headers['x-clickup-api-key']) {
159
+ apiKey = req.headers['x-clickup-api-key'];
160
+ }
161
+ // 3. Environment variables (fallback)
162
+ if (!apiKey) {
163
+ apiKey = config.clickupApiKey;
164
+ teamId = config.clickupTeamId;
165
+ }
166
+ // 4. Team ID from headers
167
+ if (!teamId && req.headers['x-clickup-team-id']) {
168
+ teamId = req.headers['x-clickup-team-id'];
169
+ }
170
+ if (!apiKey || !teamId) {
171
+ return {
172
+ success: false,
173
+ error: 'API key or team ID not provided'
174
+ };
175
+ }
176
+ // Basic validation (you might want to add actual API validation here)
177
+ if (!apiKey.startsWith('pk_')) {
178
+ return {
179
+ success: false,
180
+ error: 'Invalid API key format'
181
+ };
182
+ }
183
+ return {
184
+ success: true,
185
+ apiKey,
186
+ teamId
187
+ };
188
+ }
189
+ catch (error) {
190
+ this.logger.error('API key authentication error', { error: error.message });
191
+ return {
192
+ success: false,
193
+ error: error.message
194
+ };
195
+ }
196
+ }
197
+ /**
198
+ * Optional authentication middleware (doesn't require authentication)
199
+ * @returns Express middleware function
200
+ */
201
+ optional() {
202
+ return this.authenticate({ required: false });
203
+ }
204
+ /**
205
+ * OAuth2 only authentication (no API key fallback)
206
+ * @returns Express middleware function
207
+ */
208
+ oauth2Only() {
209
+ return this.authenticate({ allowApiKey: false });
210
+ }
211
+ }
@@ -0,0 +1,306 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Simple OAuth2 Authentication Routes
6
+ *
7
+ * Handles OAuth2 authentication endpoints:
8
+ * - Authorization redirect
9
+ * - OAuth2 callback handling
10
+ * - Token refresh
11
+ * - Logout and session management
12
+ * - No complex user management (ClickUp handles this)
13
+ */
14
+ import { Router } from 'express';
15
+ import { Logger } from '../logger.js';
16
+ import { OAuth2Service } from '../services/auth/oauth2.js';
17
+ import { AuthMiddleware } from '../middleware/auth.js';
18
+ /**
19
+ * Simple OAuth2 authentication routes
20
+ */
21
+ export class AuthRoutes {
22
+ constructor(oauth2Config, sessionService) {
23
+ this.stateStore = new Map();
24
+ this.router = Router();
25
+ this.logger = new Logger('AuthRoutes');
26
+ this.oauth2Service = new OAuth2Service(oauth2Config);
27
+ this.sessionService = sessionService;
28
+ this.authMiddleware = new AuthMiddleware(sessionService);
29
+ this.setupRoutes();
30
+ // Clean up expired states every 10 minutes
31
+ setInterval(() => this.cleanupExpiredStates(), 10 * 60 * 1000);
32
+ }
33
+ /**
34
+ * Get the router instance
35
+ */
36
+ getRouter() {
37
+ return this.router;
38
+ }
39
+ /**
40
+ * Setup authentication routes (simplified)
41
+ * @private
42
+ */
43
+ setupRoutes() {
44
+ // OAuth2 authorization redirect
45
+ this.router.get('/login', this.handleLogin.bind(this));
46
+ // OAuth2 callback
47
+ this.router.get('/callback', this.handleCallback.bind(this));
48
+ // Token refresh
49
+ this.router.post('/refresh', this.handleRefresh.bind(this));
50
+ // Logout (simplified)
51
+ this.router.post('/logout', this.handleLogout.bind(this));
52
+ // Health check
53
+ this.router.get('/health', this.handleHealth.bind(this));
54
+ this.logger.info('Simple authentication routes initialized');
55
+ }
56
+ /**
57
+ * Handle OAuth2 login redirect (simplified)
58
+ */
59
+ handleLogin(req, res) {
60
+ try {
61
+ this.logger.info('Handling OAuth2 login request', {
62
+ ip: req.ip,
63
+ userAgent: req.get('User-Agent')
64
+ });
65
+ // Generate state parameter for CSRF protection
66
+ const state = this.generateState();
67
+ // Store state in memory for validation
68
+ this.stateStore.set(state, { timestamp: Date.now() });
69
+ // Generate authorization URL
70
+ const authUrl = this.oauth2Service.generateAuthorizationUrl(state);
71
+ this.logger.info('Redirecting to OAuth2 authorization', {
72
+ state,
73
+ redirectUrl: authUrl
74
+ });
75
+ res.redirect(authUrl);
76
+ }
77
+ catch (error) {
78
+ this.logger.error('Login handler error', { error: error.message });
79
+ res.status(500).json({
80
+ error: 'Login error',
81
+ message: 'Failed to initiate OAuth2 login',
82
+ code: 'LOGIN_ERROR'
83
+ });
84
+ }
85
+ }
86
+ /**
87
+ * Handle OAuth2 callback (simplified)
88
+ */
89
+ async handleCallback(req, res) {
90
+ try {
91
+ const { code, state, error } = req.query;
92
+ this.logger.info('Handling OAuth2 callback', {
93
+ hasCode: !!code,
94
+ hasState: !!state,
95
+ hasError: !!error
96
+ });
97
+ // Check for OAuth2 error
98
+ if (error) {
99
+ this.logger.error('OAuth2 callback error', { error });
100
+ res.status(400).json({
101
+ error: 'OAuth2 error',
102
+ message: error,
103
+ code: 'OAUTH2_ERROR'
104
+ });
105
+ return;
106
+ }
107
+ // Validate required parameters
108
+ if (!code || typeof code !== 'string') {
109
+ res.status(400).json({
110
+ error: 'Missing authorization code',
111
+ message: 'Authorization code is required',
112
+ code: 'MISSING_CODE'
113
+ });
114
+ return;
115
+ }
116
+ // Validate state parameter (CSRF protection)
117
+ if (!state || typeof state !== 'string' || !this.stateStore.has(state)) {
118
+ this.logger.warn('Invalid OAuth2 state parameter', {
119
+ receivedState: state,
120
+ hasState: this.stateStore.has(state)
121
+ });
122
+ res.status(400).json({
123
+ error: 'Invalid state parameter',
124
+ message: 'CSRF validation failed',
125
+ code: 'INVALID_STATE'
126
+ });
127
+ return;
128
+ }
129
+ // Remove used state
130
+ this.stateStore.delete(state);
131
+ // Exchange code for tokens
132
+ const authResult = await this.oauth2Service.exchangeCodeForToken(code);
133
+ if (!authResult.success || !authResult.tokens || !authResult.user) {
134
+ this.logger.error('Token exchange failed', { error: authResult.error });
135
+ res.status(400).json({
136
+ error: 'Token exchange failed',
137
+ message: authResult.error?.message || 'Failed to exchange authorization code',
138
+ code: 'TOKEN_EXCHANGE_FAILED'
139
+ });
140
+ return;
141
+ }
142
+ // Create session directly (no user management needed)
143
+ const sessionResult = await this.sessionService.createSession({
144
+ clickupUserId: authResult.user.id.toString(),
145
+ tokens: authResult.tokens
146
+ });
147
+ if (!sessionResult.success || !sessionResult.data) {
148
+ this.logger.error('Session creation failed', { error: sessionResult.error });
149
+ res.status(500).json({
150
+ error: 'Session creation error',
151
+ message: 'Failed to create user session',
152
+ code: 'SESSION_CREATION_ERROR'
153
+ });
154
+ return;
155
+ }
156
+ const session = sessionResult.data;
157
+ // Set session cookie
158
+ res.cookie('sessionId', session.id, {
159
+ httpOnly: true,
160
+ secure: process.env.NODE_ENV === 'production',
161
+ maxAge: 24 * 60 * 60 * 1000, // 24 hours
162
+ sameSite: 'lax'
163
+ });
164
+ this.logger.info('OAuth2 authentication successful', {
165
+ clickupUserId: authResult.user.id,
166
+ sessionId: session.id
167
+ });
168
+ // Redirect to success page or return JSON
169
+ if (req.query.format === 'json') {
170
+ res.json({
171
+ success: true,
172
+ message: 'Authentication successful',
173
+ user: {
174
+ id: authResult.user.id,
175
+ username: authResult.user.username,
176
+ email: authResult.user.email
177
+ },
178
+ sessionId: session.id
179
+ });
180
+ }
181
+ else {
182
+ // Redirect to dashboard or configured success URL
183
+ const redirectUrl = process.env.OAUTH2_SUCCESS_REDIRECT || '/dashboard';
184
+ res.redirect(redirectUrl);
185
+ }
186
+ }
187
+ catch (error) {
188
+ this.logger.error('Callback handler error', { error: error.message });
189
+ res.status(500).json({
190
+ error: 'Callback error',
191
+ message: 'Failed to process OAuth2 callback',
192
+ code: 'CALLBACK_ERROR'
193
+ });
194
+ }
195
+ }
196
+ /**
197
+ * Handle token refresh (simplified)
198
+ */
199
+ async handleRefresh(req, res) {
200
+ try {
201
+ const { refreshToken, sessionId } = req.body;
202
+ if (!refreshToken || !sessionId) {
203
+ res.status(400).json({
204
+ error: 'Missing parameters',
205
+ message: 'Refresh token and session ID are required',
206
+ code: 'MISSING_PARAMETERS'
207
+ });
208
+ return;
209
+ }
210
+ // Refresh token
211
+ const refreshResult = await this.oauth2Service.refreshToken(refreshToken);
212
+ if (!refreshResult.success || !refreshResult.tokens) {
213
+ res.status(400).json({
214
+ error: 'Token refresh failed',
215
+ message: refreshResult.error?.message || 'Failed to refresh token',
216
+ code: 'TOKEN_REFRESH_FAILED'
217
+ });
218
+ return;
219
+ }
220
+ // Update session with new tokens
221
+ const updateResult = await this.sessionService.updateSessionTokens(sessionId, refreshResult.tokens);
222
+ if (!updateResult.success) {
223
+ res.status(500).json({
224
+ error: 'Session update failed',
225
+ message: 'Failed to update session with new tokens',
226
+ code: 'SESSION_UPDATE_FAILED'
227
+ });
228
+ return;
229
+ }
230
+ res.json({
231
+ success: true,
232
+ message: 'Token refreshed successfully',
233
+ tokens: refreshResult.tokens
234
+ });
235
+ }
236
+ catch (error) {
237
+ this.logger.error('Refresh handler error', { error: error.message });
238
+ res.status(500).json({
239
+ error: 'Refresh error',
240
+ message: 'Failed to refresh token',
241
+ code: 'REFRESH_ERROR'
242
+ });
243
+ }
244
+ }
245
+ /**
246
+ * Handle logout (simplified)
247
+ */
248
+ async handleLogout(req, res) {
249
+ try {
250
+ // Get session ID from cookie or header
251
+ const sessionId = req.cookies?.sessionId ||
252
+ (req.headers.authorization?.startsWith('Bearer ') ?
253
+ req.headers.authorization.substring(7) : null);
254
+ if (sessionId) {
255
+ await this.sessionService.deleteSession(sessionId);
256
+ }
257
+ // Clear session cookie
258
+ res.clearCookie('sessionId');
259
+ res.json({
260
+ success: true,
261
+ message: 'Logged out successfully'
262
+ });
263
+ }
264
+ catch (error) {
265
+ this.logger.error('Logout handler error', { error: error.message });
266
+ res.status(500).json({
267
+ error: 'Logout error',
268
+ message: 'Failed to logout',
269
+ code: 'LOGOUT_ERROR'
270
+ });
271
+ }
272
+ }
273
+ /**
274
+ * Handle health check
275
+ */
276
+ handleHealth(req, res) {
277
+ res.json({
278
+ status: 'healthy',
279
+ service: 'oauth2-auth',
280
+ timestamp: new Date().toISOString()
281
+ });
282
+ }
283
+ /**
284
+ * Generate random state parameter
285
+ * @private
286
+ */
287
+ generateState() {
288
+ return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
289
+ }
290
+ /**
291
+ * Clean up expired states
292
+ * @private
293
+ */
294
+ cleanupExpiredStates() {
295
+ const now = Date.now();
296
+ const maxAge = 10 * 60 * 1000; // 10 minutes
297
+ for (const [state, data] of this.stateStore.entries()) {
298
+ if (now - data.timestamp > maxAge) {
299
+ this.stateStore.delete(state);
300
+ }
301
+ }
302
+ this.logger.debug('Cleaned up expired OAuth2 states', {
303
+ remainingStates: this.stateStore.size
304
+ });
305
+ }
306
+ }
@@ -0,0 +1,13 @@
1
+ // src/schemas/member.ts
2
+ import { z } from 'zod';
3
+ export const MemberSchema = z.object({
4
+ id: z.number(),
5
+ username: z.string().optional(),
6
+ email: z.string().optional(),
7
+ full_name: z.string().optional(),
8
+ profile_picture: z.string().optional(),
9
+ role: z.number(),
10
+ role_name: z.string().optional(),
11
+ initials: z.string().optional(),
12
+ last_active: z.string().optional(),
13
+ });
package/build/server.js CHANGED
@@ -7,6 +7,7 @@
7
7
  import { Server } from "@modelcontextprotocol/sdk/server/index.js";
8
8
  import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema, ListResourcesRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
9
9
  import config from "./config.js";
10
+ import { isLicenseValid, getLicenseErrorMessage } from "./license.js";
10
11
  import { workspaceHierarchyTool, handleGetWorkspaceHierarchy } from "./tools/workspace.js";
11
12
  import { createTaskTool, updateTaskTool, moveTaskTool, duplicateTaskTool, getTaskTool, deleteTaskTool, getTaskCommentsTool, createTaskCommentTool, createBulkTasksTool, updateBulkTasksTool, moveBulkTasksTool, deleteBulkTasksTool, attachTaskFileTool, getWorkspaceTasksTool, getTaskTimeEntriesTool, startTimeTrackingTool, stopTimeTrackingTool, addTimeEntryTool, deleteTimeEntryTool, getCurrentTimeEntryTool, handleCreateTask, handleUpdateTask, handleMoveTask, handleDuplicateTask, handleDeleteTask, handleGetTaskComments, handleCreateTaskComment, handleCreateBulkTasks, handleUpdateBulkTasks, handleMoveBulkTasks, handleDeleteBulkTasks, handleGetTask, handleAttachTaskFile, handleGetWorkspaceTasks, handleGetTaskTimeEntries, handleStartTimeTracking, handleStopTimeTracking, handleAddTimeEntry, handleDeleteTimeEntry, handleGetCurrentTimeEntry } from "./tools/task/index.js";
12
13
  import { createListTool, handleCreateList, createListInFolderTool, handleCreateListInFolder, getListTool, handleGetList, updateListTool, handleUpdateList, deleteListTool, handleDeleteList } from "./tools/list.js";
@@ -135,6 +136,19 @@ export function configureServer() {
135
136
  logger.info(`Received CallTool request for tool: ${name}`, {
136
137
  params
137
138
  });
139
+ // Check license validity FIRST - before any tool execution
140
+ if (!isLicenseValid()) {
141
+ logger.warn(`Tool execution blocked: Invalid or missing license key`);
142
+ return {
143
+ content: [
144
+ {
145
+ type: "text",
146
+ text: getLicenseErrorMessage()
147
+ }
148
+ ],
149
+ isError: true
150
+ };
151
+ }
138
152
  // Check if the tool is enabled
139
153
  if (!isToolEnabled(name)) {
140
154
  const reason = config.enabledTools.length > 0
@@ -0,0 +1,15 @@
1
+ Logging initialized to /Volumes/Code/Projects/MCP/clickup-mcp-server/build/server.log
2
+ [2025-12-24T23:37:55.704Z] [PID:59029] ERROR: [ClickUp:TaskService] Authorization failed for /list/901309789109/task/86ae4175p
3
+ {
4
+ "path": "/list/901309789109/task/86ae4175p",
5
+ "status": 403,
6
+ "method": "POST",
7
+ "requestData": {}
8
+ }
9
+ [2025-12-24T23:38:50.385Z] [PID:59029] ERROR: [ClickUp:TaskService] Authorization failed for /list/901309789109/task/86ae4175p
10
+ {
11
+ "path": "/list/901309789109/task/86ae4175p",
12
+ "status": 403,
13
+ "method": "POST",
14
+ "requestData": {}
15
+ }