@cyanheads/pubmed-mcp-server 1.0.12

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 (121) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +174 -0
  3. package/dist/config/index.d.ts +91 -0
  4. package/dist/config/index.js +324 -0
  5. package/dist/index.d.ts +24 -0
  6. package/dist/index.js +190 -0
  7. package/dist/mcp-server/resources/echoResource/echoResourceLogic.d.ts +79 -0
  8. package/dist/mcp-server/resources/echoResource/echoResourceLogic.js +82 -0
  9. package/dist/mcp-server/resources/echoResource/index.d.ts +13 -0
  10. package/dist/mcp-server/resources/echoResource/index.js +13 -0
  11. package/dist/mcp-server/resources/echoResource/registration.d.ts +30 -0
  12. package/dist/mcp-server/resources/echoResource/registration.js +168 -0
  13. package/dist/mcp-server/server.d.ts +28 -0
  14. package/dist/mcp-server/server.js +159 -0
  15. package/dist/mcp-server/tools/fetchPubMedContent/index.d.ts +6 -0
  16. package/dist/mcp-server/tools/fetchPubMedContent/index.js +6 -0
  17. package/dist/mcp-server/tools/fetchPubMedContent/logic.d.ts +63 -0
  18. package/dist/mcp-server/tools/fetchPubMedContent/logic.js +518 -0
  19. package/dist/mcp-server/tools/fetchPubMedContent/registration.d.ts +10 -0
  20. package/dist/mcp-server/tools/fetchPubMedContent/registration.js +37 -0
  21. package/dist/mcp-server/tools/getPubMedArticleConnections/index.d.ts +6 -0
  22. package/dist/mcp-server/tools/getPubMedArticleConnections/index.js +6 -0
  23. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.d.ts +9 -0
  24. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +288 -0
  25. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.d.ts +9 -0
  26. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +208 -0
  27. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.d.ts +15 -0
  28. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/index.js +82 -0
  29. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.d.ts +27 -0
  30. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/types.js +5 -0
  31. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.d.ts +6 -0
  32. package/dist/mcp-server/tools/getPubMedArticleConnections/logic.js +6 -0
  33. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.d.ts +36 -0
  34. package/dist/mcp-server/tools/getPubMedArticleConnections/registration.js +76 -0
  35. package/dist/mcp-server/tools/pubmedResearchAgent/index.d.ts +6 -0
  36. package/dist/mcp-server/tools/pubmedResearchAgent/index.js +6 -0
  37. package/dist/mcp-server/tools/pubmedResearchAgent/logic/index.d.ts +7 -0
  38. package/dist/mcp-server/tools/pubmedResearchAgent/logic/index.js +9 -0
  39. package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.d.ts +111 -0
  40. package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.js +149 -0
  41. package/dist/mcp-server/tools/pubmedResearchAgent/logic/outputTypes.d.ts +106 -0
  42. package/dist/mcp-server/tools/pubmedResearchAgent/logic/outputTypes.js +7 -0
  43. package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.d.ts +10 -0
  44. package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.js +251 -0
  45. package/dist/mcp-server/tools/pubmedResearchAgent/logic.d.ts +12 -0
  46. package/dist/mcp-server/tools/pubmedResearchAgent/logic.js +55 -0
  47. package/dist/mcp-server/tools/pubmedResearchAgent/registration.d.ts +10 -0
  48. package/dist/mcp-server/tools/pubmedResearchAgent/registration.js +36 -0
  49. package/dist/mcp-server/tools/searchPubMedArticles/index.d.ts +6 -0
  50. package/dist/mcp-server/tools/searchPubMedArticles/index.js +6 -0
  51. package/dist/mcp-server/tools/searchPubMedArticles/logic.d.ts +61 -0
  52. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +229 -0
  53. package/dist/mcp-server/tools/searchPubMedArticles/registration.d.ts +10 -0
  54. package/dist/mcp-server/tools/searchPubMedArticles/registration.js +38 -0
  55. package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +29 -0
  56. package/dist/mcp-server/transports/authentication/authMiddleware.js +174 -0
  57. package/dist/mcp-server/transports/httpTransport.d.ts +23 -0
  58. package/dist/mcp-server/transports/httpTransport.js +463 -0
  59. package/dist/mcp-server/transports/stdioTransport.d.ts +42 -0
  60. package/dist/mcp-server/transports/stdioTransport.js +63 -0
  61. package/dist/services/NCBI/ncbiConstants.d.ts +22 -0
  62. package/dist/services/NCBI/ncbiConstants.js +5 -0
  63. package/dist/services/NCBI/ncbiCoreApiClient.d.ts +24 -0
  64. package/dist/services/NCBI/ncbiCoreApiClient.js +118 -0
  65. package/dist/services/NCBI/ncbiRequestQueueManager.d.ts +36 -0
  66. package/dist/services/NCBI/ncbiRequestQueueManager.js +96 -0
  67. package/dist/services/NCBI/ncbiResponseHandler.d.ts +22 -0
  68. package/dist/services/NCBI/ncbiResponseHandler.js +192 -0
  69. package/dist/services/NCBI/ncbiService.d.ts +24 -0
  70. package/dist/services/NCBI/ncbiService.js +57 -0
  71. package/dist/services/index.d.ts +7 -0
  72. package/dist/services/index.js +7 -0
  73. package/dist/services/llm-providers/index.d.ts +7 -0
  74. package/dist/services/llm-providers/index.js +7 -0
  75. package/dist/services/llm-providers/llmFactory.d.ts +69 -0
  76. package/dist/services/llm-providers/llmFactory.js +132 -0
  77. package/dist/services/llm-providers/openRouter/index.d.ts +6 -0
  78. package/dist/services/llm-providers/openRouter/index.js +7 -0
  79. package/dist/services/llm-providers/openRouter/openRouterProvider.d.ts +99 -0
  80. package/dist/services/llm-providers/openRouter/openRouterProvider.js +329 -0
  81. package/dist/types-global/errors.d.ts +119 -0
  82. package/dist/types-global/errors.js +119 -0
  83. package/dist/types-global/pubmedXml.d.ts +350 -0
  84. package/dist/types-global/pubmedXml.js +7 -0
  85. package/dist/utils/index.d.ts +10 -0
  86. package/dist/utils/index.js +18 -0
  87. package/dist/utils/internal/errorHandler.d.ts +176 -0
  88. package/dist/utils/internal/errorHandler.js +335 -0
  89. package/dist/utils/internal/index.d.ts +9 -0
  90. package/dist/utils/internal/index.js +9 -0
  91. package/dist/utils/internal/logger.d.ts +141 -0
  92. package/dist/utils/internal/logger.js +426 -0
  93. package/dist/utils/internal/requestContext.d.ts +83 -0
  94. package/dist/utils/internal/requestContext.js +72 -0
  95. package/dist/utils/metrics/index.d.ts +7 -0
  96. package/dist/utils/metrics/index.js +7 -0
  97. package/dist/utils/metrics/tokenCounter.d.ts +35 -0
  98. package/dist/utils/metrics/tokenCounter.js +110 -0
  99. package/dist/utils/parsing/dateParser.d.ts +73 -0
  100. package/dist/utils/parsing/dateParser.js +107 -0
  101. package/dist/utils/parsing/index.d.ts +8 -0
  102. package/dist/utils/parsing/index.js +8 -0
  103. package/dist/utils/parsing/jsonParser.d.ts +82 -0
  104. package/dist/utils/parsing/jsonParser.js +126 -0
  105. package/dist/utils/parsing/ncbi-parsing/eSummaryResultParser.d.ts +32 -0
  106. package/dist/utils/parsing/ncbi-parsing/eSummaryResultParser.js +295 -0
  107. package/dist/utils/parsing/ncbi-parsing/index.d.ts +8 -0
  108. package/dist/utils/parsing/ncbi-parsing/index.js +8 -0
  109. package/dist/utils/parsing/ncbi-parsing/pubmedArticleStructureParser.d.ts +70 -0
  110. package/dist/utils/parsing/ncbi-parsing/pubmedArticleStructureParser.js +229 -0
  111. package/dist/utils/parsing/ncbi-parsing/xmlGenericHelpers.d.ts +31 -0
  112. package/dist/utils/parsing/ncbi-parsing/xmlGenericHelpers.js +76 -0
  113. package/dist/utils/security/idGenerator.d.ts +134 -0
  114. package/dist/utils/security/idGenerator.js +198 -0
  115. package/dist/utils/security/index.d.ts +9 -0
  116. package/dist/utils/security/index.js +9 -0
  117. package/dist/utils/security/rateLimiter.d.ts +108 -0
  118. package/dist/utils/security/rateLimiter.js +179 -0
  119. package/dist/utils/security/sanitization.d.ts +173 -0
  120. package/dist/utils/security/sanitization.js +439 -0
  121. package/package.json +97 -0
