@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.
- package/Dockerfile +38 -0
- package/LICENSE +21 -0
- package/README.md +470 -0
- package/build/config.js +237 -0
- package/build/index.js +87 -0
- package/build/logger.js +163 -0
- package/build/middleware/security.js +231 -0
- package/build/server.js +288 -0
- package/build/services/clickup/base.js +432 -0
- package/build/services/clickup/bulk.js +180 -0
- package/build/services/clickup/document.js +159 -0
- package/build/services/clickup/folder.js +136 -0
- package/build/services/clickup/index.js +76 -0
- package/build/services/clickup/list.js +191 -0
- package/build/services/clickup/tag.js +239 -0
- package/build/services/clickup/task/index.js +32 -0
- package/build/services/clickup/task/task-attachments.js +105 -0
- package/build/services/clickup/task/task-comments.js +114 -0
- package/build/services/clickup/task/task-core.js +604 -0
- package/build/services/clickup/task/task-custom-fields.js +107 -0
- package/build/services/clickup/task/task-search.js +986 -0
- package/build/services/clickup/task/task-service.js +104 -0
- package/build/services/clickup/task/task-tags.js +113 -0
- package/build/services/clickup/time.js +244 -0
- package/build/services/clickup/types.js +33 -0
- package/build/services/clickup/workspace.js +397 -0
- package/build/services/shared.js +61 -0
- package/build/sse_server.js +277 -0
- package/build/tools/documents.js +489 -0
- package/build/tools/folder.js +331 -0
- package/build/tools/index.js +16 -0
- package/build/tools/list.js +428 -0
- package/build/tools/member.js +106 -0
- package/build/tools/tag.js +833 -0
- package/build/tools/task/attachments.js +357 -0
- package/build/tools/task/attachments.types.js +9 -0
- package/build/tools/task/bulk-operations.js +338 -0
- package/build/tools/task/handlers.js +919 -0
- package/build/tools/task/index.js +30 -0
- package/build/tools/task/main.js +233 -0
- package/build/tools/task/single-operations.js +469 -0
- package/build/tools/task/time-tracking.js +575 -0
- package/build/tools/task/utilities.js +310 -0
- package/build/tools/task/workspace-operations.js +258 -0
- package/build/tools/tool-enhancer.js +37 -0
- package/build/tools/utils.js +12 -0
- package/build/tools/workspace-helper.js +44 -0
- package/build/tools/workspace.js +73 -0
- package/build/utils/color-processor.js +183 -0
- package/build/utils/concurrency-utils.js +248 -0
- package/build/utils/date-utils.js +542 -0
- package/build/utils/resolver-utils.js +135 -0
- package/build/utils/sponsor-service.js +93 -0
- package/build/utils/token-utils.js +49 -0
- package/package.json +77 -0
- package/smithery.yaml +23 -0
package/build/config.js
ADDED
|
@@ -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
|
+
});
|
package/build/logger.js
ADDED
|
@@ -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
|
+
});
|