plex-mcp 0.5.0 → 0.5.2

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/.dockerignore ADDED
@@ -0,0 +1,16 @@
1
+ node_modules
2
+ npm-debug.log
3
+ .git
4
+ .gitignore
5
+ README.md
6
+ README.SSE.md
7
+ SMITHERY.md
8
+ .env
9
+ .env.local
10
+ .env.*.local
11
+ coverage
12
+ .nyc_output
13
+ tests
14
+ *.test.js
15
+ .DS_Store
16
+ .vscode
package/Dockerfile CHANGED
@@ -1,18 +1,32 @@
1
1
  # Generated by https://smithery.ai. See: https://smithery.ai/docs/build/project-config
2
- # Auto-generated Dockerfile for Plex MCP Server
2
+ # Dockerfile for Plex MCP Server with SSE support
3
3
  FROM node:lts-alpine AS base
4
4
  WORKDIR /app
5
5
 
6
- # Install dependencies
6
+ # Install system dependencies and npm packages
7
+ RUN apk add --no-cache wget
7
8
  COPY package.json package-lock.json ./
8
9
  RUN npm ci --ignore-scripts
9
10
 
10
11
  # Copy source
11
12
  COPY . .
12
13
 
13
- # Default verify SSL
14
+ # Create non-root user
15
+ RUN addgroup -g 1001 -S nodejs
16
+ RUN adduser -S nextjs -u 1001
17
+
18
+ # Environment variables
14
19
  ENV PLEX_VERIFY_SSL=true
20
+ ENV MCP_TRANSPORT=sse
21
+ ENV PORT=3000
22
+
23
+ # Expose the SSE port
24
+ EXPOSE 3000
25
+
26
+ # Health check for SSE mode
27
+ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
28
+ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
15
29
 
16
30
  # Start the MCP server
17
- USER non-root
31
+ USER nextjs
18
32
  CMD ["node", "index.js"]
package/README.md CHANGED
@@ -40,6 +40,34 @@ Add to your Claude Desktop MCP settings:
40
40
 
41
41
  **Note:** Replace `your-plex-server:32400` with your actual Plex server address and port.
42
42
 
43
+ ## HTTP Debug Logging
44
+
45
+ For troubleshooting connection issues, you can enable comprehensive HTTP request/response logging:
46
+
47
+ ```json
48
+ {
49
+ "mcpServers": {
50
+ "plex": {
51
+ "command": "npx",
52
+ "args": ["plex-mcp"],
53
+ "env": {
54
+ "PLEX_URL": "http://your-plex-server:32400",
55
+ "MCP_HTTP_DEBUG": "true"
56
+ }
57
+ }
58
+ }
59
+ }
60
+ ```
61
+
62
+ When `MCP_HTTP_DEBUG=true`, the server will log:
63
+ - **Request Details**: Method, URL, headers, parameters, timing
64
+ - **Response Details**: Status codes, headers, response size, duration
65
+ - **Error Diagnostics**: Connection details, TLS/SSL certificate info, troubleshooting suggestions
66
+ - **Performance Metrics**: DNS lookup, TCP connection, TLS handshake timing
67
+ - **Security**: All sensitive headers (tokens, auth) are automatically redacted
68
+
69
+ All logs are structured JSON with correlation IDs for request/response tracking.
70
+
43
71
  ## What You Can Do
44
72
 
45
73
  **Search & Browse**
@@ -63,9 +91,22 @@ Add to your Claude Desktop MCP settings:
63
91
  - Check watch status and progress
64
92
  - View library statistics and listening stats
65
93
 
94
+ **Resources (Data Access)**
95
+ - Access structured data via URI: `plex://libraries`, `plex://recent`, `plex://playlists`
96
+ - Library-specific data: `plex://library/{id}`, `plex://library/{id}/recent`
97
+ - Server statistics and connection status
98
+ - JSON-formatted data for analysis and automation
99
+
100
+ **Prompts (AI Assistance)**
101
+ - **playlist_description**: Generate creative playlist descriptions
102
+ - **content_recommendation**: Get personalized content recommendations
103
+ - **smart_playlist_rules**: Create smart playlist criteria and rules
104
+ - **media_analysis**: Analyze library content and patterns
105
+ - **server_troubleshooting**: Diagnose server connection issues
106
+
66
107
  ## Status
