@cyanheads/pubmed-mcp-server 1.2.4 → 1.3.1

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 (92) hide show
  1. package/README.md +90 -91
  2. package/dist/config/index.d.ts +13 -52
  3. package/dist/config/index.js +61 -204
  4. package/dist/index.js +30 -60
  5. package/dist/mcp-server/server.d.ts +0 -5
  6. package/dist/mcp-server/server.js +18 -34
  7. package/dist/mcp-server/tools/fetchPubMedContent/logic.js +2 -2
  8. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +2 -2
  9. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +3 -3
  10. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +2 -2
  11. package/dist/mcp-server/transports/auth/authFactory.d.ts +10 -0
  12. package/dist/mcp-server/transports/auth/authFactory.js +41 -0
  13. package/dist/mcp-server/transports/auth/authMiddleware.d.ts +19 -0
  14. package/dist/mcp-server/transports/auth/authMiddleware.js +57 -0
  15. package/dist/mcp-server/transports/auth/index.d.ts +8 -5
  16. package/dist/mcp-server/transports/auth/index.js +6 -4
  17. package/dist/mcp-server/transports/auth/{core → lib}/authTypes.d.ts +0 -5
  18. package/dist/mcp-server/transports/auth/lib/authTypes.js +8 -0
  19. package/dist/mcp-server/transports/auth/{core → lib}/authUtils.js +21 -14
  20. package/dist/mcp-server/transports/auth/strategies/authStrategy.d.ts +17 -0
  21. package/dist/mcp-server/transports/auth/strategies/authStrategy.js +1 -0
  22. package/dist/mcp-server/transports/auth/strategies/jwtStrategy.d.ts +7 -0
  23. package/dist/mcp-server/transports/auth/strategies/jwtStrategy.js +112 -0
  24. package/dist/mcp-server/transports/auth/strategies/oauthStrategy.d.ts +7 -0
  25. package/dist/mcp-server/transports/auth/strategies/oauthStrategy.js +101 -0
  26. package/dist/mcp-server/transports/core/baseTransportManager.d.ts +17 -0
  27. package/dist/mcp-server/transports/core/baseTransportManager.js +18 -0
  28. package/dist/mcp-server/transports/core/honoNodeBridge.d.ts +23 -0
  29. package/dist/mcp-server/transports/core/honoNodeBridge.js +51 -0
  30. package/dist/mcp-server/transports/core/statefulTransportManager.d.ts +31 -0
  31. package/dist/mcp-server/transports/core/statefulTransportManager.js +233 -0
  32. package/dist/mcp-server/transports/core/statelessTransportManager.d.ts +20 -0
  33. package/dist/mcp-server/transports/core/statelessTransportManager.js +92 -0
  34. package/dist/mcp-server/transports/core/transportTypes.d.ts +68 -0
  35. package/dist/mcp-server/transports/core/transportTypes.js +5 -0
  36. package/dist/mcp-server/transports/{httpErrorHandler.d.ts → http/httpErrorHandler.d.ts} +4 -9
  37. package/dist/mcp-server/transports/{httpErrorHandler.js → http/httpErrorHandler.js} +33 -8
  38. package/dist/mcp-server/transports/http/httpTransport.d.ts +22 -0
  39. package/dist/mcp-server/transports/http/httpTransport.js +251 -0
  40. package/dist/mcp-server/transports/http/httpTypes.d.ts +16 -0
  41. package/dist/mcp-server/transports/http/httpTypes.js +5 -0
  42. package/dist/mcp-server/transports/http/index.d.ts +7 -0
  43. package/dist/mcp-server/transports/http/index.js +6 -0
  44. package/dist/mcp-server/transports/http/mcpTransportMiddleware.d.ts +25 -0
  45. package/dist/mcp-server/transports/http/mcpTransportMiddleware.js +63 -0
  46. package/dist/mcp-server/transports/stdio/index.d.ts +5 -0
  47. package/dist/mcp-server/transports/stdio/index.js +5 -0
  48. package/dist/mcp-server/transports/{stdioTransport.d.ts → stdio/stdioTransport.d.ts} +2 -2
  49. package/dist/mcp-server/transports/{stdioTransport.js → stdio/stdioTransport.js} +10 -5
  50. package/dist/services/NCBI/{ncbiConstants.d.ts → core/ncbiConstants.d.ts} +1 -1
  51. package/dist/services/NCBI/{ncbiConstants.js → core/ncbiConstants.js} +1 -1
  52. package/dist/services/NCBI/{ncbiCoreApiClient.d.ts → core/ncbiCoreApiClient.d.ts} +2 -2
  53. package/dist/services/NCBI/{ncbiCoreApiClient.js → core/ncbiCoreApiClient.js} +4 -4
  54. package/dist/services/NCBI/{ncbiRequestQueueManager.d.ts → core/ncbiRequestQueueManager.d.ts} +2 -2
  55. package/dist/services/NCBI/{ncbiRequestQueueManager.js → core/ncbiRequestQueueManager.js} +3 -3
  56. package/dist/services/NCBI/{ncbiResponseHandler.d.ts → core/ncbiResponseHandler.d.ts} +2 -2
  57. package/dist/services/NCBI/{ncbiResponseHandler.js → core/ncbiResponseHandler.js} +3 -3
  58. package/dist/services/NCBI/{ncbiService.d.ts → core/ncbiService.d.ts} +3 -3
  59. package/dist/services/NCBI/{ncbiService.js → core/ncbiService.js} +2 -2
  60. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/eSummaryResultParser.d.ts +1 -1
  61. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/eSummaryResultParser.js +1 -1
  62. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/index.d.ts +1 -1
  63. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/index.js +1 -1
  64. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/pubmedArticleStructureParser.d.ts +1 -1
  65. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/pubmedArticleStructureParser.js +1 -1
  66. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/xmlGenericHelpers.d.ts +1 -1
  67. package/dist/{utils/parsing/ncbi-parsing → services/NCBI/parsing}/xmlGenericHelpers.js +1 -1
  68. package/dist/types-global/errors.d.ts +2 -0
  69. package/dist/types-global/errors.js +2 -0
  70. package/dist/utils/internal/errorHandler.js +1 -1
  71. package/dist/utils/internal/logger.d.ts +13 -1
  72. package/dist/utils/internal/logger.js +43 -9
  73. package/dist/utils/network/fetchWithTimeout.d.ts +21 -0
  74. package/dist/utils/network/fetchWithTimeout.js +59 -0
  75. package/dist/utils/network/index.d.ts +6 -0
  76. package/dist/utils/network/index.js +5 -0
  77. package/dist/utils/scheduling/index.d.ts +6 -0
  78. package/dist/utils/scheduling/index.js +6 -0
  79. package/dist/utils/scheduling/scheduler.d.ts +72 -0
  80. package/dist/utils/scheduling/scheduler.js +150 -0
  81. package/dist/utils/security/sanitization.js +35 -18
  82. package/package.json +9 -7
  83. package/dist/mcp-server/transports/auth/core/authTypes.js +0 -5
  84. package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.d.ts +0 -27
  85. package/dist/mcp-server/transports/auth/strategies/jwt/jwtMiddleware.js +0 -149
  86. package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.d.ts +0 -20
  87. package/dist/mcp-server/transports/auth/strategies/oauth/oauthMiddleware.js +0 -124
  88. package/dist/mcp-server/transports/httpTransport.d.ts +0 -21
  89. package/dist/mcp-server/transports/httpTransport.js +0 -208
  90. /package/dist/mcp-server/transports/auth/{core → lib}/authContext.d.ts +0 -0
  91. /package/dist/mcp-server/transports/auth/{core → lib}/authContext.js +0 -0
  92. /package/dist/mcp-server/transports/auth/{core → lib}/authUtils.d.ts +0 -0