@@ -0,0 +1,439 @@
1
+ /**
2
+ * @fileoverview Provides a comprehensive `Sanitization` class for various input cleaning and validation tasks.
3
+ * This module includes utilities for sanitizing HTML, strings, URLs, file paths, JSON, numbers,
4
+ * and for redacting sensitive information from data intended for logging.
5
+ * @module src/utils/security/sanitization
6
+ */
7
+ import path from "path";
8
+ import sanitizeHtml from "sanitize-html";
9
+ import validator from "validator";
10
+ import { BaseErrorCode, McpError } from "../../types-global/errors.js";
11
+ import { logger, requestContextService } from "../index.js";
12
+ /**
13
+ * A singleton class providing various methods for input sanitization.
14
+ * Aims to protect against common vulnerabilities like XSS and path traversal.
15
+ */
16
+ export class Sanitization {
17
+ /** @private */
18
+ constructor() {
19
+ /**
20
+ * Default list of field names considered sensitive for log redaction.
21
+ * Case-insensitive matching is applied.
22
+ * @private
23
+ */
24
+ this.sensitiveFields = [
25
+ "password",
26
+ "token",
27
+ "secret",
28
+ "key",
29
+ "apiKey",
30
+ "auth",
31
+ "credential",
32
+ "jwt",
33
+ "ssn",
34
+ "credit",
35
+ "card",
36
+ "cvv",
37
+ "authorization",
38
+ ];
39
+ /**
40
+ * Default configuration for HTML sanitization.
41
+ * @private
42
+ */
43
+ this.defaultHtmlSanitizeConfig = {
44
+ allowedTags: [
45
+ "h1",
46
+ "h2",
47
+ "h3",
48
+ "h4",
49
+ "h5",
50
+ "h6",
51
+ "p",
52
+ "a",
53
+ "ul",
54
+ "ol",
55
+ "li",
56
+ "b",
57
+ "i",
58
+ "strong",
59
+ "em",
60
+ "strike",
61
+ "code",
62
+ "hr",
63
+ "br",
64
+ "div",
65
+ "table",
66
+ "thead",
67
+ "tbody",
68
+ "tr",
69
+ "th",
70
+ "td",
71
+ "pre",
72
+ ],
73
+ allowedAttributes: {
74
+ a: ["href", "name", "target"],
75
+ img: ["src", "alt", "title", "width", "height"],
76
+ "*": ["class", "id", "style"],
77
+ },
78
+ preserveComments: false,
79
+ };
80
+ }
81
+ /**
82
+ * Retrieves the singleton instance of the `Sanitization` class.
83
+ * @returns The singleton `Sanitization` instance.
84
+ */
85
+ static getInstance() {
86
+ if (!Sanitization.instance) {
87
+ Sanitization.instance = new Sanitization();
88
+ }
89
+ return Sanitization.instance;
90
+ }
91
+ /**
92
+ * Sets or extends the list of sensitive field names for log sanitization.
93
+ * @param fields - An array of field names to add to the sensitive list.
94
+ */
95
+ setSensitiveFields(fields) {
96
+ this.sensitiveFields = [
97
+ ...new Set([
98
+ ...this.sensitiveFields,
99
+ ...fields.map((f) => f.toLowerCase()),
100
+ ]),
101
+ ];
102
+ const logContext = requestContextService.createRequestContext({
103
+ operation: "Sanitization.setSensitiveFields",
104
+ newSensitiveFieldCount: this.sensitiveFields.length,
105
+ });
106
+ logger.debug("Updated sensitive fields list for log sanitization", logContext);
107
+ }
108
+ /**
109
+ * Gets a copy of the current list of sensitive field names.
110
+ * @returns An array of sensitive field names.
111
+ */
112
+ getSensitiveFields() {
113
+ return [...this.sensitiveFields];
114
+ }
115
+ /**
116
+ * Sanitizes an HTML string by removing potentially malicious tags and attributes.
117
+ * @param input - The HTML string to sanitize.
118
+ * @param config - Optional custom configuration for `sanitize-html`.
119
+ * @returns The sanitized HTML string. Returns an empty string if input is falsy.
120
+ */
121
+ sanitizeHtml(input, config) {
122
+ if (!input)
123
+ return "";
124
+ const effectiveConfig = { ...this.defaultHtmlSanitizeConfig, ...config };
125
+ const options = {
126
+ allowedTags: effectiveConfig.allowedTags,
127
+ allowedAttributes: effectiveConfig.allowedAttributes,
128
+ transformTags: effectiveConfig.transformTags,
129
+ };
130
+ if (effectiveConfig.preserveComments) {
131
+ options.allowedTags = [...(options.allowedTags || []), "!--"];
132
+ }
133
+ return sanitizeHtml(input, options);
134
+ }
135
+ /**
136
+ * Sanitizes a string based on its intended context (e.g., HTML, URL, text).
137
+ * **Important:** `context: 'javascript'` is disallowed due to security risks.
138
+ *
139
+ * @param input - The string to sanitize.
140
+ * @param options - Options specifying the sanitization context.
141
+ * @returns The sanitized string. Returns an empty string if input is falsy.
142
+ * @throws {McpError} If `options.context` is 'javascript', or URL validation fails.
143
+ */
144
+ sanitizeString(input, options = {}) {
145
+ if (!input)
146
+ 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
+ });
155
+ case "attribute":
156
+ return sanitizeHtml(input, { allowedTags: [], allowedAttributes: {} });
157
+ case "url":
158
+ if (!validator.isURL(input, {
159
+ protocols: ["http", "https"],
160
+ require_protocol: true,
161
+ require_host: true,
162
+ })) {
163
+ logger.warning("Potentially invalid URL detected during string sanitization (context: url)", requestContextService.createRequestContext({
164
+ operation: "Sanitization.sanitizeString.urlWarning",
165
+ invalidUrlAttempt: input,
166
+ }));
167
+ return "";
168
+ }
169
+ return validator.trim(input);
170
+ case "javascript":
171
+ logger.error("Attempted JavaScript sanitization via sanitizeString, which is disallowed.", requestContextService.createRequestContext({
172
+ operation: "Sanitization.sanitizeString.jsAttempt",
173
+ inputSnippet: input.substring(0, 50),
174
+ }));
175
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "JavaScript sanitization is not supported through sanitizeString due to security risks.");
176
+ case "text":
177
+ default:
178
+ return sanitizeHtml(input, { allowedTags: [], allowedAttributes: {} });
179
+ }
180
+ }
181
+ /**
182
+ * Converts attribute format for `sanitizeHtml`.
183
+ * @param attrs - Attributes in `{ tagName: ['attr1'] }` format.
184
+ * @returns Attributes in `sanitize-html` expected format.
185
+ * @private
186
+ */
187
+ convertAttributesFormat(attrs) {
188
+ return attrs;
189
+ }
190
+ /**
191
+ * Sanitizes a URL string by validating its format and protocol.
192
+ * @param input - The URL string to sanitize.
193
+ * @param allowedProtocols - Array of allowed URL protocols. Default: `['http', 'https']`.
194
+ * @returns The sanitized and trimmed URL string.
195
+ * @throws {McpError} If the URL is invalid or uses a disallowed protocol.
196
+ */
197
+ sanitizeUrl(input, allowedProtocols = ["http", "https"]) {
198
+ try {
199
+ const trimmedInput = input.trim();
200
+ if (!validator.isURL(trimmedInput, {
201
+ protocols: allowedProtocols,
202
+ require_protocol: true,
203
+ require_host: true,
204
+ })) {
205
+ throw new Error("Invalid URL format or protocol not in allowed list.");
206
+ }
207
+ if (trimmedInput.toLowerCase().startsWith("javascript:")) {
208
+ throw new Error("JavaScript pseudo-protocol is not allowed in URLs.");
209
+ }
210
+ return trimmedInput;
211
+ }
212
+ catch (error) {
213
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, error instanceof Error
214
+ ? error.message
215
+ : "Invalid or unsafe URL provided.", { input });
216
+ }
217
+ }
218
+ /**
219
+ * Sanitizes a file path to prevent path traversal and normalize format.
220
+ * @param input - The file path string to sanitize.
221
+ * @param options - Options to control sanitization behavior.
222
+ * @returns An object with the sanitized path and sanitization metadata.
223
+ * @throws {McpError} If the path is invalid or unsafe.
224
+ */
225
+ sanitizePath(input, options = {}) {
226
+ const originalInput = input;
227
+ const effectiveOptions = {
228
+ toPosix: options.toPosix ?? false,
229
+ allowAbsolute: options.allowAbsolute ?? false,
230
+ rootDir: options.rootDir ? path.resolve(options.rootDir) : undefined,
231
+ };
232
+ let wasAbsoluteInitially = false;
233
+ let convertedToRelative = false;
234
+ try {
235
+ if (!input || typeof input !== "string")
236
+ throw new Error("Invalid path input: must be a non-empty string.");
237
+ if (input.includes("\0"))
238
+ throw new Error("Path contains null byte, which is disallowed.");
239
+ let normalized = path.normalize(input);
240
+ wasAbsoluteInitially = path.isAbsolute(normalized);
241
+ if (effectiveOptions.toPosix) {
242
+ normalized = normalized.replace(/\\/g, "/");
243
+ }
244
+ let finalSanitizedPath;
245
+ if (effectiveOptions.rootDir) {
246
+ const fullPath = path.resolve(effectiveOptions.rootDir, normalized);
247
+ if (!fullPath.startsWith(effectiveOptions.rootDir + path.sep) &&
248
+ fullPath !== effectiveOptions.rootDir) {
249
+ throw new Error("Path traversal detected: attempts to escape the defined root directory.");
250
+ }
251
+ finalSanitizedPath = path.relative(effectiveOptions.rootDir, fullPath);
252
+ finalSanitizedPath =
253
+ finalSanitizedPath === "" ? "." : finalSanitizedPath;
254
+ if (path.isAbsolute(finalSanitizedPath) &&
255
+ !effectiveOptions.allowAbsolute) {
256
+ throw new Error("Path resolved to absolute outside root when absolute paths are disallowed.");
257
+ }
258
+ }
259
+ else {
260
+ if (path.isAbsolute(normalized)) {
261
+ if (!effectiveOptions.allowAbsolute) {
262
+ finalSanitizedPath = normalized.replace(/^(?:[A-Za-z]:)?[/\\]+/, "");
263
+ convertedToRelative = true;
264
+ }
265
+ else {
266
+ finalSanitizedPath = normalized;
267
+ }
268
+ }
269
+ else {
270
+ const resolvedAgainstCwd = path.resolve(normalized);
271
+ const currentWorkingDir = path.resolve(".");
272
+ if (!resolvedAgainstCwd.startsWith(currentWorkingDir + path.sep) &&
273
+ resolvedAgainstCwd !== currentWorkingDir) {
274
+ throw new Error("Relative path traversal detected (escapes current working directory context).");
275
+ }
276
+ finalSanitizedPath = normalized;
277
+ }
278
+ }
279
+ return {
280
+ sanitizedPath: finalSanitizedPath,
281
+ originalInput,
282
+ wasAbsolute: wasAbsoluteInitially,
283
+ convertedToRelative: wasAbsoluteInitially &&
284
+ !path.isAbsolute(finalSanitizedPath) &&
285
+ !effectiveOptions.allowAbsolute,
286
+ optionsUsed: effectiveOptions,
287
+ };
288
+ }
289
+ catch (error) {
290
+ logger.warning("Path sanitization error", requestContextService.createRequestContext({
291
+ operation: "Sanitization.sanitizePath.error",
292
+ originalPathInput: originalInput,
293
+ pathOptionsUsed: effectiveOptions,
294
+ errorMessage: error instanceof Error ? error.message : String(error),
295
+ }));
296
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, error instanceof Error
297
+ ? error.message
298
+ : "Invalid or unsafe path provided.", { input: originalInput });
299
+ }
300
+ }
301
+ /**
302
+ * Sanitizes a JSON string by parsing it to validate its format.
303
+ * Optionally checks if the JSON string exceeds a maximum allowed size.
304
+ * @template T The expected type of the parsed JSON object. Defaults to `unknown`.
305
+ * @param input - The JSON string to sanitize/validate.
306
+ * @param maxSize - Optional maximum allowed size of the JSON string in bytes.
307
+ * @returns The parsed JavaScript object.
308
+ * @throws {McpError} If input is not a string, too large, or invalid JSON.
309
+ */
310
+ sanitizeJson(input, maxSize) {
311
+ try {
312
+ if (typeof input !== "string")
313
+ throw new Error("Invalid input: expected a JSON string.");
314
+ if (maxSize !== undefined && Buffer.byteLength(input, "utf8") > maxSize) {
315
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `JSON string exceeds maximum allowed size of ${maxSize} bytes.`, { actualSize: Buffer.byteLength(input, "utf8"), maxSize });
316
+ }
317
+ return JSON.parse(input);
318
+ }
319
+ catch (error) {
320
+ if (error instanceof McpError)
321
+ throw error;
322
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, error instanceof Error ? error.message : "Invalid JSON format.", {
323
+ inputPreview: input.length > 100 ? `${input.substring(0, 100)}...` : input,
324
+ });
325
+ }
326
+ }
327
+ /**
328
+ * Validates and sanitizes a numeric input, converting strings to numbers.
329
+ * Clamps the number to `min`/`max` if provided.
330
+ * @param input - The number or string to validate and sanitize.
331
+ * @param min - Minimum allowed value (inclusive).
332
+ * @param max - Maximum allowed value (inclusive).
333
+ * @returns The sanitized (and potentially clamped) number.
334
+ * @throws {McpError} If input is not a valid number, NaN, or Infinity.
335
+ */
336
+ sanitizeNumber(input, min, max) {
337
+ let value;
338
+ if (typeof input === "string") {
339
+ const trimmedInput = input.trim();
340
+ if (trimmedInput === "" || !validator.isNumeric(trimmedInput)) {
341
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Invalid number format: input is empty or not numeric.", { input });
342
+ }
343
+ value = parseFloat(trimmedInput);
344
+ }
345
+ else if (typeof input === "number") {
346
+ value = input;
347
+ }
348
+ else {
349
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Invalid input type: expected number or string.", { input: String(input) });
350
+ }
351
+ if (isNaN(value) || !isFinite(value)) {
352
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Invalid number value (NaN or Infinity).", { input });
353
+ }
354
+ let clamped = false;
355
+ let originalValueForLog = value;
356
+ if (min !== undefined && value < min) {
357
+ value = min;
358
+ clamped = true;
359
+ }
360
+ if (max !== undefined && value > max) {
361
+ value = max;
362
+ clamped = true;
363
+ }
364
+ if (clamped) {
365
+ logger.debug("Number clamped to range.", requestContextService.createRequestContext({
366
+ operation: "Sanitization.sanitizeNumber.clamped",
367
+ originalInput: String(input),
368
+ parsedValue: originalValueForLog,
369
+ minValue: min,
370
+ maxValue: max,
371
+ clampedValue: value,
372
+ }));
373
+ }
374
+ return value;
375
+ }
376
+ /**
377
+ * Sanitizes input for logging by redacting sensitive fields.
378
+ * Creates a deep clone and replaces values of fields matching `this.sensitiveFields`
379
+ * (case-insensitive substring match) with "[REDACTED]".
380
+ * @param input - The input data to sanitize for logging.
381
+ * @returns A sanitized (deep cloned) version of the input, safe for logging.
382
+ * Returns original input if not object/array, or "[Log Sanitization Failed]" on error.
383
+ */
384
+ sanitizeForLogging(input) {
385
+ try {
386
+ if (!input || typeof input !== "object")
387
+ return input;
388
+ const clonedInput = typeof structuredClone === "function"
389
+ ? structuredClone(input)
390
+ : JSON.parse(JSON.stringify(input));
391
+ this.redactSensitiveFields(clonedInput);
392
+ return clonedInput;
393
+ }
394
+ catch (error) {
395
+ logger.error("Error during log sanitization, returning placeholder.", requestContextService.createRequestContext({
396
+ operation: "Sanitization.sanitizeForLogging.error",
397
+ errorMessage: error instanceof Error ? error.message : String(error),
398
+ }));
399
+ return "[Log Sanitization Failed]";
400
+ }
401
+ }
402
+ /**
403
+ * Recursively redacts sensitive fields in an object or array in place.
404
+ * @param obj - The object or array to redact.
405
+ * @private
406
+ */
407
+ redactSensitiveFields(obj) {
408
+ if (!obj || typeof obj !== "object")
409
+ return;
410
+ if (Array.isArray(obj)) {
411
+ obj.forEach((item) => this.redactSensitiveFields(item));
412
+ return;
413
+ }
414
+ for (const key in obj) {
415
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
416
+ const value = obj[key];
417
+ const lowerKey = key.toLowerCase();
418
+ const isSensitive = this.sensitiveFields.some((field) => lowerKey.includes(field));
419
+ if (isSensitive) {
420
+ obj[key] = "[REDACTED]";
421
+ }
422
+ else if (value && typeof value === "object") {
423
+ this.redactSensitiveFields(value);
424
+ }
425
+ }
426
+ }
427
+ }
428
+ }
429
+ /**
430
+ * Singleton instance of the `Sanitization` class.
431
+ * Use this for all input sanitization tasks.
432
+ */
433
+ export const sanitization = Sanitization.getInstance();
434
+ /**
435
+ * Convenience function calling `sanitization.sanitizeForLogging`.
436
+ * @param input - The input data to sanitize.
437
+ * @returns A sanitized version of the input, safe for logging.
438
+ */
439
+ export const sanitizeInputForLogging = (input) => sanitization.sanitizeForLogging(input);
package/package.json ADDED
@@ -0,0 +1,97 @@
1
+ {
2
+ "name": "@cyanheads/pubmed-mcp-server",
3
+ "version": "1.0.12",
4
+ "description": "A Model Context Protocol (MCP) server enabling AI agents to intelligently search, retrieve, and analyze biomedical literature from PubMed via NCBI E-utilities. Built on the mcp-ts-template for robust, production-ready performance.",
5
+ "main": "dist/index.js",
6
+ "files": [
7
+ "dist"
8
+ ],
9
+ "bin": {
10
+ "pubmed-mcp-server": "dist/index.js"
11
+ },
12
+ "exports": "./dist/index.js",
13
+ "type": "module",
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "git+https://github.com/cyanheads/pubmed-mcp-server.git"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/cyanheads/pubmed-mcp-server/issues"
20
+ },
21
+ "homepage": "https://github.com/cyanheads/pubmed-mcp-server#readme",
22
+ "scripts": {
23
+ "build": "tsc && node --loader ts-node/esm scripts/make-executable.ts dist/index.js",
24
+ "start": "node dist/index.js",
25
+ "start:stdio": "MCP_LOG_LEVEL=debug MCP_TRANSPORT_TYPE=stdio node dist/index.js",
26
+ "start:http": "MCP_LOG_LEVEL=debug MCP_TRANSPORT_TYPE=http node dist/index.js",
27
+ "rebuild": "ts-node --esm scripts/clean.ts && npm run build",
28
+ "docs:generate": "typedoc --tsconfig ./tsconfig.typedoc.json",
29
+ "tree": "ts-node --esm scripts/tree.ts",
30
+ "fetch-spec": "ts-node --esm scripts/fetch-openapi-spec.ts",
31
+ "format": "prettier --write \"**/*.{ts,js,json,md,html,css}\"",
32
+ "inspector": "mcp-inspector --config mcp.json --server pubmed-mcp-server",
33
+ "start:client-cli": "node dist/mcp-client/cli/mcp-client-cli.js"
34
+ },
35
+ "dependencies": {
36
+ "axios": "^1.9.0",
37
+ "fast-xml-parser": "^5.2.3",
38
+ "@google/genai": "^1.0.1",
39
+ "@modelcontextprotocol/sdk": "^1.12.0",
40
+ "@types/jsonwebtoken": "^9.0.9",
41
+ "@types/node": "^22.15.21",
42
+ "@types/sanitize-html": "^2.16.0",
43
+ "@types/validator": "13.15.1",
44
+ "chalk": "^5.4.1",
45
+ "chrono-node": "^2.8.0",
46
+ "cli-table3": "^0.6.5",
47
+ "dotenv": "^16.5.0",
48
+ "express": "^5.1.0",
49
+ "ignore": "^7.0.4",
50
+ "jsonwebtoken": "^9.0.2",
51
+ "openai": "^4.103.0",
52
+ "partial-json": "^0.1.7",
53
+ "sanitize-html": "^2.17.0",
54
+ "tiktoken": "^1.0.21",
55
+ "ts-node": "^10.9.2",
56
+ "typescript": "^5.8.3",
57
+ "validator": "13.15.0",
58
+ "winston": "^3.17.0",
59
+ "winston-daily-rotate-file": "^5.0.0",
60
+ "yargs": "^17.7.2",
61
+ "zod": "^3.25.28"
62
+ },
63
+ "keywords": [
64
+ "mcp",
65
+ "model-context-protocol",
66
+ "ai-agent",
67
+ "llm-integration",
68
+ "pubmed",
69
+ "ncbi",
70
+ "e-utilities",
71
+ "biomedical-research",
72
+ "scientific-literature",
73
+ "api-server",
74
+ "typescript",
75
+ "node",
76
+ "ai-tools",
77
+ "bioinformatics",
78
+ "health-tech",
79
+ "literature-search",
80
+ "pubmed-api"
81
+ ],
82
+ "author": "cyanheads <casey@caseyjhand.com> (https://github.com/cyanheads/pubmed-mcp-server#readme)",
83
+ "license": "Apache-2.0",
84
+ "publishConfig": {
85
+ "access": "public"
86
+ },
87
+ "engines": {
88
+ "node": ">=16.0.0"
89
+ },
90
+ "devDependencies": {
91
+ "@types/express": "^5.0.2",
92
+ "@types/js-yaml": "^4.0.9",
93
+ "js-yaml": "^4.1.0",
94
+ "prettier": "^3.5.3",
95
+ "typedoc": "^0.28.4"
96
+ }
97
+ }