67
108
 
68
- **✅ Working:** Search, browse, playlists, media info, library stats, watch history, collections, music discovery
109
+ **✅ Working:** Search, browse, playlists, media info, library stats, watch history, collections, music discovery, resources, prompts
69
110
 
70
111
  **❌ Disabled:** Smart playlists (filter logic broken)
71
112
 
package/TODO.md CHANGED
@@ -22,6 +22,10 @@
22
22
 
23
23
  ## Unimplemented Plex API Features
24
24
 
25
+ # Random human shit
26
+
27
+ SMithery integration shows tools but no "provided resources" or "provided prompts" - we should identify what these mean and whether they are useful in the context of the Plex MCP.
28
+
25
29
  ### Critical Missing APIs (High Priority)
26
30
 
27
31
  #### Session Management
@@ -0,0 +1,189 @@
1
+ const js = require('@eslint/js');
2
+
3
+ module.exports = [
4
+ js.configs.recommended,
5
+ {
6
+ files: ['**/*.js'],
7
+ ignores: [
8
+ 'node_modules/',
9
+ 'coverage/',
10
+ '*.min.js',
11
+ 'dist/',
12
+ 'build/',
13
+ '.git/'
14
+ ],
15
+ languageOptions: {
16
+ ecmaVersion: 2022,
17
+ sourceType: 'commonjs',
18
+ globals: {
19
+ console: 'readonly',
20
+ process: 'readonly',
21
+ Buffer: 'readonly',
22
+ __dirname: 'readonly',
23
+ __filename: 'readonly',
24
+ module: 'readonly',
25
+ require: 'readonly',
26
+ exports: 'readonly',
27
+ global: 'readonly',
28
+ setTimeout: 'readonly',
29
+ setInterval: 'readonly',
30
+ clearTimeout: 'readonly',
31
+ clearInterval: 'readonly',
32
+ URLSearchParams: 'readonly'
33
+ }
34
+ },
35
+ rules: {
36
+ // Error prevention
37
+ 'no-console': 'off', // We need console for this CLI tool
38
+ 'no-unused-vars': ['warn', { // Changed to warn instead of error
39
+ argsIgnorePattern: '^_',
40
+ varsIgnorePattern: '^_',
41
+ caughtErrorsIgnorePattern: '^_'
42
+ }],
43
+ 'no-undef': 'error',
44
+ 'no-unreachable': 'error',
45
+ 'no-constant-condition': 'error',
46
+ 'no-dupe-keys': 'error',
47
+ 'no-duplicate-case': 'error',
48
+ 'no-empty': ['error', { allowEmptyCatch: true }],
49
+ 'no-ex-assign': 'error',
50
+ 'no-extra-boolean-cast': 'error',
51
+ 'no-func-assign': 'error',
52
+ 'no-inner-declarations': 'error',
53
+ 'no-invalid-regexp': 'error',
54
+ 'no-irregular-whitespace': 'error',
55
+ 'no-obj-calls': 'error',
56
+ 'no-regex-spaces': 'error',
57
+ 'no-sparse-arrays': 'error',
58
+ 'no-unexpected-multiline': 'error',
59
+ 'use-isnan': 'error',
60
+ 'valid-typeof': 'error',
61
+
62
+ // Best practices
63
+ curly: ['error', 'all'],
64
+ 'dot-notation': 'error',
65
+ eqeqeq: ['error', 'always'],
66
+ 'no-alert': 'error',
67
+ 'no-caller': 'error',
68
+ 'no-eval': 'error',
69
+ 'no-extend-native': 'error',
70
+ 'no-extra-bind': 'error',
71
+ 'no-fallthrough': 'error',
72
+ 'no-case-declarations': 'off', // Allow declarations in case blocks
73
+ 'no-floating-decimal': 'error',
74
+ 'no-implied-eval': 'error',
75
+ 'no-lone-blocks': 'error',
76
+ 'no-loop-func': 'error',
77
+ 'no-multi-spaces': 'error',
78
+ 'no-new': 'error',
79
+ 'no-new-func': 'error',
80
+ 'no-new-wrappers': 'error',
81
+ 'no-octal': 'error',
82
+ 'no-octal-escape': 'error',
83
+ 'no-redeclare': 'error',
84
+ 'no-return-assign': 'error',
85
+ 'no-script-url': 'error',
86
+ 'no-self-assign': 'error',
87
+ 'no-self-compare': 'error',
88
+ 'no-sequences': 'error',
89
+ 'no-throw-literal': 'error',
90
+ 'no-unmodified-loop-condition': 'error',
91
+ 'no-unused-expressions': 'error',
92
+ 'no-useless-call': 'error',
93
+ 'no-useless-concat': 'error',
94
+ 'no-void': 'error',
95
+ radix: 'error',
96
+ 'wrap-iife': ['error', 'any'],
97
+ yoda: 'error',
98
+
99
+ // Variables
100
+ 'no-delete-var': 'error',
101
+ 'no-label-var': 'error',
102
+ 'no-restricted-globals': 'error',
103
+ 'no-shadow': 'error',
104
+ 'no-shadow-restricted-names': 'error',
105
+ 'no-undef-init': 'error',
106
+ 'no-use-before-define': ['error', { functions: false, classes: true, variables: true }],
107
+
108
+ // Style
109
+ 'array-bracket-spacing': ['error', 'never'],
110
+ 'block-spacing': 'error',
111
+ 'brace-style': ['warn', '1tbs', { allowSingleLine: true }], // Changed to warn
112
+ camelcase: ['error', { properties: 'never', allow: [
113
+ 'play_count_min', 'play_count_max', 'last_played_after', 'last_played_before',
114
+ 'played_in_last_days', 'never_played', 'content_rating', 'audio_format',
115
+ 'bpm_min', 'bmp_max', 'musical_key', 'dynamic_range_min', 'dynamic_range_max',
116
+ 'loudness_min', 'loudness_max', 'acoustic_ratio_min', 'acoustic_ratio_max',
117
+ 'file_size_min', 'file_size_max', 'year_min', 'year_max', 'rating_min',
118
+ 'rating_max', 'duration_min', 'duration_max', 'added_after', 'added_before',
119
+ 'library_id', 'chunk_size', 'chunk_offset', 'time_period', 'music_library_id',
120
+ 'include_recommendations', 'include_details', 'account_id', 'playlist_type',
121
+ 'playlist_id', 'item_key', 'item_keys', 'collection_id', 'pin_id'
122
+ ] }],
123
+ 'comma-dangle': ['error', 'never'],
124
+ 'comma-spacing': ['error', { before: false, after: true }],
125
+ 'comma-style': ['error', 'last'],
126
+ 'computed-property-spacing': ['error', 'never'],
127
+ 'eol-last': 'error',
128
+ 'func-call-spacing': ['error', 'never'],
129
+ indent: ['error', 2, { SwitchCase: 1 }],
130
+ 'key-spacing': ['error', { beforeColon: false, afterColon: true }],
131
+ 'keyword-spacing': ['error', { before: true, after: true }],
132
+ 'linebreak-style': ['error', 'unix'],
133
+ 'max-len': ['error', {
134
+ code: 150, // Increased for this complex codebase
135
+ tabWidth: 2,
136
+ ignoreUrls: true,
137
+ ignoreStrings: true,
138
+ ignoreTemplateLiterals: true,
139
+ ignoreRegExpLiterals: true,
140
+ ignoreComments: true
141
+ }],
142
+ 'new-cap': ['error', { newIsCap: true, capIsNew: false }],
143
+ 'new-parens': 'error',
144
+ 'no-array-constructor': 'error',
145
+ 'no-mixed-spaces-and-tabs': 'error',
146
+ 'no-multiple-empty-lines': ['error', { max: 2, maxEOF: 1 }],
147
+ 'no-new-object': 'error',
148
+ 'no-spaced-func': 'error',
149
+ 'no-trailing-spaces': 'error',
150
+ 'no-unneeded-ternary': 'error',
151
+ 'object-curly-spacing': ['error', 'always'],
152
+ 'one-var': ['error', 'never'],
153
+ 'operator-assignment': ['error', 'always'],
154
+ 'operator-linebreak': ['error', 'after'],
155
+ 'padded-blocks': ['error', 'never'],
156
+ 'quote-props': ['error', 'as-needed'],
157
+ quotes: ['error', 'single', { avoidEscape: true }],
158
+ semi: ['error', 'always'],
159
+ 'semi-spacing': ['error', { before: false, after: true }],
160
+ 'space-before-blocks': 'error',
161
+ 'space-before-function-paren': ['error', 'never'],
162
+ 'space-in-parens': ['error', 'never'],
163
+ 'space-infix-ops': 'error',
164
+ 'space-unary-ops': ['error', { words: true, nonwords: false }],
165
+ 'spaced-comment': ['error', 'always']
166
+ }
167
+ },
168
+ {
169
+ files: ['tests/**/*.js', '**/*.test.js'],
170
+ languageOptions: {
171
+ globals: {
172
+ describe: 'readonly',
173
+ it: 'readonly',
174
+ test: 'readonly',
175
+ expect: 'readonly',
176
+ beforeEach: 'readonly',
177
+ afterEach: 'readonly',
178
+ beforeAll: 'readonly',
179
+ afterAll: 'readonly',
180
+ jest: 'readonly'
181
+ }
182
+ },
183
+ rules: {
184
+ 'no-unused-expressions': 'off', // Allow expect().toBe() etc
185
+ 'max-len': ['error', { code: 999 }] // Longer lines allowed in tests
186
+ }
187
+ }
188
+ ];
189
+
package/http-logger.js ADDED
@@ -0,0 +1,387 @@
1
+ /**
2
+ * HTTP Logger - A reusable HTTP logging utility for MCP servers
3
+ *
4
+ * Features:
5
+ * - Request/response logging with correlation IDs
6
+ * - Comprehensive diagnostic information
7
+ * - Configurable debug levels
8
+ * - Header sanitization for security
9
+ * - Connection and TLS error details
10
+ * - Performance timing
11
+ * - Structured JSON output
12
+ */
13
+
14
+ class HttpLogger {
15
+ constructor(options = {}) {
16
+ this.debugLogging = options.debug ?? (process.env.MCP_HTTP_DEBUG === 'true');
17
+ this.logLevel = options.logLevel || 'debug';
18
+ this.requestCounter = 0;
19
+ this.serviceName = options.serviceName || 'mcp-server';
20
+ }
21
+
22
+ /**
23
+ * Generate unique correlation ID for request tracking
24
+ */
25
+ generateCorrelationId() {
26
+ return `req_${Date.now()}_${++this.requestCounter}`;
27
+ }
28
+
29
+ /**
30
+ * Log HTTP request details
31
+ */
32
+ logRequest(config, correlationId) {
33
+ if (!this.debugLogging) { return; }
34
+
35
+ const logData = {
36
+ type: 'HTTP_REQUEST',
37
+ service: this.serviceName,
38
+ correlationId,
39
+ timestamp: new Date().toISOString(),
40
+ request: {
41
+ method: config.method?.toUpperCase() || 'GET',
42
+ url: config.url,
43
+ baseURL: config.baseURL,
44
+ fullUrl: this.buildFullUrl(config),
45
+ headers: this.sanitizeHeaders(config.headers || {}),
46
+ params: config.params,
47
+ timeout: config.timeout,
48
+ httpsAgent: config.httpsAgent ? 'configured' : 'default',
49
+ maxRedirects: config.maxRedirects,
50
+ validateStatus: typeof config.validateStatus === 'function' ? 'custom' : 'default'
51
+ }
52
+ };
53
+
54
+ this.log('debug', JSON.stringify(logData, null, 2));
55
+ }
56
+
57
+ /**
58
+ * Log HTTP response details
59
+ */
60
+ logResponse(response, correlationId, startTime) {
61
+ if (!this.debugLogging) { return; }
62
+
63
+ const duration = Date.now() - startTime;
64
+ const logData = {
65
+ type: 'HTTP_RESPONSE',
66
+ service: this.serviceName,
67
+ correlationId,
68
+ timestamp: new Date().toISOString(),
69
+ response: {
70
+ status: response.status,
71
+ statusText: response.statusText,
72
+ headers: this.sanitizeHeaders(response.headers || {}),
73
+ duration: `${duration}ms`,
74
+ dataSize: this.getDataSize(response.data),
75
+ url: response.config?.url,
76
+ method: response.config?.method?.toUpperCase(),
77
+ redirected: response.request?._redirectCount > 0,
78
+ redirectCount: response.request?._redirectCount || 0
79
+ },
80
+ performance: {
81
+ totalTime: duration,
82
+ dnsLookup: this.extractTimingInfo(response, 'lookup'),
83
+ tcpConnection: this.extractTimingInfo(response, 'connect'),
84
+ tlsHandshake: this.extractTimingInfo(response, 'secureConnect'),
85
+ serverProcessing: this.extractTimingInfo(response, 'response')
86
+ }
87
+ };
88
+
89
+ this.log('debug', JSON.stringify(logData, null, 2));
90
+ }
91
+
92
+ /**
93
+ * Log HTTP errors with comprehensive diagnostic information
94
+ */
95
+ logError(error, correlationId, startTime) {
96
+ if (!this.debugLogging) { return; }
97
+
98
+ const duration = startTime ? Date.now() - startTime : null;
99
+ const logData = {
100
+ type: 'HTTP_ERROR',
101
+ service: this.serviceName,
102
+ correlationId,
103
+ timestamp: new Date().toISOString(),
104
+ error: {
105
+ message: error.message,
106
+ code: error.code,
107
+ errno: error.errno,
108
+ syscall: error.syscall,
109
+ status: error.response?.status,
110
+ statusText: error.response?.statusText,
111
+ headers: error.response?.headers ? this.sanitizeHeaders(error.response.headers) : undefined,
112
+ data: error.response?.data ? this.truncateData(error.response.data) : undefined
113
+ },
114
+ request: {
115
+ url: error.config?.url,
116
+ method: error.config?.method?.toUpperCase(),
117
+ timeout: error.config?.timeout,
118
+ duration: duration ? `${duration}ms` : 'unknown'
119
+ },
120
+ connectionInfo: this.getConnectionInfo(error),
121
+ troubleshooting: this.getTroubleshootingInfo(error)
122
+ };
123
+
124
+ this.log('error', JSON.stringify(logData, null, 2));
125
+ }
126
+
127
+ /**
128
+ * Sanitize headers to remove sensitive information
129
+ */
130
+ sanitizeHeaders(headers) {
131
+ const sanitized = { ...headers };
132
+ const sensitivePatterns = [
133
+ 'token', 'authorization', 'cookie', 'auth', 'key', 'secret', 'password'
134
+ ];
135
+
136
+ Object.keys(sanitized).forEach(key => {
137
+ const lowerKey = key.toLowerCase();
138
+ if (sensitivePatterns.some(pattern => lowerKey.includes(pattern))) {
139
+ sanitized[key] = '[REDACTED]';
140
+ }
141
+ });
142
+
143
+ return sanitized;
144
+ }
145
+
146
+ /**
147
+ * Get human-readable data size
148
+ */
149
+ getDataSize(data) {
150
+ if (!data) { return '0 bytes'; }
151
+
152
+ try {
153
+ const size = typeof data === 'string' ? data.length : JSON.stringify(data).length;
154
+ if (size < 1024) { return `${size} bytes`; }
155
+ if (size < 1024 * 1024) { return `${(size / 1024).toFixed(2)} KB`; }
156
+ return `${(size / (1024 * 1024)).toFixed(2)} MB`;
157
+ } catch {
158
+ return 'unknown';
159
+ }
160
+ }
161
+
162
+ /**
163
+ * Extract connection and network diagnostic information
164
+ */
165
+ getConnectionInfo(error) {
166
+ const info = {};
167
+
168
+ if (error.code) { info.errorCode = error.code; }
169
+ if (error.address) { info.address = error.address; }
170
+ if (error.port) { info.port = error.port; }
171
+ if (error.syscall) { info.syscall = error.syscall; }
172
+ if (error.errno) { info.errno = error.errno; }
173
+
174
+ // DNS resolution errors
175
+ if (error.code === 'ENOTFOUND') { info.dnsResolution = 'failed'; }
176
+ if (error.code === 'EAI_AGAIN') { info.dnsResolution = 'temporary_failure'; }
177
+
178
+ // Connection errors
179
+ if (error.code === 'ECONNREFUSED') { info.connection = 'refused'; }
180
+ if (error.code === 'ECONNRESET') { info.connection = 'reset'; }
181
+ if (error.code === 'ETIMEDOUT') { info.connection = 'timeout'; }
182
+ if (error.code === 'ECONNABORTED') { info.connection = 'aborted'; }
183
+
184
+ // TLS/SSL certificate information
185
+ if (error.code === 'CERT_HAS_EXPIRED') { info.tlsError = 'certificate_expired'; }
186
+ if (error.code === 'SELF_SIGNED_CERT_IN_CHAIN') { info.tlsError = 'self_signed_certificate'; }
187
+ if (error.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') { info.tlsError = 'certificate_verification_failed'; }
188
+ if (error.code === 'DEPTH_ZERO_SELF_SIGNED_CERT') { info.tlsError = 'self_signed_root_certificate'; }
189
+
190
+ return Object.keys(info).length > 0 ? info : undefined;
191
+ }
192
+
193
+ /**
194
+ * Get troubleshooting suggestions based on error type
195
+ */
196
+ getTroubleshootingInfo(error) {
197
+ const suggestions = [];
198
+
199
+ switch (error.code) {
200
+ case 'ENOTFOUND':
201
+ suggestions.push('Check if the hostname is correct');
202
+ suggestions.push('Verify DNS resolution is working');
203
+ break;
204
+ case 'ECONNREFUSED':
205
+ suggestions.push('Check if the server is running');
206
+ suggestions.push('Verify the port number is correct');
207
+ suggestions.push('Check firewall rules');
208
+ break;
209
+ case 'ETIMEDOUT':
210
+ suggestions.push('Check network connectivity');
211
+ suggestions.push('Consider increasing timeout value');
212
+ suggestions.push('Verify server is responding');
213
+ break;
214
+ case 'CERT_HAS_EXPIRED':
215
+ suggestions.push('Update server certificate');
216
+ suggestions.push('Consider using httpsAgent with rejectUnauthorized: false for testing');
217
+ break;
218
+ case 'SELF_SIGNED_CERT_IN_CHAIN':
219
+ suggestions.push('Add certificate to trusted store');
220
+ suggestions.push('Use httpsAgent with custom CA for self-signed certificates');
221
+ break;
222
+ }
223
+
224
+ if (error.response?.status === 401) {
225
+ suggestions.push('Check authentication credentials');
226
+ suggestions.push('Verify API token is valid');
227
+ }
228
+
229
+ if (error.response?.status === 403) {
230
+ suggestions.push('Check user permissions');
231
+ suggestions.push('Verify API access is enabled');
232
+ }
233
+
234
+ if (error.response?.status >= 500) {
235
+ suggestions.push('Server error - check server logs');
236
+ suggestions.push('Consider retry with exponential backoff');
237
+ }
238
+
239
+ return suggestions.length > 0 ? suggestions : undefined;
240
+ }
241
+
242
+ /**
243
+ * Build full URL from axios config
244
+ */
245
+ buildFullUrl(config) {
246
+ try {
247
+ const baseURL = config.baseURL || '';
248
+ const url = config.url || '';
249
+ const fullUrl = baseURL + url;
250
+
251
+ if (config.params) {
252
+ const params = new URLSearchParams(config.params);
253
+ return `${fullUrl}?${params.toString()}`;
254
+ }
255
+
256
+ return fullUrl;
257
+ } catch {
258
+ return config.url || 'unknown';
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Extract timing information from response
264
+ */
265
+ extractTimingInfo(response, phase) {
266
+ try {
267
+ return response.request?.connection?.[`${phase}Time`] || 'unavailable';
268
+ } catch {
269
+ return 'unavailable';
270
+ }
271
+ }
272
+
273
+ /**
274
+ * Truncate large response data for logging
275
+ */
276
+ truncateData(data, maxLength = 1000) {
277
+ try {
278
+ const str = typeof data === 'string' ? data : JSON.stringify(data);
279
+ return str.length > maxLength ? str.substring(0, maxLength) + '...[truncated]' : str;
280
+ } catch {
281
+ return '[unable to serialize]';
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Generic log method that can be extended for different logging backends
287
+ */
288
+ log(level, message) {
289
+ switch (level) {
290
+ case 'debug':
291
+ console.debug(message);
292
+ break;
293
+ case 'info':
294
+ console.info(message);
295
+ break;
296
+ case 'warn':
297
+ console.warn(message);
298
+ break;
299
+ case 'error':
300
+ console.error(message);
301
+ break;
302
+ default:
303
+ console.log(message);
304
+ }
305
+ }
306
+
307
+ /**
308
+ * Create axios instance with HTTP logging interceptors
309
+ */
310
+ createAxiosInstance(baseConfig = {}) {
311
+ const axios = require('axios');
312
+ const instance = axios.create(baseConfig);
313
+
314
+ // Request interceptor
315
+ instance.interceptors.request.use(
316
+ (config) => {
317
+ const correlationId = this.generateCorrelationId();
318
+ const startTime = Date.now();
319
+
320
+ config.metadata = { correlationId, startTime };
321
+ this.logRequest(config, correlationId);
322
+
323
+ return config;
324
+ },
325
+ (error) => {
326
+ this.log('error', `Request interceptor error: ${error.message}`);
327
+ return Promise.reject(error);
328
+ }
329
+ );
330
+
331
+ // Response interceptor
332
+ instance.interceptors.response.use(
333
+ (response) => {
334
+ const { correlationId, startTime } = response.config.metadata || {};
335
+ this.logResponse(response, correlationId, startTime);
336
+ return response;
337
+ },
338
+ (error) => {
339
+ const { correlationId, startTime } = error.config?.metadata || {};
340
+ this.logError(error, correlationId, startTime);
341
+ return Promise.reject(error);
342
+ }
343
+ );
344
+
345
+ return instance;
346
+ }
347
+
348
+ /**
349
+ * Wrap existing axios instance with logging
350
+ */
351
+ wrapAxiosInstance(axiosInstance) {
352
+ // Request interceptor
353
+ axiosInstance.interceptors.request.use(
354
+ (config) => {
355
+ const correlationId = this.generateCorrelationId();
356
+ const startTime = Date.now();
357
+
358
+ config.metadata = { correlationId, startTime };
359
+ this.logRequest(config, correlationId);
360
+
361
+ return config;
362
+ },
363
+ (error) => {
364
+ this.log('error', `Request interceptor error: ${error.message}`);
365
+ return Promise.reject(error);
366
+ }
367
+ );
368
+
369
+ // Response interceptor
370
+ axiosInstance.interceptors.response.use(
371
+ (response) => {
372
+ const { correlationId, startTime } = response.config.metadata || {};
373
+ this.logResponse(response, correlationId, startTime);
374
+ return response;
375
+ },
376
+ (error) => {
377
+ const { correlationId, startTime } = error.config?.metadata || {};
378
+ this.logError(error, correlationId, startTime);
379
+ return Promise.reject(error);
380
+ }
381
+ );
382
+
383
+ return axiosInstance;
384
+ }
385
+ }
386
+
387
+ module.exports = { HttpLogger };