@cyanheads/pubmed-mcp-server 1.0.16 → 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.
Files changed (42) hide show
  1. package/dist/config/index.d.ts +12 -22
  2. package/dist/config/index.js +29 -51
  3. package/dist/index.js +77 -29
  4. package/dist/mcp-server/server.d.ts +3 -2
  5. package/dist/mcp-server/server.js +16 -14
  6. package/dist/mcp-server/tools/fetchPubMedContent/logic.js +2 -1
  7. package/dist/mcp-server/tools/generatePubMedChart/logic.js +2 -1
  8. package/dist/mcp-server/tools/generatePubMedChart/registration.js +2 -2
  9. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/citationFormatter.js +2 -1
  10. package/dist/mcp-server/tools/getPubMedArticleConnections/logic/elinkHandler.js +2 -1
  11. package/dist/mcp-server/tools/pubmedResearchAgent/logic/inputSchema.js +1 -1
  12. package/dist/mcp-server/tools/pubmedResearchAgent/logic/planOrchestrator.js +19 -10
  13. package/dist/mcp-server/tools/searchPubMedArticles/logic.js +2 -1
  14. package/dist/mcp-server/transports/authentication/authContext.d.ts +33 -0
  15. package/dist/mcp-server/transports/authentication/authContext.js +24 -0
  16. package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +21 -15
  17. package/dist/mcp-server/transports/authentication/authMiddleware.js +51 -69
  18. package/dist/mcp-server/transports/authentication/authUtils.d.ts +18 -0
  19. package/dist/mcp-server/transports/authentication/authUtils.js +45 -0
  20. package/dist/mcp-server/transports/authentication/oauthMiddleware.d.ts +24 -0
  21. package/dist/mcp-server/transports/authentication/oauthMiddleware.js +109 -0
  22. package/dist/mcp-server/transports/authentication/types.d.ts +17 -0
  23. package/dist/mcp-server/transports/authentication/types.js +5 -0
  24. package/dist/mcp-server/transports/httpTransport.d.ts +5 -4
  25. package/dist/mcp-server/transports/httpTransport.js +177 -143
  26. package/dist/services/NCBI/ncbiCoreApiClient.js +0 -5
  27. package/dist/services/NCBI/ncbiRequestQueueManager.js +2 -4
  28. package/dist/services/NCBI/ncbiResponseHandler.js +0 -3
  29. package/dist/services/NCBI/ncbiService.d.ts +1 -1
  30. package/dist/services/NCBI/ncbiService.js +11 -4
  31. package/dist/utils/internal/logger.js +52 -74
  32. package/package.json +21 -9
  33. package/dist/services/index.d.ts +0 -7
  34. package/dist/services/index.js +0 -7
  35. package/dist/services/llm-providers/index.d.ts +0 -7
  36. package/dist/services/llm-providers/index.js +0 -7
  37. package/dist/services/llm-providers/llmFactory.d.ts +0 -69
  38. package/dist/services/llm-providers/llmFactory.js +0 -132
  39. package/dist/services/llm-providers/openRouter/index.d.ts +0 -6
  40. package/dist/services/llm-providers/openRouter/index.js +0 -7
  41. package/dist/services/llm-providers/openRouter/openRouterProvider.d.ts +0 -99
  42. package/dist/services/llm-providers/openRouter/openRouterProvider.js +0 -329
