@alanse/clickup-multi-mcp-server 1.0.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.
Files changed (56) hide show
  1. package/Dockerfile +38 -0
  2. package/LICENSE +21 -0
  3. package/README.md +470 -0
  4. package/build/config.js +237 -0
  5. package/build/index.js +87 -0
  6. package/build/logger.js +163 -0
  7. package/build/middleware/security.js +231 -0
  8. package/build/server.js +288 -0
  9. package/build/services/clickup/base.js +432 -0
  10. package/build/services/clickup/bulk.js +180 -0
  11. package/build/services/clickup/document.js +159 -0
  12. package/build/services/clickup/folder.js +136 -0
  13. package/build/services/clickup/index.js +76 -0
  14. package/build/services/clickup/list.js +191 -0
  15. package/build/services/clickup/tag.js +239 -0
  16. package/build/services/clickup/task/index.js +32 -0
  17. package/build/services/clickup/task/task-attachments.js +105 -0
  18. package/build/services/clickup/task/task-comments.js +114 -0
  19. package/build/services/clickup/task/task-core.js +604 -0
  20. package/build/services/clickup/task/task-custom-fields.js +107 -0
  21. package/build/services/clickup/task/task-search.js +986 -0
  22. package/build/services/clickup/task/task-service.js +104 -0
  23. package/build/services/clickup/task/task-tags.js +113 -0
  24. package/build/services/clickup/time.js +244 -0
  25. package/build/services/clickup/types.js +33 -0
  26. package/build/services/clickup/workspace.js +397 -0
  27. package/build/services/shared.js +61 -0
  28. package/build/sse_server.js +277 -0
  29. package/build/tools/documents.js +489 -0
  30. package/build/tools/folder.js +331 -0
  31. package/build/tools/index.js +16 -0
  32. package/build/tools/list.js +428 -0
  33. package/build/tools/member.js +106 -0
  34. package/build/tools/tag.js +833 -0
  35. package/build/tools/task/attachments.js +357 -0
  36. package/build/tools/task/attachments.types.js +9 -0
  37. package/build/tools/task/bulk-operations.js +338 -0
  38. package/build/tools/task/handlers.js +919 -0
  39. package/build/tools/task/index.js +30 -0
  40. package/build/tools/task/main.js +233 -0
  41. package/build/tools/task/single-operations.js +469 -0
  42. package/build/tools/task/time-tracking.js +575 -0
  43. package/build/tools/task/utilities.js +310 -0
  44. package/build/tools/task/workspace-operations.js +258 -0
  45. package/build/tools/tool-enhancer.js +37 -0
  46. package/build/tools/utils.js +12 -0
  47. package/build/tools/workspace-helper.js +44 -0
  48. package/build/tools/workspace.js +73 -0
  49. package/build/utils/color-processor.js +183 -0
  50. package/build/utils/concurrency-utils.js +248 -0
  51. package/build/utils/date-utils.js +542 -0
  52. package/build/utils/resolver-utils.js +135 -0
  53. package/build/utils/sponsor-service.js +93 -0
  54. package/build/utils/token-utils.js +49 -0
  55. package/package.json +77 -0
  56. package/smithery.yaml +23 -0
