mcp-ts-template 1.2.4 → 1.2.7

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.
package/README.md CHANGED
@@ -1,9 +1,9 @@
1
1
  # MCP TypeScript Template 🚀
2
2
 
3
3
  [![TypeScript](https://img.shields.io/badge/TypeScript-^5.8.3-blue.svg)](https://www.typescriptlang.org/)
4
- [![Model Context Protocol SDK](https://img.shields.io/badge/MCP%20SDK-1.11.4-green.svg)](https://github.com/modelcontextprotocol/typescript-sdk)
4
+ [![Model Context Protocol SDK](https://img.shields.io/badge/MCP%20SDK-1.11.5-green.svg)](https://github.com/modelcontextprotocol/typescript-sdk)
5
5
  [![MCP Spec Version](https://img.shields.io/badge/MCP%20Spec-2025--03--26-lightgrey.svg)](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/changelog.mdx)
6
- [![Version](https://img.shields.io/badge/Version-1.2.4-blue.svg)](./CHANGELOG.md)
6
+ [![Version](https://img.shields.io/badge/Version-1.2.7-blue.svg)](./CHANGELOG.md)
7
7
  [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
8
8
  [![Status](https://img.shields.io/badge/Status-Stable-green.svg)](https://github.com/cyanheads/mcp-ts-template/issues)
9
9
  [![GitHub](https://img.shields.io/github/stars/cyanheads/mcp-ts-template?style=social)](https://github.com/cyanheads/mcp-ts-template)
@@ -38,8 +38,9 @@ This template is already powering several MCP servers, demonstrating its flexibi
38
38
  | [**git-mcp-server**](https://github.com/cyanheads/git-mcp-server) | Provides an enterprise-ready MCP interface for Git operations. Allows LLM agents to initialize, clone, branch, commit, and manage repositories via STDIO & Streamable HTTP. | Actively using this template. |
39
39
  | [**obsidian-mcp-server**](https://github.com/cyanheads/obsidian-mcp-server/tree/mcp-ts-template-refactor) | Enables LLMs to interact securely with Obsidian vaults via MCP. Offers token-aware tools for searching, navigating, and updating Obsidian notes, facilitating seamless knowledge base management with Properties management. | Refactor in progress using this template ([see branch](https://github.com/cyanheads/obsidian-mcp-server/tree/mcp-ts-template-refactor)). |
40
40
  | [**filesystem-mcp-server**](https://github.com/cyanheads/filesystem-mcp-server) | Offers platform-agnostic file system capabilities for AI agents via MCP. Enables reading, writing, updating, and managing files/directories, featuring advanced search/replace and directory traversal. | Actively using this template. |
41
+ | [**atlas-mcp-server**](https://github.com/cyanheads/atlas-mcp-server) | Advanced task and knowledge management system with Neo4j backend, enabling structured data organization and complex querying for AI agents. | Aligned with this template (as of v2.8.8). |
41
42
 
42
- _Note: [**toolkit-mcp-server**](https://github.com/cyanheads/toolkit-mcp-server) and [**atlas-mcp-server**](https://github.com/cyanheads/atlas-mcp-server) were initially built using an older version of this template and are pending updates to the latest structure._
43
+ _Note: [**toolkit-mcp-server**](https://github.com/cyanheads/toolkit-mcp-server) was initially built using an older version of this template and is pending updates to the latest structure._
43
44
 
44
45
  You can also **see my [GitHub profile](https://github.com/cyanheads/)** for additional MCP servers I've created, many of which are planned to be migrated to or built upon this template in the future.
45
46
 
@@ -104,13 +105,14 @@ Configure the MCP server's behavior using these environment variables:
104
105
  | `MCP_ALLOWED_ORIGINS` | Comma-separated allowed origins for CORS (if `MCP_TRANSPORT_TYPE=http`). | (none) |
105
106
  | `MCP_SERVER_NAME` | Optional server name (used in MCP initialization). | (from package.json) |
106
107
  | `MCP_SERVER_VERSION` | Optional server version (used in MCP initialization). | (from package.json) |
107
- | `MCP_LOG_LEVEL` | Server logging level (`debug`, `info`, `warning`, `error`, etc.). | `info` |
108
+ | `MCP_LOG_LEVEL` | Server logging level (`debug`, `info`, `warning`, `error`, etc.). | `debug` |
109
+ | `LOGS_DIR` | Directory for log files. | `logs/` (in project root) |
108
110
  | `NODE_ENV` | Runtime environment (`development`, `production`). | `development` |
109
111
  | `MCP_AUTH_SECRET_KEY` | **Required for HTTP transport.** Secret key (min 32 chars) for signing/verifying auth tokens (JWT). | (none - **MUST be set in production**) |
110
- | `OPENROUTER_APP_URL` | URL of the application (used by OpenRouter service for HTTP Referer). | `https://caseyjhand.com` |
112
+ | `OPENROUTER_APP_URL` | URL of the application (used by OpenRouter service for HTTP Referer). | `http://localhost:3000` |
111
113
  | `OPENROUTER_APP_NAME` | Name of the application (used by OpenRouter service for X-Title header). | 'mcp-ts-template' |
112
114
  | `OPENROUTER_API_KEY` | API key for OpenRouter.ai service. Optional, but service will be unconfigured without it. | (none) |
113
- | `LLM_DEFAULT_MODEL` | Default model to use for LLM calls via OpenRouter. | `google/gemini-2.5-flash-preview:thinking` |
115
+ | `LLM_DEFAULT_MODEL` | Default model to use for LLM calls via OpenRouter. | `google/gemini-2.5-flash-preview-05-20` |
114
116
  | `LLM_DEFAULT_TEMPERATURE` | Default temperature for LLM calls (0-2). Optional. | (none) |
115
117
  | `LLM_DEFAULT_TOP_P` | Default top_p for LLM calls (0-1). Optional. | (none) |
116
118
  | `LLM_DEFAULT_MAX_TOKENS` | Default max_tokens for LLM calls. Optional. | (none) |
@@ -171,6 +173,12 @@ The `src/` directory is organized for clarity:
171
173
  - `resources/`: Example resource implementations (e.g., EchoResource).
172
174
  - `tools/`: Example tool implementations (e.g., EchoTool).
173
175
  - `transports/`: Handles `stdio` and `http` communication for the server.
176
+ - `services/`: Contains service integrations.
177
+ - `llm-providers/`: API Providers for Large Language Models.
178
+ - `openRouter/`: OpenRouter provider implementation.
179
+ - `llmFactory.ts`: Factory for creating LLM provider clients.
180
+ - `index.ts`: Barrel file for all LLM providers.
181
+ - `index.ts`: Barrel file for services.
174
182
  - `types-global/`: Shared TypeScript definitions (Errors, MCP types).
175
183
  - `utils/`: Reusable utilities (logging, errors, security, parsing, etc.). Exported via `index.ts`.
176
184
 
@@ -210,37 +218,38 @@ This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE
210
218
 
211
219
  ## Detailed Features Table
212
220
 
213
- | Category | Feature | Description | Location(s) |
214
- | :----------------------- | :------------------------------ | :----------------------------------------------------------------------------------------------------------- | :----------------------------------------------- |
215
- | **Core Components** | MCP Server | Core server logic, tool/resource registration, transport handling. Includes Echo Tool & Resource examples. | `src/mcp-server/` |
216
- | | MCP Client | Logic for connecting to external MCP servers (updated to **MCP 2025-03-26 spec**). | `src/mcp-client/` |
217
- | | Configuration | Environment-aware settings with Zod validation. | `src/config/`, `src/mcp-client/configLoader.ts` |
218
- | | HTTP Transport | Express-based server with SSE, session management, CORS, port retries. | `src/mcp-server/transports/httpTransport.ts` |
219
- | | Stdio Transport | Handles MCP communication over standard input/output. | `src/mcp-server/transports/stdioTransport.ts` |
220
- | **Utilities (Core)** | Logger | Structured, context-aware logging (files with rotation & MCP notifications). | `src/utils/internal/logger.ts` |
221
- | | ErrorHandler | Centralized error processing, classification, and logging. | `src/utils/internal/errorHandler.ts` |
222
- | | RequestContext | Request/operation tracking and correlation. | `src/utils/internal/requestContext.ts` |
223
- | **Utilities (Metrics)** | TokenCounter | Estimates token counts using `tiktoken`. | `src/utils/metrics/tokenCounter.ts` |
224
- | **Utilities (Parsing)** | DateParser | Parses natural language date strings using `chrono-node`. | `src/utils/parsing/dateParser.ts` |
225
- | | JsonParser | Parses potentially partial JSON, handles `<think>` blocks. | `src/utils/parsing/jsonParser.ts` |
226
- | **Utilities (Security)** | IdGenerator | Generates unique IDs (prefixed or UUIDs). | `src/utils/security/idGenerator.ts` |
227
- | | RateLimiter | Request throttling based on keys. | `src/utils/security/rateLimiter.ts` |
228
- | | Sanitization | Input validation/cleaning (HTML, paths, URLs, numbers, JSON) & log redaction (`validator`, `sanitize-html`). | `src/utils/security/sanitization.ts` |
229
- | **Services** | OpenRouter Provider | Service for interacting with OpenRouter API via OpenAI SDK compatibility. | `src/services/openRouterProvider.ts` |
230
- | **Type Safety** | Global Types | Shared TypeScript definitions for consistent interfaces (Errors, MCP types). | `src/types-global/` |
231
- | | Zod Schemas | Used for robust validation of configuration files and tool/resource inputs. | Throughout (`config`, `mcp-client`, tools, etc.) |
232
- | **Error Handling** | Pattern-Based Classification | Automatically categorize errors based on message patterns. | `src/utils/internal/errorHandler.ts` |
233
- | | Consistent Formatting | Standardized error responses with additional context. | `src/utils/internal/errorHandler.ts` |
234
- | | Safe Try/Catch Patterns | Centralized error processing helpers (`ErrorHandler.tryCatch`). | `src/utils/internal/errorHandler.ts` |
235
- | | Client/Transport Error Handling | Specific handlers for MCP client and transport error handling. | `src/mcp-client/client.ts`, `transport.ts` |
236
- | **Security** | Input Validation | Using `validator` and `zod` for various data type checks. | `src/utils/security/sanitization.ts`, etc. |
237
- | | Input Sanitization | Using `sanitize-html` to prevent injection attacks. | `src/utils/security/sanitization.ts` |
238
- | | Sensitive Data Redaction | Automatic redaction in logs. | `src/utils/security/sanitization.ts` |
239
- | | Configuration Fallback | Safely falls back to `mcp-config.json.example` if primary client config is missing. | `src/mcp-client/configLoader.ts` |
240
- | **Scripts** | Clean Script | Removes `dist` and `logs` directories (or custom targets). | `scripts/clean.ts` |
241
- | | Make Executable Script | Sets executable permissions (`chmod +x`) on specified files (Unix-like only). | `scripts/make-executable.ts` |
242
- | | Tree Script | Generates a directory structure tree, respecting `.gitignore`. | `scripts/tree.ts` |
243
- | | Fetch OpenAPI Spec Script | Fetches an OpenAPI spec (YAML/JSON) from a URL with fallbacks, saves locally. | `scripts/fetch-openapi-spec.ts` |
221
+ | Category | Feature | Description | Location(s) |
222
+ | :----------------------- | :------------------------------ | :----------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------ |
223
+ | **Core Components** | MCP Server | Core server logic, tool/resource registration, transport handling. Includes Echo Tool & Resource examples. | `src/mcp-server/` |
224
+ | | MCP Client | Logic for connecting to external MCP servers (updated to **MCP 2025-03-26 spec**). | `src/mcp-client/` |
225
+ | | Configuration | Environment-aware settings with Zod validation. | `src/config/`, `src/mcp-client/configLoader.ts` |
226
+ | | HTTP Transport | Express-based server with SSE, session management, CORS, port retries. | `src/mcp-server/transports/httpTransport.ts` |
227
+ | | Stdio Transport | Handles MCP communication over standard input/output. | `src/mcp-server/transports/stdioTransport.ts` |
228
+ | **Utilities (Core)** | Logger | Structured, context-aware logging (files with rotation & MCP notifications). | `src/utils/internal/logger.ts` |
229
+ | | ErrorHandler | Centralized error processing, classification, and logging. | `src/utils/internal/errorHandler.ts` |
230
+ | | RequestContext | Request/operation tracking and correlation. | `src/utils/internal/requestContext.ts` |
231
+ | **Utilities (Metrics)** | TokenCounter | Estimates token counts using `tiktoken`. | `src/utils/metrics/tokenCounter.ts` |
232
+ | **Utilities (Parsing)** | DateParser | Parses natural language date strings using `chrono-node`. | `src/utils/parsing/dateParser.ts` |
233
+ | | JsonParser | Parses potentially partial JSON, handles `<think>` blocks. | `src/utils/parsing/jsonParser.ts` |
234
+ | **Utilities (Security)** | IdGenerator | Generates unique IDs (prefixed or UUIDs). | `src/utils/security/idGenerator.ts` |
235
+ | | RateLimiter | Request throttling based on keys. | `src/utils/security/rateLimiter.ts` |
236
+ | | Sanitization | Input validation/cleaning (HTML, paths, URLs, numbers, JSON) & log redaction (`validator`, `sanitize-html`). | `src/utils/security/sanitization.ts` |
237
+ | **Services** | OpenRouter Provider | Service for interacting with OpenRouter API via OpenAI SDK compatibility. | `src/services/llm-providers/openRouter/openRouterProvider.ts` |
238
+ | | LLM Provider Factory | Centralized factory for creating LLM client instances (e.g., OpenRouter, Gemini (partial integration in LLMFactory but not usable yet)). | `src/services/llm-providers/llmFactory.ts` |
239
+ | **Type Safety** | Global Types | Shared TypeScript definitions for consistent interfaces (Errors, MCP types). | `src/types-global/` |
240
+ | | Zod Schemas | Used for robust validation of configuration files and tool/resource inputs. | Throughout (`config`, `mcp-client`, tools, etc.) |
241
+ | **Error Handling** | Pattern-Based Classification | Automatically categorize errors based on message patterns. | `src/utils/internal/errorHandler.ts` |
242
+ | | Consistent Formatting | Standardized error responses with additional context. | `src/utils/internal/errorHandler.ts` |
243
+ | | Safe Try/Catch Patterns | Centralized error processing helpers (`ErrorHandler.tryCatch`). | `src/utils/internal/errorHandler.ts` |
244
+ | | Client/Transport Error Handling | Specific handlers for MCP client and transport error handling. | `src/mcp-client/client.ts`, `transport.ts` |
245
+ | **Security** | Input Validation | Using `validator` and `zod` for various data type checks. | `src/utils/security/sanitization.ts`, etc. |
246
+ | | Input Sanitization | Using `sanitize-html` to prevent injection attacks. | `src/utils/security/sanitization.ts` |
247
+ | | Sensitive Data Redaction | Automatic redaction in logs. | `src/utils/security/sanitization.ts` |
248
+ | | Configuration Fallback | Safely falls back to `mcp-config.json.example` if primary client config is missing. | `src/mcp-client/configLoader.ts` |
249
+ | **Scripts** | Clean Script | Removes `dist` and `logs` directories (or custom targets). | `scripts/clean.ts` |
250
+ | | Make Executable Script | Sets executable permissions (`chmod +x`) on specified files (Unix-like only). | `scripts/make-executable.ts` |
251
+ | | Tree Script | Generates a directory structure tree, respecting `.gitignore`. | `scripts/tree.ts` |
252
+ | | Fetch OpenAPI Spec Script | Fetches an OpenAPI spec (YAML/JSON) from a URL with fallbacks, saves locally. | `scripts/fetch-openapi-spec.ts` |
244
253
 
245
254
  ---
246
255
 
@@ -25,6 +25,8 @@ 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
30
  /** Runtime environment. From `NODE_ENV` env var. Default: "development". */
29
31
  environment: string;
30
32
  /** MCP transport type ('stdio' or 'http'). From `MCP_TRANSPORT_TYPE` env var. Default: "stdio". */
@@ -55,6 +57,8 @@ export declare const config: {
55
57
  llmDefaultTopK: number | undefined;
56
58
  /** Default LLM min_p. From `LLM_DEFAULT_MIN_P`. */
57
59
  llmDefaultMinP: number | undefined;
60
+ /** Gemini API Key. From `GEMINI_API_KEY`. */
61
+ geminiApiKey: string | undefined;
58
62
  /** OAuth Proxy configurations. Undefined if no related env vars are set. */
59
63
  oauthProxy: {
60
64
  authorizationUrl: string | undefined;
@@ -15,13 +15,48 @@
15
15
  * @module src/config/index
16
16
  */
17
17
  import dotenv from "dotenv";
18
- import { readFileSync } from "fs";
19
- import { dirname, join } from "path";
18
+ import { existsSync, mkdirSync, readFileSync, statSync } from "fs";
19
+ import path, { dirname, join } from "path";
20
20
  import { fileURLToPath } from "url";
21
21
  import { z } from "zod";
22
22
  dotenv.config();
23
- const __dirname = dirname(fileURLToPath(import.meta.url));
24
- const pkgPath = join(__dirname, "../../package.json");
23
+ // --- 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
+ const findProjectRoot = (startDir) => {
30
+ let currentDir = startDir;
31
+ while (true) {
32
+ const packageJsonPath = join(currentDir, 'package.json');
33
+ if (existsSync(packageJsonPath)) {
34
+ return currentDir;
35
+ }
36
+ const parentDir = dirname(currentDir);
37
+ if (parentDir === currentDir) {
38
+ // Reached the root of the filesystem without finding package.json
39
+ throw new Error(`Could not find project root (package.json) starting from ${startDir}`);
40
+ }
41
+ currentDir = parentDir;
42
+ }
43
+ };
44
+ let projectRoot;
45
+ try {
46
+ // For ESM, __dirname is not available directly.
47
+ // import.meta.url gives the URL of the current module.
48
+ const currentModuleDir = dirname(fileURLToPath(import.meta.url));
49
+ projectRoot = findProjectRoot(currentModuleDir);
50
+ }
51
+ catch (error) {
52
+ 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
+ projectRoot = process.cwd();
56
+ console.warn(`Warning: Using process.cwd() (${projectRoot}) as fallback project root.`);
57
+ }
58
+ // --- End Determine Project Root ---
59
+ const pkgPath = join(projectRoot, "package.json"); // Use determined projectRoot
25
60
  let pkg = { name: "mcp-ts-template", version: "0.0.0" };
26
61
  try {
27
62
  pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
@@ -43,6 +78,8 @@ const EnvSchema = z.object({
43
78
  MCP_SERVER_VERSION: z.string().optional(),
44
79
  /** Minimum logging level. See `McpLogLevel` in logger utility. Default: "debug". */
45
80
  MCP_LOG_LEVEL: z.string().default("debug"),
81
+ /** Directory for log files. Defaults to "logs" in project root. */
82
+ LOGS_DIR: z.string().default(path.join(projectRoot, "logs")),
46
83
  /** Runtime environment (e.g., "development", "production"). Default: "development". */
47
84
  NODE_ENV: z.string().default("development"),
48
85
  /** MCP communication transport ("stdio" or "http"). Default: "stdio". */
@@ -70,7 +107,7 @@ const EnvSchema = z.object({
70
107
  /** Default LLM model. Default: "google/gemini-2.5-flash-preview:thinking". */
71
108
  LLM_DEFAULT_MODEL: z
72
109
  .string()
73
- .default("google/gemini-2.5-flash-preview:thinking"),
110
+ .default("google/gemini-2.5-flash-preview-05-20"),
74
111
  /** Optional. Default LLM temperature (0.0-2.0). */
75
112
  LLM_DEFAULT_TEMPERATURE: z.coerce.number().min(0).max(2).optional(),
76
113
  /** Optional. Default LLM top_p (0.0-1.0). */
@@ -81,6 +118,8 @@ const EnvSchema = z.object({
81
118
  LLM_DEFAULT_TOP_K: z.coerce.number().int().nonnegative().optional(),
82
119
  /** Optional. Default LLM min_p (0.0-1.0). */
83
120
  LLM_DEFAULT_MIN_P: z.coerce.number().min(0).max(1).optional(),
121
+ /** Optional. API key for Google Gemini services. */
122
+ GEMINI_API_KEY: z.string().optional(),
84
123
  /** Optional. OAuth provider authorization endpoint URL. */
85
124
  OAUTH_PROXY_AUTHORIZATION_URL: z
86
125
  .string()
@@ -117,6 +156,67 @@ if (!parsedEnv.success) {
117
156
  // Consider throwing an error in production for critical misconfigurations.
118
157
  }
119
158
  const env = parsedEnv.success ? parsedEnv.data : EnvSchema.parse({});
159
+ // --- Directory Ensurance Function ---
160
+ /**
161
+ * Ensures a directory exists and is within the project root.
162
+ * @param dirPath The desired path for the directory (can be relative or absolute).
163
+ * @param rootDir The root directory of the project to contain the directory.
164
+ * @param dirName The name of the directory type for logging (e.g., "logs").
165
+ * @returns The validated, absolute path to the directory, or null if invalid.
166
+ */
167
+ const ensureDirectory = (dirPath, rootDir, dirName) => {
168
+ const resolvedDirPath = path.isAbsolute(dirPath) ? dirPath : path.resolve(rootDir, dirPath);
169
+ // Ensure the resolved path is within the project root boundary
170
+ if (!resolvedDirPath.startsWith(rootDir + path.sep) && resolvedDirPath !== rootDir) {
171
+ if (process.stdout.isTTY) {
172
+ console.error(`Error: ${dirName} path "${dirPath}" resolves to "${resolvedDirPath}", which is outside the project boundary "${rootDir}".`);
173
+ }
174
+ return null;
175
+ }
176
+ if (!existsSync(resolvedDirPath)) {
177
+ try {
178
+ mkdirSync(resolvedDirPath, { recursive: true });
179
+ if (process.stdout.isTTY) {
180
+ console.log(`Created ${dirName} directory: ${resolvedDirPath}`);
181
+ }
182
+ }
183
+ catch (err) {
184
+ const errorMessage = err instanceof Error ? err.message : String(err);
185
+ if (process.stdout.isTTY) {
186
+ console.error(`Error creating ${dirName} directory at ${resolvedDirPath}: ${errorMessage}`);
187
+ }
188
+ return null;
189
+ }
190
+ }
191
+ else {
192
+ try {
193
+ const stats = statSync(resolvedDirPath);
194
+ if (!stats.isDirectory()) {
195
+ if (process.stdout.isTTY) {
196
+ console.error(`Error: ${dirName} path ${resolvedDirPath} exists but is not a directory.`);
197
+ }
198
+ return null;
199
+ }
200
+ }
201
+ catch (statError) {
202
+ if (process.stdout.isTTY) {
203
+ console.error(`Error accessing ${dirName} path ${resolvedDirPath}: ${statError.message}`);
204
+ }
205
+ return null;
206
+ }
207
+ }
208
+ return resolvedDirPath;
209
+ };
210
+ // --- End Directory Ensurance Function ---
211
+ // --- 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.");
216
+ }
217
+ process.exit(1); // Exit if logs directory is not usable
218
+ }
219
+ // --- End Logs Directory Handling ---
120
220
  /**
121
221
  * Main application configuration object.
122
222
  * Aggregates settings from validated environment variables and `package.json`.
@@ -128,6 +228,8 @@ export const config = {
128
228
  mcpServerVersion: env.MCP_SERVER_VERSION || pkg.version,
129
229
  /** Logging level. From `MCP_LOG_LEVEL` env var. Default: "debug". */
130
230
  logLevel: env.MCP_LOG_LEVEL,
231
+ /** Absolute path to the logs directory. From `LOGS_DIR` env var. */
232
+ logsPath: validatedLogsPath,
131
233
  /** Runtime environment. From `NODE_ENV` env var. Default: "development". */
132
234
  environment: env.NODE_ENV,
133
235
  /** MCP transport type ('stdio' or 'http'). From `MCP_TRANSPORT_TYPE` env var. Default: "stdio". */
@@ -160,6 +262,8 @@ export const config = {
160
262
  llmDefaultTopK: env.LLM_DEFAULT_TOP_K,
161
263
  /** Default LLM min_p. From `LLM_DEFAULT_MIN_P`. */
162
264
  llmDefaultMinP: env.LLM_DEFAULT_MIN_P,
265
+ /** Gemini API Key. From `GEMINI_API_KEY`. */
266
+ geminiApiKey: env.GEMINI_API_KEY,
163
267
  /** OAuth Proxy configurations. Undefined if no related env vars are set. */
164
268
  oauthProxy: env.OAUTH_PROXY_AUTHORIZATION_URL ||
165
269
  env.OAUTH_PROXY_TOKEN_URL ||
@@ -406,7 +406,11 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
406
406
  ...baseSessionReqContext,
407
407
  sessionId,
408
408
  });
409
- res.status(404).send("Session not found or expired");
409
+ res.status(404).json({
410
+ jsonrpc: "2.0",
411
+ error: { code: -32004, message: "Session not found or expired" },
412
+ id: null // Or a relevant request identifier if available from context
413
+ });
410
414
  return;
411
415
  }
412
416
  try {
@@ -422,7 +426,11 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
422
426
  stack: err instanceof Error ? err.stack : undefined,
423
427
  });
424
428
  if (!res.headersSent) {
425
- res.status(500).send("Internal Server Error");
429
+ res.status(500).json({
430
+ jsonrpc: "2.0",
431
+ error: { code: -32603, message: "Internal Server Error" },
432
+ id: null // Or a relevant request identifier
433
+ });
426
434
  }
427
435
  }
428
436
  };
@@ -433,10 +441,16 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
433
441
  try {
434
442
  logger.debug("Attempting to start HTTP server with retry logic...", transportContext);
435
443
  const actualPort = await startHttpServerWithRetry(serverInstance, config.mcpHttpPort, config.mcpHttpHost, MAX_PORT_RETRIES, transportContext);
436
- const protocol = config.environment === "production" ? "https" : "http";
437
- const serverAddress = `${protocol}://${config.mcpHttpHost}:${actualPort}${MCP_ENDPOINT_PATH}`;
444
+ let serverAddressLog = `http://${config.mcpHttpHost}:${actualPort}${MCP_ENDPOINT_PATH}`;
445
+ let productionNote = "";
446
+ if (config.environment === "production") {
447
+ // The server itself runs HTTP, but it's expected to be behind an HTTPS proxy in production.
448
+ // The log reflects the effective public-facing URL.
449
+ serverAddressLog = `https://${config.mcpHttpHost}:${actualPort}${MCP_ENDPOINT_PATH}`;
450
+ productionNote = ` (via HTTPS, ensure reverse proxy is configured)`;
451
+ }
438
452
  if (process.stdout.isTTY) {
439
- console.log(`\n🚀 MCP Server running in HTTP mode at: ${serverAddress}\n (MCP Spec: 2025-03-26 Streamable HTTP Transport)\n`);
453
+ console.log(`\n🚀 MCP Server running in HTTP mode at: ${serverAddressLog}${productionNote}\n (MCP Spec: 2025-03-26 Streamable HTTP Transport)\n`);
440
454
  }
441
455
  }
442
456
  catch (err) {
@@ -1 +1,7 @@
1
+ /**
2
+ * @fileoverview Main barrel file for all services.
3
+ * This file re-exports all service modules, providing a single entry point
4
+ * for accessing various services within the application.
5
+ * @module src/services/index
6
+ */
1
7
  export * from './llm-providers';
@@ -1 +1,7 @@
1
+ /**
2
+ * @fileoverview Main barrel file for all services.
3
+ * This file re-exports all service modules, providing a single entry point
4
+ * for accessing various services within the application.
5
+ * @module src/services/index
6
+ */
1
7
  export * from './llm-providers';
@@ -1 +1,7 @@
1
- export * from './openRouterProvider';
1
+ /**
2
+ * @fileoverview Barrel file for LLM provider services.
3
+ * This file re-exports all services related to different Large Language Model providers,
4
+ * making them easily accessible from a single import path.
5
+ * @module src/services/llm-providers/index
6
+ */
7
+ export * from './openRouter';
@@ -1 +1,7 @@
1
- export * from './openRouterProvider';
1
+ /**
2
+ * @fileoverview Barrel file for LLM provider services.
3
+ * This file re-exports all services related to different Large Language Model providers,
4
+ * making them easily accessible from a single import path.
5
+ * @module src/services/llm-providers/index
6
+ */
7
+ export * from './openRouter'; // Changed to export from the new barrel file
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @fileoverview Factory for creating LLM client instances.
3
+ * Provides a centralized way to instantiate clients for different LLM providers
4
+ * like OpenRouter and Google Gemini, handling API key configuration and
5
+ * basic client setup.
6
+ * @module src/services/llm-providers/llmFactory
7
+ */
8
+ import { GoogleGenAI } from '@google/genai';
9
+ import OpenAI from 'openai';
10
+ import { RequestContext } from '../../utils/index.js';
11
+ /**
12
+ * Defines the supported LLM providers.
13
+ */
14
+ export type LlmProviderType = 'openrouter' | 'gemini';
15
+ /**
16
+ * Options for configuring the OpenRouter client.
17
+ */
18
+ export interface OpenRouterClientOptions {
19
+ apiKey?: string;
20
+ baseURL?: string;
21
+ siteUrl?: string;
22
+ siteName?: string;
23
+ }
24
+ /**
25
+ * Options for configuring the Gemini client using @google/genai.
26
+ * The factory will return a GoogleGenAI instance.
27
+ * Vertex AI specific options are included here.
28
+ */
29
+ export interface GeminiClientOptions {
30
+ apiKey?: string;
31
+ useVertexAi?: boolean;
32
+ project?: string;
33
+ location?: string;
34
+ }
35
+ /**
36
+ * Union type for all LLM client options.
37
+ */
38
+ export type LlmClientOptions = OpenRouterClientOptions | GeminiClientOptions;
39
+ /**
40
+ * LLM Factory class to create and configure LLM clients.
41
+ */
42
+ declare class LlmFactory {
43
+ /**
44
+ * Creates and returns an LLM client instance for the specified provider.
45
+ *
46
+ * @param provider - The LLM provider to create a client for.
47
+ * @param context - The request context for logging.
48
+ * @param options - Optional provider-specific configuration options.
49
+ * @returns A Promise resolving to an instance of OpenAI (for OpenRouter)
50
+ * or GoogleGenAI (for Gemini).
51
+ * @throws {McpError} If the provider is unsupported or API key/config is missing.
52
+ */
53
+ getLlmClient(provider: LlmProviderType, context: RequestContext, options?: LlmClientOptions): Promise<OpenAI | GoogleGenAI>;
54
+ /**
55
+ * Creates an OpenAI client configured for OpenRouter.
56
+ * @private
57
+ */
58
+ private createOpenRouterClient;
59
+ /**
60
+ * Creates a GoogleGenAI client for Gemini, supporting standard API key or Vertex AI.
61
+ * @private
62
+ */
63
+ private createGeminiClient;
64
+ }
65
+ /**
66
+ * Singleton instance of the LlmFactory.
67
+ */
68
+ export declare const llmFactory: LlmFactory;
69
+ export {};
@@ -0,0 +1,124 @@
1
+ /**
2
+ * @fileoverview Factory for creating LLM client instances.
3
+ * Provides a centralized way to instantiate clients for different LLM providers
4
+ * like OpenRouter and Google Gemini, handling API key configuration and
5
+ * basic client setup.
6
+ * @module src/services/llm-providers/llmFactory
7
+ */
8
+ import { GoogleGenAI } from '@google/genai'; // Updated import path
9
+ import OpenAI from 'openai';
10
+ import { config } from '../../config/index.js';
11
+ import { BaseErrorCode, McpError } from '../../types-global/errors.js';
12
+ import { logger } from '../../utils/index.js';
13
+ /**
14
+ * LLM Factory class to create and configure LLM clients.
15
+ */
16
+ class LlmFactory {
17
+ /**
18
+ * Creates and returns an LLM client instance for the specified provider.
19
+ *
20
+ * @param provider - The LLM provider to create a client for.
21
+ * @param context - The request context for logging.
22
+ * @param options - Optional provider-specific configuration options.
23
+ * @returns A Promise resolving to an instance of OpenAI (for OpenRouter)
24
+ * or GoogleGenAI (for Gemini).
25
+ * @throws {McpError} If the provider is unsupported or API key/config is missing.
26
+ */
27
+ async getLlmClient(provider, context, options) {
28
+ const operation = `LlmFactory.getLlmClient.${provider}`;
29
+ logger.info(`[${operation}] Requesting LLM client`, { ...context, provider });
30
+ switch (provider) {
31
+ case 'openrouter':
32
+ return this.createOpenRouterClient(context, options);
33
+ case 'gemini':
34
+ return this.createGeminiClient(context, options);
35
+ default:
36
+ logger.error(`[${operation}] Unsupported LLM provider requested: ${provider}`, context);
37
+ throw new McpError(BaseErrorCode.CONFIGURATION_ERROR, `Unsupported LLM provider: ${provider}`, { operation, provider });
38
+ }
39
+ }
40
+ /**
41
+ * Creates an OpenAI client configured for OpenRouter.
42
+ * @private
43
+ */
44
+ createOpenRouterClient(context, options) {
45
+ const operation = 'LlmFactory.createOpenRouterClient';
46
+ const apiKey = options?.apiKey || config.openrouterApiKey;
47
+ const baseURL = options?.baseURL || 'https://openrouter.ai/api/v1';
48
+ const siteUrl = options?.siteUrl || config.openrouterAppUrl;
49
+ const siteName = options?.siteName || config.openrouterAppName;
50
+ if (!apiKey) {
51
+ logger.error(`[${operation}] OPENROUTER_API_KEY is not set.`, context);
52
+ throw new McpError(BaseErrorCode.CONFIGURATION_ERROR, 'OpenRouter API key is not configured.', { operation });
53
+ }
54
+ try {
55
+ const client = new OpenAI({
56
+ baseURL,
57
+ apiKey,
58
+ defaultHeaders: {
59
+ 'HTTP-Referer': siteUrl,
60
+ 'X-Title': siteName,
61
+ },
62
+ });
63
+ logger.info(`[${operation}] OpenRouter client created successfully.`, context);
64
+ return client;
65
+ }
66
+ catch (error) {
67
+ logger.error(`[${operation}] Failed to create OpenRouter client`, { ...context, error: error.message });
68
+ throw new McpError(BaseErrorCode.INITIALIZATION_FAILED, `Failed to initialize OpenRouter client: ${error.message}`, { operation, cause: error });
69
+ }
70
+ }
71
+ /**
72
+ * Creates a GoogleGenAI client for Gemini, supporting standard API key or Vertex AI.
73
+ * @private
74
+ */
75
+ createGeminiClient(context, options) {
76
+ const operation = 'LlmFactory.createGeminiClient';
77
+ if (options?.useVertexAi) {
78
+ if (!options.project || !options.location) {
79
+ logger.error(`[${operation}] Vertex AI project and location are required when useVertexAi is true.`, context);
80
+ throw new McpError(BaseErrorCode.CONFIGURATION_ERROR, 'Vertex AI project and location must be configured if useVertexAi is true.', { operation });
81
+ }
82
+ try {
83
+ // For Vertex AI, apiKey in GoogleGenAI constructor is optional if ADC are set up.
84
+ // The SDK handles ADC automatically if apiKey is not provided.
85
+ const clientConfig = {
86
+ project: options.project,
87
+ location: options.location,
88
+ vertexai: true,
89
+ };
90
+ if (options.apiKey) { // Allow API key to be passed for Vertex if specific auth needed
91
+ clientConfig.apiKey = options.apiKey;
92
+ }
93
+ const genAI = new GoogleGenAI(clientConfig);
94
+ logger.info(`[${operation}] GoogleGenAI client for Vertex AI created successfully.`, context);
95
+ return genAI;
96
+ }
97
+ catch (error) {
98
+ logger.error(`[${operation}] Failed to create Gemini client for Vertex AI`, { ...context, error: error.message });
99
+ throw new McpError(BaseErrorCode.INITIALIZATION_FAILED, `Failed to initialize Gemini client for Vertex AI: ${error.message}`, { operation, cause: error });
100
+ }
101
+ }
102
+ else {
103
+ // Standard Gemini API key authentication
104
+ const apiKey = options?.apiKey || config.geminiApiKey;
105
+ if (!apiKey) {
106
+ logger.error(`[${operation}] GEMINI_API_KEY is not set for standard API usage.`, context);
107
+ throw new McpError(BaseErrorCode.CONFIGURATION_ERROR, 'Gemini API key is not configured for standard API usage.', { operation });
108
+ }
109
+ try {
110
+ const genAI = new GoogleGenAI({ apiKey });
111
+ logger.info(`[${operation}] GoogleGenAI client (standard API key) created successfully.`, context);
112
+ return genAI;
113
+ }
114
+ catch (error) {
115
+ logger.error(`[${operation}] Failed to create Gemini client (standard API key)`, { ...context, error: error.message });
116
+ throw new McpError(BaseErrorCode.INITIALIZATION_FAILED, `Failed to initialize Gemini client (standard API key): ${error.message}`, { operation, cause: error });
117
+ }
118
+ }
119
+ }
120
+ }
121
+ /**
122
+ * Singleton instance of the LlmFactory.
123
+ */
124
+ export const llmFactory = new LlmFactory();
@@ -0,0 +1,6 @@
1
+ /**
2
+ * @fileoverview Barrel file for the OpenRouter provider service.
3
+ * Exports the OpenRouterProvider class and any related types.
4
+ * @module services/llm-providers/openRouter/index
5
+ */
6
+ export * from './openRouterProvider';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * @fileoverview Barrel file for the OpenRouter provider service.
3
+ * Exports the OpenRouterProvider class and any related types.
4
+ * @module services/llm-providers/openRouter/index
5
+ */
6
+ export * from './openRouterProvider';
7
+ // Add other exports from this module if any in the future
@@ -1,6 +1,7 @@
1
+ import { OpenRouterClientOptions } from "../llmFactory.js";
1
2
  import { ChatCompletion, ChatCompletionChunk, ChatCompletionCreateParamsNonStreaming, ChatCompletionCreateParamsStreaming } from "openai/resources/chat/completions";
2
3
  import { Stream } from "openai/streaming";
3
- import { OperationContext, RequestContext } from "../../utils/internal/requestContext.js";
4
+ import { OperationContext, RequestContext } from "../../../utils/internal/requestContext.js";
4
5
  /**
5
6
  * Defines the parameters for an OpenRouter chat completion request.
6
7
  * This type extends standard OpenAI chat completion parameters and includes
@@ -42,7 +43,7 @@ declare class OpenRouterProvider {
42
43
  * - `ready`: Client initialized successfully and service is usable.
43
44
  * - `error`: An error occurred during initialization.
44
45
  */
45
- readonly status: "unconfigured" | "initializing" | "ready" | "error";
46
+ status: "unconfigured" | "initializing" | "ready" | "error";
46
47
  /**
47
48
  * Stores any error that occurred during client initialization.
48
49
  * @private
@@ -55,7 +56,7 @@ declare class OpenRouterProvider {
55
56
  * @param apiKey - The OpenRouter API key. If undefined, the service remains 'unconfigured'.
56
57
  * @param parentOpContext - Optional parent operation context for linked logging.
57
58
  */
58
- constructor(apiKey: string | undefined, parentOpContext?: OperationContext);
59
+ constructor(options?: OpenRouterClientOptions, parentOpContext?: OperationContext);
59
60
  /**
60
61
  * Checks if the service is ready to make API calls.
61
62
  * @param operation - The name of the operation attempting to use the service.
@@ -1,20 +1,11 @@
1
- /**
2
- * @fileoverview Provides a service class (`OpenRouterProvider`) for interacting with the
3
- * OpenRouter API, using the OpenAI SDK for chat completions. It handles API key
4
- * configuration, default parameters, rate limiting, model-specific parameter adjustments,
5
- * and error handling.
6
- * @module src/services/openRouterProvider
7
- */
8
- import OpenAI from "openai";
9
- import { config } from "../../config/index.js";
10
- import { BaseErrorCode, McpError } from "../../types-global/errors.js";
11
- import { ErrorHandler } from "../../utils/internal/errorHandler.js";
12
- import { logger } from "../../utils/internal/logger.js";
13
- import { requestContextService, } from "../../utils/internal/requestContext.js";
14
- import { rateLimiter } from "../../utils/security/rateLimiter.js";
15
- import { sanitization } from "../../utils/security/sanitization.js";
16
- const YOUR_SITE_URL = config.openrouterAppUrl;
17
- const YOUR_SITE_NAME = config.openrouterAppName;
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";
18
9
  /**
19
10
  * Service class for interacting with the OpenRouter API.
20
11
  * Uses the OpenAI SDK for chat completions, configured for OpenRouter.
@@ -29,7 +20,7 @@ class OpenRouterProvider {
29
20
  * @param apiKey - The OpenRouter API key. If undefined, the service remains 'unconfigured'.
30
21
  * @param parentOpContext - Optional parent operation context for linked logging.
31
22
  */
32
- constructor(apiKey, parentOpContext) {
23
+ constructor(options, parentOpContext) {
33
24
  /**
34
25
  * Stores any error that occurred during client initialization.
35
26
  * @private
@@ -43,36 +34,34 @@ class OpenRouterProvider {
43
34
  parentRequestId: parentOpContext?.requestId,
44
35
  });
45
36
  this.status = "initializing";
46
- if (!apiKey) {
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) {
47
42
  this.status = "unconfigured";
48
- logger.warning("OPENROUTER_API_KEY is not set. OpenRouter service is not configured.", { ...opContext, service: "OpenRouterProvider" });
49
- return;
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.
50
46
  }
51
- try {
52
- this.client = new OpenAI({
53
- baseURL: "https://openrouter.ai/api/v1",
54
- apiKey: apiKey,
55
- defaultHeaders: {
56
- "HTTP-Referer": YOUR_SITE_URL,
57
- "X-Title": YOUR_SITE_NAME,
58
- },
59
- });
47
+ llmFactory.getLlmClient('openrouter', opContext, options)
48
+ .then(client => {
49
+ this.client = client; // Factory returns OpenAI for 'openrouter'
60
50
  this.status = "ready";
61
- logger.info("OpenRouter Service Initialized and Ready", {
51
+ logger.info("OpenRouter Service Initialized and Ready via LlmFactory", {
62
52
  ...opContext,
63
53
  service: "OpenRouterProvider",
64
54
  });
65
- }
66
- catch (error) {
55
+ })
56
+ .catch(error => {
67
57
  this.status = "error";
68
- this.initializationError =
69
- error instanceof Error ? error : new Error(String(error));
70
- logger.error("Failed to initialize OpenRouter client", {
58
+ this.initializationError = error instanceof Error ? error : new McpError(BaseErrorCode.INITIALIZATION_FAILED, String(error));
59
+ logger.error("Failed to initialize OpenRouter client via LlmFactory", {
71
60
  ...opContext,
72
61
  service: "OpenRouterProvider",
73
62
  error: this.initializationError.message,
74
63
  });
75
- }
64
+ });
76
65
  }
77
66
  /**
78
67
  * Checks if the service is ready to make API calls.
@@ -328,5 +317,9 @@ class OpenRouterProvider {
328
317
  * Singleton instance of the `OpenRouterProvider`.
329
318
  * Initialized with the OpenRouter API key from application configuration.
330
319
  */
331
- const openRouterProviderInstance = new OpenRouterProvider(config.openrouterApiKey);
320
+ // Update instantiation to pass options if needed, or rely on factory's use of global config.
321
+ // For a singleton, it usually relies on global config.
322
+ // If the constructor now takes OpenRouterClientOptions, and we want the singleton
323
+ // to use global config, we'd pass undefined or an empty object for options.
324
+ const openRouterProviderInstance = new OpenRouterProvider(undefined);
332
325
  export { openRouterProviderInstance as openRouterProvider };
@@ -42,6 +42,7 @@ export declare class Logger {
42
42
  private mcpNotificationSender?;
43
43
  private currentMcpLevel;
44
44
  private currentWinstonLevel;
45
+ private readonly MCP_NOTIFICATION_STACK_TRACE_MAX_LENGTH;
45
46
  private readonly LOG_FILE_MAX_SIZE;
46
47
  private readonly LOG_MAX_FILES;
47
48
  /** @private */
@@ -62,6 +63,13 @@ export declare class Logger {
62
63
  * @param newLevel - The new minimum MCP log level to set.
63
64
  */
64
65
  setLevel(newLevel: McpLogLevel): void;
66
+ /**
67
+ * Configures the console transport based on the current log level and TTY status.
68
+ * Adds or removes the console transport as needed.
69
+ * @returns {{ enabled: boolean, message: string | null }} Status of console logging.
70
+ * @private
71
+ */
72
+ private _configureConsoleTransport;
65
73
  /**
66
74
  * Gets the singleton instance of the Logger.
67
75
  * @returns The singleton Logger instance.
@@ -36,19 +36,9 @@ const mcpToWinstonLevel = {
36
36
  alert: "error",
37
37
  emerg: "error",
38
38
  };
39
- const projectRoot = process.cwd(); // Use current working directory as project root
40
- const logsDir = path.join(projectRoot, "logs");
41
- // Security check for the logs directory path
42
- const resolvedLogsDir = path.resolve(logsDir); // Should be projectRoot/logs
43
- const isLogsDirSafe = resolvedLogsDir.startsWith(projectRoot + path.sep) &&
44
- resolvedLogsDir !== projectRoot;
45
- if (!isLogsDirSafe) {
46
- // This case should ideally not be hit if logsDir is simply projectRoot + /logs
47
- // But it's a safeguard if path.join or path.resolve behaves unexpectedly or logsDir is manipulated.
48
- if (process.stdout.isTTY) {
49
- console.error(`FATAL: Resolved logs directory "${resolvedLogsDir}" is not safely within project root "${projectRoot}". File logging will be disabled.`);
50
- }
51
- }
39
+ // The logsPath from config is already resolved and validated by src/config/index.ts
40
+ const resolvedLogsDir = config.logsPath;
41
+ const isLogsDirSafe = !!resolvedLogsDir; // If logsPath is set, it's considered safe by config logic.
52
42
  /**
53
43
  * Creates the Winston console log format.
54
44
  * @returns The Winston log format for console output.
@@ -95,6 +85,7 @@ export class Logger {
95
85
  this.initialized = false;
96
86
  this.currentMcpLevel = "info";
97
87
  this.currentWinstonLevel = "info";
88
+ this.MCP_NOTIFICATION_STACK_TRACE_MAX_LENGTH = 1024;
98
89
  this.LOG_FILE_MAX_SIZE = 5 * 1024 * 1024; // 5MB
99
90
  this.LOG_MAX_FILES = 5;
100
91
  }
@@ -114,21 +105,27 @@ export class Logger {
114
105
  }
115
106
  this.currentMcpLevel = level;
116
107
  this.currentWinstonLevel = mcpToWinstonLevel[level];
117
- let logsDirCreatedMessage = null;
108
+ let logsDirCreatedMessage = null; // This message is now informational as creation is handled by config
118
109
  if (isLogsDirSafe) {
119
- try {
120
- if (!fs.existsSync(resolvedLogsDir)) {
110
+ // Directory creation is handled by config/index.ts ensureDirectory.
111
+ // We can log if it was newly created by checking if it existed before config ran,
112
+ // but that's complex. For now, we assume config handled it.
113
+ // If resolvedLogsDir is set, config ensures it exists.
114
+ if (!fs.existsSync(resolvedLogsDir)) {
115
+ // This case should ideally not be hit if config.logsPath is correctly set up and validated.
116
+ // However, if it somehow occurs (e.g. dir deleted after config init but before logger init),
117
+ // we attempt to create it.
118
+ try {
121
119
  await fs.promises.mkdir(resolvedLogsDir, { recursive: true });
122
- logsDirCreatedMessage = `Created logs directory: ${resolvedLogsDir}`;
120
+ logsDirCreatedMessage = `Re-created logs directory (should have been created by config): ${resolvedLogsDir}`;
123
121
  }
124
- }
125
- catch (err) {
126
- if (process.stdout.isTTY) {
127
- const errorMessage = err instanceof Error ? err.message : String(err);
128
- console.error(`Error creating logs directory at ${resolvedLogsDir}: ${errorMessage}. File logging disabled.`);
122
+ catch (err) {
123
+ if (process.stdout.isTTY) {
124
+ const errorMessage = err instanceof Error ? err.message : String(err);
125
+ console.error(`Error creating logs directory at ${resolvedLogsDir}: ${errorMessage}. File logging disabled.`);
126
+ }
127
+ throw err; // Critical if logs dir cannot be ensured
129
128
  }
130
- // Rethrow the error to ensure startup fails if logs directory cannot be created
131
- throw err;
132
129
  }
133
130
  }
134
131
  const fileFormat = winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json());
@@ -163,48 +160,33 @@ export class Logger {
163
160
  }
164
161
  else {
165
162
  if (process.stdout.isTTY) {
166
- console.warn("File logging disabled due to unsafe logs directory path.");
163
+ console.warn("File logging disabled as logsPath is not configured or invalid.");
167
164
  }
168
165
  }
169
- let consoleLoggingEnabledMessage = null;
170
- let consoleLoggingSkippedMessage = null;
171
- if (this.currentMcpLevel === "debug" && process.stdout.isTTY) {
172
- const consoleFormat = createWinstonConsoleFormat();
173
- transports.push(new winston.transports.Console({
174
- level: "debug",
175
- format: consoleFormat,
176
- }));
177
- consoleLoggingEnabledMessage =
178
- "Console logging enabled at level: debug (stdout is TTY)";
179
- }
180
- else if (this.currentMcpLevel === "debug" && !process.stdout.isTTY) {
181
- consoleLoggingSkippedMessage =
182
- "Console logging skipped: Level is debug, but stdout is not a TTY (likely stdio transport).";
183
- }
184
166
  this.winstonLogger = winston.createLogger({
185
167
  level: this.currentWinstonLevel,
186
168
  transports,
187
169
  exitOnError: false,
188
170
  });
171
+ // Configure console transport after Winston logger is created
172
+ const consoleStatus = this._configureConsoleTransport();
189
173
  const initialContext = {
190
174
  loggerSetup: true,
191
175
  requestId: "logger-init-deferred",
192
176
  timestamp: new Date().toISOString(),
193
177
  };
194
- if (logsDirCreatedMessage) {
178
+ if (logsDirCreatedMessage) { // Log if we had to re-create it
195
179
  this.info(logsDirCreatedMessage, initialContext);
196
180
  }
197
- if (consoleLoggingEnabledMessage) {
198
- this.info(consoleLoggingEnabledMessage, initialContext);
199
- }
200
- if (consoleLoggingSkippedMessage) {
201
- this.info(consoleLoggingSkippedMessage, initialContext);
181
+ if (consoleStatus.message) {
182
+ this.info(consoleStatus.message, initialContext);
202
183
  }
203
184
  this.initialized = true;
204
- this.info(`Logger initialized. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}. Console logging: ${process.stdout.isTTY && this.currentMcpLevel === "debug" ? "enabled" : "disabled"}`, {
185
+ this.info(`Logger initialized. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}. Console logging: ${consoleStatus.enabled ? "enabled" : "disabled"}`, {
205
186
  loggerSetup: true,
206
187
  requestId: "logger-post-init",
207
188
  timestamp: new Date().toISOString(),
189
+ logsPathUsed: resolvedLogsDir,
208
190
  });
209
191
  }
210
192
  /**
@@ -243,24 +225,46 @@ export class Logger {
243
225
  const oldLevel = this.currentMcpLevel;
244
226
  this.currentMcpLevel = newLevel;
245
227
  this.currentWinstonLevel = mcpToWinstonLevel[newLevel];
246
- this.winstonLogger.level = this.currentWinstonLevel;
228
+ if (this.winstonLogger) { // Ensure winstonLogger is defined
229
+ this.winstonLogger.level = this.currentWinstonLevel;
230
+ }
231
+ const consoleStatus = this._configureConsoleTransport();
232
+ if (oldLevel !== newLevel) {
233
+ this.info(`Log level changed. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}. Console logging: ${consoleStatus.enabled ? "enabled" : "disabled"}`, setLevelContext);
234
+ if (consoleStatus.message && consoleStatus.message !== "Console logging status unchanged.") {
235
+ this.info(consoleStatus.message, setLevelContext);
236
+ }
237
+ }
238
+ }
239
+ /**
240
+ * Configures the console transport based on the current log level and TTY status.
241
+ * Adds or removes the console transport as needed.
242
+ * @returns {{ enabled: boolean, message: string | null }} Status of console logging.
243
+ * @private
244
+ */
245
+ _configureConsoleTransport() {
246
+ if (!this.winstonLogger) {
247
+ return { enabled: false, message: "Cannot configure console: Winston logger not initialized." };
248
+ }
247
249
  const consoleTransport = this.winstonLogger.transports.find((t) => t instanceof winston.transports.Console);
248
- const shouldHaveConsole = newLevel === "debug" && process.stdout.isTTY;
250
+ const shouldHaveConsole = this.currentMcpLevel === "debug" && process.stdout.isTTY;
251
+ let message = null;
249
252
  if (shouldHaveConsole && !consoleTransport) {
250
253
  const consoleFormat = createWinstonConsoleFormat();
251
254
  this.winstonLogger.add(new winston.transports.Console({
252
- level: "debug",
255
+ level: "debug", // Console always logs debug if enabled
253
256
  format: consoleFormat,
254
257
  }));
255
- this.info("Console logging dynamically enabled.", setLevelContext);
258
+ message = "Console logging enabled (level: debug, stdout is TTY).";
256
259
  }
257
260
  else if (!shouldHaveConsole && consoleTransport) {
258
261
  this.winstonLogger.remove(consoleTransport);
259
- this.info("Console logging dynamically disabled.", setLevelContext);
262
+ message = "Console logging disabled (level not debug or stdout not TTY).";
260
263
  }
261
- if (oldLevel !== newLevel) {
262
- this.info(`Log level changed. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}. Console logging: ${shouldHaveConsole ? "enabled" : "disabled"}`, setLevelContext);
264
+ else {
265
+ message = "Console logging status unchanged.";
263
266
  }
267
+ return { enabled: shouldHaveConsole, message };
264
268
  }
265
269
  /**
266
270
  * Gets the singleton instance of the Logger.
@@ -316,7 +320,7 @@ export class Logger {
316
320
  mcpDataPayload.error = { message: error.message };
317
321
  // Include stack trace in debug mode for MCP notifications, truncated for brevity
318
322
  if (this.currentMcpLevel === "debug" && error.stack) {
319
- mcpDataPayload.error.stack = error.stack.substring(0, 500);
323
+ mcpDataPayload.error.stack = error.stack.substring(0, this.MCP_NOTIFICATION_STACK_TRACE_MAX_LENGTH);
320
324
  }
321
325
  }
322
326
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-ts-template",
3
- "version": "1.2.4",
3
+ "version": "1.2.7",
4
4
  "description": "TypeScript template for building Model Context Protocol (MCP) servers & clients. Features production-ready utilities, stdio/HTTP transports (with JWT auth), examples, and type safety. Ideal starting point for creating MCP-based applications.",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -9,6 +9,7 @@
9
9
  "bin": {
10
10
  "mcp-ts-template": "./dist/index.js"
11
11
  },
12
+ "exports": "./dist/index.js",
12
13
  "type": "module",
13
14
  "repository": {
14
15
  "type": "git",
@@ -31,17 +32,18 @@
31
32
  "inspector": "mcp-inspector --config mcp.json --server mcp-ts-template"
32
33
  },
33
34
  "dependencies": {
34
- "@modelcontextprotocol/sdk": "^1.11.4",
35
+ "@google/genai": "^1.0.1",
36
+ "@modelcontextprotocol/sdk": "^1.11.5",
35
37
  "@types/jsonwebtoken": "^9.0.9",
36
- "@types/node": "^22.15.18",
38
+ "@types/node": "^22.15.21",
37
39
  "@types/sanitize-html": "^2.16.0",
38
- "@types/validator": "13.15.0",
40
+ "@types/validator": "13.15.1",
39
41
  "chrono-node": "^2.8.0",
40
42
  "dotenv": "^16.5.0",
41
43
  "express": "^5.1.0",
42
44
  "ignore": "^7.0.4",
43
45
  "jsonwebtoken": "^9.0.2",
44
- "openai": "^4.100.0",
46
+ "openai": "^4.102.0",
45
47
  "partial-json": "^0.1.7",
46
48
  "sanitize-html": "^2.17.0",
47
49
  "tiktoken": "^1.0.21",
@@ -51,7 +53,7 @@
51
53
  "winston": "^3.17.0",
52
54
  "winston-daily-rotate-file": "^5.0.0",
53
55
  "yargs": "^17.7.2",
54
- "zod": "^3.24.4"
56
+ "zod": "^3.25.20"
55
57
  },
56
58
  "keywords": [
57
59
  "typescript",
@@ -68,8 +70,11 @@
68
70
  "jwt",
69
71
  "authentication"
70
72
  ],
71
- "author": "Casey Hand @cyanheads",
73
+ "author": "Casey Hand <casey@caseyjhand.com> (https://github.com/cyanheads/mcp-ts-template#readme)",
72
74
  "license": "Apache-2.0",
75
+ "engines": {
76
+ "node": ">=16.0.0"
77
+ },
73
78
  "devDependencies": {
74
79
  "@types/express": "^5.0.2",
75
80
  "@types/js-yaml": "^4.0.9",