@@ -1,329 +0,0 @@
1
- import { llmFactory } from "../llmFactory.js"; // Import factory
2
- import { config } from "../../../config/index.js";
3
- import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
4
- import { ErrorHandler } from "../../../utils/internal/errorHandler.js";
5
- import { logger } from "../../../utils/internal/logger.js";
6
- import { requestContextService, } from "../../../utils/internal/requestContext.js";
7
- import { rateLimiter } from "../../../utils/security/rateLimiter.js";
8
- import { sanitization } from "../../../utils/security/sanitization.js";
9
- /**
10
- * Service class for interacting with the OpenRouter API.
11
- * Uses the OpenAI SDK for chat completions, configured for OpenRouter.
12
- * Handles API key management, default headers, model-specific parameter adjustments,
13
- * and provides methods for chat completions and listing models.
14
- */
15
- class OpenRouterProvider {
16
- /**
17
- * Constructs an `OpenRouterProvider` instance.
18
- * Initializes the OpenAI client for OpenRouter if an API key is provided.
19
- * Sets default headers required by OpenRouter.
20
- * @param apiKey - The OpenRouter API key. If undefined, the service remains 'unconfigured'.
21
- * @param parentOpContext - Optional parent operation context for linked logging.
22
- */
23
- constructor(options, parentOpContext) {
24
- /**
25
- * Stores any error that occurred during client initialization.
26
- * @private
27
- */
28
- this.initializationError = null;
29
- const operationName = parentOpContext?.operation
30
- ? `${parentOpContext.operation}.OpenRouterProvider.constructor`
31
- : "OpenRouterProvider.constructor";
32
- const opContext = requestContextService.createRequestContext({
33
- operation: operationName,
34
- parentRequestId: parentOpContext?.requestId,
35
- });
36
- this.status = "initializing";
37
- // The factory will use config.openrouterApiKey if options.apiKey is not provided.
38
- // If neither is available, the factory will throw a CONFIGURATION_ERROR.
39
- // The 'unconfigured' status here might become less relevant if factory handles all key checks.
40
- // However, we can keep it for cases where the service is instantiated without attempting client creation immediately.
41
- if (!options?.apiKey && !config.openrouterApiKey) {
42
- this.status = "unconfigured";
43
- logger.warning("OpenRouter API key not provided in options or global config. Service is unconfigured.", { ...opContext, service: "OpenRouterProvider" });
44
- // Early return if no key is available at all, factory would fail anyway.
45
- // Or, let the factory attempt and catch the error. For now, let's try to initialize.
46
- }
47
- llmFactory
48
- .getLlmClient("openrouter", opContext, options)
49
- .then((client) => {
50
- this.client = client; // Factory returns OpenAI for 'openrouter'
51
- this.status = "ready";
52
- logger.info("OpenRouter Service Initialized and Ready via LlmFactory", {
53
- ...opContext,
54
- service: "OpenRouterProvider",
55
- });
56
- })
57
- .catch((error) => {
58
- this.status = "error";
59
- this.initializationError =
60
- error instanceof Error
61
- ? error
62
- : new McpError(BaseErrorCode.INITIALIZATION_FAILED, String(error));
63
- logger.error("Failed to initialize OpenRouter client via LlmFactory", {
64
- ...opContext,
65
- service: "OpenRouterProvider",
66
- error: this.initializationError.message,
67
- });
68
- });
69
- }
70
- /**
71
- * Checks if the service is ready to make API calls.
72
- * @param operation - The name of the operation attempting to use the service.
73
- * @param context - The request context for logging.
74
- * @throws {McpError} If the service is not ready.
75
- * @private
76
- */
77
- checkReady(operation, context) {
78
- if (this.status !== "ready") {
79
- let errorCode = BaseErrorCode.SERVICE_UNAVAILABLE;
80
- let message = `OpenRouter service is not available (status: ${this.status}).`;
81
- if (this.status === "unconfigured") {
82
- errorCode = BaseErrorCode.CONFIGURATION_ERROR;
83
- message = "OpenRouter service is not configured (missing API key).";
84
- }
85
- else if (this.status === "error") {
86
- errorCode = BaseErrorCode.INITIALIZATION_FAILED;
87
- message = `OpenRouter service failed to initialize: ${this.initializationError?.message || "Unknown error"}`;
88
- }
89
- logger.error(`[${operation}] Attempted to use OpenRouter service when not ready.`, { ...context, status: this.status });
90
- throw new McpError(errorCode, message, {
91
- operation,
92
- status: this.status,
93
- cause: this.initializationError,
94
- });
95
- }
96
- if (!this.client) {
97
- // This should ideally not happen if status is 'ready', but as a safeguard:
98
- logger.error(`[${operation}] Service status is ready, but client is missing.`, { ...context });
99
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, "Internal inconsistency: OpenRouter client is missing despite ready status.", { operation });
100
- }
101
- }
102
- /**
103
- * Creates a chat completion using the OpenRouter API.
104
- * Can return either a single response or a stream of chunks.
105
- * Applies rate limiting and handles model-specific parameter adjustments.
106
- *
107
- * @param params - Parameters for the chat completion request.
108
- * @param context - Request context for logging, error handling, and rate limiting.
109
- * @returns A promise resolving with either a `ChatCompletion` or a `Stream<ChatCompletionChunk>`.
110
- * @throws {McpError} If service not ready, rate limit exceeded, or API call fails.
111
- */
112
- async chatCompletion(params, context) {
113
- const operation = "OpenRouterProvider.chatCompletion";
114
- this.checkReady(operation, context);
115
- const isStreaming = params.stream === true;
116
- const effectiveModelId = params.model || config.llmDefaultModel;
117
- const standardParams = {
118
- model: effectiveModelId,
119
- messages: params.messages,
120
- ...(params.temperature !== undefined ||
121
- config.llmDefaultTemperature !== undefined
122
- ? { temperature: params.temperature ?? config.llmDefaultTemperature }
123
- : {}),
124
- ...(params.top_p !== undefined || config.llmDefaultTopP !== undefined
125
- ? { top_p: params.top_p ?? config.llmDefaultTopP }
126
- : {}),
127
- ...(params.presence_penalty !== undefined
128
- ? { presence_penalty: params.presence_penalty }
129
- : {}),
130
- ...(params.stream !== undefined && { stream: params.stream }),
131
- ...(params.tools !== undefined && { tools: params.tools }),
132
- ...(params.tool_choice !== undefined && {
133
- tool_choice: params.tool_choice,
134
- }),
135
- ...(params.response_format !== undefined && {
136
- response_format: params.response_format,
137
- }),
138
- ...(params.stop !== undefined && { stop: params.stop }),
139
- ...(params.seed !== undefined && { seed: params.seed }),
140
- ...(params.frequency_penalty !== undefined
141
- ? { frequency_penalty: params.frequency_penalty }
142
- : {}),
143
- ...(params.logit_bias !== undefined && { logit_bias: params.logit_bias }),
144
- };
145
- const extraBody = {};
146
- const standardKeys = new Set(Object.keys(standardParams));
147
- standardKeys.add("messages");
148
- for (const key in params) {
149
- if (Object.prototype.hasOwnProperty.call(params, key) &&
150
- !standardKeys.has(key) &&
151
- key !== "max_tokens") {
152
- extraBody[key] = params[key];
153
- }
154
- }
155
- if (extraBody.top_k === undefined && config.llmDefaultTopK !== undefined) {
156
- extraBody.top_k = config.llmDefaultTopK;
157
- }
158
- if (extraBody.min_p === undefined && config.llmDefaultMinP !== undefined) {
159
- extraBody.min_p = config.llmDefaultMinP;
160
- }
161
- if (extraBody.provider && typeof extraBody.provider === "object") {
162
- if (!extraBody.provider.sort)
163
- extraBody.provider.sort = "throughput";
164
- }
165
- else if (extraBody.provider === undefined) {
166
- extraBody.provider = { sort: "throughput" };
167
- }
168
- // Conditional logic for max_tokens vs max_completion_tokens
169
- // Certain underlying models (e.g., newer OpenAI models like the o1 series)
170
- // may require `max_completion_tokens` instead of `max_tokens`.
171
- // This client sends `max_completion_tokens` in `extra_body` for these models if a limit is specified.
172
- // For other models, `max_tokens` is used as a standard parameter.
173
- const modelsRequiringMaxCompletionTokens = ["openai/o1", "openai/gpt-4.1"];
174
- const needsMaxCompletionTokens = modelsRequiringMaxCompletionTokens.some((modelPrefix) => effectiveModelId.startsWith(modelPrefix));
175
- const effectiveMaxTokensValue = params.max_tokens ?? config.llmDefaultMaxTokens;
176
- if (effectiveMaxTokensValue !== undefined) {
177
- if (needsMaxCompletionTokens) {
178
- extraBody.max_completion_tokens = effectiveMaxTokensValue;
179
- logger.info(`[${operation}] Using 'max_completion_tokens: ${effectiveMaxTokensValue}' for model ${effectiveModelId} (sent via extra_body).`, context);
180
- }
181
- else {
182
- // For models not in the list, or if OpenRouter handles the mapping transparently,
183
- // send max_tokens as a standard parameter.
184
- standardParams.max_tokens = effectiveMaxTokensValue;
185
- logger.info(`[${operation}] Using 'max_tokens: ${effectiveMaxTokensValue}' for model ${effectiveModelId}.`, context);
186
- }
187
- }
188
- const allEffectiveParams = { ...standardParams, ...extraBody };
189
- const sanitizedParams = sanitization.sanitizeForLogging(allEffectiveParams);
190
- logger.info(`[${operation}] Request received`, {
191
- ...context,
192
- params: sanitizedParams,
193
- streaming: isStreaming,
194
- });
195
- const rateLimitKey = context.requestId || "openrouter_default_key";
196
- try {
197
- rateLimiter.check(rateLimitKey, context);
198
- logger.debug(`[${operation}] Rate limit check passed`, {
199
- ...context,
200
- key: rateLimitKey,
201
- });
202
- }
203
- catch (error) {
204
- logger.warning(`[${operation}] Rate limit exceeded`, {
205
- ...context,
206
- key: rateLimitKey,
207
- error: error instanceof Error ? error.message : String(error),
208
- });
209
- throw error;
210
- }
211
- return await ErrorHandler.tryCatch(async () => {
212
- if (!this.client)
213
- throw new Error("Client missing despite ready status");
214
- const apiParams = { ...standardParams };
215
- if (Object.keys(extraBody).length > 0) {
216
- apiParams.extra_body = extraBody;
217
- }
218
- try {
219
- if (isStreaming) {
220
- const stream = await this.client.chat.completions.create(apiParams);
221
- logger.info(`[${operation}] Streaming request successful`, {
222
- ...context,
223
- model: apiParams.model,
224
- });
225
- return stream;
226
- }
227
- else {
228
- const completion = await this.client.chat.completions.create(apiParams);
229
- logger.info(`[${operation}] Non-streaming request successful`, {
230
- ...context,
231
- model: apiParams.model,
232
- });
233
- return completion;
234
- }
235
- }
236
- catch (error) {
237
- logger.error(`[${operation}] API call failed`, {
238
- ...context,
239
- error: error.message,
240
- status: error.status,
241
- });
242
- const errorDetails = {
243
- providerStatus: error.status,
244
- providerMessage: error.message,
245
- cause: error?.cause,
246
- };
247
- if (error.status === 401) {
248
- throw new McpError(BaseErrorCode.UNAUTHORIZED, `OpenRouter authentication failed: ${error.message}`, errorDetails);
249
- }
250
- else if (error.status === 429) {
251
- throw new McpError(BaseErrorCode.RATE_LIMITED, `OpenRouter rate limit exceeded: ${error.message}`, errorDetails);
252
- }
253
- else if (error.status === 402) {
254
- throw new McpError(BaseErrorCode.FORBIDDEN, `OpenRouter insufficient credits or payment required: ${error.message}`, errorDetails);
255
- }
256
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `OpenRouter API error (${error.status || "unknown status"}): ${error.message}`, errorDetails);
257
- }
258
- }, {
259
- operation,
260
- context,
261
- input: sanitizedParams,
262
- errorCode: BaseErrorCode.INTERNAL_ERROR,
263
- });
264
- }
265
- /**
266
- * Lists available models from the OpenRouter API.
267
- * Makes a direct `fetch` call to the `/models` endpoint.
268
- *
269
- * @param context - Request context for logging and error handling.
270
- * @returns A promise resolving with the JSON response from the OpenRouter API.
271
- * @throws {McpError} If the service is not ready, or if the API call fails.
272
- */
273
- async listModels(context) {
274
- const operation = "OpenRouterProvider.listModels";
275
- this.checkReady(operation, context);
276
- logger.info(`[${operation}] Request received`, context);
277
- return await ErrorHandler.tryCatch(async () => {
278
- try {
279
- const response = await fetch("https://openrouter.ai/api/v1/models", {
280
- method: "GET",
281
- headers: {
282
- "Content-Type": "application/json",
283
- // Authorization header might be needed if OpenRouter changes their /models endpoint access
284
- // "Authorization": `Bearer ${this.client?.apiKey}`, // apiKey is private on OpenAI client
285
- },
286
- });
287
- if (!response.ok) {
288
- const errorBody = await response.text();
289
- const errorDetails = {
290
- providerStatus: response.status,
291
- providerMessage: errorBody,
292
- };
293
- logger.error(`[${operation}] Failed to list models`, {
294
- ...context,
295
- ...errorDetails,
296
- });
297
- throw new McpError(BaseErrorCode.INTERNAL_ERROR, `OpenRouter list models API request failed with status ${response.status}.`, errorDetails);
298
- }
299
- const models = await response.json();
300
- logger.info(`[${operation}] Successfully listed models`, context);
301
- return models;
302
- }
303
- catch (error) {
304
- logger.error(`[${operation}] Error listing models`, {
305
- ...context,
306
- error: error.message,
307
- });
308
- if (error instanceof McpError) {
309
- throw error;
310
- }
311
- throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, `Network or unexpected error listing OpenRouter models: ${error.message}`, { cause: error });
312
- }
313
- }, {
314
- operation,
315
- context,
316
- errorCode: BaseErrorCode.INTERNAL_ERROR,
317
- });
318
- }
319
- }
320
- /**
321
- * Singleton instance of the `OpenRouterProvider`.
322
- * Initialized with the OpenRouter API key from application configuration.
323
- */
324
- // Update instantiation to pass options if needed, or rely on factory's use of global config.
325
- // For a singleton, it usually relies on global config.
326
- // If the constructor now takes OpenRouterClientOptions, and we want the singleton
327
- // to use global config, we'd pass undefined or an empty object for options.
328
- const openRouterProviderInstance = new OpenRouterProvider(undefined);
329
- export { openRouterProviderInstance as openRouterProvider };