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 +46 -37
- package/dist/config/index.d.ts +4 -0
- package/dist/config/index.js +109 -5
- package/dist/mcp-server/transports/httpTransport.js +19 -5
- package/dist/services/index.d.ts +6 -0
- package/dist/services/index.js +6 -0
- package/dist/services/llm-providers/index.d.ts +7 -1
- package/dist/services/llm-providers/index.js +7 -1
- package/dist/services/llm-providers/llmFactory.d.ts +69 -0
- package/dist/services/llm-providers/llmFactory.js +124 -0
- package/dist/services/llm-providers/openRouter/index.d.ts +6 -0
- package/dist/services/llm-providers/openRouter/index.js +7 -0
- package/dist/services/llm-providers/{openRouterProvider.d.ts → openRouter/openRouterProvider.d.ts} +4 -3
- package/dist/services/llm-providers/{openRouterProvider.js → openRouter/openRouterProvider.js} +31 -38
- package/dist/utils/internal/logger.d.ts +8 -0
- package/dist/utils/internal/logger.js +59 -55
- package/package.json +12 -7
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# MCP TypeScript Template 🚀
|
|
2
2
|
|
|
3
3
|
[](https://www.typescriptlang.org/)
|
|
4
|
-
[](https://github.com/modelcontextprotocol/typescript-sdk)
|
|
5
5
|
[](https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/changelog.mdx)
|
|
6
|
-
[](./CHANGELOG.md)
|
|
7
7
|
[](https://opensource.org/licenses/Apache-2.0)
|
|
8
8
|
[](https://github.com/cyanheads/mcp-ts-template/issues)
|
|
9
9
|
[](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)
|
|
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.). | `
|
|
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). | `
|
|
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
|
|
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
|
-
|
|
|
231
|
-
|
|
|
232
|
-
|
|
|
233
|
-
|
|
|
234
|
-
| |
|
|
235
|
-
| |
|
|
236
|
-
|
|
|
237
|
-
|
|
|
238
|
-
| |
|
|
239
|
-
| |
|
|
240
|
-
|
|
|
241
|
-
|
|
|
242
|
-
| |
|
|
243
|
-
| |
|
|
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
|
|
package/dist/config/index.d.ts
CHANGED
|
@@ -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;
|
package/dist/config/index.js
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
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
|
|
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).
|
|
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).
|
|
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
|
-
|
|
437
|
-
|
|
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: ${
|
|
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) {
|
package/dist/services/index.d.ts
CHANGED
package/dist/services/index.js
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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,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
|
package/dist/services/llm-providers/{openRouterProvider.d.ts → openRouter/openRouterProvider.d.ts}
RENAMED
|
@@ -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 "
|
|
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
|
-
|
|
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(
|
|
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.
|
package/dist/services/llm-providers/{openRouterProvider.js → openRouter/openRouterProvider.js}
RENAMED
|
@@ -1,20 +1,11 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import
|
|
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(
|
|
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
|
|
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("
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
55
|
+
})
|
|
56
|
+
.catch(error => {
|
|
67
57
|
this.status = "error";
|
|
68
|
-
this.initializationError =
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
40
|
-
const
|
|
41
|
-
//
|
|
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
|
-
|
|
120
|
-
|
|
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 = `
|
|
120
|
+
logsDirCreatedMessage = `Re-created logs directory (should have been created by config): ${resolvedLogsDir}`;
|
|
123
121
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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 (
|
|
198
|
-
this.info(
|
|
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: ${
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
262
|
+
message = "Console logging disabled (level not debug or stdout not TTY).";
|
|
260
263
|
}
|
|
261
|
-
|
|
262
|
-
|
|
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,
|
|
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.
|
|
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
|
-
"@
|
|
35
|
+
"@google/genai": "^1.0.1",
|
|
36
|
+
"@modelcontextprotocol/sdk": "^1.11.5",
|
|
35
37
|
"@types/jsonwebtoken": "^9.0.9",
|
|
36
|
-
"@types/node": "^22.15.
|
|
38
|
+
"@types/node": "^22.15.21",
|
|
37
39
|
"@types/sanitize-html": "^2.16.0",
|
|
38
|
-
"@types/validator": "13.15.
|
|
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.
|
|
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.
|
|
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",
|