@@ -4,14 +4,6 @@
4
4
  * environment variables and `package.json`. It uses Zod for schema validation
5
5
  * to ensure type safety and correctness of configuration parameters.
6
6
  *
7
- * Key responsibilities:
8
- * - Load environment variables from a `.env` file.
9
- * - Read `package.json` for default server name and version.
10
- * - Define a Zod schema for all expected environment variables.
11
- * - Validate environment variables against the schema.
12
- * - Construct and export a comprehensive `config` object.
13
- * - Export individual configuration values like `logLevel` and `environment` for convenience.
14
- *
15
7
  * @module src/config/index
16
8
  */
17
9
  import dotenv from "dotenv";
@@ -21,11 +13,6 @@ import { fileURLToPath } from "url";
21
13
  import { z } from "zod";
22
14
  dotenv.config();
23
15
  // --- Determine Project Root ---
24
- /**
25
- * Finds the project root directory by searching upwards for package.json.
26
- * @param startDir The directory to start searching from.
27
- * @returns The absolute path to the project root, or throws an error if not found.
28
- */
29
16
  const findProjectRoot = (startDir) => {
30
17
  let currentDir = startDir;
31
18
  while (true) {
@@ -35,7 +22,6 @@ const findProjectRoot = (startDir) => {
35
22
  }
36
23
  const parentDir = dirname(currentDir);
37
24
  if (parentDir === currentDir) {
38
- // Reached the root of the filesystem without finding package.json
39
25
  throw new Error(`Could not find project root (package.json) starting from ${startDir}`);
40
26
  }
41
27
  currentDir = parentDir;
@@ -43,21 +29,17 @@ const findProjectRoot = (startDir) => {
43
29
  };
44
30
  let projectRoot;
45
31
  try {
46
- // For ESM, __dirname is not available directly.
47
- // import.meta.url gives the URL of the current module.
48
32
  const currentModuleDir = dirname(fileURLToPath(import.meta.url));
49
33
  projectRoot = findProjectRoot(currentModuleDir);
50
34
  }
51
35
  catch (error) {
52
36
  console.error(`FATAL: Error determining project root: ${error.message}`);
53
- // Fallback to process.cwd() if project root cannot be determined.
54
- // This might happen in unusual execution environments.
55
37
  projectRoot = process.cwd();
56
38
  console.warn(`Warning: Using process.cwd() (${projectRoot}) as fallback project root.`);
57
39
  }
58
40
  // --- End Determine Project Root ---
59
- const pkgPath = join(projectRoot, "package.json"); // Use determined projectRoot
60
- let pkg = { name: "mcp-ts-template", version: "0.0.0" };
41
+ const pkgPath = join(projectRoot, "package.json");
42
+ let pkg = { name: "pubmed-mcp-server", version: "0.0.0" };
61
43
  try {
62
44
  pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
63
45
  }
@@ -66,90 +48,57 @@ catch (error) {
66
48
  console.error("Warning: Could not read package.json for default config values. Using hardcoded defaults.", error);
67
49
  }
68
50
  }
69
- /**
70
- * Zod schema for validating environment variables.
71
- * Provides type safety, validation, defaults, and clear error messages.
72
- * @private
73
- */
74
51
  const EnvSchema = z
75
52
  .object({
76
- /** Optional. The desired name for the MCP server. Defaults to `package.json` name. */
53
+ // Core Server Config
77
54
  MCP_SERVER_NAME: z.string().optional(),
78
- /** Optional. The version of the MCP server. Defaults to `package.json` version. */
79
55
  MCP_SERVER_VERSION: z.string().optional(),
80
- /** Minimum logging level. See `McpLogLevel` in logger utility. Default: "debug". */
56
+ NODE_ENV: z.string().default("development"),
57
+ // Logging
81
58
  MCP_LOG_LEVEL: z.string().default("debug"),
82
- /** Directory for log files. Defaults to "logs" in project root. */
83
59
  LOGS_DIR: z.string().default(path.join(projectRoot, "logs")),
84
- /** Defines the logging output mode. "file" for logs in LOGS_DIR, "stdout" for console logging. */
85
- LOG_OUTPUT_MODE: z.enum(["file", "stdout"]).default("file"),
86
- /** Runtime environment (e.g., "development", "production"). Default: "development". */
87
- NODE_ENV: z.string().default("development"),
88
- /** MCP communication transport ("stdio" or "http"). Default: "stdio". */
60
+ // Transport
89
61
  MCP_TRANSPORT_TYPE: z.enum(["stdio", "http"]).default("stdio"),
90
- /** HTTP server port (if MCP_TRANSPORT_TYPE is "http"). Default: 3010. */
62
+ MCP_SESSION_MODE: z.enum(["stateless", "stateful", "auto"]).default("auto"),
91
63
  MCP_HTTP_PORT: z.coerce.number().int().positive().default(3010),
92
- /** HTTP server host (if MCP_TRANSPORT_TYPE is "http"). Default: "127.0.0.1". */
93
64
  MCP_HTTP_HOST: z.string().default("127.0.0.1"),
94
- /** Optional. Comma-separated allowed origins for CORS (HTTP transport). */
65
+ MCP_HTTP_ENDPOINT_PATH: z.string().default("/mcp"),
66
+ MCP_HTTP_MAX_PORT_RETRIES: z.coerce
67
+ .number()
68
+ .int()
69
+ .nonnegative()
70
+ .default(15),
71
+ MCP_HTTP_PORT_RETRY_DELAY_MS: z.coerce
72
+ .number()
73
+ .int()
74
+ .nonnegative()
75
+ .default(50),
76
+ MCP_STATEFUL_SESSION_STALE_TIMEOUT_MS: z.coerce
77
+ .number()
78
+ .int()
79
+ .positive()
80
+ .default(1800000),
95
81
  MCP_ALLOWED_ORIGINS: z.string().optional(),
96
- /** Optional. Secret key (min 32 chars) for auth tokens (HTTP transport). CRITICAL for production. */
82
+ // Authentication
83
+ MCP_AUTH_MODE: z.enum(["jwt", "oauth", "none"]).default("none"),
97
84
  MCP_AUTH_SECRET_KEY: z
98
85
  .string()
99
- .min(32, "MCP_AUTH_SECRET_KEY must be at least 32 characters long for security reasons.")
86
+ .min(32, "MCP_AUTH_SECRET_KEY must be at least 32 characters long.")
100
87
  .optional(),
101
- /** Authentication mode ('jwt' or 'oauth'). Default: 'jwt'. */
102
- MCP_AUTH_MODE: z.enum(["jwt", "oauth"]).default("jwt"),
103
- /** OAuth: The expected issuer of the JWT. */
104
88
  OAUTH_ISSUER_URL: z.string().url().optional(),
105
- /** OAuth: The expected audience of the JWT. */
106
- OAUTH_AUDIENCE: z.string().optional(),
107
- /** OAuth: The URI of the JWKS endpoint. */
108
89
  OAUTH_JWKS_URI: z.string().url().optional(),
109
- /** Optional. OAuth provider authorization endpoint URL. */
110
- OAUTH_PROXY_AUTHORIZATION_URL: z
111
- .string()
112
- .url("OAUTH_PROXY_AUTHORIZATION_URL must be a valid URL.")
113
- .optional(),
114
- /** Optional. OAuth provider token endpoint URL. */
115
- OAUTH_PROXY_TOKEN_URL: z
116
- .string()
117
- .url("OAUTH_PROXY_TOKEN_URL must be a valid URL.")
118
- .optional(),
119
- /** Optional. OAuth provider revocation endpoint URL. */
120
- OAUTH_PROXY_REVOCATION_URL: z
121
- .string()
122
- .url("OAUTH_PROXY_REVOCATION_URL must be a valid URL.")
123
- .optional(),
124
- /** Optional. OAuth provider issuer URL. */
125
- OAUTH_PROXY_ISSUER_URL: z
126
- .string()
127
- .url("OAUTH_PROXY_ISSUER_URL must be a valid URL.")
128
- .optional(),
129
- /** Optional. OAuth service documentation URL. */
130
- OAUTH_PROXY_SERVICE_DOCUMENTATION_URL: z
131
- .string()
132
- .url("OAUTH_PROXY_SERVICE_DOCUMENTATION_URL must be a valid URL.")
133
- .optional(),
134
- /** Optional. Comma-separated default OAuth client redirect URIs. */
135
- OAUTH_PROXY_DEFAULT_CLIENT_REDIRECT_URIS: z.string().optional(),
136
- // NCBI E-utilities Configuration
137
- /** NCBI API Key. Optional, but highly recommended for higher rate limits. */
90
+ OAUTH_AUDIENCE: z.string().optional(),
91
+ // Dev mode JWT
92
+ DEV_MCP_CLIENT_ID: z.string().optional(),
93
+ DEV_MCP_SCOPES: z.string().optional(),
94
+ // NCBI E-utilities
138
95
  NCBI_API_KEY: z.string().optional(),
139
- /** Tool identifier sent to NCBI. Defaults to MCP_SERVER_NAME/MCP_SERVER_VERSION. */
140
96
  NCBI_TOOL_IDENTIFIER: z.string().optional(),
141
- /** Administrator's email for NCBI contact. Optional, but recommended if using an API key. */
142
- NCBI_ADMIN_EMAIL: z
143
- .string()
144
- .email("NCBI_ADMIN_EMAIL must be a valid email address.")
145
- .optional(),
146
- /** Milliseconds to wait between NCBI requests. Default: 100 (for API key), 334 (without API key). */
147
- NCBI_REQUEST_DELAY_MS: z.coerce.number().int().positive().optional(), // Default will be set conditionally
148
- /** Maximum number of retries for failed NCBI requests. Default: 3. */
97
+ NCBI_ADMIN_EMAIL: z.string().email().optional(),
98
+ NCBI_REQUEST_DELAY_MS: z.coerce.number().int().positive().optional(),
149
99
  NCBI_MAX_RETRIES: z.coerce.number().int().nonnegative().default(3),
150
100
  })
151
101
  .superRefine((data, ctx) => {
152
- // Rule 1: MCP_AUTH_SECRET_KEY is required for http transport in production with jwt auth
153
102
  if (data.NODE_ENV === "production" &&
154
103
  data.MCP_TRANSPORT_TYPE === "http" &&
155
104
  data.MCP_AUTH_MODE === "jwt" &&
@@ -157,30 +106,22 @@ const EnvSchema = z
157
106
  ctx.addIssue({
158
107
  code: z.ZodIssueCode.custom,
159
108
  path: ["MCP_AUTH_SECRET_KEY"],
160
- message: "MCP_AUTH_SECRET_KEY is required for 'jwt' auth with 'http' transport in a 'production' environment.",
109
+ message: "MCP_AUTH_SECRET_KEY is required for 'jwt' auth in production with 'http' transport.",
161
110
  });
162
111
  }
163
- // Rule 2: Core OAuth variables are required when MCP_AUTH_MODE is 'oauth'
164
112
  if (data.MCP_AUTH_MODE === "oauth") {
165
113
  if (!data.OAUTH_ISSUER_URL) {
166
114
  ctx.addIssue({
167
115
  code: z.ZodIssueCode.custom,
168
116
  path: ["OAUTH_ISSUER_URL"],
169
- message: "OAUTH_ISSUER_URL is required when MCP_AUTH_MODE is 'oauth'.",
117
+ message: "OAUTH_ISSUER_URL is required for 'oauth' mode.",
170
118
  });
171
119
  }
172
120
  if (!data.OAUTH_AUDIENCE) {
173
121
  ctx.addIssue({
174
122
  code: z.ZodIssueCode.custom,
175
123
  path: ["OAUTH_AUDIENCE"],
176
- message: "OAUTH_AUDIENCE is required when MCP_AUTH_MODE is 'oauth'.",
177
- });
178
- }
179
- if (!data.OAUTH_JWKS_URI) {
180
- ctx.addIssue({
181
- code: z.ZodIssueCode.custom,
182
- path: ["OAUTH_JWKS_URI"],
183
- message: "OAUTH_JWKS_URI is required when MCP_AUTH_MODE is 'oauth'.",
124
+ message: "OAUTH_AUDIENCE is required for 'oauth' mode.",
184
125
  });
185
126
  }
186
127
  }
@@ -188,153 +129,69 @@ const EnvSchema = z
188
129
  const parsedEnv = EnvSchema.safeParse(process.env);
189
130
  if (!parsedEnv.success) {
190
131
  if (process.stdout.isTTY) {
191
- console.error("❌ Invalid environment variables found:", parsedEnv.error.flatten().fieldErrors);
132
+ console.error("❌ Invalid environment variables:", parsedEnv.error.flatten().fieldErrors);
192
133
  }
193
- // Consider throwing an error in production for critical misconfigurations.
194
134
  }
195
135
  const env = parsedEnv.success ? parsedEnv.data : EnvSchema.parse({});
196
- // --- Directory Ensurance Function ---
197
- /**
198
- * Ensures a directory exists and is within the project root.
199
- * @param dirPath The desired path for the directory (can be relative or absolute).
200
- * @param rootDir The root directory of the project to contain the directory.
201
- * @param dirName The name of the directory type for logging (e.g., "logs").
202
- * @returns The validated, absolute path to the directory, or null if invalid.
203
- */
204
136
  const ensureDirectory = (dirPath, rootDir, dirName) => {
205
137
  const resolvedDirPath = path.isAbsolute(dirPath)
206
138
  ? dirPath
207
139
  : path.resolve(rootDir, dirPath);
208
- // Ensure the resolved path is within the project root boundary
209
- if (!resolvedDirPath.startsWith(rootDir + path.sep) &&
210
- resolvedDirPath !== rootDir) {
140
+ if (!resolvedDirPath.startsWith(rootDir)) {
211
141
  if (process.stdout.isTTY) {
212
- console.error(`Error: ${dirName} path "${dirPath}" resolves to "${resolvedDirPath}", which is outside the project boundary "${rootDir}".`);
142
+ console.error(`Error: ${dirName} path "${dirPath}" is outside the project boundary "${rootDir}".`);
213
143
  }
214
144
  return null;
215
145
  }
216
- if (!existsSync(resolvedDirPath)) {
217
- try {
146
+ try {
147
+ if (!existsSync(resolvedDirPath)) {
218
148
  mkdirSync(resolvedDirPath, { recursive: true });
219
- if (process.stdout.isTTY) {
220
- console.log(`Created ${dirName} directory: ${resolvedDirPath}`);
221
- }
222
149
  }
223
- catch (err) {
224
- const errorMessage = err instanceof Error ? err.message : String(err);
225
- if (process.stdout.isTTY) {
226
- console.error(`Error creating ${dirName} directory at ${resolvedDirPath}: ${errorMessage}`);
227
- }
228
- return null;
229
- }
230
- }
231
- else {
232
- try {
233
- const stats = statSync(resolvedDirPath);
234
- if (!stats.isDirectory()) {
235
- if (process.stdout.isTTY) {
236
- console.error(`Error: ${dirName} path ${resolvedDirPath} exists but is not a directory.`);
237
- }
150
+ else {
151
+ if (!statSync(resolvedDirPath).isDirectory()) {
152
+ console.error(`Error: ${dirName} path ${resolvedDirPath} exists but is not a directory.`);
238
153
  return null;
239
154
  }
240
155
  }
241
- catch (statError) {
242
- if (process.stdout.isTTY) {
243
- console.error(`Error accessing ${dirName} path ${resolvedDirPath}: ${statError.message}`);
244
- }
245
- return null;
246
- }
156
+ return resolvedDirPath;
247
157
  }
248
- return resolvedDirPath;
249
- };
250
- // --- End Directory Ensurance Function ---
251
- // --- Logs Directory Handling ---
252
- let validatedLogsPath = null;
253
- if (env.LOG_OUTPUT_MODE === "file") {
254
- validatedLogsPath = ensureDirectory(env.LOGS_DIR, projectRoot, "logs");
255
- if (!validatedLogsPath) {
256
- if (process.stdout.isTTY) {
257
- 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.");
258
- }
259
- process.exit(1); // Exit if file logging is configured but directory is not usable
158
+ catch (error) {
159
+ console.error(`Error ensuring ${dirName} directory at ${resolvedDirPath}: ${error.message}`);
160
+ return null;
260
161
  }
261
- }
262
- // --- End Logs Directory Handling ---
263
- /**
264
- * Main application configuration object.
265
- * Aggregates settings from validated environment variables and `package.json`.
266
- */
162
+ };
163
+ const validatedLogsPath = ensureDirectory(env.LOGS_DIR, projectRoot, "logs");
267
164
  export const config = {
268
- /** MCP server name. Env `MCP_SERVER_NAME` > `package.json` name > "mcp-ts-template". */
165
+ pkg,
269
166
  mcpServerName: env.MCP_SERVER_NAME || pkg.name,
270
- /** MCP server version. Env `MCP_SERVER_VERSION` > `package.json` version > "0.0.0". */
271
167
  mcpServerVersion: env.MCP_SERVER_VERSION || pkg.version,
272
- /** Logging level. From `MCP_LOG_LEVEL` env var. Default: "debug". */
273
168
  logLevel: env.MCP_LOG_LEVEL,
274
- /** Defines the logging output mode ('file' or 'stdout'). From `LOG_OUTPUT_MODE`. */
275
- logOutputMode: env.LOG_OUTPUT_MODE,
276
- /** Absolute path to the logs directory (if logOutputMode is 'file'). From `LOGS_DIR`. */
277
169
  logsPath: validatedLogsPath,
278
- /** Runtime environment. From `NODE_ENV` env var. Default: "development". */
279
170
  environment: env.NODE_ENV,
280
- /** MCP transport type ('stdio' or 'http'). From `MCP_TRANSPORT_TYPE` env var. Default: "stdio". */
281
171
  mcpTransportType: env.MCP_TRANSPORT_TYPE,
282
- /** HTTP server port (if http transport). From `MCP_HTTP_PORT` env var. Default: 3010. */
172
+ mcpSessionMode: env.MCP_SESSION_MODE,
283
173
  mcpHttpPort: env.MCP_HTTP_PORT,
284
- /** HTTP server host (if http transport). From `MCP_HTTP_HOST` env var. Default: "127.0.0.1". */
285
174
  mcpHttpHost: env.MCP_HTTP_HOST,
286
- /** Array of allowed CORS origins (http transport). From `MCP_ALLOWED_ORIGINS` (comma-separated). */
175
+ mcpHttpEndpointPath: env.MCP_HTTP_ENDPOINT_PATH,
176
+ mcpHttpMaxPortRetries: env.MCP_HTTP_MAX_PORT_RETRIES,
177
+ mcpHttpPortRetryDelayMs: env.MCP_HTTP_PORT_RETRY_DELAY_MS,
178
+ mcpStatefulSessionStaleTimeoutMs: env.MCP_STATEFUL_SESSION_STALE_TIMEOUT_MS,
287
179
  mcpAllowedOrigins: env.MCP_ALLOWED_ORIGINS?.split(",")
288
- .map((origin) => origin.trim())
180
+ .map((o) => o.trim())
289
181
  .filter(Boolean),
290
- /** Auth secret key (JWTs, http transport). From `MCP_AUTH_SECRET_KEY`. CRITICAL. */
291
- mcpAuthSecretKey: env.MCP_AUTH_SECRET_KEY,
292
- /** Auth mode ('jwt' or 'oauth'). From `MCP_AUTH_MODE`. */
293
182
  mcpAuthMode: env.MCP_AUTH_MODE,
294
- /** OAuth Issuer URL. From `OAUTH_ISSUER_URL`. */
183
+ mcpAuthSecretKey: env.MCP_AUTH_SECRET_KEY,
295
184
  oauthIssuerUrl: env.OAUTH_ISSUER_URL,
296
- /** OAuth Audience. From `OAUTH_AUDIENCE`. */
297
- oauthAudience: env.OAUTH_AUDIENCE,
298
- /** OAuth JWKS URI. From `OAUTH_JWKS_URI`. */
299
185
  oauthJwksUri: env.OAUTH_JWKS_URI,
300
- // NCBI Configuration
301
- /** NCBI API Key. From `NCBI_API_KEY`. */
186
+ oauthAudience: env.OAUTH_AUDIENCE,
187
+ devMcpClientId: env.DEV_MCP_CLIENT_ID,
188
+ devMcpScopes: env.DEV_MCP_SCOPES?.split(",").map((s) => s.trim()),
302
189
  ncbiApiKey: env.NCBI_API_KEY,
303
- /** NCBI Tool Identifier. From `NCBI_TOOL_IDENTIFIER`. Defaults to server name/version. */
304
190
  ncbiToolIdentifier: env.NCBI_TOOL_IDENTIFIER ||
305
191
  `${env.MCP_SERVER_NAME || pkg.name}/${env.MCP_SERVER_VERSION || pkg.version}`,
306
- /** NCBI Admin Email. From `NCBI_ADMIN_EMAIL`. */
307
192
  ncbiAdminEmail: env.NCBI_ADMIN_EMAIL,
308
- /** NCBI Request Delay in MS. From `NCBI_REQUEST_DELAY_MS`. Dynamically set based on API key presence. */
309
193
  ncbiRequestDelayMs: env.NCBI_REQUEST_DELAY_MS ?? (env.NCBI_API_KEY ? 100 : 334),
310
- /** NCBI Max Retries. From `NCBI_MAX_RETRIES`. */
311
194
  ncbiMaxRetries: env.NCBI_MAX_RETRIES,
312
- /** OAuth Proxy configurations. Undefined if no related env vars are set. */
313
- oauthProxy: env.OAUTH_PROXY_AUTHORIZATION_URL ||
314
- env.OAUTH_PROXY_TOKEN_URL ||
315
- env.OAUTH_PROXY_REVOCATION_URL ||
316
- env.OAUTH_PROXY_ISSUER_URL ||
317
- env.OAUTH_PROXY_SERVICE_DOCUMENTATION_URL ||
318
- env.OAUTH_PROXY_DEFAULT_CLIENT_REDIRECT_URIS
319
- ? {
320
- authorizationUrl: env.OAUTH_PROXY_AUTHORIZATION_URL,
321
- tokenUrl: env.OAUTH_PROXY_TOKEN_URL,
322
- revocationUrl: env.OAUTH_PROXY_REVOCATION_URL,
323
- issuerUrl: env.OAUTH_PROXY_ISSUER_URL,
324
- serviceDocumentationUrl: env.OAUTH_PROXY_SERVICE_DOCUMENTATION_URL,
325
- defaultClientRedirectUris: env.OAUTH_PROXY_DEFAULT_CLIENT_REDIRECT_URIS?.split(",")
326
- .map((uri) => uri.trim())
327
- .filter(Boolean),
328
- }
329
- : undefined,
330
195
  };
331
- /**
332
- * Configured logging level for the application.
333
- * Exported for convenience.
334
- */
335
196
  export const logLevel = config.logLevel;
336
- /**
337
- * Configured runtime environment ("development", "production", etc.).
338
- * Exported for convenience.
339
- */
340
197
  export const environment = config.environment;
package/dist/index.js CHANGED
@@ -51,70 +51,40 @@ const shutdown = async (signal) => {
51
51
  triggerEvent: signal,
52
52
  });
53
53
  logger.info(`Received ${signal}. Initiating graceful shutdown...`, shutdownContext);
54
- let mcpClosed = false;
55
- let httpClosed = false;
56
- let closeError = null;
57
- const checkAndExit = () => {
58
- if (closeError) {
59
- logger.error("Critical error encountered during shutdown process.", {
60
- ...shutdownContext,
61
- errorMessage: closeError.message,
62
- errorStack: closeError.stack,
63
- });
64
- process.exit(1);
65
- }
66
- else if (mcpClosed && httpClosed) {
67
- logger.info("Graceful shutdown completed successfully. Exiting.", shutdownContext);
68
- process.exit(0);
54
+ try {
55
+ let closePromise = Promise.resolve();
56
+ const transportType = config.mcpTransportType;
57
+ if (transportType === "stdio" && mcpStdioServer) {
58
+ logger.info("Attempting to close main MCP server (STDIO)...", shutdownContext);
59
+ closePromise = mcpStdioServer.close();
69
60
  }
70
- };
71
- if (mcpStdioServer) {
72
- logger.info("Attempting to close main MCP server (STDIO)...", shutdownContext);
73
- mcpStdioServer
74
- .close()
75
- .then(() => {
76
- logger.info("Main MCP server (STDIO) closed successfully.", shutdownContext);
77
- mcpClosed = true;
78
- checkAndExit();
79
- })
80
- .catch((err) => {
81
- logger.error("Error closing MCP server (STDIO).", {
82
- ...shutdownContext,
83
- error: err,
61
+ else if (transportType === "http" && actualHttpServer) {
62
+ logger.info("Attempting to close HTTP server...", shutdownContext);
63
+ closePromise = new Promise((resolve, reject) => {
64
+ actualHttpServer.close((err) => {
65
+ if (err) {
66
+ logger.error("Error closing HTTP server.", {
67
+ ...shutdownContext,
68
+ error: err.message,
69
+ });
70
+ return reject(err);
71
+ }
72
+ logger.info("HTTP server closed successfully.", shutdownContext);
73
+ resolve();
74
+ });
84
75
  });
85
- mcpClosed = true; // Consider it closed even on error to allow exit
86
- if (!closeError)
87
- closeError = err;
88
- checkAndExit();
89
- });
90
- }
91
- else {
92
- mcpClosed = true; // No STDIO McpServer to close
76
+ }
77
+ await closePromise;
78
+ logger.info("Graceful shutdown completed successfully. Exiting.", shutdownContext);
79
+ process.exit(0);
93
80
  }
94
- if (actualHttpServer) {
95
- logger.info("Attempting to close HTTP server...", shutdownContext);
96
- actualHttpServer.close((err) => {
97
- if (err) {
98
- logger.error("Error closing HTTP server.", {
99
- ...shutdownContext,
100
- error: err,
101
- });
102
- if (!closeError)
103
- closeError = err;
104
- }
105
- else {
106
- logger.info("HTTP server closed successfully.", shutdownContext);
107
- }
108
- httpClosed = true;
109
- checkAndExit();
81
+ catch (error) {
82
+ logger.error("Critical error during shutdown process.", {
83
+ ...shutdownContext,
84
+ errorMessage: error instanceof Error ? error.message : String(error),
85
+ errorStack: error instanceof Error ? error.stack : undefined,
110
86
  });
111
- }
112
- else {
113
- httpClosed = true; // No HTTP server to close
114
- }
115
- // Initial check in case no servers needed closing
116
- if (mcpClosed && httpClosed) {
117
- checkAndExit();
87
+ process.exit(1);
118
88
  }
119
89
  };
120
90
  /**
@@ -7,16 +7,11 @@
7
7
  * based on configuration.
8
8
  * 4. Handles top-level error management during startup.
9
9
  *
10
- * MCP Specification References:
11
- * - Lifecycle: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/lifecycle.mdx
12
- * - Overview (Capabilities): https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/index.mdx
13
- * - Transports: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx
14
10
  * @module src/mcp-server/server
15
11
  */
16
12
  import { ServerType } from "@hono/node-server";
17
13
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
18
14
  /**
19
15
  * Main application entry point. Initializes and starts the MCP server.
20
- * Orchestrates server startup, transport selection, and top-level error handling.
21
16
  */
22
17
  export declare function initializeAndStartServer(): Promise<void | McpServer | ServerType>;
@@ -7,10 +7,6 @@
7
7
  * based on configuration.
8
8
  * 4. Handles top-level error management during startup.
9
9
  *
10
- * MCP Specification References:
11
- * - Lifecycle: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/lifecycle.mdx
12
- * - Overview (Capabilities): https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/index.mdx
13
- * - Transports: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/transports.mdx
14
10
  * @module src/mcp-server/server
15
11
  */
16
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -22,19 +18,11 @@ import { registerGeneratePubMedChartTool } from "./tools/generatePubMedChart/ind
22
18
  import { registerGetPubMedArticleConnectionsTool } from "./tools/getPubMedArticleConnections/index.js";
23
19
  import { registerPubMedResearchAgentTool } from "./tools/pubmedResearchAgent/index.js";
24
20
  import { registerSearchPubMedArticlesTool } from "./tools/searchPubMedArticles/index.js";
25
- import { startHttpTransport } from "./transports/httpTransport.js";
26
- import { connectStdioTransport } from "./transports/stdioTransport.js";
21
+ import { startHttpTransport } from "./transports/http/index.js";
22
+ import { startStdioTransport } from "./transports/stdio/index.js";
27
23
  /**
28
24
  * Creates and configures a new instance of the `McpServer`.
29
25
  *
30
- * This function defines the server's identity and capabilities as presented
31
- * to clients during MCP initialization.
32
- *
33
- * MCP Spec Relevance:
34
- * - Server Identity (`serverInfo`): `name` and `version` are part of `ServerInformation`.
35
- * - Capabilities Declaration: Declares supported features (logging, dynamic resources/tools).
36
- * - Resource/Tool Registration: Makes capabilities discoverable and invocable.
37
- *
38
26
  * @returns A promise resolving with the configured `McpServer` instance.
39
27
  * @throws {McpError} If any resource or tool registration fails.
40
28
  * @private
@@ -44,21 +32,16 @@ async function createMcpServerInstance() {
44
32
  operation: "createMcpServerInstance",
45
33
  });
46
34
  logger.info("Initializing MCP server instance", context);
47
- requestContextService.configure({
48
- appName: config.mcpServerName,
49
- appVersion: config.mcpServerVersion,
50
- environment,
51
- });
52
35
  const server = new McpServer({ name: config.mcpServerName, version: config.mcpServerVersion }, {
53
36
  capabilities: {
54
- logging: {}, // Server can receive logging/setLevel and send notifications/message
55
- resources: { listChanged: true }, // Server supports dynamic resource lists
56
- tools: { listChanged: true }, // Server supports dynamic tool lists
37
+ logging: {},
38
+ resources: { listChanged: true },
39
+ tools: { listChanged: true },
57
40
  },
58
41
  });
59
42
  await ErrorHandler.tryCatch(async () => {
60
43
  logger.debug("Registering resources and tools...", context);
61
- // IMPORTANT: Keep tool registrations in alphabetical order. Do not remove this comment.
44
+ // IMPORTANT: Keep tool registrations in alphabetical order.
62
45
  await registerFetchPubMedContentTool(server);
63
46
  await registerGeneratePubMedChartTool(server);
64
47
  await registerGetPubMedArticleConnectionsTool(server);
@@ -66,7 +49,7 @@ async function createMcpServerInstance() {
66
49
  await registerSearchPubMedArticlesTool(server);
67
50
  logger.info("Resources and tools registered successfully", context);
68
51
  }, {
69
- operation: "registerAllTools",
52
+ operation: "registerAllCapabilities",
70
53
  context,
71
54
  errorCode: BaseErrorCode.INITIALIZATION_FAILED,
72
55
  critical: true,
@@ -76,11 +59,6 @@ async function createMcpServerInstance() {
76
59
  /**
77
60
  * Selects, sets up, and starts the appropriate MCP transport layer based on configuration.
78
61
  *
79
- * MCP Spec Relevance:
80
- * - Transport Selection: Uses `config.mcpTransportType` ('stdio' or 'http').
81
- * - Transport Connection: Calls dedicated functions for chosen transport.
82
- * - Server Instance Lifecycle: Single instance for 'stdio', per-session for 'http'.
83
- *
84
62
  * @returns Resolves with `McpServer` for 'stdio', `http.Server` for 'http', or `void`.
85
63
  * @throws {Error} If transport type is unsupported or setup fails.
86
64
  * @private
@@ -92,25 +70,31 @@ async function startTransport() {
92
70
  transport: transportType,
93
71
  });
94
72
  logger.info(`Starting transport: ${transportType}`, context);
73
+ const serverFactory = createMcpServerInstance;
95
74
  if (transportType === "http") {
96
- return startHttpTransport(createMcpServerInstance, context);
75
+ const { server } = await startHttpTransport(serverFactory, context);
76
+ return server;
97
77
  }
98
78
  if (transportType === "stdio") {
99
- const server = await createMcpServerInstance();
100
- await connectStdioTransport(server, context);
79
+ const server = await serverFactory();
80
+ await startStdioTransport(server, context);
101
81
  return server;
102
82
  }
103
83
  throw new Error(`Unsupported transport type: ${transportType}. Must be 'stdio' or 'http'.`);
104
84
  }
105
85
  /**
106
86
  * Main application entry point. Initializes and starts the MCP server.
107
- * Orchestrates server startup, transport selection, and top-level error handling.
108
87
  */
109
88
  export async function initializeAndStartServer() {
110
89
  const context = requestContextService.createRequestContext({
111
90
  operation: "initializeAndStartServer",
112
91
  });
113
92
  logger.info("MCP Server initialization sequence started.", context);
93
+ requestContextService.configure({
94
+ appName: config.mcpServerName,
95
+ appVersion: config.mcpServerVersion,
96
+ environment,
97
+ });
114
98
  try {
115
99
  const result = await startTransport();
116
100
  logger.info("MCP Server initialization sequence completed successfully.", context);
@@ -121,7 +105,7 @@ export async function initializeAndStartServer() {
121
105
  operation: "initializeAndStartServer",
122
106
  context: context,
123
107
  critical: true,
124
- rethrow: false, // Ensure we don't rethrow, so we can exit gracefully.
108
+ rethrow: false,
125
109
  });
126
110
  logger.info("Exiting process due to critical initialization error.", context);
127
111
  process.exit(1);
@@ -6,10 +6,10 @@
6
6
  * @module src/mcp-server/tools/fetchPubMedContent/logic
7
7
  */
8
8
  import { z } from "zod";
9
- import { getNcbiService } from "../../../services/NCBI/ncbiService.js";
9
+ import { getNcbiService } from "../../../services/NCBI/core/ncbiService.js";
10
10
  import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
11
11
  import { logger, requestContextService, sanitizeInputForLogging, } from "../../../utils/index.js";
12
- import { ensureArray, extractAbstractText, extractArticleDates, extractAuthors, extractDoi, extractGrants, extractJournalInfo, extractKeywords, extractMeshTerms, extractPmid, extractPublicationTypes, getText, } from "../../../utils/parsing/ncbi-parsing/index.js";
12
+ import { ensureArray, extractAbstractText, extractArticleDates, extractAuthors, extractDoi, extractGrants, extractJournalInfo, extractKeywords, extractMeshTerms, extractPmid, extractPublicationTypes, getText, } from "../../../services/NCBI/parsing/index.js";
13
13
  export const FetchPubMedContentInputSchema = z
14
14
  .object({
15
15
  pmids: z