mcp-ts-template 1.4.3 → 1.4.5

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
@@ -3,7 +3,7 @@
3
3
  [![TypeScript](https://img.shields.io/badge/TypeScript-^5.8.3-blue.svg)](https://www.typescriptlang.org/)
4
4
  [![Model Context Protocol SDK](https://img.shields.io/badge/MCP%20SDK-1.12.1-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.4.3-blue.svg)](./CHANGELOG.md)
6
+ [![Version](https://img.shields.io/badge/Version-1.4.5-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)
@@ -40,18 +40,8 @@ export function createStdioClientTransport(transportConfig, parentContext) {
40
40
  // Inheriting all of process.env is a security risk.
41
41
  // If specific variables from process.env are needed, they should be explicitly
42
42
  // listed in the mcp-config.json for that server or handled by an allowlist mechanism.
43
- const filteredProcessEnv = {};
44
- for (const key in process.env) {
45
- if (Object.prototype.hasOwnProperty.call(process.env, key)) {
46
- const value = process.env[key];
47
- if (value !== undefined) {
48
- filteredProcessEnv[key] = value;
49
- }
50
- }
51
- }
52
43
  const serverSpecificEnv = {
53
- ...filteredProcessEnv, // Inherit filtered parent process environment
54
- ...(transportConfig.env || {}), // Apply server-specific overrides/additions
44
+ ...(transportConfig.env || {}), // Only use explicitly defined env vars from config
55
45
  };
56
46
  logger.debug("Creating StdioClientTransport with merged environment", {
57
47
  ...context,
@@ -5,9 +5,8 @@
5
5
  * @module src/mcp-server/tools/catFactFetcher/catFactFetcherLogic
6
6
  */
7
7
  import { z } from "zod";
8
- import { logger, fetchWithTimeout, // Added import
9
- } from "../../../utils/index.js";
10
- import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
8
+ import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
9
+ import { fetchWithTimeout, logger, } from "../../../utils/index.js";
11
10
  /**
12
11
  * Asynchronously fetches a random cat fact from the Cat Fact Ninja API.
13
12
  * @param maxLength - Optional maximum length for the cat fact.
@@ -16,11 +15,13 @@ import { McpError, BaseErrorCode } from "../../../types-global/errors.js";
16
15
  * @throws {McpError} If the API request fails or returns an error.
17
16
  */
18
17
  async function fetchRandomCatFactFromApi(maxLength, context) {
18
+ // Best practice: API URLs should be configurable, e.g., via environment variables or a config file.
19
19
  let apiUrl = "https://catfact.ninja/fact";
20
20
  if (maxLength !== undefined) {
21
21
  apiUrl += `?max_length=${maxLength}`;
22
22
  }
23
23
  logger.info(`Fetching random cat fact from: ${apiUrl}`, context);
24
+ // Best practice: Timeouts should be configurable.
24
25
  const CAT_FACT_API_TIMEOUT_MS = 5000;
25
26
  try {
26
27
  // Use the fetchWithTimeout utility
@@ -31,7 +32,7 @@ async function fetchRandomCatFactFromApi(maxLength, context) {
31
32
  throw new McpError(BaseErrorCode.SERVICE_UNAVAILABLE, `Cat Fact API request to ${apiUrl} failed: ${response.status} ${response.statusText}`, {
32
33
  ...context,
33
34
  httpStatusCode: response.status,
34
- responseBodyBrief: errorText.substring(0, 200), // Log a snippet of the response
35
+ responseBodyBrief: errorText,
35
36
  errorSource: "CatFactApiNonOkResponse",
36
37
  });
37
38
  }
@@ -2,9 +2,10 @@
2
2
  * @fileoverview Registration for the fetch_image_test MCP tool.
3
3
  * @module src/mcp-server/tools/imageTest/registration
4
4
  */
5
- import { BaseErrorCode, McpError } from "../../../types-global/errors.js";
6
- import { ErrorHandler, logger, // Added logger import
7
- requestContextService, } from "../../../utils/index.js";
5
+ import { BaseErrorCode } from "../../../types-global/errors.js";
6
+ import { ErrorHandler, logger, requestContextService,
7
+ // RequestContext, // No longer needed directly in this function signature
8
+ } from "../../../utils/index.js";
8
9
  import { FetchImageTestInputSchema, fetchImageTestLogic, } from "./logic.js";
9
10
  /**
10
11
  * Registers the fetch_image_test tool with the MCP server.
@@ -12,27 +13,35 @@ import { FetchImageTestInputSchema, fetchImageTestLogic, } from "./logic.js";
12
13
  */
13
14
  export function registerFetchImageTestTool(server) {
14
15
  const operation = "registerFetchImageTestTool";
15
- const context = requestContextService.createRequestContext({ operation });
16
- try {
16
+ const registrationContext = requestContextService.createRequestContext({
17
+ operation,
18
+ });
19
+ ErrorHandler.tryCatch(() => {
17
20
  server.tool("fetch_image_test", "Fetches a random cat image from an external API (cataas.com) and returns it as a blob. Useful for testing image handling capabilities.", FetchImageTestInputSchema.shape, // CRITICAL: Pass the .shape
18
21
  async (validatedInput, mcpProvidedContext) => {
22
+ // Create a new context for each tool invocation.
23
+ // Link to an initial request ID if available from mcpProvidedContext or use registration context's ID as a fallback.
24
+ const parentRequestId = mcpProvidedContext?.requestId || registrationContext.requestId;
19
25
  const handlerRequestContext = requestContextService.createRequestContext({
20
- parentRequestId: context.requestId, // Optional: link to registration context
26
+ parentRequestId,
21
27
  operation: "fetchImageTestToolHandler",
22
- mcpToolContext: mcpProvidedContext, // Context from MCP SDK during call
28
+ toolName: "fetch_image_test",
29
+ // Include any other relevant details from mcpProvidedContext if needed
30
+ // For example, if mcpProvidedContext itself is a RequestContext or has useful fields:
31
+ // ...(typeof mcpProvidedContext === 'object' && mcpProvidedContext !== null ? mcpProvidedContext : {}),
23
32
  });
24
33
  return fetchImageTestLogic(validatedInput, handlerRequestContext);
25
34
  });
26
- logger.notice(`Tool 'fetch_image_test' registered.`, context);
27
- }
28
- catch (error) {
29
- ErrorHandler.handleError(new McpError(BaseErrorCode.INITIALIZATION_FAILED, `Failed to register 'fetch_image_test'`, {
30
- originalError: error instanceof Error ? error.message : String(error),
31
- }), {
32
- operation,
33
- context,
34
- errorCode: BaseErrorCode.INITIALIZATION_FAILED,
35
- critical: true,
36
- });
37
- }
35
+ logger.notice(`Tool 'fetch_image_test' registered.`, registrationContext);
36
+ }, {
37
+ operation, // Operation name for error handling
38
+ context: registrationContext, // Context for error handling
39
+ errorCode: BaseErrorCode.INITIALIZATION_FAILED, // Default error code if registration fails
40
+ critical: true, // Registration failures are typically critical
41
+ // Note: `rethrow` is not an option for `ErrorHandler.tryCatch`.
42
+ // `tryCatch` internally calls `ErrorHandler.handleError` with `rethrow: true`.
43
+ // If non-rethrowing behavior is essential for a specific registration,
44
+ // a manual try/catch block calling `ErrorHandler.handleError` with `rethrow: false` would be needed.
45
+ // For consistency with the typical use of `tryCatch`, this assumes rethrowing is acceptable.
46
+ });
38
47
  }
@@ -13,8 +13,8 @@
13
13
  * @see {@link https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/2025-03-26/basic/authorization.mdx | MCP Authorization Specification}
14
14
  * @module src/mcp-server/transports/authentication/authMiddleware
15
15
  */
16
- import { NextFunction, Request, Response } from "express";
17
16
  import { AuthInfo } from "@modelcontextprotocol/sdk/server/auth/types.js";
17
+ import { NextFunction, Request, Response } from "express";
18
18
  declare global {
19
19
  namespace Express {
20
20
  interface Request {
@@ -111,18 +111,29 @@ export function mcpAuthMiddleware(req, res, next) {
111
111
  scopesFromToken = [decoded.scope.trim()];
112
112
  }
113
113
  else if (scopesFromToken.length === 0 && decoded.scope.trim() === "") {
114
- // If scope is an empty string, treat as no scopes rather than erroring, or use a default.
115
- // Depending on strictness, could also error here. For now, allow empty array if scope was empty string.
114
+ // If scope is an empty string, treat as no scopes.
115
+ // This will now lead to an error if scopes are considered mandatory.
116
116
  logger.debug("JWT 'scope' claim was an empty string, resulting in empty scopes array.", context);
117
117
  }
118
118
  }
119
119
  else {
120
120
  // If scopes are strictly mandatory and not found or invalid format
121
- logger.warning("Authentication failed: JWT 'scp' or 'scope' claim is missing, not an array of strings, or not a valid space-separated string. Assigning default empty array.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
122
- scopesFromToken = []; // Default to empty array if scopes are mandatory but not found/invalid
123
- // Or, if truly mandatory and must be non-empty:
124
- // res.status(401).json({ error: "Unauthorized: Invalid token, missing or invalid scopes." });
125
- // return;
121
+ logger.warning("Authentication failed: JWT 'scp' or 'scope' claim is missing, not an array of strings, or not a valid space-separated string.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
122
+ res.status(401).json({
123
+ error: "Unauthorized: Invalid token, missing or invalid scopes.",
124
+ });
125
+ return;
126
+ }
127
+ // If, after parsing, scopesFromToken is empty and scopes are considered mandatory for any operation.
128
+ // This check assumes that all valid tokens must have at least one scope.
129
+ // If some tokens are legitimately allowed to have no scopes for certain operations,
130
+ // this check might need to be adjusted or handled downstream.
131
+ if (scopesFromToken.length === 0) {
132
+ logger.warning("Authentication failed: Token resulted in an empty scope array, and scopes are required.", { ...context, jwtPayloadKeys: Object.keys(decoded) });
133
+ res.status(401).json({
134
+ error: "Unauthorized: Token must contain valid, non-empty scopes.",
135
+ });
136
+ return;
126
137
  }
127
138
  // Construct req.auth with only the properties defined in SDK's AuthInfo
128
139
  // All other claims from 'decoded' are not part of req.auth for type safety.
@@ -1,10 +1,3 @@
1
- /**
2
- * @fileoverview Provides a singleton Logger class that wraps Winston for file logging
3
- * and supports sending MCP (Model Context Protocol) `notifications/message`.
4
- * It handles different log levels compliant with RFC 5424 and MCP specifications.
5
- * @module src/utils/internal/logger
6
- */
7
- import fs from "fs";
8
1
  import path from "path";
9
2
  import winston from "winston";
10
3
  import { config } from "../../config/index.js";
@@ -105,34 +98,13 @@ export class Logger {
105
98
  return;
106
99
  }
107
100
  // Set initialized to true at the beginning of the initialization process.
108
- // If initialization fails critically later, the logger might be in an inconsistent state,
109
- // but this will prevent "Logger not initialized" messages from within initialize() itself.
110
101
  this.initialized = true;
111
102
  this.currentMcpLevel = level;
112
103
  this.currentWinstonLevel = mcpToWinstonLevel[level];
113
- let logsDirCreatedMessage = null; // This message is now informational as creation is handled by config
114
- if (isLogsDirSafe) {
115
- // Directory creation is handled by config/index.ts ensureDirectory.
116
- // We can log if it was newly created by checking if it existed before config ran,
117
- // but that's complex. For now, we assume config handled it.
118
- // If resolvedLogsDir is set, config ensures it exists.
119
- if (!fs.existsSync(resolvedLogsDir)) {
120
- // This case should ideally not be hit if config.logsPath is correctly set up and validated.
121
- // However, if it somehow occurs (e.g. dir deleted after config init but before logger init),
122
- // we attempt to create it.
123
- try {
124
- await fs.promises.mkdir(resolvedLogsDir, { recursive: true });
125
- logsDirCreatedMessage = `Re-created logs directory (should have been created by config): ${resolvedLogsDir}`;
126
- }
127
- catch (err) {
128
- if (process.stdout.isTTY) {
129
- const errorMessage = err instanceof Error ? err.message : String(err);
130
- console.error(`Error creating logs directory at ${resolvedLogsDir}: ${errorMessage}. File logging disabled.`);
131
- }
132
- throw err; // Critical if logs dir cannot be ensured
133
- }
134
- }
135
- }
104
+ // The logs directory (config.logsPath / resolvedLogsDir) is expected to be created and validated
105
+ // by the configuration module (src/config/index.ts) before logger initialization.
106
+ // If isLogsDirSafe is true, we assume resolvedLogsDir exists and is usable.
107
+ // No redundant directory creation logic here.
136
108
  const fileFormat = winston.format.combine(winston.format.timestamp(), winston.format.errors({ stack: true }), winston.format.json());
137
109
  const transports = [];
138
110
  const fileTransportOptions = {
@@ -180,23 +152,17 @@ export class Logger {
180
152
  requestId: "logger-init-deferred",
181
153
  timestamp: new Date().toISOString(),
182
154
  };
183
- if (logsDirCreatedMessage) {
184
- // Log if we had to re-create it
185
- this.info(logsDirCreatedMessage, initialContext);
186
- }
155
+ // Removed logging of logsDirCreatedMessage as it's no longer set
187
156
  if (consoleStatus.message) {
188
157
  this.info(consoleStatus.message, initialContext);
189
158
  }
190
- this.initialized = true;
159
+ this.initialized = true; // Ensure this is set after successful setup
191
160
  this.info(`Logger initialized. File logging level: ${this.currentWinstonLevel}. MCP logging level: ${this.currentMcpLevel}. Console logging: ${consoleStatus.enabled ? "enabled" : "disabled"}`, {
192
161
  loggerSetup: true,
193
162
  requestId: "logger-post-init",
194
163
  timestamp: new Date().toISOString(),
195
164
  logsPathUsed: resolvedLogsDir,
196
165
  });
197
- // Note: If a critical error occurs during initialization after this point,
198
- // this.initialized remains true. Consider adding a try/catch around the core
199
- // initialization logic to set this.initialized = false on failure if needed.
200
166
  }
201
167
  /**
202
168
  * Sets the function used to send MCP 'notifications/message'.
@@ -85,6 +85,7 @@ export declare class IdGenerator {
85
85
  * @param id - The ID string to validate.
86
86
  * @param entityType - The expected entity type of the ID.
87
87
  * @param options - Optional parameters used during generation for validation consistency.
88
+ * The `charset` from these options will be used for validation.
88
89
  * @returns `true` if the ID is valid, `false` otherwise.
89
90
  */
90
91
  isValid(id: string, entityType: string, options?: IdGenerationOptions): boolean;
@@ -112,7 +113,9 @@ export declare class IdGenerator {
112
113
  getEntityType(id: string, separator?: string): string;
113
114
  /**
114
115
  * Normalizes an entity ID to ensure the prefix matches the registered case
115
- * and the random part is uppercase.
116
+ * and the random part is uppercase. Note: This assumes the charset characters
117
+ * have a meaningful uppercase version if case-insensitivity is desired for the random part.
118
+ * For default charset (A-Z0-9), this is fine. For custom charsets, behavior might vary.
116
119
  * @param id - The ID to normalize (e.g., "proj_a6b3j0").
117
120
  * @param separator - The separator used in the ID. Defaults to `IdGenerator.DEFAULT_SEPARATOR`.
118
121
  * @returns The normalized ID (e.g., "PROJ_A6B3J0").
@@ -101,16 +101,21 @@ export class IdGenerator {
101
101
  * @param id - The ID string to validate.
102
102
  * @param entityType - The expected entity type of the ID.
103
103
  * @param options - Optional parameters used during generation for validation consistency.
104
+ * The `charset` from these options will be used for validation.
104
105
  * @returns `true` if the ID is valid, `false` otherwise.
105
106
  */
106
107
  isValid(id, entityType, options = {}) {
107
108
  const prefix = this.entityPrefixes[entityType];
108
- const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR, } = options;
109
+ const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR, charset = IdGenerator.DEFAULT_CHARSET, // Use charset from options or default
110
+ } = options;
109
111
  if (!prefix) {
110
112
  return false;
111
113
  }
112
- // Assumes default charset characters (uppercase letters and digits) for regex.
113
- const pattern = new RegExp(`^${this.escapeRegex(prefix)}${this.escapeRegex(separator)}[A-Z0-9]{${length}}$`);
114
+ // Build regex character class from the charset
115
+ // Escape characters that have special meaning inside a regex character class `[]`
116
+ const escapedCharsetForClass = charset.replace(/[[\]\\^-]/g, "\\$&");
117
+ const charsetRegexPart = `[${escapedCharsetForClass}]`;
118
+ const pattern = new RegExp(`^${this.escapeRegex(prefix)}${this.escapeRegex(separator)}${charsetRegexPart}{${length}}$`);
114
119
  return pattern.test(id);
115
120
  }
116
121
  /**
@@ -153,7 +158,9 @@ export class IdGenerator {
153
158
  }
154
159
  /**
155
160
  * Normalizes an entity ID to ensure the prefix matches the registered case
156
- * and the random part is uppercase.
161
+ * and the random part is uppercase. Note: This assumes the charset characters
162
+ * have a meaningful uppercase version if case-insensitivity is desired for the random part.
163
+ * For default charset (A-Z0-9), this is fine. For custom charsets, behavior might vary.
157
164
  * @param id - The ID to normalize (e.g., "proj_a6b3j0").
158
165
  * @param separator - The separator used in the ID. Defaults to `IdGenerator.DEFAULT_SEPARATOR`.
159
166
  * @returns The normalized ID (e.g., "PROJ_A6B3J0").
@@ -164,6 +171,8 @@ export class IdGenerator {
164
171
  const registeredPrefix = this.entityPrefixes[entityType];
165
172
  const idParts = id.split(separator);
166
173
  const randomPart = idParts.slice(1).join(separator);
174
+ // Consider if randomPart.toUpperCase() is always correct for custom charsets.
175
+ // For now, maintaining existing behavior.
167
176
  return `${registeredPrefix}${separator}${randomPart.toUpperCase()}`;
168
177
  }
169
178
  }
@@ -148,6 +148,17 @@ export declare class Sanitization {
148
148
  * Sanitizes input for logging by redacting sensitive fields.
149
149
  * Creates a deep clone and replaces values of fields matching `this.sensitiveFields`
150
150
  * (case-insensitive substring match) with "[REDACTED]".
151
+ *
152
+ * It uses `structuredClone` if available for a high-fidelity deep clone.
153
+ * If `structuredClone` is not available (e.g., in older Node.js environments),
154
+ * it falls back to `JSON.parse(JSON.stringify(input))`. This fallback has limitations:
155
+ * - `Date` objects are converted to ISO date strings.
156
+ * - `undefined` values within objects are removed.
157
+ * - `Map`, `Set`, `RegExp` objects are converted to empty objects (`{}`).
158
+ * - Functions are removed.
159
+ * - `BigInt` values will throw an error during `JSON.stringify` unless a `toJSON` method is provided.
160
+ * - Circular references will cause `JSON.stringify` to throw an error.
161
+ *
151
162
  * @param input - The input data to sanitize for logging.
152
163
  * @returns A sanitized (deep cloned) version of the input, safe for logging.
153
164
  * Returns original input if not object/array, or "[Log Sanitization Failed]" on error.
@@ -377,6 +377,17 @@ export class Sanitization {
377
377
  * Sanitizes input for logging by redacting sensitive fields.
378
378
  * Creates a deep clone and replaces values of fields matching `this.sensitiveFields`
379
379
  * (case-insensitive substring match) with "[REDACTED]".
380
+ *
381
+ * It uses `structuredClone` if available for a high-fidelity deep clone.
382
+ * If `structuredClone` is not available (e.g., in older Node.js environments),
383
+ * it falls back to `JSON.parse(JSON.stringify(input))`. This fallback has limitations:
384
+ * - `Date` objects are converted to ISO date strings.
385
+ * - `undefined` values within objects are removed.
386
+ * - `Map`, `Set`, `RegExp` objects are converted to empty objects (`{}`).
387
+ * - Functions are removed.
388
+ * - `BigInt` values will throw an error during `JSON.stringify` unless a `toJSON` method is provided.
389
+ * - Circular references will cause `JSON.stringify` to throw an error.
390
+ *
380
391
  * @param input - The input data to sanitize for logging.
381
392
  * @returns A sanitized (deep cloned) version of the input, safe for logging.
382
393
  * Returns original input if not object/array, or "[Log Sanitization Failed]" on error.
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
+ "$schema": "http://json.schemastore.org/package",
2
3
  "name": "mcp-ts-template",
3
- "version": "1.4.3",
4
+ "version": "1.4.5",
4
5
  "description": "TypeScript template for building Model Context Protocol (MCP) Servers & Clients. Features extensive utilities (logger, requestContext, etc.), STDIO & Streamable HTTP (with authMiddleware), examples, and type safety. Ideal starting point for creating production-ready MCP Servers & Clients.",
5
6
  "main": "dist/index.js",
6
7
  "files": [
@@ -10,6 +11,7 @@
10
11
  "mcp-ts-template": "dist/index.js"
11
12
  },
12
13
  "exports": "./dist/index.js",
14
+ "types": "dist/index.d.ts",
13
15
  "type": "module",
14
16
  "repository": {
15
17
  "type": "git",
@@ -46,7 +48,7 @@
46
48
  "express": "^5.1.0",
47
49
  "ignore": "^7.0.5",
48
50
  "jsonwebtoken": "^9.0.2",
49
- "openai": "^5.0.2",
51
+ "openai": "^5.1.0",
50
52
  "partial-json": "^0.1.7",
51
53
  "sanitize-html": "^2.17.0",
52
54
  "tiktoken": "^1.0.21",
@@ -56,7 +58,7 @@
56
58
  "winston": "^3.17.0",
57
59
  "winston-daily-rotate-file": "^5.0.0",
58
60
  "yargs": "^18.0.0",
59
- "zod": "^3.25.49"
61
+ "zod": "^3.25.51"
60
62
  },
61
63
  "keywords": [
62
64
  "typescript",
@@ -74,8 +76,18 @@
74
76
  ],
75
77
  "author": "cyanheads <casey@caseyjhand.com> (https://github.com/cyanheads/mcp-ts-template#readme)",
76
78
  "license": "Apache-2.0",
79
+ "funding": [
80
+ {
81
+ "type": "github",
82
+ "url": "https://github.com/sponsors/cyanheads"
83
+ },
84
+ {
85
+ "type": "buy_me_a_coffee",
86
+ "url": "https://www.buymeacoffee.com/cyanheads"
87
+ }
88
+ ],
77
89
  "engines": {
78
- "node": ">=16.0.0"
90
+ "node": ">=20.0.0"
79
91
  },
80
92
  "devDependencies": {
81
93
  "@types/express": "^5.0.2",