@@ -0,0 +1,237 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Configuration handling for ClickUp API credentials and application settings
6
+ *
7
+ * Multiple workspace support:
8
+ * - CLICKUP_WORKSPACES: JSON string with workspace configurations (takes precedence)
9
+ * - CLICKUP_API_KEY and CLICKUP_TEAM_ID: Legacy single workspace mode (backwards compatible)
10
+ *
11
+ * The document support is optional and can be passed via command line arguments.
12
+ * The default value is 'false' (string), which means document support will be disabled if
13
+ * no parameter is passed. Pass it as 'true' (string) to enable it.
14
+ *
15
+ * Tool filtering options:
16
+ * - ENABLED_TOOLS: Comma-separated list of tools to enable (takes precedence over DISABLED_TOOLS)
17
+ * - DISABLED_TOOLS: Comma-separated list of tools to disable (ignored if ENABLED_TOOLS is specified)
18
+ *
19
+ * Server transport options:
20
+ * - ENABLE_SSE: Enable Server-Sent Events transport (default: false)
21
+ * - SSE_PORT: Port for SSE server (default: 3000)
22
+ * - ENABLE_STDIO: Enable STDIO transport (default: true)
23
+ */
24
+ // Parse any command line environment arguments
25
+ const args = process.argv.slice(2);
26
+ const envArgs = {};
27
+ for (let i = 0; i < args.length; i++) {
28
+ if (args[i] === '--env' && i + 1 < args.length) {
29
+ const [key, value] = args[i + 1].split('=');
30
+ if (key === 'CLICKUP_API_KEY')
31
+ envArgs.clickupApiKey = value;
32
+ if (key === 'CLICKUP_TEAM_ID')
33
+ envArgs.clickupTeamId = value;
34
+ if (key === 'CLICKUP_WORKSPACES')
35
+ envArgs.clickupWorkspaces = value;
36
+ if (key === 'DOCUMENT_SUPPORT')
37
+ envArgs.documentSupport = value;
38
+ if (key === 'LOG_LEVEL')
39
+ envArgs.logLevel = value;
40
+ if (key === 'DISABLED_TOOLS')
41
+ envArgs.disabledTools = value;
42
+ if (key === 'ENABLED_TOOLS')
43
+ envArgs.enabledTools = value;
44
+ if (key === 'ENABLE_SSE')
45
+ envArgs.enableSSE = value;
46
+ if (key === 'SSE_PORT')
47
+ envArgs.ssePort = value;
48
+ if (key === 'ENABLE_STDIO')
49
+ envArgs.enableStdio = value;
50
+ if (key === 'PORT')
51
+ envArgs.port = value;
52
+ i++;
53
+ }
54
+ }
55
+ // Log levels enum
56
+ export var LogLevel;
57
+ (function (LogLevel) {
58
+ LogLevel[LogLevel["TRACE"] = 0] = "TRACE";
59
+ LogLevel[LogLevel["DEBUG"] = 1] = "DEBUG";
60
+ LogLevel[LogLevel["INFO"] = 2] = "INFO";
61
+ LogLevel[LogLevel["WARN"] = 3] = "WARN";
62
+ LogLevel[LogLevel["ERROR"] = 4] = "ERROR";
63
+ })(LogLevel || (LogLevel = {}));
64
+ // Parse LOG_LEVEL string to LogLevel enum
65
+ const parseLogLevel = (levelStr) => {
66
+ if (!levelStr)
67
+ return LogLevel.ERROR; // Default to ERROR if not specified
68
+ switch (levelStr.toUpperCase()) {
69
+ case 'TRACE': return LogLevel.TRACE;
70
+ case 'DEBUG': return LogLevel.DEBUG;
71
+ case 'INFO': return LogLevel.INFO;
72
+ case 'WARN': return LogLevel.WARN;
73
+ case 'ERROR': return LogLevel.ERROR;
74
+ default:
75
+ // Don't use console.error as it interferes with JSON-RPC communication
76
+ return LogLevel.ERROR;
77
+ }
78
+ };
79
+ // Parse boolean string
80
+ const parseBoolean = (value, defaultValue) => {
81
+ if (value === undefined)
82
+ return defaultValue;
83
+ return value.toLowerCase() === 'true';
84
+ };
85
+ // Parse integer string
86
+ const parseInteger = (value, defaultValue) => {
87
+ if (value === undefined)
88
+ return defaultValue;
89
+ const parsed = parseInt(value, 10);
90
+ return isNaN(parsed) ? defaultValue : parsed;
91
+ };
92
+ // Parse comma-separated origins list
93
+ const parseOrigins = (value, defaultValue) => {
94
+ if (!value)
95
+ return defaultValue;
96
+ return value.split(',').map(origin => origin.trim()).filter(origin => origin !== '');
97
+ };
98
+ // Parse workspace configuration
99
+ const parseWorkspaces = () => {
100
+ const workspacesJson = envArgs.clickupWorkspaces || process.env.CLICKUP_WORKSPACES;
101
+ if (workspacesJson) {
102
+ try {
103
+ const parsed = JSON.parse(workspacesJson);
104
+ // Validate the structure
105
+ if (!parsed.default || !parsed.workspaces || typeof parsed.workspaces !== 'object') {
106
+ throw new Error('Invalid CLICKUP_WORKSPACES format: must have "default" and "workspaces" properties');
107
+ }
108
+ // Validate each workspace has required fields
109
+ for (const [key, workspace] of Object.entries(parsed.workspaces)) {
110
+ const ws = workspace;
111
+ if (!ws.token || !ws.teamId) {
112
+ throw new Error(`Invalid workspace "${key}": must have "token" and "teamId" properties`);
113
+ }
114
+ }
115
+ return parsed;
116
+ }
117
+ catch (error) {
118
+ throw new Error(`Failed to parse CLICKUP_WORKSPACES: ${error.message}`);
119
+ }
120
+ }
121
+ return undefined;
122
+ };
123
+ // Load configuration from command line args or environment variables
124
+ const configuration = {
125
+ clickupApiKey: envArgs.clickupApiKey || process.env.CLICKUP_API_KEY || '',
126
+ clickupTeamId: envArgs.clickupTeamId || process.env.CLICKUP_TEAM_ID || '',
127
+ clickupWorkspaces: parseWorkspaces(),
128
+ enableSponsorMessage: process.env.ENABLE_SPONSOR_MESSAGE !== 'false',
129
+ documentSupport: envArgs.documentSupport || process.env.DOCUMENT_SUPPORT || process.env.DOCUMENT_MODULE || process.env.DOCUMENT_MODEL || 'false',
130
+ logLevel: parseLogLevel(envArgs.logLevel || process.env.LOG_LEVEL),
131
+ disabledTools: ((envArgs.disabledTools || process.env.DISABLED_TOOLS || process.env.DISABLED_COMMANDS)?.split(',').map(cmd => cmd.trim()).filter(cmd => cmd !== '') || []),
132
+ enabledTools: ((envArgs.enabledTools || process.env.ENABLED_TOOLS)?.split(',').map(cmd => cmd.trim()).filter(cmd => cmd !== '') || []),
133
+ enableSSE: parseBoolean(envArgs.enableSSE || process.env.ENABLE_SSE, false),
134
+ ssePort: parseInteger(envArgs.ssePort || process.env.SSE_PORT, 3000),
135
+ enableStdio: parseBoolean(envArgs.enableStdio || process.env.ENABLE_STDIO, true),
136
+ port: envArgs.port || process.env.PORT || '3231',
137
+ // Security configuration (opt-in for backwards compatibility)
138
+ enableSecurityFeatures: parseBoolean(process.env.ENABLE_SECURITY_FEATURES, false),
139
+ enableOriginValidation: parseBoolean(process.env.ENABLE_ORIGIN_VALIDATION, false),
140
+ enableRateLimit: parseBoolean(process.env.ENABLE_RATE_LIMIT, false),
141
+ enableCors: parseBoolean(process.env.ENABLE_CORS, false),
142
+ allowedOrigins: parseOrigins(process.env.ALLOWED_ORIGINS, [
143
+ 'http://127.0.0.1:3231',
144
+ 'http://localhost:3231',
145
+ 'http://127.0.0.1:3000',
146
+ 'http://localhost:3000',
147
+ 'https://127.0.0.1:3443',
148
+ 'https://localhost:3443',
149
+ 'https://127.0.0.1:3231',
150
+ 'https://localhost:3231'
151
+ ]),
152
+ rateLimitMax: parseInteger(process.env.RATE_LIMIT_MAX, 100),
153
+ rateLimitWindowMs: parseInteger(process.env.RATE_LIMIT_WINDOW_MS, 60000),
154
+ maxRequestSize: process.env.MAX_REQUEST_SIZE || '10mb',
155
+ // HTTPS configuration
156
+ enableHttps: parseBoolean(process.env.ENABLE_HTTPS, false),
157
+ httpsPort: process.env.HTTPS_PORT || '3443',
158
+ sslKeyPath: process.env.SSL_KEY_PATH,
159
+ sslCertPath: process.env.SSL_CERT_PATH,
160
+ sslCaPath: process.env.SSL_CA_PATH,
161
+ };
162
+ // Don't log to console as it interferes with JSON-RPC communication
163
+ // Validate configuration
164
+ // If CLICKUP_WORKSPACES is provided, use it (multi-workspace mode)
165
+ // Otherwise, require CLICKUP_API_KEY and CLICKUP_TEAM_ID (single workspace mode)
166
+ if (configuration.clickupWorkspaces) {
167
+ // Multi-workspace mode - validate workspace configuration
168
+ const defaultWorkspace = configuration.clickupWorkspaces.default;
169
+ if (!configuration.clickupWorkspaces.workspaces[defaultWorkspace]) {
170
+ throw new Error(`Default workspace "${defaultWorkspace}" not found in workspaces configuration`);
171
+ }
172
+ }
173
+ else {
174
+ // Single workspace mode (backwards compatible) - validate legacy variables
175
+ const requiredVars = ['clickupApiKey', 'clickupTeamId'];
176
+ const missingEnvVars = requiredVars
177
+ .filter(key => !configuration[key])
178
+ .map(key => key);
179
+ if (missingEnvVars.length > 0) {
180
+ throw new Error(`Missing required environment variables: ${missingEnvVars.join(', ')}. ` +
181
+ `Either provide CLICKUP_API_KEY and CLICKUP_TEAM_ID, or CLICKUP_WORKSPACES for multi-workspace support.`);
182
+ }
183
+ }
184
+ /**
185
+ * Get workspace configuration by workspace identifier
186
+ * @param workspaceId - Workspace identifier (optional, uses default if not specified)
187
+ * @returns Workspace configuration with token and teamId
188
+ */
189
+ export function getWorkspaceConfig(workspaceId) {
190
+ if (configuration.clickupWorkspaces) {
191
+ // Multi-workspace mode
192
+ const wsId = workspaceId || configuration.clickupWorkspaces.default;
193
+ const workspace = configuration.clickupWorkspaces.workspaces[wsId];
194
+ if (!workspace) {
195
+ const available = Object.keys(configuration.clickupWorkspaces.workspaces).join(', ');
196
+ throw new Error(`Workspace "${wsId}" not found. Available workspaces: ${available}`);
197
+ }
198
+ return workspace;
199
+ }
200
+ else {
201
+ // Single workspace mode (backwards compatible)
202
+ if (workspaceId && workspaceId !== 'default') {
203
+ throw new Error(`Multiple workspaces not configured. Only default workspace is available. ` +
204
+ `Set CLICKUP_WORKSPACES environment variable to enable multi-workspace support.`);
205
+ }
206
+ return {
207
+ token: configuration.clickupApiKey,
208
+ teamId: configuration.clickupTeamId,
209
+ description: 'Default workspace'
210
+ };
211
+ }
212
+ }
213
+ /**
214
+ * Get list of available workspace identifiers
215
+ * @returns Array of workspace identifiers
216
+ */
217
+ export function getAvailableWorkspaces() {
218
+ if (configuration.clickupWorkspaces) {
219
+ return Object.keys(configuration.clickupWorkspaces.workspaces);
220
+ }
221
+ else {
222
+ return ['default'];
223
+ }
224
+ }
225
+ /**
226
+ * Get the default workspace identifier
227
+ * @returns Default workspace identifier
228
+ */
229
+ export function getDefaultWorkspace() {
230
+ if (configuration.clickupWorkspaces) {
231
+ return configuration.clickupWorkspaces.default;
232
+ }
233
+ else {
234
+ return 'default';
235
+ }
236
+ }
237
+ export default configuration;
package/build/index.js ADDED
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
4
+ * SPDX-License-Identifier: MIT
5
+ *
6
+ * ClickUp MCP Server
7
+ *
8
+ * This custom server implements the Model Context Protocol (MCP) specification to enable
9
+ * AI applications to interact with ClickUp workspaces. It provides a standardized
10
+ * interface for managing tasks, lists, folders and other ClickUp entities using Natural Language.
11
+ *
12
+ * Key Features:
13
+ * - Complete task management (CRUD operations, moving, duplicating)
14
+ * - Workspace organization (spaces, folders, lists)
15
+ * - Bulk operations with concurrent processing
16
+ * - Natural language date parsing
17
+ * - File attachments support
18
+ * - Name-based entity resolution
19
+ * - Markdown formatting
20
+ * - Built-in rate limiting
21
+ * - Multiple transport options (STDIO, SSE, HTTP Streamable)
22
+ *
23
+ * For full documentation and usage examples, please refer to the README.md file.
24
+ */
25
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
26
+ import { configureServer, server } from './server.js';
27
+ import { info, error } from './logger.js';
28
+ import config from './config.js';
29
+ import { dirname } from 'path';
30
+ import { fileURLToPath } from 'url';
31
+ import { startSSEServer } from './sse_server.js';
32
+ // Get directory name for module paths
33
+ const __dirname = dirname(fileURLToPath(import.meta.url));
34
+ // Handle uncaught exceptions
35
+ process.on('uncaughtException', (err) => {
36
+ error("Uncaught Exception", { message: err.message, stack: err.stack });
37
+ process.exit(1);
38
+ });
39
+ // Handle unhandled promise rejections
40
+ process.on('unhandledRejection', (reason, promise) => {
41
+ error("Unhandled Rejection", { reason });
42
+ process.exit(1);
43
+ });
44
+ async function startStdioServer() {
45
+ info('Starting ClickUp MCP Server...');
46
+ // Log essential information about the environment
47
+ info('Server environment', {
48
+ pid: process.pid,
49
+ node: process.version,
50
+ os: process.platform,
51
+ arch: process.arch,
52
+ });
53
+ // Configure the server with all handlers
54
+ info('Configuring server request handlers');
55
+ await configureServer();
56
+ // Connect using stdio transport
57
+ info('Connecting to MCP stdio transport');
58
+ const transport = new StdioServerTransport();
59
+ await server.connect(transport);
60
+ info('Server startup complete - ready to handle requests');
61
+ }
62
+ /**
63
+ * Application entry point that configures and starts the MCP server.
64
+ */
65
+ async function main() {
66
+ try {
67
+ if (config.enableSSE) {
68
+ // Start the new SSE server with HTTP Streamable support
69
+ startSSEServer();
70
+ }
71
+ else {
72
+ // Start the traditional STDIO server
73
+ await startStdioServer();
74
+ }
75
+ }
76
+ catch (err) {
77
+ error('Error during server startup', {
78
+ message: err.message,
79
+ stack: err.stack,
80
+ });
81
+ process.exit(1);
82
+ }
83
+ }
84
+ main().catch((err) => {
85
+ error("Unhandled server error", { message: err.message, stack: err.stack });
86
+ process.exit(1);
87
+ });
@@ -0,0 +1,163 @@
1
+ /**
2
+ * SPDX-FileCopyrightText: © 2025 Talib Kareem <taazkareem@icloud.com>
3
+ * SPDX-License-Identifier: MIT
4
+ *
5
+ * Logger module for MCP Server
6
+ *
7
+ * This module provides logging functionality for the server,
8
+ * writing logs to only the log file to avoid interfering with JSON-RPC.
9
+ */
10
+ import { createWriteStream } from 'fs';
11
+ import { join, dirname } from 'path';
12
+ import { fileURLToPath } from 'url';
13
+ import config, { LogLevel } from './config.js';
14
+ // Get the directory name of the current module
15
+ const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ // Current process ID for logging
17
+ const pid = process.pid;
18
+ // Create a write stream for logging - use a fixed filename in the build directory
19
+ const logFileName = 'server.log';
20
+ const logStream = createWriteStream(join(__dirname, logFileName), { flags: 'w' });
21
+ // Write init message to log file only
22
+ logStream.write(`Logging initialized to ${join(__dirname, logFileName)}\n`);
23
+ // Use the configured log level from config.ts
24
+ const configuredLevel = config.logLevel;
25
+ // Re-export LogLevel enum
26
+ export { LogLevel };
27
+ /**
28
+ * Check if a log level is enabled based on the configured level
29
+ * @param level The log level to check
30
+ * @returns True if the level should be logged
31
+ */
32
+ function isLevelEnabled(level) {
33
+ return level >= configuredLevel;
34
+ }
35
+ /**
36
+ * Log function that writes only to file to avoid interfering with JSON-RPC
37
+ * @param level Log level (trace, debug, info, warn, error)
38
+ * @param message Message to log
39
+ * @param data Optional data to include in log
40
+ */
41
+ function log(level, message, data) {
42
+ const levelEnum = level === 'trace' ? LogLevel.TRACE
43
+ : level === 'debug' ? LogLevel.DEBUG
44
+ : level === 'info' ? LogLevel.INFO
45
+ : level === 'warn' ? LogLevel.WARN
46
+ : LogLevel.ERROR;
47
+ // Skip if level is below configured level
48
+ if (!isLevelEnabled(levelEnum)) {
49
+ return;
50
+ }
51
+ const timestamp = new Date().toISOString();
52
+ // Format the log message differently based on the level and data
53
+ let logMessage = `[${timestamp}] [PID:${pid}] ${level.toUpperCase()}: ${message}`;
54
+ // Format data differently based on content and log level
55
+ if (data) {
56
+ // For debugging and trace levels, try to make the data more readable
57
+ if (level === 'debug' || level === 'trace') {
58
+ // If data is a simple object with few properties, format it inline
59
+ if (typeof data === 'object' && data !== null && !Array.isArray(data) &&
60
+ Object.keys(data).length <= 4 && Object.keys(data).every(k => typeof data[k] !== 'object' || data[k] === null)) {
61
+ const dataStr = Object.entries(data)
62
+ .map(([k, v]) => `${k}=${v === undefined ? 'undefined' :
63
+ (v === null ? 'null' :
64
+ (typeof v === 'string' ? `"${v}"` : v))}`)
65
+ .join(' ');
66
+ logMessage += ` (${dataStr})`;
67
+ }
68
+ else {
69
+ // For more complex data, keep the JSON format but on new lines
70
+ logMessage += '\n' + JSON.stringify(data, null, 2);
71
+ }
72
+ }
73
+ else {
74
+ // For other levels, keep the original JSON format
75
+ logMessage += '\n' + JSON.stringify(data, null, 2);
76
+ }
77
+ }
78
+ // Write to file only, not to stderr which would interfere with JSON-RPC
79
+ logStream.write(logMessage + '\n');
80
+ }
81
+ /**
82
+ * Shorthand for info level logs
83
+ * @param message Message to log
84
+ * @param data Optional data to include in log
85
+ */
86
+ export function info(message, data) {
87
+ log('info', message, data);
88
+ }
89
+ /**
90
+ * Shorthand for error level logs
91
+ * @param message Message to log
92
+ * @param data Optional data to include in log
93
+ */
94
+ export function error(message, data) {
95
+ log('error', message, data);
96
+ }
97
+ /**
98
+ * Logger class for creating context-specific loggers
99
+ */
100
+ export class Logger {
101
+ /**
102
+ * Create a new logger with context
103
+ * @param context The context to prepend to log messages
104
+ */
105
+ constructor(context) {
106
+ this.context = context;
107
+ }
108
+ /**
109
+ * Check if a log level is enabled for this logger
110
+ * @param level The level to check
111
+ * @returns True if logging at this level is enabled
112
+ */
113
+ isLevelEnabled(level) {
114
+ return isLevelEnabled(level);
115
+ }
116
+ /**
117
+ * Log at trace level
118
+ * @param message Message to log
119
+ * @param data Optional data to include in log
120
+ */
121
+ trace(message, data) {
122
+ log('trace', `[${this.context}] ${message}`, data);
123
+ }
124
+ /**
125
+ * Log at debug level
126
+ * @param message Message to log
127
+ * @param data Optional data to include in log
128
+ */
129
+ debug(message, data) {
130
+ log('debug', `[${this.context}] ${message}`, data);
131
+ }
132
+ /**
133
+ * Log at info level
134
+ * @param message Message to log
135
+ * @param data Optional data to include in log
136
+ */
137
+ info(message, data) {
138
+ log('info', `[${this.context}] ${message}`, data);
139
+ }
140
+ /**
141
+ * Log at warn level
142
+ * @param message Message to log
143
+ * @param data Optional data to include in log
144
+ */
145
+ warn(message, data) {
146
+ log('warn', `[${this.context}] ${message}`, data);
147
+ }
148
+ /**
149
+ * Log at error level
150
+ * @param message Message to log
151
+ * @param data Optional data to include in log
152
+ */
153
+ error(message, data) {
154
+ log('error', `[${this.context}] ${message}`, data);
155
+ }
156
+ }
157
+ // Handle SIGTERM for clean shutdown
158
+ process.on('SIGTERM', () => {
159
+ log('info', 'Received SIGTERM signal, shutting down...');
160
+ logStream.end(() => {
161
+ process.exit(0);
162
+ });
163
+ });