@cyanheads/pubmed-mcp-server 1.2.4 → 1.3.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/README.md +2 -2
- package/dist/config/index.d.ts +13 -52
- package/dist/config/index.js +51 -222
- package/dist/mcp-server/server.d.ts +0 -5
- package/dist/mcp-server/server.js +18 -34
- package/dist/mcp-server/tools/fetchPubMedContent/logic.js +2 -2
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +2 -2
- package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +3 -3
- package/dist/mcp-server/tools/searchPubMedArticles/logic.js +2 -2
- package/dist/mcp-server/transports/auth/authFactory.d.ts +10 -0
- package/dist/mcp-server/transports/auth/authFactory.js +41 -0
- package/dist/mcp-server/transports/auth/authMiddleware.d.ts +19 -0
- package/dist/mcp-server/transports/auth/authMiddleware.js +57 -0
- package/dist/mcp-server/transports/auth/index.d.ts +8 -5
- package/dist/mcp-server/transports/auth/index.js +6 -4
- package/dist/mcp-server/transports/auth/{core → lib}/authTypes.d.ts +0 -5
- package/dist/mcp-server/transports/auth/lib/authTypes.js +8 -0
- package/dist/mcp-server/transports/auth/{core → lib}/authUtils.js +21 -14
- package/dist/mcp-server/transports/auth/strategies/authStrategy.d.ts +17 -0
- package/dist/mcp-server/transports/auth/strategies/authStrategy.js +1 -0
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.d.ts +7 -0
- package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js +112 -0
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.d.ts +7 -0
- package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js +101 -0
- package/dist/mcp-server/transports/core/baseTransportManager.d.ts +17 -0
- package/dist/mcp-server/transports/core/baseTransportManager.js +18 -0
- package/dist/mcp-server/transports/core/honoNodeBridge.d.ts +23 -0
- package/dist/mcp-server/transports/core/honoNodeBridge.js +51 -0
- package/dist/mcp-server/transports/core/statefulTransportManager.d.ts +31 -0
- package/dist/mcp-server/transports/core/statefulTransportManager.js +233 -0
- package/dist/mcp-server/transports/core/statelessTransportManager.d.ts +20 -0
- package/dist/mcp-server/transports/core/statelessTransportManager.js +92 -0
- package/dist/mcp-server/transports/core/transportTypes.d.ts +68 -0
- package/dist/mcp-server/transports/core/transportTypes.js +5 -0
- package/dist/mcp-server/transports/{httpErrorHandler.d.ts → http/httpErrorHandler.d.ts} +4 -9
- package/dist/mcp-server/transports/{httpErrorHandler.js → http/httpErrorHandler.js} +33 -8
- package/dist/mcp-server/transports/http/httpTransport.d.ts +22 -0
- package/dist/mcp-server/transports/http/httpTransport.js +251 -0
- package/dist/mcp-server/transports/http/httpTypes.d.ts +16 -0
- package/dist/mcp-server/transports/http/httpTypes.js +5 -0
- package/dist/mcp-server/transports/http/index.d.ts +7 -0
- package/dist/mcp-server/transports/http/index.js +6 -0
- package/dist/mcp-server/transports/http/mcpTransportMiddleware.d.ts +25 -0
- package/dist/mcp-server/transports/http/mcpTransportMiddleware.js +63 -0
- package/dist/mcp-server/transports/stdio/index.d.ts +5 -0
- package/dist/mcp-server/transports/stdio/index.js +5 -0
- package/dist/mcp-server/transports/{stdioTransport.d.ts → stdio/stdioTransport.d.ts} +2 -2
- package/dist/mcp-server/transports/{stdioTransport.js → stdio/stdioTransport.js} +10 -5
- package/dist/services/NCBI/{ncbiConstants.d.ts → core/ncbiConstants.d.ts} +1 -1
- package/dist/services/NCBI/{ncbiConstants.js → core/ncbiConstants.js} +1 -1
- package/dist/services/NCBI/{ncbiCoreApiClient.d.ts → core/ncbiCoreApiClient.d.ts} +2 -2
- package/dist/services/NCBI/{ncbiCoreApiClient.js → core/ncbiCoreApiClient.js} +4 -4
- package/dist/services/NCBI/{ncbiRequestQueueManager.d.ts → core/ncbiRequestQueueManager.d.ts} +2 -2
- package/dist/services/NCBI/{ncbiRequestQueueManager.js → core/ncbiRequestQueueManager.js} +3 -3
- package/dist/services/NCBI/{ncbiResponseHandler.d.ts → core/ncbiResponseHandler.d.ts} +2 -2
- package/dist/services/NCBI/{ncbiResponseHandler.js → core/ncbiResponseHandler.js} +3 -3
- package/dist/services/NCBI/{ncbiService.d.ts → core/ncbiService.d.ts} +3 -3
- package/dist/services/NCBI/{ncbiService.js → core/ncbiService.js} +2 -2
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/eSummaryResultParser.d.ts +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/eSummaryResultParser.js +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/index.d.ts +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/index.js +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/pubmedArticleStructureParser.d.ts +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/pubmedArticleStructureParser.js +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/xmlGenericHelpers.d.ts +1 -1
- package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/xmlGenericHelpers.js +1 -1
- package/dist/types-global/errors.d.ts +2 -0
- package/dist/types-global/errors.js +2 -0
- package/dist/utils/internal/errorHandler.js +1 -1
- package/dist/utils/internal/logger.d.ts +13 -1
- package/dist/utils/internal/logger.js +43 -9
- package/dist/utils/network/fetchWithTimeout.d.ts +21 -0
- package/dist/utils/network/fetchWithTimeout.js +59 -0
- package/dist/utils/network/index.d.ts +6 -0
- package/dist/utils/network/index.js +5 -0
- package/dist/utils/scheduling/index.d.ts +6 -0
- package/dist/utils/scheduling/index.js +6 -0
- package/dist/utils/scheduling/scheduler.d.ts +72 -0
- package/dist/utils/scheduling/scheduler.js +150 -0
- package/dist/utils/security/sanitization.js +35 -18
- package/package.json +9 -7
- package/dist/mcp-server/transports/auth/core/authTypes.js +0 -5
- package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.d.ts +0 -27
- package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.js +0 -149
- package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.d.ts +0 -20
- package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.js +0 -124
- package/dist/mcp-server/transports/httpTransport.d.ts +0 -21
- package/dist/mcp-server/transports/httpTransport.js +0 -208
- /package/dist/mcp-server/transports/auth/{core → lib}/authContext.d.ts +0 -0
- /package/dist/mcp-server/transports/auth/{core → lib}/authContext.js +0 -0
- /package/dist/mcp-server/transports/auth/{core → lib}/authUtils.d.ts +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Generic helper functions for parsing XML data, particularly
|
|
3
3
|
* structures from fast-xml-parser.
|
|
4
|
-
* @module src/
|
|
4
|
+
* @module src/services/NCBI/parsing/xmlGenericHelpers
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
7
|
* Ensures that the input is an array. If it's not an array, it wraps it in one.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @fileoverview Generic helper functions for parsing XML data, particularly
|
|
3
3
|
* structures from fast-xml-parser.
|
|
4
|
-
* @module src/
|
|
4
|
+
* @module src/services/NCBI/parsing/xmlGenericHelpers
|
|
5
5
|
*/
|
|
6
6
|
/**
|
|
7
7
|
* Ensures that the input is an array. If it's not an array, it wraps it in one.
|
|
@@ -23,6 +23,8 @@ export declare enum BaseErrorCode {
|
|
|
23
23
|
CONFLICT = "CONFLICT",
|
|
24
24
|
/** The request failed due to invalid input parameters or data. */
|
|
25
25
|
VALIDATION_ERROR = "VALIDATION_ERROR",
|
|
26
|
+
/** The provided input is invalid, but not necessarily a schema validation failure. */
|
|
27
|
+
INVALID_INPUT = "INVALID_INPUT",
|
|
26
28
|
/** An error occurred while parsing input data (e.g., date string, JSON). */
|
|
27
29
|
PARSING_ERROR = "PARSING_ERROR",
|
|
28
30
|
/** The request was rejected because the client has exceeded rate limits. */
|
|
@@ -24,6 +24,8 @@ export var BaseErrorCode;
|
|
|
24
24
|
BaseErrorCode["CONFLICT"] = "CONFLICT";
|
|
25
25
|
/** The request failed due to invalid input parameters or data. */
|
|
26
26
|
BaseErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR";
|
|
27
|
+
/** The provided input is invalid, but not necessarily a schema validation failure. */
|
|
28
|
+
BaseErrorCode["INVALID_INPUT"] = "INVALID_INPUT";
|
|
27
29
|
/** An error occurred while parsing input data (e.g., date string, JSON). */
|
|
28
30
|
BaseErrorCode["PARSING_ERROR"] = "PARSING_ERROR";
|
|
29
31
|
/** The request was rejected because the client has exceeded rate limits. */
|
|
@@ -126,7 +126,7 @@ function getErrorMessage(error) {
|
|
|
126
126
|
try {
|
|
127
127
|
return `Non-Error object encountered: ${JSON.stringify(error)}`;
|
|
128
128
|
}
|
|
129
|
-
catch
|
|
129
|
+
catch {
|
|
130
130
|
return `Unstringifyable non-Error object encountered (constructor: ${error.constructor?.name || "Unknown"})`;
|
|
131
131
|
}
|
|
132
132
|
}
|
|
@@ -17,7 +17,7 @@ export interface McpLogPayload {
|
|
|
17
17
|
message: string;
|
|
18
18
|
stack?: string;
|
|
19
19
|
};
|
|
20
|
-
[key: string]:
|
|
20
|
+
[key: string]: unknown;
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* Type for the `data` parameter of the `McpNotificationSender` function.
|
|
@@ -38,6 +38,7 @@ export type McpNotificationSender = (level: McpLogLevel, data: McpNotificationDa
|
|
|
38
38
|
export declare class Logger {
|
|
39
39
|
private static instance;
|
|
40
40
|
private winstonLogger?;
|
|
41
|
+
private interactionLogger?;
|
|
41
42
|
private initialized;
|
|
42
43
|
private mcpNotificationSender?;
|
|
43
44
|
private currentMcpLevel;
|
|
@@ -75,6 +76,11 @@ export declare class Logger {
|
|
|
75
76
|
* @returns The singleton Logger instance.
|
|
76
77
|
*/
|
|
77
78
|
static getInstance(): Logger;
|
|
79
|
+
/**
|
|
80
|
+
* Resets the singleton instance.
|
|
81
|
+
* This is intended for use in testing environments only.
|
|
82
|
+
*/
|
|
83
|
+
static resetForTesting(): void;
|
|
78
84
|
/**
|
|
79
85
|
* Ensures the logger has been initialized.
|
|
80
86
|
* @returns True if initialized, false otherwise.
|
|
@@ -133,6 +139,12 @@ export declare class Logger {
|
|
|
133
139
|
* @param context - Optional. RequestContext if `err` is an Error.
|
|
134
140
|
*/
|
|
135
141
|
fatal(msg: string, err?: Error | RequestContext, context?: RequestContext): void;
|
|
142
|
+
/**
|
|
143
|
+
* Logs a structured interaction object to a dedicated file.
|
|
144
|
+
* @param interactionName - A name for the interaction type (e.g., 'OpenRouterIO').
|
|
145
|
+
* @param data - The structured data to log.
|
|
146
|
+
*/
|
|
147
|
+
logInteraction(interactionName: string, data: Record<string, unknown>): void;
|
|
136
148
|
}
|
|
137
149
|
/**
|
|
138
150
|
* The singleton instance of the Logger.
|
|
@@ -35,9 +35,8 @@ const mcpToWinstonLevel = {
|
|
|
35
35
|
alert: "error",
|
|
36
36
|
emerg: "error",
|
|
37
37
|
};
|
|
38
|
-
// The logsPath from config is
|
|
39
|
-
|
|
40
|
-
const isLogsDirSafe = !!resolvedLogsDir; // If logsPath is set, it's considered safe by config logic.
|
|
38
|
+
// The logsPath from config is resolved and validated by src/config/index.ts.
|
|
39
|
+
// It can be null if the directory is invalid or inaccessible, in which case file logging will be disabled.
|
|
41
40
|
/**
|
|
42
41
|
* Creates the Winston console log format.
|
|
43
42
|
* @returns The Winston log format for console output.
|
|
@@ -107,10 +106,7 @@ export class Logger {
|
|
|
107
106
|
this.initialized = true;
|
|
108
107
|
this.currentMcpLevel = level;
|
|
109
108
|
this.currentWinstonLevel = mcpToWinstonLevel[level];
|
|
110
|
-
|
|
111
|
-
// by the configuration module (src/config/index.ts) before logger initialization.
|
|
112
|
-
// If isLogsDirSafe is true, we assume resolvedLogsDir exists and is usable.
|
|
113
|
-
// No redundant directory creation logic here.
|
|
109
|
+
const resolvedLogsDir = config.logsPath;
|
|
114
110
|
const fileFormat = winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json());
|
|
115
111
|
const transports = [];
|
|
116
112
|
const fileTransportOptions = {
|
|
@@ -119,7 +115,7 @@ export class Logger {
|
|
|
119
115
|
maxFiles: this.LOG_MAX_FILES,
|
|
120
116
|
tailable: true,
|
|
121
117
|
};
|
|
122
|
-
if (
|
|
118
|
+
if (resolvedLogsDir) {
|
|
123
119
|
transports.push(new winston.transports.File({
|
|
124
120
|
filename: path.join(resolvedLogsDir, "error.log"),
|
|
125
121
|
level: "error",
|
|
@@ -151,6 +147,18 @@ export class Logger {
|
|
|
151
147
|
transports,
|
|
152
148
|
exitOnError: false,
|
|
153
149
|
});
|
|
150
|
+
// Initialize a separate logger for structured interactions
|
|
151
|
+
if (resolvedLogsDir) {
|
|
152
|
+
this.interactionLogger = winston.createLogger({
|
|
153
|
+
format: winston.format.combine(winston.format.timestamp(), winston.format.json({ space: 2 })),
|
|
154
|
+
transports: [
|
|
155
|
+
new winston.transports.File({
|
|
156
|
+
filename: path.join(resolvedLogsDir, "interactions.log"),
|
|
157
|
+
...fileTransportOptions,
|
|
158
|
+
}),
|
|
159
|
+
],
|
|
160
|
+
});
|
|
161
|
+
}
|
|
154
162
|
// Configure console transport after Winston logger is created
|
|
155
163
|
const consoleStatus = this._configureConsoleTransport();
|
|
156
164
|
const initialContext = {
|
|
@@ -167,7 +175,7 @@ export class Logger {
|
|
|
167
175
|
loggerSetup: true,
|
|
168
176
|
requestId: "logger-post-init",
|
|
169
177
|
timestamp: new Date().toISOString(),
|
|
170
|
-
logsPathUsed: resolvedLogsDir,
|
|
178
|
+
logsPathUsed: resolvedLogsDir ?? "none",
|
|
171
179
|
});
|
|
172
180
|
}
|
|
173
181
|
/**
|
|
@@ -262,6 +270,20 @@ export class Logger {
|
|
|
262
270
|
}
|
|
263
271
|
return Logger.instance;
|
|
264
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Resets the singleton instance.
|
|
275
|
+
* This is intended for use in testing environments only.
|
|
276
|
+
*/
|
|
277
|
+
static resetForTesting() {
|
|
278
|
+
// This is a clear indication that this method is for testing purposes.
|
|
279
|
+
if (process.env.NODE_ENV !== "test") {
|
|
280
|
+
console.warn("Warning: `resetForTesting` should only be called in a test environment.");
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
// De-reference the instance to allow garbage collection
|
|
284
|
+
// and force re-creation on next getInstance() call.
|
|
285
|
+
Logger.instance = undefined;
|
|
286
|
+
}
|
|
265
287
|
/**
|
|
266
288
|
* Ensures the logger has been initialized.
|
|
267
289
|
* @returns True if initialized, false otherwise.
|
|
@@ -398,6 +420,18 @@ export class Logger {
|
|
|
398
420
|
const actualContext = err instanceof Error ? context : err;
|
|
399
421
|
this.log("emerg", msg, actualContext, errorObj);
|
|
400
422
|
}
|
|
423
|
+
/**
|
|
424
|
+
* Logs a structured interaction object to a dedicated file.
|
|
425
|
+
* @param interactionName - A name for the interaction type (e.g., 'OpenRouterIO').
|
|
426
|
+
* @param data - The structured data to log.
|
|
427
|
+
*/
|
|
428
|
+
logInteraction(interactionName, data) {
|
|
429
|
+
if (!this.interactionLogger) {
|
|
430
|
+
this.warning("Interaction logger not available. File logging may be disabled.", data.context);
|
|
431
|
+
return;
|
|
432
|
+
}
|
|
433
|
+
this.interactionLogger.info({ interactionName, ...data });
|
|
434
|
+
}
|
|
401
435
|
}
|
|
402
436
|
/**
|
|
403
437
|
* The singleton instance of the Logger.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Provides a utility function to make fetch requests with a specified timeout.
|
|
3
|
+
* @module src/utils/network/fetchWithTimeout
|
|
4
|
+
*/
|
|
5
|
+
import type { RequestContext } from "../internal/requestContext.js";
|
|
6
|
+
/**
|
|
7
|
+
* Options for the fetchWithTimeout utility.
|
|
8
|
+
* Extends standard RequestInit but omits 'signal' as it's handled internally.
|
|
9
|
+
*/
|
|
10
|
+
export type FetchWithTimeoutOptions = Omit<RequestInit, "signal">;
|
|
11
|
+
/**
|
|
12
|
+
* Fetches a resource with a specified timeout.
|
|
13
|
+
*
|
|
14
|
+
* @param url - The URL to fetch.
|
|
15
|
+
* @param timeoutMs - The timeout duration in milliseconds.
|
|
16
|
+
* @param context - The request context for logging.
|
|
17
|
+
* @param options - Optional fetch options (RequestInit), excluding 'signal'.
|
|
18
|
+
* @returns A promise that resolves to the Response object.
|
|
19
|
+
* @throws {McpError} If the request times out or another fetch-related error occurs.
|
|
20
|
+
*/
|
|
21
|
+
export declare function fetchWithTimeout(url: string | URL, timeoutMs: number, context: RequestContext, options?: FetchWithTimeoutOptions): Promise<Response>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Provides a utility function to make fetch requests with a specified timeout.
|
|
3
|
+
* @module src/utils/network/fetchWithTimeout
|
|
4
|
+
*/
|
|
5
|
+
import { logger } from "../internal/logger.js"; // Adjusted import path
|
|
6
|
+
import { McpError, BaseErrorCode } from "../../types-global/errors.js";
|
|
7
|
+
/**
|
|
8
|
+
* Fetches a resource with a specified timeout.
|
|
9
|
+
*
|
|
10
|
+
* @param url - The URL to fetch.
|
|
11
|
+
* @param timeoutMs - The timeout duration in milliseconds.
|
|
12
|
+
* @param context - The request context for logging.
|
|
13
|
+
* @param options - Optional fetch options (RequestInit), excluding 'signal'.
|
|
14
|
+
* @returns A promise that resolves to the Response object.
|
|
15
|
+
* @throws {McpError} If the request times out or another fetch-related error occurs.
|
|
16
|
+
*/
|
|
17
|
+
export async function fetchWithTimeout(url, timeoutMs, context, options) {
|
|
18
|
+
const controller = new AbortController();
|
|
19
|
+
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
20
|
+
const urlString = url.toString();
|
|
21
|
+
const operationDescription = `fetch ${options?.method || "GET"} ${urlString}`;
|
|
22
|
+
logger.debug(`Attempting ${operationDescription} with ${timeoutMs}ms timeout.`, context);
|
|
23
|
+
try {
|
|
24
|
+
const response = await fetch(url, {
|
|
25
|
+
...options,
|
|
26
|
+
signal: controller.signal,
|
|
27
|
+
});
|
|
28
|
+
clearTimeout(timeoutId);
|
|
29
|
+
logger.debug(`Successfully fetched ${urlString}. Status: ${response.status}`, context);
|
|
30
|
+
return response;
|
|
31
|
+
}
|
|
32
|
+
catch (error) {
|
|
33
|
+
clearTimeout(timeoutId);
|
|
34
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
35
|
+
logger.error(`${operationDescription} timed out after ${timeoutMs}ms.`, {
|
|
36
|
+
...context,
|
|
37
|
+
errorSource: "FetchTimeout",
|
|
38
|
+
});
|
|
39
|
+
throw new McpError(BaseErrorCode.TIMEOUT, `${operationDescription} timed out.`, { ...context, errorSource: "FetchTimeout" });
|
|
40
|
+
}
|
|
41
|
+
// Log and re-throw other errors as McpError
|
|
42
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
43
|
+
logger.error(`Network error during ${operationDescription}: ${errorMessage}`, {
|
|
44
|
+
...context,
|
|
45
|
+
originalErrorName: error instanceof Error ? error.name : "UnknownError",
|
|
46
|
+
errorSource: "FetchNetworkError",
|
|
47
|
+
});
|
|
48
|
+
if (error instanceof McpError) {
|
|
49
|
+
// If it's already an McpError, re-throw it
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, // Generic error for network/service issues
|
|
53
|
+
`Network error during ${operationDescription}: ${errorMessage}`, {
|
|
54
|
+
...context,
|
|
55
|
+
originalErrorName: error instanceof Error ? error.name : "UnknownError",
|
|
56
|
+
errorSource: "FetchNetworkErrorWrapper",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Provides a singleton service for scheduling and managing cron jobs.
|
|
3
|
+
* This service wraps the 'node-cron' library to offer a unified interface for
|
|
4
|
+
* defining, starting, stopping, and listing recurring tasks within the application.
|
|
5
|
+
* @module src/utils/scheduling/scheduler
|
|
6
|
+
*/
|
|
7
|
+
import { ScheduledTask } from "node-cron";
|
|
8
|
+
import { RequestContext } from "../internal/index.js";
|
|
9
|
+
/**
|
|
10
|
+
* Represents a scheduled job managed by the SchedulerService.
|
|
11
|
+
*/
|
|
12
|
+
export interface Job {
|
|
13
|
+
/** A unique identifier for the job. */
|
|
14
|
+
id: string;
|
|
15
|
+
/** The cron pattern defining the job's schedule. */
|
|
16
|
+
schedule: string;
|
|
17
|
+
/** A description of what the job does. */
|
|
18
|
+
description: string;
|
|
19
|
+
/** The underlying 'node-cron' task instance. */
|
|
20
|
+
task: ScheduledTask;
|
|
21
|
+
/** Indicates whether the job is currently running. */
|
|
22
|
+
isRunning: boolean;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A singleton service for scheduling and managing cron jobs.
|
|
26
|
+
*/
|
|
27
|
+
export declare class SchedulerService {
|
|
28
|
+
private static instance;
|
|
29
|
+
private jobs;
|
|
30
|
+
/** @private */
|
|
31
|
+
private constructor();
|
|
32
|
+
/**
|
|
33
|
+
* Gets the singleton instance of the SchedulerService.
|
|
34
|
+
* @returns The singleton SchedulerService instance.
|
|
35
|
+
*/
|
|
36
|
+
static getInstance(): SchedulerService;
|
|
37
|
+
/**
|
|
38
|
+
* Schedules a new job.
|
|
39
|
+
*
|
|
40
|
+
* @param id - A unique identifier for the job.
|
|
41
|
+
* @param schedule - The cron pattern for the schedule (e.g., '* * * * *').
|
|
42
|
+
* @param taskFunction - The function to execute on schedule. It receives a RequestContext.
|
|
43
|
+
* @param description - A description of the job.
|
|
44
|
+
* @returns The newly created Job object.
|
|
45
|
+
*/
|
|
46
|
+
schedule(id: string, schedule: string, taskFunction: (context: RequestContext) => void | Promise<void>, description: string): Job;
|
|
47
|
+
/**
|
|
48
|
+
* Starts a scheduled job.
|
|
49
|
+
* @param id - The ID of the job to start.
|
|
50
|
+
*/
|
|
51
|
+
start(id: string): void;
|
|
52
|
+
/**
|
|
53
|
+
* Stops a scheduled job.
|
|
54
|
+
* @param id - The ID of the job to stop.
|
|
55
|
+
*/
|
|
56
|
+
stop(id: string): void;
|
|
57
|
+
/**
|
|
58
|
+
* Removes a job from the scheduler. The job is stopped before being removed.
|
|
59
|
+
* @param id - The ID of the job to remove.
|
|
60
|
+
*/
|
|
61
|
+
remove(id: string): void;
|
|
62
|
+
/**
|
|
63
|
+
* Gets a list of all scheduled jobs.
|
|
64
|
+
* @returns An array of all Job objects.
|
|
65
|
+
*/
|
|
66
|
+
listJobs(): Job[];
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The singleton instance of the SchedulerService.
|
|
70
|
+
* Use this instance for all job scheduling operations.
|
|
71
|
+
*/
|
|
72
|
+
export declare const schedulerService: SchedulerService;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Provides a singleton service for scheduling and managing cron jobs.
|
|
3
|
+
* This service wraps the 'node-cron' library to offer a unified interface for
|
|
4
|
+
* defining, starting, stopping, and listing recurring tasks within the application.
|
|
5
|
+
* @module src/utils/scheduling/scheduler
|
|
6
|
+
*/
|
|
7
|
+
import cron, { createTask } from "node-cron";
|
|
8
|
+
import { logger } from "../internal/index.js";
|
|
9
|
+
import { requestContextService } from "../internal/requestContext.js";
|
|
10
|
+
/**
|
|
11
|
+
* A singleton service for scheduling and managing cron jobs.
|
|
12
|
+
*/
|
|
13
|
+
export class SchedulerService {
|
|
14
|
+
/** @private */
|
|
15
|
+
constructor() {
|
|
16
|
+
this.jobs = new Map();
|
|
17
|
+
logger.info("SchedulerService initialized.", {
|
|
18
|
+
requestId: "scheduler-init",
|
|
19
|
+
timestamp: new Date().toISOString(),
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Gets the singleton instance of the SchedulerService.
|
|
24
|
+
* @returns The singleton SchedulerService instance.
|
|
25
|
+
*/
|
|
26
|
+
static getInstance() {
|
|
27
|
+
if (!SchedulerService.instance) {
|
|
28
|
+
SchedulerService.instance = new SchedulerService();
|
|
29
|
+
}
|
|
30
|
+
return SchedulerService.instance;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Schedules a new job.
|
|
34
|
+
*
|
|
35
|
+
* @param id - A unique identifier for the job.
|
|
36
|
+
* @param schedule - The cron pattern for the schedule (e.g., '* * * * *').
|
|
37
|
+
* @param taskFunction - The function to execute on schedule. It receives a RequestContext.
|
|
38
|
+
* @param description - A description of the job.
|
|
39
|
+
* @returns The newly created Job object.
|
|
40
|
+
*/
|
|
41
|
+
schedule(id, schedule, taskFunction, description) {
|
|
42
|
+
if (this.jobs.has(id)) {
|
|
43
|
+
throw new Error(`Job with ID '${id}' already exists.`);
|
|
44
|
+
}
|
|
45
|
+
if (!cron.validate(schedule)) {
|
|
46
|
+
throw new Error(`Invalid cron schedule: ${schedule}`);
|
|
47
|
+
}
|
|
48
|
+
const task = createTask(schedule, async () => {
|
|
49
|
+
const job = this.jobs.get(id);
|
|
50
|
+
if (job && job.isRunning) {
|
|
51
|
+
logger.warning(`Job '${id}' is already running. Skipping this execution.`, {
|
|
52
|
+
requestId: `job-skip-${id}`,
|
|
53
|
+
timestamp: new Date().toISOString(),
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (job) {
|
|
58
|
+
job.isRunning = true;
|
|
59
|
+
}
|
|
60
|
+
const context = requestContextService.createRequestContext({
|
|
61
|
+
jobId: id,
|
|
62
|
+
schedule,
|
|
63
|
+
});
|
|
64
|
+
logger.info(`Starting job '${id}'...`, context);
|
|
65
|
+
try {
|
|
66
|
+
await Promise.resolve(taskFunction(context));
|
|
67
|
+
logger.info(`Job '${id}' completed successfully.`, context);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
logger.error(`Job '${id}' failed.`, error, context);
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
if (job) {
|
|
74
|
+
job.isRunning = false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
const newJob = {
|
|
79
|
+
id,
|
|
80
|
+
schedule,
|
|
81
|
+
description,
|
|
82
|
+
task,
|
|
83
|
+
isRunning: false,
|
|
84
|
+
};
|
|
85
|
+
this.jobs.set(id, newJob);
|
|
86
|
+
logger.info(`Job '${id}' scheduled: ${description}`, {
|
|
87
|
+
requestId: `job-schedule-${id}`,
|
|
88
|
+
timestamp: new Date().toISOString(),
|
|
89
|
+
});
|
|
90
|
+
return newJob;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Starts a scheduled job.
|
|
94
|
+
* @param id - The ID of the job to start.
|
|
95
|
+
*/
|
|
96
|
+
start(id) {
|
|
97
|
+
const job = this.jobs.get(id);
|
|
98
|
+
if (!job) {
|
|
99
|
+
throw new Error(`Job with ID '${id}' not found.`);
|
|
100
|
+
}
|
|
101
|
+
job.task.start();
|
|
102
|
+
logger.info(`Job '${id}' started.`, {
|
|
103
|
+
requestId: `job-start-${id}`,
|
|
104
|
+
timestamp: new Date().toISOString(),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Stops a scheduled job.
|
|
109
|
+
* @param id - The ID of the job to stop.
|
|
110
|
+
*/
|
|
111
|
+
stop(id) {
|
|
112
|
+
const job = this.jobs.get(id);
|
|
113
|
+
if (!job) {
|
|
114
|
+
throw new Error(`Job with ID '${id}' not found.`);
|
|
115
|
+
}
|
|
116
|
+
job.task.stop();
|
|
117
|
+
logger.info(`Job '${id}' stopped.`, {
|
|
118
|
+
requestId: `job-stop-${id}`,
|
|
119
|
+
timestamp: new Date().toISOString(),
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Removes a job from the scheduler. The job is stopped before being removed.
|
|
124
|
+
* @param id - The ID of the job to remove.
|
|
125
|
+
*/
|
|
126
|
+
remove(id) {
|
|
127
|
+
const job = this.jobs.get(id);
|
|
128
|
+
if (!job) {
|
|
129
|
+
throw new Error(`Job with ID '${id}' not found.`);
|
|
130
|
+
}
|
|
131
|
+
job.task.stop();
|
|
132
|
+
this.jobs.delete(id);
|
|
133
|
+
logger.info(`Job '${id}' removed.`, {
|
|
134
|
+
requestId: `job-remove-${id}`,
|
|
135
|
+
timestamp: new Date().toISOString(),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Gets a list of all scheduled jobs.
|
|
140
|
+
* @returns An array of all Job objects.
|
|
141
|
+
*/
|
|
142
|
+
listJobs() {
|
|
143
|
+
return Array.from(this.jobs.values());
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The singleton instance of the SchedulerService.
|
|
148
|
+
* Use this instance for all job scheduling operations.
|
|
149
|
+
*/
|
|
150
|
+
export const schedulerService = SchedulerService.getInstance();
|
|
@@ -121,14 +121,25 @@ export class Sanitization {
|
|
|
121
121
|
sanitizeHtml(input, config) {
|
|
122
122
|
if (!input)
|
|
123
123
|
return "";
|
|
124
|
-
const effectiveConfig = {
|
|
124
|
+
const effectiveConfig = {
|
|
125
|
+
allowedTags: config?.allowedTags ?? this.defaultHtmlSanitizeConfig.allowedTags,
|
|
126
|
+
allowedAttributes: config?.allowedAttributes ??
|
|
127
|
+
this.defaultHtmlSanitizeConfig.allowedAttributes,
|
|
128
|
+
transformTags: config?.transformTags, // Can be undefined
|
|
129
|
+
preserveComments: config?.preserveComments ??
|
|
130
|
+
this.defaultHtmlSanitizeConfig.preserveComments,
|
|
131
|
+
};
|
|
125
132
|
const options = {
|
|
126
133
|
allowedTags: effectiveConfig.allowedTags,
|
|
127
134
|
allowedAttributes: effectiveConfig.allowedAttributes,
|
|
128
135
|
transformTags: effectiveConfig.transformTags,
|
|
129
136
|
};
|
|
130
137
|
if (effectiveConfig.preserveComments) {
|
|
131
|
-
|
|
138
|
+
// Ensure allowedTags is an array before spreading
|
|
139
|
+
const baseTags = Array.isArray(options.allowedTags)
|
|
140
|
+
? options.allowedTags
|
|
141
|
+
: [];
|
|
142
|
+
options.allowedTags = [...baseTags, "!--"];
|
|
132
143
|
}
|
|
133
144
|
return sanitizeHtml(input, options);
|
|
134
145
|
}
|
|
@@ -144,14 +155,18 @@ export class Sanitization {
|
|
|
144
155
|
sanitizeString(input, options = {}) {
|
|
145
156
|
if (!input)
|
|
146
157
|
return "";
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
const context = options.context ?? "text";
|
|
159
|
+
switch (context) {
|
|
160
|
+
case "html": {
|
|
161
|
+
const config = {};
|
|
162
|
+
if (options.allowedTags) {
|
|
163
|
+
config.allowedTags = options.allowedTags;
|
|
164
|
+
}
|
|
165
|
+
if (options.allowedAttributes) {
|
|
166
|
+
config.allowedAttributes = this.convertAttributesFormat(options.allowedAttributes);
|
|
167
|
+
}
|
|
168
|
+
return this.sanitizeHtml(input, config);
|
|
169
|
+
}
|
|
155
170
|
case "attribute":
|
|
156
171
|
return sanitizeHtml(input, { allowedTags: [], allowedAttributes: {} });
|
|
157
172
|
case "url":
|
|
@@ -233,7 +248,6 @@ export class Sanitization {
|
|
|
233
248
|
rootDir: options.rootDir ? path.resolve(options.rootDir) : undefined,
|
|
234
249
|
};
|
|
235
250
|
let wasAbsoluteInitially = false;
|
|
236
|
-
let convertedToRelative = false;
|
|
237
251
|
try {
|
|
238
252
|
if (!input || typeof input !== "string")
|
|
239
253
|
throw new Error("Invalid path input: must be a non-empty string.");
|
|
@@ -262,8 +276,7 @@ export class Sanitization {
|
|
|
262
276
|
else {
|
|
263
277
|
if (path.isAbsolute(normalized)) {
|
|
264
278
|
if (!effectiveOptions.allowAbsolute) {
|
|
265
|
-
|
|
266
|
-
convertedToRelative = true;
|
|
279
|
+
throw new Error("Absolute paths are disallowed by current options.");
|
|
267
280
|
}
|
|
268
281
|
else {
|
|
269
282
|
finalSanitizedPath = normalized;
|
|
@@ -355,7 +368,7 @@ export class Sanitization {
|
|
|
355
368
|
throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Invalid number value (NaN or Infinity).", { input });
|
|
356
369
|
}
|
|
357
370
|
let clamped = false;
|
|
358
|
-
|
|
371
|
+
const originalValueForLog = value;
|
|
359
372
|
if (min !== undefined && value < min) {
|
|
360
373
|
value = min;
|
|
361
374
|
clamped = true;
|
|
@@ -399,8 +412,8 @@ export class Sanitization {
|
|
|
399
412
|
try {
|
|
400
413
|
if (!input || typeof input !== "object")
|
|
401
414
|
return input;
|
|
402
|
-
const clonedInput = typeof structuredClone === "function"
|
|
403
|
-
? structuredClone(input)
|
|
415
|
+
const clonedInput = typeof globalThis.structuredClone === "function"
|
|
416
|
+
? globalThis.structuredClone(input)
|
|
404
417
|
: JSON.parse(JSON.stringify(input));
|
|
405
418
|
this.redactSensitiveFields(clonedInput);
|
|
406
419
|
return clonedInput;
|
|
@@ -428,8 +441,12 @@ export class Sanitization {
|
|
|
428
441
|
for (const key in obj) {
|
|
429
442
|
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
|
430
443
|
const value = obj[key];
|
|
431
|
-
|
|
432
|
-
const
|
|
444
|
+
// Split camelCase and snake_case/kebab-case keys into words
|
|
445
|
+
const keyWords = key
|
|
446
|
+
.replace(/([A-Z])/g, " $1") // Add space before uppercase letters
|
|
447
|
+
.toLowerCase()
|
|
448
|
+
.split(/[\s_-]+/); // Split by space, underscore, or hyphen
|
|
449
|
+
const isSensitive = keyWords.some((word) => this.sensitiveFields.includes(word));
|
|
433
450
|
if (isSensitive) {
|
|
434
451
|
obj[key] = "[REDACTED]";
|
|
435
452
|
}
|