@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.
Files changed (91) hide show
  1. package/README.md +2 -2
  2. package/dist/config/index.d.ts +13 -52
  3. package/dist/config/index.js +51 -222
  4. package/dist/mcp-server/server.d.ts +0 -5
  5. package/dist/mcp-server/server.js +18 -34
  6. package/dist/mcp-server/tools/fetchPubMedContent/logic.js +2 -2
  7. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +2 -2
  8. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +3 -3
  9. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +2 -2
  10. package/dist/mcp-server/transports/auth/authFactory.d.ts +10 -0
  11. package/dist/mcp-server/transports/auth/authFactory.js +41 -0
  12. package/dist/mcp-server/transports/auth/authMiddleware.d.ts +19 -0
  13. package/dist/mcp-server/transports/auth/authMiddleware.js +57 -0
  14. package/dist/mcp-server/transports/auth/index.d.ts +8 -5
  15. package/dist/mcp-server/transports/auth/index.js +6 -4
  16. package/dist/mcp-server/transports/auth/{core → lib}/authTypes.d.ts +0 -5
  17. package/dist/mcp-server/transports/auth/lib/authTypes.js +8 -0
  18. package/dist/mcp-server/transports/auth/{core → lib}/authUtils.js +21 -14
  19. package/dist/mcp-server/transports/auth/strategies/authStrategy.d.ts +17 -0
  20. package/dist/mcp-server/transports/auth/strategies/authStrategy.js +1 -0
  21. package/dist/mcp-server/transports/auth/strategies/jwtStrategy.d.ts +7 -0
  22. package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js +112 -0
  23. package/dist/mcp-server/transports/auth/strategies/oauthStrategy.d.ts +7 -0
  24. package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js +101 -0
  25. package/dist/mcp-server/transports/core/baseTransportManager.d.ts +17 -0
  26. package/dist/mcp-server/transports/core/baseTransportManager.js +18 -0
  27. package/dist/mcp-server/transports/core/honoNodeBridge.d.ts +23 -0
  28. package/dist/mcp-server/transports/core/honoNodeBridge.js +51 -0
  29. package/dist/mcp-server/transports/core/statefulTransportManager.d.ts +31 -0
  30. package/dist/mcp-server/transports/core/statefulTransportManager.js +233 -0
  31. package/dist/mcp-server/transports/core/statelessTransportManager.d.ts +20 -0
  32. package/dist/mcp-server/transports/core/statelessTransportManager.js +92 -0
  33. package/dist/mcp-server/transports/core/transportTypes.d.ts +68 -0
  34. package/dist/mcp-server/transports/core/transportTypes.js +5 -0
  35. package/dist/mcp-server/transports/{httpErrorHandler.d.ts → http/httpErrorHandler.d.ts} +4 -9
  36. package/dist/mcp-server/transports/{httpErrorHandler.js → http/httpErrorHandler.js} +33 -8
  37. package/dist/mcp-server/transports/http/httpTransport.d.ts +22 -0
  38. package/dist/mcp-server/transports/http/httpTransport.js +251 -0
  39. package/dist/mcp-server/transports/http/httpTypes.d.ts +16 -0
  40. package/dist/mcp-server/transports/http/httpTypes.js +5 -0
  41. package/dist/mcp-server/transports/http/index.d.ts +7 -0
  42. package/dist/mcp-server/transports/http/index.js +6 -0
  43. package/dist/mcp-server/transports/http/mcpTransportMiddleware.d.ts +25 -0
  44. package/dist/mcp-server/transports/http/mcpTransportMiddleware.js +63 -0
  45. package/dist/mcp-server/transports/stdio/index.d.ts +5 -0
  46. package/dist/mcp-server/transports/stdio/index.js +5 -0
  47. package/dist/mcp-server/transports/{stdioTransport.d.ts → stdio/stdioTransport.d.ts} +2 -2
  48. package/dist/mcp-server/transports/{stdioTransport.js → stdio/stdioTransport.js} +10 -5
  49. package/dist/services/NCBI/{ncbiConstants.d.ts → core/ncbiConstants.d.ts} +1 -1
  50. package/dist/services/NCBI/{ncbiConstants.js → core/ncbiConstants.js} +1 -1
  51. package/dist/services/NCBI/{ncbiCoreApiClient.d.ts → core/ncbiCoreApiClient.d.ts} +2 -2
  52. package/dist/services/NCBI/{ncbiCoreApiClient.js → core/ncbiCoreApiClient.js} +4 -4
  53. package/dist/services/NCBI/{ncbiRequestQueueManager.d.ts → core/ncbiRequestQueueManager.d.ts} +2 -2
  54. package/dist/services/NCBI/{ncbiRequestQueueManager.js → core/ncbiRequestQueueManager.js} +3 -3
  55. package/dist/services/NCBI/{ncbiResponseHandler.d.ts → core/ncbiResponseHandler.d.ts} +2 -2
  56. package/dist/services/NCBI/{ncbiResponseHandler.js → core/ncbiResponseHandler.js} +3 -3
  57. package/dist/services/NCBI/{ncbiService.d.ts → core/ncbiService.d.ts} +3 -3
  58. package/dist/services/NCBI/{ncbiService.js → core/ncbiService.js} +2 -2
  59. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/eSummaryResultParser.d.ts +1 -1
  60. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/eSummaryResultParser.js +1 -1
  61. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/index.d.ts +1 -1
  62. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/index.js +1 -1
  63. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/pubmedArticleStructureParser.d.ts +1 -1
  64. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/pubmedArticleStructureParser.js +1 -1
  65. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/xmlGenericHelpers.d.ts +1 -1
  66. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/xmlGenericHelpers.js +1 -1
  67. package/dist/types-global/errors.d.ts +2 -0
  68. package/dist/types-global/errors.js +2 -0
  69. package/dist/utils/internal/errorHandler.js +1 -1
  70. package/dist/utils/internal/logger.d.ts +13 -1
  71. package/dist/utils/internal/logger.js +43 -9
  72. package/dist/utils/network/fetchWithTimeout.d.ts +21 -0
  73. package/dist/utils/network/fetchWithTimeout.js +59 -0
  74. package/dist/utils/network/index.d.ts +6 -0
  75. package/dist/utils/network/index.js +5 -0
  76. package/dist/utils/scheduling/index.d.ts +6 -0
  77. package/dist/utils/scheduling/index.js +6 -0
  78. package/dist/utils/scheduling/scheduler.d.ts +72 -0
  79. package/dist/utils/scheduling/scheduler.js +150 -0
  80. package/dist/utils/security/sanitization.js +35 -18
  81. package/package.json +9 -7
  82. package/dist/mcp-server/transports/auth/core/authTypes.js +0 -5
  83. package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.d.ts +0 -27
  84. package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.js +0 -149
  85. package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.d.ts +0 -20
  86. package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.js +0 -124
  87. package/dist/mcp-server/transports/httpTransport.d.ts +0 -21
  88. package/dist/mcp-server/transports/httpTransport.js +0 -208
  89. /package/dist/mcp-server/transports/auth/{core → lib}/authContext.d.ts +0 -0
  90. /package/dist/mcp-server/transports/auth/{core → lib}/authContext.js +0 -0
  91. /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/utils/parsing/ncbi-parsing/xmlGenericHelpers
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/utils/parsing/ncbi-parsing/xmlGenericHelpers
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 (stringifyError) {
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]: any;
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 already resolved and validated by src/config/index.ts
39
- const resolvedLogsDir = config.logsPath;
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
- // The logs directory (config.logsPath / resolvedLogsDir) is expected to be created and validated
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 (isLogsDirSafe) {
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,6 @@
1
+ /**
2
+ * @fileoverview Barrel file for network utilities.
3
+ * @module src/utils/network/index
4
+ */
5
+ export * from "./fetchWithTimeout.js";
6
+ export type { FetchWithTimeoutOptions } from "./fetchWithTimeout.js";
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @fileoverview Barrel file for network utilities.
3
+ * @module src/utils/network/index
4
+ */
5
+ export * from "./fetchWithTimeout.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @fileoverview Barrel file for the scheduling module.
3
+ * Exports the singleton schedulerService for application-wide use.
4
+ * @module src/utils/scheduling
5
+ */
6
+ export * from "./scheduler.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @fileoverview Barrel file for the scheduling module.
3
+ * Exports the singleton schedulerService for application-wide use.
4
+ * @module src/utils/scheduling
5
+ */
6
+ export * from "./scheduler.js";
@@ -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 = { ...this.defaultHtmlSanitizeConfig, ...config };
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
- options.allowedTags = [...(options.allowedTags || []), "!--"];
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
- switch (options.context) {
148
- case "html":
149
- return this.sanitizeHtml(input, {
150
- allowedTags: options.allowedTags,
151
- allowedAttributes: options.allowedAttributes
152
- ? this.convertAttributesFormat(options.allowedAttributes)
153
- : undefined,
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
- finalSanitizedPath = normalized.replace(/^(?:[A-Za-z]:)?[/\\]+/, "");
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
- let originalValueForLog = value;
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
- const lowerKey = key.toLowerCase();
432
- const isSensitive = this.sensitiveFields.some((field) => lowerKey.includes(field));
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
  }