@cyanheads/pubmed-mcp-server 1.1.1 → 1.1.2

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.
@@ -25,8 +25,10 @@ export declare const config: {
25
25
  mcpServerVersion: string;
26
26
  /** Logging level. From `MCP_LOG_LEVEL` env var. Default: "debug". */
27
27
  logLevel: string;
28
- /** Absolute path to the logs directory. From `LOGS_DIR` env var. */
29
- logsPath: string;
28
+ /** Defines the logging output mode ('file' or 'stdout'). From `LOG_OUTPUT_MODE`. */
29
+ logOutputMode: "file" | "stdout";
30
+ /** Absolute path to the logs directory (if logOutputMode is 'file'). From `LOGS_DIR`. */
31
+ logsPath: string | null;
30
32
  /** Runtime environment. From `NODE_ENV` env var. Default: "development". */
31
33
  environment: string;
32
34
  /** MCP transport type ('stdio' or 'http'). From `MCP_TRANSPORT_TYPE` env var. Default: "stdio". */
@@ -80,6 +80,8 @@ const EnvSchema = z.object({
80
80
  MCP_LOG_LEVEL: z.string().default("debug"),
81
81
  /** Directory for log files. Defaults to "logs" in project root. */
82
82
  LOGS_DIR: z.string().default(path.join(projectRoot, "logs")),
83
+ /** Defines the logging output mode. "file" for logs in LOGS_DIR, "stdout" for console logging. */
84
+ LOG_OUTPUT_MODE: z.enum(["file", "stdout"]).default("file"),
83
85
  /** Runtime environment (e.g., "development", "production"). Default: "development". */
84
86
  NODE_ENV: z.string().default("development"),
85
87
  /** MCP communication transport ("stdio" or "http"). Default: "stdio". */
@@ -209,12 +211,15 @@ const ensureDirectory = (dirPath, rootDir, dirName) => {
209
211
  };
210
212
  // --- End Directory Ensurance Function ---
211
213
  // --- Logs Directory Handling ---
212
- const validatedLogsPath = ensureDirectory(env.LOGS_DIR, projectRoot, "logs");
213
- if (!validatedLogsPath) {
214
- if (process.stdout.isTTY) {
215
- console.error("FATAL: Logs directory configuration is invalid or could not be created. Please check permissions and path. Exiting.");
214
+ let validatedLogsPath = null;
215
+ if (env.LOG_OUTPUT_MODE === "file") {
216
+ validatedLogsPath = ensureDirectory(env.LOGS_DIR, projectRoot, "logs");
217
+ if (!validatedLogsPath) {
218
+ if (process.stdout.isTTY) {
219
+ console.error("FATAL: Log mode is 'file' but logs directory is invalid or could not be created. Please check LOGS_DIR, permissions, and path. Exiting.");
220
+ }
221
+ process.exit(1); // Exit if file logging is configured but directory is not usable
216
222
  }
217
- process.exit(1); // Exit if logs directory is not usable
218
223
  }
219
224
  // --- End Logs Directory Handling ---
220
225
  /**
@@ -228,7 +233,9 @@ export const config = {
228
233
  mcpServerVersion: env.MCP_SERVER_VERSION || pkg.version,
229
234
  /** Logging level. From `MCP_LOG_LEVEL` env var. Default: "debug". */
230
235
  logLevel: env.MCP_LOG_LEVEL,
231
- /** Absolute path to the logs directory. From `LOGS_DIR` env var. */
236
+ /** Defines the logging output mode ('file' or 'stdout'). From `LOG_OUTPUT_MODE`. */
237
+ logOutputMode: env.LOG_OUTPUT_MODE,
238
+ /** Absolute path to the logs directory (if logOutputMode is 'file'). From `LOGS_DIR`. */
232
239
  logsPath: validatedLogsPath,
233
240
  /** Runtime environment. From `NODE_ENV` env var. Default: "development". */
234
241
  environment: env.NODE_ENV,
@@ -83,7 +83,8 @@ export async function generatePubMedChartLogic(input, parentRequestContext) {
83
83
  input: sanitizeInputForLogging(input),
84
84
  });
85
85
  logger.info(`Executing 'generate_pubmed_chart'. Chart type: ${input.chartType}, Output format: ${input.outputFormat}`, operationContext);
86
- if (input.outputFormat !== "png") { // Changed from svg to png
86
+ if (input.outputFormat !== "png") {
87
+ // Changed from svg to png
87
88
  const unsupportedFormatError = new McpError(BaseErrorCode.VALIDATION_ERROR, `Unsupported output format: ${input.outputFormat}. Currently, only 'png' is supported.`, // Changed message
88
89
  { requestedFormat: input.outputFormat });
89
90
  logger.warning(unsupportedFormatError.message, operationContext);
@@ -35,9 +35,7 @@ function removeEmptyObjectsRecursively(obj) {
35
35
  }
36
36
  if (Array.isArray(obj)) {
37
37
  // If it's an array, recurse on each element and filter out empty objects/arrays
38
- const newArr = obj
39
- .map(removeEmptyObjectsRecursively)
40
- .filter((item) => {
38
+ const newArr = obj.map(removeEmptyObjectsRecursively).filter((item) => {
41
39
  if (item === null || item === undefined)
42
40
  return false;
43
41
  if (Array.isArray(item) && item.length === 0)
@@ -35,11 +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.
41
38
  /**
42
- * Creates the Winston console log format.
39
+ * Creates the Winston console log format for interactive TTY sessions.
43
40
  * @returns The Winston log format for console output.
44
41
  * @private
45
42
  */
@@ -103,47 +100,45 @@ export class Logger {
103
100
  });
104
101
  return;
105
102
  }
106
- // Set initialized to true at the beginning of the initialization process.
107
103
  this.initialized = true;
108
104
  this.currentMcpLevel = level;
109
105
  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.
114
- const fileFormat = winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json());
115
106
  const transports = [];
116
- const fileTransportOptions = {
117
- format: fileFormat,
118
- maxsize: this.LOG_FILE_MAX_SIZE,
119
- maxFiles: this.LOG_MAX_FILES,
120
- tailable: true,
121
- };
122
- if (isLogsDirSafe) {
123
- transports.push(new winston.transports.File({
124
- filename: path.join(resolvedLogsDir, "error.log"),
125
- level: "error",
126
- ...fileTransportOptions,
127
- }), new winston.transports.File({
128
- filename: path.join(resolvedLogsDir, "warn.log"),
129
- level: "warn",
130
- ...fileTransportOptions,
131
- }), new winston.transports.File({
132
- filename: path.join(resolvedLogsDir, "info.log"),
133
- level: "info",
134
- ...fileTransportOptions,
135
- }), new winston.transports.File({
136
- filename: path.join(resolvedLogsDir, "debug.log"),
137
- level: "debug",
138
- ...fileTransportOptions,
139
- }), new winston.transports.File({
140
- filename: path.join(resolvedLogsDir, "combined.log"),
141
- ...fileTransportOptions,
107
+ if (config.logOutputMode === "stdout") {
108
+ transports.push(new winston.transports.Console({
109
+ format: winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json()),
142
110
  }));
143
111
  }
144
112
  else {
145
- if (process.stdout.isTTY) {
146
- console.warn("File logging disabled as logsPath is not configured or invalid.");
113
+ const resolvedLogsDir = config.logsPath;
114
+ if (resolvedLogsDir) {
115
+ const fileFormat = winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json());
116
+ const fileTransportOptions = {
117
+ format: fileFormat,
118
+ maxsize: this.LOG_FILE_MAX_SIZE,
119
+ maxFiles: this.LOG_MAX_FILES,
120
+ tailable: true,
121
+ };
122
+ transports.push(new winston.transports.File({
123
+ filename: path.join(resolvedLogsDir, "error.log"),
124
+ level: "error",
125
+ ...fileTransportOptions,
126
+ }), new winston.transports.File({
127
+ filename: path.join(resolvedLogsDir, "warn.log"),
128
+ level: "warn",
129
+ ...fileTransportOptions,
130
+ }), new winston.transports.File({
131
+ filename: path.join(resolvedLogsDir, "info.log"),
132
+ level: "info",
133
+ ...fileTransportOptions,
134
+ }), new winston.transports.File({
135
+ filename: path.join(resolvedLogsDir, "debug.log"),
136
+ level: "debug",
137
+ ...fileTransportOptions,
138
+ }), new winston.transports.File({
139
+ filename: path.join(resolvedLogsDir, "combined.log"),
140
+ ...fileTransportOptions,
141
+ }));
147
142
  }
148
143
  }
149
144
  this.winstonLogger = winston.createLogger({
@@ -151,23 +146,20 @@ export class Logger {
151
146
  transports,
152
147
  exitOnError: false,
153
148
  });
154
- // Configure console transport after Winston logger is created
155
149
  const consoleStatus = this._configureConsoleTransport();
156
150
  const initialContext = {
157
151
  loggerSetup: true,
158
152
  requestId: "logger-init-deferred",
159
153
  timestamp: new Date().toISOString(),
160
154
  };
161
- // Removed logging of logsDirCreatedMessage as it's no longer set
162
155
  if (consoleStatus.message) {
163
156
  this.info(consoleStatus.message, initialContext);
164
157
  }
165
- this.initialized = true; // Ensure this is set after successful setup
166
- this.info(`Logger initialized. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}. Console logging: ${consoleStatus.enabled ? "enabled" : "disabled"}`, {
158
+ this.info(`Logger initialized. Mode: ${config.logOutputMode}. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}.`, {
167
159
  loggerSetup: true,
168
160
  requestId: "logger-post-init",
169
161
  timestamp: new Date().toISOString(),
170
- logsPathUsed: resolvedLogsDir,
162
+ logsPathUsed: config.logsPath,
171
163
  });
172
164
  }
173
165
  /**
@@ -207,7 +199,6 @@ export class Logger {
207
199
  this.currentMcpLevel = newLevel;
208
200
  this.currentWinstonLevel = mcpToWinstonLevel[newLevel];
209
201
  if (this.winstonLogger) {
210
- // Ensure winstonLogger is defined
211
202
  this.winstonLogger.level = this.currentWinstonLevel;
212
203
  }
213
204
  const consoleStatus = this._configureConsoleTransport();
@@ -226,6 +217,12 @@ export class Logger {
226
217
  * @private
227
218
  */
228
219
  _configureConsoleTransport() {
220
+ if (config.logOutputMode === "stdout") {
221
+ return {
222
+ enabled: true,
223
+ message: "Stdout logging is enabled by configuration.",
224
+ };
225
+ }
229
226
  if (!this.winstonLogger) {
230
227
  return {
231
228
  enabled: false,
@@ -238,17 +235,19 @@ export class Logger {
238
235
  if (shouldHaveConsole && !consoleTransport) {
239
236
  const consoleFormat = createWinstonConsoleFormat();
240
237
  this.winstonLogger.add(new winston.transports.Console({
241
- level: "debug", // Console always logs debug if enabled
238
+ level: "debug",
242
239
  format: consoleFormat,
243
240
  }));
244
- message = "Console logging enabled (level: debug, stdout is TTY).";
241
+ message =
242
+ "Interactive console logging enabled (level: debug, stdout is TTY).";
245
243
  }
246
244
  else if (!shouldHaveConsole && consoleTransport) {
247
245
  this.winstonLogger.remove(consoleTransport);
248
- message = "Console logging disabled (level not debug or stdout not TTY).";
246
+ message =
247
+ "Interactive console logging disabled (level not debug or stdout not TTY).";
249
248
  }
250
249
  else {
251
- message = "Console logging status unchanged.";
250
+ message = "Interactive console logging status unchanged.";
252
251
  }
253
252
  return { enabled: shouldHaveConsole, message };
254
253
  }
@@ -288,7 +287,7 @@ export class Logger {
288
287
  if (!this.ensureInitialized())
289
288
  return;
290
289
  if (mcpLevelSeverity[level] > mcpLevelSeverity[this.currentMcpLevel]) {
291
- return; // Do not log if message level is less severe than currentMcpLevel
290
+ return;
292
291
  }
293
292
  const logData = { ...context };
294
293
  const winstonLevel = mcpToWinstonLevel[level];
@@ -304,7 +303,6 @@ export class Logger {
304
303
  mcpDataPayload.context = context;
305
304
  if (error) {
306
305
  mcpDataPayload.error = { message: error.message };
307
- // Include stack trace in debug mode for MCP notifications, truncated for brevity
308
306
  if (this.currentMcpLevel === "debug" && error.stack) {
309
307
  mcpDataPayload.error.stack = error.stack.substring(0, this.MCP_NOTIFICATION_STACK_TRACE_MAX_LENGTH);
310
308
  }
@@ -321,7 +319,7 @@ export class Logger {
321
319
  originalLevel: level,
322
320
  originalMessage: msg,
323
321
  sendError: errorMessage,
324
- mcpPayload: JSON.stringify(mcpDataPayload).substring(0, 500), // Log a preview
322
+ mcpPayload: JSON.stringify(mcpDataPayload).substring(0, 500),
325
323
  };
326
324
  this.winstonLogger.error("Failed to send MCP log notification", internalErrorContext);
327
325
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyanheads/pubmed-mcp-server",
3
- "version": "1.1.1",
3
+ "version": "1.1.2",
4
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
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -29,8 +29,7 @@
29
29
  "tree": "ts-node --esm scripts/tree.ts",
30
30
  "fetch-spec": "ts-node --esm scripts/fetch-openapi-spec.ts",
31
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"
32
+ "inspector": "mcp-inspector --config mcp.json --server pubmed-mcp-server"
34
33
  },
35
34
  "dependencies": {
36
35
  "@hono/node-server": "^1.14.4",