@taazkareem/clickup-mcp-server 0.8.4 → 0.9.0
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.
- package/LICENSE +9 -17
- package/README.md +33 -38
- package/build/enhanced_server.js +262 -0
- package/build/index.js +9 -3
- package/build/license.js +172 -0
- package/build/middleware/auth.js +211 -0
- package/build/routes/auth.js +306 -0
- package/build/schemas/member.js +13 -0
- package/build/server.js +15 -1
- package/build/server.log +15 -0
- package/build/services/auth/oauth2.js +236 -0
- package/build/services/auth/session.js +337 -0
- package/build/services/clickup/adapter.js +281 -0
- package/build/services/clickup/factory.js +339 -0
- package/build/services/clickup/task/task-attachments.js +20 -12
- package/build/services/clickup/task/task-comments.js +19 -9
- package/build/services/clickup/task/task-core.js +68 -4
- package/build/services/clickup/task/task-custom-fields.js +23 -13
- package/build/services/clickup/task/task-search.js +79 -71
- package/build/services/clickup/task/task-service.js +88 -9
- package/build/services/clickup/task/task-tags.js +25 -13
- package/build/sse_server.js +4 -4
- package/build/tools/documents.js +11 -4
- package/build/tools/health.js +23 -0
- package/build/tools/member.js +2 -4
- package/build/tools/task/bulk-operations.js +5 -5
- package/build/tools/task/handlers.js +62 -12
- package/build/tools/task/single-operations.js +9 -9
- package/build/tools/task/time-tracking.js +61 -170
- package/build/tools/task/utilities.js +56 -22
- package/build/utils/date-utils.js +341 -141
- package/build/utils/schema-compatibility.js +222 -0
- package/build/utils/universal-schema-compatibility.js +171 -0
- package/build/virtual-sdk/generator.js +53 -0
- package/build/virtual-sdk/registry.js +45 -0
- package/package.json +2 -2
|
@@ -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";
|
|
@@ -45,7 +46,7 @@ const isToolEnabled = (toolName) => {
|
|
|
45
46
|
};
|
|
46
47
|
export const server = new Server({
|
|
47
48
|
name: "clickup-mcp-server",
|
|
48
|
-
version: "0.8.
|
|
49
|
+
version: "0.8.5",
|
|
49
50
|
}, {
|
|
50
51
|
capabilities: {
|
|
51
52
|
tools: {},
|
|
@@ -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
|
package/build/server.log
ADDED
|
@@ -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
|
+
}
|