@uteamup/mcp-server 1.0.0-beta.7 → 1.0.0-beta.8

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/README.md CHANGED
@@ -6,6 +6,13 @@
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
7
  [![Node.js Version](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen)](https://nodejs.org)
8
8
 
9
+ ## Links
10
+
11
+ - **📦 [npm Package](https://www.npmjs.com/package/@uteamup/mcp-server)** - Install with `npx -y @uteamup/mcp-server`
12
+ - **📖 [Documentation](https://docs.uteamup.com/mcp)** - Full MCP integration guide
13
+ - **🐛 [Issues](https://github.com/uteamup/mcp-server/issues)** - Report bugs and request features
14
+ - **💬 [Support](mailto:support@uteamup.com)** - Get help
15
+
9
16
  ## What is this?
10
17
 
11
18
  This package enables AI assistants like Claude Desktop and Claude Code to connect to your UteamUP CMMS system using the Model Context Protocol (MCP). It handles OAuth 2.0 authentication automatically, so you can manage work orders, assets, inventory, and more through natural conversation.
package/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  /**
4
4
  * UteamUP MCP Proxy - Production-ready OAuth handler with enhanced debugging
5
- * Version: 1.0.0-beta.7
5
+ * Version: 1.0.0-beta.8
6
6
  *
7
7
  * Features:
8
8
  * - OAuth 2.0 Authorization Code + PKCE authentication
@@ -22,7 +22,7 @@ const { startTimer, recordOAuthAttempt, recordRequest, recordCacheHit, recordCac
22
22
  const { retryWithBackoff } = require('./lib/retry');
23
23
 
24
24
  // Package version
25
- const VERSION = '1.0.0-beta.7';
25
+ const VERSION = '1.0.0-beta.8';
26
26
 
27
27
  // CLI argument handling
28
28
  const args = process.argv.slice(2);
package/lib/config.js ADDED
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Configuration validation using Zod
3
+ * Validates environment variables and provides type-safe configuration
4
+ */
5
+
6
+ const { z } = require('zod');
7
+
8
+ // Configuration schema
9
+ const configSchema = z.object({
10
+ // Required credentials
11
+ apiKey: z.string()
12
+ .length(32, 'API Key must be exactly 32 characters')
13
+ .describe('MCP-enabled API key from UteamUP'),
14
+
15
+ secret: z.string()
16
+ .min(64, 'Secret must be at least 64 characters')
17
+ .describe('API key secret from UteamUP'),
18
+
19
+ // Optional configuration
20
+ baseUrl: z.string()
21
+ .url('Base URL must be a valid URL')
22
+ .default('https://api.uteamup.com')
23
+ .describe('API endpoint URL'),
24
+
25
+ // Logging configuration
26
+ logLevel: z.enum(['TRACE', 'DEBUG', 'INFO', 'WARN', 'ERROR'])
27
+ .default('INFO')
28
+ .describe('Logging level'),
29
+
30
+ logFormat: z.enum(['text', 'json'])
31
+ .default('text')
32
+ .describe('Log output format'),
33
+
34
+ // Legacy debug flag (for backward compatibility)
35
+ debug: z.boolean()
36
+ .default(false)
37
+ .describe('Legacy debug mode'),
38
+
39
+ // Request configuration
40
+ requestTimeout: z.number()
41
+ .int()
42
+ .min(1000)
43
+ .max(300000) // 5 minutes max
44
+ .default(30000)
45
+ .describe('Request timeout in milliseconds'),
46
+
47
+ maxRetries: z.number()
48
+ .int()
49
+ .min(0)
50
+ .max(10)
51
+ .default(3)
52
+ .describe('Maximum retry attempts for network errors'),
53
+
54
+ // Token management
55
+ tokenCacheDir: z.string()
56
+ .optional()
57
+ .describe('Optional persistent token cache directory'),
58
+
59
+ tokenRefreshMargin: z.number()
60
+ .int()
61
+ .min(0)
62
+ .max(3600) // 1 hour max
63
+ .default(300) // 5 minutes
64
+ .describe('Token refresh margin in seconds')
65
+ });
66
+
67
+ /**
68
+ * Loads and validates configuration from environment variables
69
+ * @returns {object} Validated configuration object
70
+ * @throws {Error} If configuration is invalid
71
+ */
72
+ function loadConfig() {
73
+ // Parse environment variables
74
+ const rawConfig = {
75
+ apiKey: process.env.UTEAMUP_API_KEY,
76
+ secret: process.env.UTEAMUP_SECRET,
77
+ baseUrl: process.env.UTEAMUP_API_BASE_URL || 'https://api.uteamup.com',
78
+ logLevel: (process.env.UTEAMUP_LOG_LEVEL || 'INFO').toUpperCase(),
79
+ logFormat: (process.env.UTEAMUP_LOG_FORMAT || 'text').toLowerCase(),
80
+ debug: process.env.UTEAMUP_DEBUG === '1' || process.env.UTEAMUP_DEBUG === 'true',
81
+ requestTimeout: parseInt(process.env.UTEAMUP_REQUEST_TIMEOUT || '30000', 10),
82
+ maxRetries: parseInt(process.env.UTEAMUP_MAX_RETRIES || '3', 10),
83
+ tokenCacheDir: process.env.UTEAMUP_TOKEN_CACHE_DIR,
84
+ tokenRefreshMargin: parseInt(process.env.UTEAMUP_TOKEN_REFRESH_MARGIN || '300', 10)
85
+ };
86
+
87
+ // If debug mode is enabled, upgrade log level to DEBUG
88
+ if (rawConfig.debug && rawConfig.logLevel === 'INFO') {
89
+ rawConfig.logLevel = 'DEBUG';
90
+ }
91
+
92
+ try {
93
+ // Validate configuration
94
+ const config = configSchema.parse(rawConfig);
95
+ return config;
96
+ } catch (error) {
97
+ if (error instanceof z.ZodError) {
98
+ // Format validation errors nicely
99
+ const errors = error.errors.map(err => {
100
+ const path = err.path.join('.');
101
+ return ` - ${path}: ${err.message}`;
102
+ });
103
+
104
+ throw new Error(
105
+ 'Configuration validation failed:\n' +
106
+ errors.join('\n') +
107
+ '\n\nRequired environment variables:\n' +
108
+ ' UTEAMUP_API_KEY - Your MCP-enabled API key (32 characters)\n' +
109
+ ' UTEAMUP_SECRET - Your API key secret (64+ characters)\n\n' +
110
+ 'Optional environment variables:\n' +
111
+ ' UTEAMUP_API_BASE_URL - API endpoint (default: https://api.uteamup.com)\n' +
112
+ ' UTEAMUP_LOG_LEVEL - Log level (default: INFO)\n' +
113
+ ' UTEAMUP_LOG_FORMAT - Log format (default: text)\n' +
114
+ ' UTEAMUP_DEBUG - Enable debug mode (1 or true)\n' +
115
+ ' UTEAMUP_REQUEST_TIMEOUT - Request timeout in ms (default: 30000)\n' +
116
+ ' UTEAMUP_MAX_RETRIES - Max retry attempts (default: 3)\n' +
117
+ ' UTEAMUP_TOKEN_CACHE_DIR - Persistent token cache directory\n' +
118
+ ' UTEAMUP_TOKEN_REFRESH_MARGIN - Token refresh margin in seconds (default: 300)'
119
+ );
120
+ }
121
+ throw error;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Validates configuration without throwing (returns validation result)
127
+ * @returns {{success: boolean, config?: object, error?: string}} Validation result
128
+ */
129
+ function validateConfig() {
130
+ try {
131
+ const config = loadConfig();
132
+ return { success: true, config };
133
+ } catch (error) {
134
+ return { success: false, error: error.message };
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Prints configuration summary (with secrets redacted)
140
+ * @param {object} config - Configuration object
141
+ */
142
+ function printConfigSummary(config) {
143
+ console.error('[MCP Proxy] Configuration:');
144
+ console.error(` Base URL: ${config.baseUrl}`);
145
+ console.error(` API Key: ${config.apiKey.substring(0, 8)}...`);
146
+ console.error(` Log Level: ${config.logLevel}`);
147
+ console.error(` Log Format: ${config.logFormat}`);
148
+ console.error(` Request Timeout: ${config.requestTimeout}ms`);
149
+ console.error(` Max Retries: ${config.maxRetries}`);
150
+ console.error(` Token Refresh Margin: ${config.tokenRefreshMargin}s`);
151
+ if (config.tokenCacheDir) {
152
+ console.error(` Token Cache Dir: ${config.tokenCacheDir}`);
153
+ }
154
+ }
155
+
156
+ module.exports = {
157
+ loadConfig,
158
+ validateConfig,
159
+ printConfigSummary,
160
+ configSchema
161
+ };
package/lib/logger.js ADDED
@@ -0,0 +1,170 @@
1
+ /**
2
+ * Structured logging module using Pino
3
+ * Provides production-ready logging with levels, formatting, and redaction
4
+ */
5
+
6
+ const pino = require('pino');
7
+
8
+ // Log levels mapping
9
+ const LOG_LEVELS = {
10
+ TRACE: 10,
11
+ DEBUG: 20,
12
+ INFO: 30,
13
+ WARN: 40,
14
+ ERROR: 50
15
+ };
16
+
17
+ // Get log level from environment (default: INFO)
18
+ const logLevel = process.env.UTEAMUP_LOG_LEVEL?.toUpperCase() || 'INFO';
19
+ const logFormat = process.env.UTEAMUP_LOG_FORMAT?.toLowerCase() || 'text';
20
+
21
+ // Legacy UTEAMUP_DEBUG support (maps to DEBUG level)
22
+ const effectiveLevel = process.env.UTEAMUP_DEBUG === '1' || process.env.UTEAMUP_DEBUG === 'true'
23
+ ? 'DEBUG'
24
+ : logLevel;
25
+
26
+ // Pino configuration
27
+ const pinoOptions = {
28
+ level: (LOG_LEVELS[effectiveLevel] !== undefined ? effectiveLevel : 'INFO').toLowerCase(),
29
+ // Use stderr for all logs (stdout reserved for JSON-RPC protocol)
30
+ browser: { write: (msg) => process.stderr.write(msg) },
31
+
32
+ // Redact sensitive fields
33
+ redact: {
34
+ paths: [
35
+ 'Authorization',
36
+ 'client_secret',
37
+ 'code_verifier',
38
+ 'access_token',
39
+ 'accessToken',
40
+ 'bearer',
41
+ '*.Authorization',
42
+ '*.client_secret',
43
+ '*.code_verifier',
44
+ '*.access_token',
45
+ '*.accessToken'
46
+ ],
47
+ remove: true
48
+ },
49
+
50
+ // Production: JSON format, Development: pretty-printed
51
+ transport: logFormat === 'json' || process.env.NODE_ENV === 'production'
52
+ ? undefined
53
+ : {
54
+ target: 'pino-pretty',
55
+ options: {
56
+ colorize: true,
57
+ translateTime: 'HH:MM:ss.l',
58
+ ignore: 'pid,hostname',
59
+ messageFormat: '[MCP Proxy] {msg}'
60
+ }
61
+ }
62
+ };
63
+
64
+ // Create logger instance
65
+ const logger = pino(pinoOptions, pino.destination({ dest: 2 })); // 2 = stderr
66
+
67
+ /**
68
+ * Redacts sensitive information from objects for logging
69
+ * @param {any} data - Data to redact
70
+ * @returns {any} Redacted data
71
+ */
72
+ function redactSensitive(data) {
73
+ if (!data) return data;
74
+ if (typeof data !== 'object') return data;
75
+
76
+ const sensitiveKeys = [
77
+ 'authorization',
78
+ 'client_secret',
79
+ 'code_verifier',
80
+ 'access_token',
81
+ 'accesstoken',
82
+ 'bearer',
83
+ 'password',
84
+ 'secret'
85
+ ];
86
+
87
+ const redacted = Array.isArray(data) ? [...data] : { ...data };
88
+
89
+ for (const key in redacted) {
90
+ const lowerKey = key.toLowerCase();
91
+
92
+ if (sensitiveKeys.some(sk => lowerKey.includes(sk))) {
93
+ redacted[key] = '[REDACTED]';
94
+ } else if (typeof redacted[key] === 'object' && redacted[key] !== null) {
95
+ redacted[key] = redactSensitive(redacted[key]);
96
+ }
97
+ }
98
+
99
+ return redacted;
100
+ }
101
+
102
+ /**
103
+ * Creates a child logger with additional context
104
+ * @param {object} context - Context to add to all logs
105
+ * @returns {object} Child logger
106
+ */
107
+ function createChildLogger(context) {
108
+ return logger.child(context);
109
+ }
110
+
111
+ /**
112
+ * Trace-level logging (most verbose)
113
+ * @param {string} message - Log message
114
+ * @param {object} context - Additional context
115
+ */
116
+ function trace(message, context = {}) {
117
+ logger.trace(redactSensitive(context), message);
118
+ }
119
+
120
+ /**
121
+ * Debug-level logging
122
+ * @param {string} message - Log message
123
+ * @param {object} context - Additional context
124
+ */
125
+ function debug(message, context = {}) {
126
+ logger.debug(redactSensitive(context), message);
127
+ }
128
+
129
+ /**
130
+ * Info-level logging
131
+ * @param {string} message - Log message
132
+ * @param {object} context - Additional context
133
+ */
134
+ function info(message, context = {}) {
135
+ logger.info(redactSensitive(context), message);
136
+ }
137
+
138
+ /**
139
+ * Warning-level logging
140
+ * @param {string} message - Log message
141
+ * @param {object} context - Additional context
142
+ */
143
+ function warn(message, context = {}) {
144
+ logger.warn(redactSensitive(context), message);
145
+ }
146
+
147
+ /**
148
+ * Error-level logging
149
+ * @param {string} message - Log message
150
+ * @param {object|Error} contextOrError - Additional context or Error object
151
+ */
152
+ function error(message, contextOrError = {}) {
153
+ if (contextOrError instanceof Error) {
154
+ logger.error({ err: contextOrError }, message);
155
+ } else {
156
+ logger.error(redactSensitive(contextOrError), message);
157
+ }
158
+ }
159
+
160
+ module.exports = {
161
+ logger,
162
+ createChildLogger,
163
+ redactSensitive,
164
+ trace,
165
+ debug,
166
+ info,
167
+ warn,
168
+ error,
169
+ LOG_LEVELS
170
+ };
package/lib/metrics.js ADDED
@@ -0,0 +1,201 @@
1
+ /**
2
+ * Performance monitoring and metrics tracking
3
+ * Tracks timing, cache hits/misses, and other performance metrics
4
+ */
5
+
6
+ const { debug, info } = require('./logger');
7
+
8
+ // In-memory metrics storage
9
+ const metrics = {
10
+ oauth: {
11
+ totalAttempts: 0,
12
+ successCount: 0,
13
+ failureCount: 0,
14
+ totalDurationMs: 0,
15
+ avgDurationMs: 0
16
+ },
17
+ requests: {
18
+ totalCount: 0,
19
+ successCount: 0,
20
+ errorCount: 0,
21
+ totalDurationMs: 0,
22
+ avgDurationMs: 0
23
+ },
24
+ tokenCache: {
25
+ hits: 0,
26
+ misses: 0,
27
+ hitRate: 0
28
+ }
29
+ };
30
+
31
+ /**
32
+ * Starts a timer and returns a function to stop it
33
+ * @param {string} label - Timer label for logging
34
+ * @returns {Function} Function to stop the timer and return elapsed time
35
+ */
36
+ function startTimer(label) {
37
+ const startTime = Date.now();
38
+
39
+ return function stopTimer() {
40
+ const elapsed = Date.now() - startTime;
41
+ debug(`Timer [${label}] completed`, { durationMs: elapsed });
42
+ return elapsed;
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Records OAuth flow metrics
48
+ * @param {boolean} success - Whether OAuth succeeded
49
+ * @param {number} durationMs - Duration in milliseconds
50
+ */
51
+ function recordOAuthAttempt(success, durationMs) {
52
+ metrics.oauth.totalAttempts++;
53
+
54
+ if (success) {
55
+ metrics.oauth.successCount++;
56
+ } else {
57
+ metrics.oauth.failureCount++;
58
+ }
59
+
60
+ metrics.oauth.totalDurationMs += durationMs;
61
+ metrics.oauth.avgDurationMs = Math.round(
62
+ metrics.oauth.totalDurationMs / metrics.oauth.totalAttempts
63
+ );
64
+
65
+ info('OAuth attempt recorded', {
66
+ success,
67
+ durationMs,
68
+ avgDurationMs: metrics.oauth.avgDurationMs,
69
+ successRate: (metrics.oauth.successCount / metrics.oauth.totalAttempts * 100).toFixed(2) + '%'
70
+ });
71
+ }
72
+
73
+ /**
74
+ * Records MCP request metrics
75
+ * @param {boolean} success - Whether request succeeded
76
+ * @param {number} durationMs - Duration in milliseconds
77
+ * @param {string} method - Request method
78
+ */
79
+ function recordRequest(success, durationMs, method) {
80
+ metrics.requests.totalCount++;
81
+
82
+ if (success) {
83
+ metrics.requests.successCount++;
84
+ } else {
85
+ metrics.requests.errorCount++;
86
+ }
87
+
88
+ metrics.requests.totalDurationMs += durationMs;
89
+ metrics.requests.avgDurationMs = Math.round(
90
+ metrics.requests.totalDurationMs / metrics.requests.totalCount
91
+ );
92
+
93
+ debug('Request recorded', {
94
+ method,
95
+ success,
96
+ durationMs,
97
+ avgDurationMs: metrics.requests.avgDurationMs,
98
+ successRate: (metrics.requests.successCount / metrics.requests.totalCount * 100).toFixed(2) + '%'
99
+ });
100
+ }
101
+
102
+ /**
103
+ * Records token cache hit
104
+ */
105
+ function recordCacheHit() {
106
+ metrics.tokenCache.hits++;
107
+ updateCacheHitRate();
108
+ debug('Token cache hit', {
109
+ hits: metrics.tokenCache.hits,
110
+ hitRate: metrics.tokenCache.hitRate
111
+ });
112
+ }
113
+
114
+ /**
115
+ * Records token cache miss
116
+ */
117
+ function recordCacheMiss() {
118
+ metrics.tokenCache.misses++;
119
+ updateCacheHitRate();
120
+ debug('Token cache miss', {
121
+ misses: metrics.tokenCache.misses,
122
+ hitRate: metrics.tokenCache.hitRate
123
+ });
124
+ }
125
+
126
+ /**
127
+ * Updates token cache hit rate
128
+ */
129
+ function updateCacheHitRate() {
130
+ const total = metrics.tokenCache.hits + metrics.tokenCache.misses;
131
+ metrics.tokenCache.hitRate = total > 0
132
+ ? parseFloat((metrics.tokenCache.hits / total * 100).toFixed(2))
133
+ : 0;
134
+ }
135
+
136
+ /**
137
+ * Gets current metrics snapshot
138
+ * @returns {object} Current metrics
139
+ */
140
+ function getMetrics() {
141
+ return JSON.parse(JSON.stringify(metrics)); // Deep copy
142
+ }
143
+
144
+ /**
145
+ * Resets all metrics (useful for testing)
146
+ */
147
+ function resetMetrics() {
148
+ metrics.oauth.totalAttempts = 0;
149
+ metrics.oauth.successCount = 0;
150
+ metrics.oauth.failureCount = 0;
151
+ metrics.oauth.totalDurationMs = 0;
152
+ metrics.oauth.avgDurationMs = 0;
153
+
154
+ metrics.requests.totalCount = 0;
155
+ metrics.requests.successCount = 0;
156
+ metrics.requests.errorCount = 0;
157
+ metrics.requests.totalDurationMs = 0;
158
+ metrics.requests.avgDurationMs = 0;
159
+
160
+ metrics.tokenCache.hits = 0;
161
+ metrics.tokenCache.misses = 0;
162
+ metrics.tokenCache.hitRate = 0;
163
+ }
164
+
165
+ /**
166
+ * Prints metrics summary
167
+ */
168
+ function printMetrics() {
169
+ info('Performance Metrics Summary', {
170
+ oauth: {
171
+ attempts: metrics.oauth.totalAttempts,
172
+ successRate: metrics.oauth.totalAttempts > 0
173
+ ? (metrics.oauth.successCount / metrics.oauth.totalAttempts * 100).toFixed(2) + '%'
174
+ : 'N/A',
175
+ avgDurationMs: metrics.oauth.avgDurationMs
176
+ },
177
+ requests: {
178
+ total: metrics.requests.totalCount,
179
+ successRate: metrics.requests.totalCount > 0
180
+ ? (metrics.requests.successCount / metrics.requests.totalCount * 100).toFixed(2) + '%'
181
+ : 'N/A',
182
+ avgDurationMs: metrics.requests.avgDurationMs
183
+ },
184
+ tokenCache: {
185
+ hits: metrics.tokenCache.hits,
186
+ misses: metrics.tokenCache.misses,
187
+ hitRate: metrics.tokenCache.hitRate + '%'
188
+ }
189
+ });
190
+ }
191
+
192
+ module.exports = {
193
+ startTimer,
194
+ recordOAuthAttempt,
195
+ recordRequest,
196
+ recordCacheHit,
197
+ recordCacheMiss,
198
+ getMetrics,
199
+ resetMetrics,
200
+ printMetrics
201
+ };
package/lib/retry.js ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Retry logic with exponential backoff
3
+ * Provides resilient error recovery for transient failures
4
+ */
5
+
6
+ const { warn, error } = require('./logger');
7
+
8
+ /**
9
+ * Sleep utility
10
+ * @param {number} ms - Milliseconds to sleep
11
+ * @returns {Promise<void>}
12
+ */
13
+ function sleep(ms) {
14
+ return new Promise(resolve => setTimeout(resolve, ms));
15
+ }
16
+
17
+ /**
18
+ * Determines if an error is retryable
19
+ * @param {Error} err - Error to check
20
+ * @returns {boolean} Whether error is retryable
21
+ */
22
+ function isRetryableError(err) {
23
+ if (!err) return false;
24
+
25
+ // Network errors are retryable
26
+ const networkErrors = ['ECONNREFUSED', 'ECONNRESET', 'ETIMEDOUT', 'ENOTFOUND', 'EAI_AGAIN'];
27
+ if (networkErrors.includes(err.code)) {
28
+ return true;
29
+ }
30
+
31
+ // Timeout errors are retryable
32
+ if (err.message && err.message.toLowerCase().includes('timeout')) {
33
+ return true;
34
+ }
35
+
36
+ // HTTP 429 (rate limiting) is retryable
37
+ if (err.statusCode === 429 || err.status === 429) {
38
+ return true;
39
+ }
40
+
41
+ // HTTP 5xx errors are retryable
42
+ if (err.statusCode >= 500 && err.statusCode < 600) {
43
+ return true;
44
+ }
45
+ if (err.status >= 500 && err.status < 600) {
46
+ return true;
47
+ }
48
+
49
+ // Not retryable
50
+ return false;
51
+ }
52
+
53
+ /**
54
+ * Calculates exponential backoff delay
55
+ * @param {number} attempt - Current attempt number (1-indexed)
56
+ * @param {number} baseDelay - Base delay in ms (default: 1000)
57
+ * @param {number} maxDelay - Maximum delay in ms (default: 10000)
58
+ * @returns {number} Delay in milliseconds
59
+ */
60
+ function calculateBackoff(attempt, baseDelay = 1000, maxDelay = 10000) {
61
+ const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
62
+ // Add jitter (±20%) to prevent thundering herd
63
+ const jitter = delay * 0.2 * (Math.random() * 2 - 1);
64
+ return Math.floor(delay + jitter);
65
+ }
66
+
67
+ /**
68
+ * Retries a function with exponential backoff
69
+ * @param {Function} fn - Async function to retry
70
+ * @param {object} options - Retry options
71
+ * @param {number} options.maxRetries - Maximum retry attempts (default: 3)
72
+ * @param {number} options.baseDelay - Base delay in ms (default: 1000)
73
+ * @param {number} options.maxDelay - Maximum delay in ms (default: 10000)
74
+ * @param {Function} options.shouldRetry - Custom function to determine if error is retryable
75
+ * @param {string} options.operationName - Name of operation for logging
76
+ * @returns {Promise<any>} Result of successful function call
77
+ * @throws {Error} Last error if all retries fail
78
+ */
79
+ async function retryWithBackoff(fn, options = {}) {
80
+ const {
81
+ maxRetries = 3,
82
+ baseDelay = 1000,
83
+ maxDelay = 10000,
84
+ shouldRetry = isRetryableError,
85
+ operationName = 'operation'
86
+ } = options;
87
+
88
+ let lastError;
89
+
90
+ for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
91
+ try {
92
+ const result = await fn();
93
+ return result;
94
+ } catch (err) {
95
+ lastError = err;
96
+
97
+ // If this was the last attempt, throw the error
98
+ if (attempt > maxRetries) {
99
+ error(`${operationName} failed after ${maxRetries} retries`, { error: err.message });
100
+ throw err;
101
+ }
102
+
103
+ // Check if error is retryable
104
+ if (!shouldRetry(err)) {
105
+ error(`${operationName} failed with non-retryable error`, { error: err.message });
106
+ throw err;
107
+ }
108
+
109
+ // Calculate backoff delay
110
+ const delay = calculateBackoff(attempt, baseDelay, maxDelay);
111
+
112
+ warn(`${operationName} failed (attempt ${attempt}/${maxRetries + 1}), retrying in ${delay}ms`, {
113
+ error: err.message,
114
+ attempt,
115
+ maxRetries,
116
+ delayMs: delay
117
+ });
118
+
119
+ // Wait before retrying
120
+ await sleep(delay);
121
+ }
122
+ }
123
+
124
+ // Should never reach here, but just in case
125
+ throw lastError;
126
+ }
127
+
128
+ /**
129
+ * Retries a function with a simple fixed delay
130
+ * @param {Function} fn - Async function to retry
131
+ * @param {number} maxRetries - Maximum retry attempts
132
+ * @param {number} delayMs - Fixed delay between retries in ms
133
+ * @returns {Promise<any>} Result of successful function call
134
+ * @throws {Error} Last error if all retries fail
135
+ */
136
+ async function retryWithFixedDelay(fn, maxRetries = 3, delayMs = 1000) {
137
+ return retryWithBackoff(fn, {
138
+ maxRetries,
139
+ baseDelay: delayMs,
140
+ maxDelay: delayMs // Same as base = fixed delay
141
+ });
142
+ }
143
+
144
+ module.exports = {
145
+ sleep,
146
+ isRetryableError,
147
+ calculateBackoff,
148
+ retryWithBackoff,
149
+ retryWithFixedDelay
150
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uteamup/mcp-server",
3
- "version": "1.0.0-beta.7",
3
+ "version": "1.0.0-beta.8",
4
4
  "description": "MCP server client for UteamUP - enables Claude Desktop/Code integration with automatic OAuth 2.0 authentication",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -57,6 +57,7 @@
57
57
  },
58
58
  "files": [
59
59
  "index.js",
60
+ "lib/",
60
61
  "README.md",
61
62
  "LICENSE",
62
63
  "CHANGELOG.md"