mcp-ts-template 1.4.4 → 1.4.6
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 +1 -1
- package/dist/mcp-client/transports/stdioClientTransport.js +1 -11
- package/dist/mcp-server/tools/catFactFetcher/logic.js +5 -4
- package/dist/mcp-server/tools/imageTest/registration.js +28 -19
- package/dist/mcp-server/transports/authentication/authMiddleware.d.ts +1 -1
- package/dist/mcp-server/transports/authentication/authMiddleware.js +18 -7
- package/dist/mcp-server/transports/httpTransport.js +76 -9
- package/dist/utils/internal/logger.js +6 -40
- package/dist/utils/security/idGenerator.d.ts +4 -1
- package/dist/utils/security/idGenerator.js +24 -7
- package/dist/utils/security/sanitization.d.ts +11 -0
- package/dist/utils/security/sanitization.js +16 -2
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.typescriptlang.org/)
|
|
4
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)
|
|
@@ -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
|
-
...
|
|
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 {
|
|
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
|
|
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
|
|
6
|
-
import { ErrorHandler, logger,
|
|
7
|
-
|
|
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
|
|
16
|
-
|
|
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
|
|
26
|
+
parentRequestId,
|
|
21
27
|
operation: "fetchImageTestToolHandler",
|
|
22
|
-
|
|
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.`,
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
|
115
|
-
//
|
|
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.
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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.
|
|
@@ -16,7 +16,8 @@ import express from "express";
|
|
|
16
16
|
import http from "http";
|
|
17
17
|
import { randomUUID } from "node:crypto";
|
|
18
18
|
import { config } from "../../config/index.js";
|
|
19
|
-
import {
|
|
19
|
+
import { BaseErrorCode, McpError } from "../../types-global/errors.js"; // For McpError type check
|
|
20
|
+
import { logger, rateLimiter, requestContextService, } from "../../utils/index.js";
|
|
20
21
|
import { mcpAuthMiddleware } from "./authentication/authMiddleware.js";
|
|
21
22
|
/**
|
|
22
23
|
* The port number for the HTTP transport, configured via `MCP_HTTP_PORT` environment variable.
|
|
@@ -68,28 +69,52 @@ const httpTransports = {};
|
|
|
68
69
|
*/
|
|
69
70
|
function isOriginAllowed(req, res) {
|
|
70
71
|
const origin = req.headers.origin;
|
|
71
|
-
|
|
72
|
-
const isLocalhostBinding = ["127.0.0.1", "::1", "localhost"].includes(
|
|
72
|
+
// Determine if the server is bound to a localhost interface using the configured HTTP_HOST.
|
|
73
|
+
const isLocalhostBinding = ["127.0.0.1", "::1", "localhost"].includes(config.mcpHttpHost);
|
|
73
74
|
const allowedOrigins = config.mcpAllowedOrigins || [];
|
|
74
75
|
const context = requestContextService.createRequestContext({
|
|
75
76
|
operation: "isOriginAllowed",
|
|
76
|
-
origin,
|
|
77
|
-
host
|
|
77
|
+
requestOrigin: origin, // Use a more descriptive key for the request's origin
|
|
78
|
+
serverBindingHost: config.mcpHttpHost, // Log the server's binding host for context
|
|
78
79
|
isLocalhostBinding,
|
|
79
|
-
allowedOrigins,
|
|
80
|
+
configuredAllowedOrigins: allowedOrigins, // Use a more descriptive key
|
|
80
81
|
});
|
|
81
82
|
logger.debug("Checking origin allowance", context);
|
|
82
|
-
const allowed = (origin && allowedOrigins.includes(origin)) ||
|
|
83
|
-
(isLocalhostBinding && (!origin || origin === "null"));
|
|
83
|
+
const allowed = (origin && allowedOrigins.includes(origin)) || // Origin is explicitly in the whitelist
|
|
84
|
+
(isLocalhostBinding && (!origin || origin === "null")); // Or server is localhost and origin is missing or "null"
|
|
84
85
|
if (allowed && origin) {
|
|
85
|
-
|
|
86
|
+
// If the origin is "null", we must not set Access-Control-Allow-Origin to "null"
|
|
87
|
+
// when Access-Control-Allow-Credentials is also true (which it is in this block).
|
|
88
|
+
// This combination is a security risk. By not setting ACAO to "null",
|
|
89
|
+
// a credentialed request from a "null" origin will likely be blocked by the browser, which is safer.
|
|
90
|
+
if (origin === "null") {
|
|
91
|
+
logger.debug(`Origin is "null". Not setting Access-Control-Allow-Origin to "null" due to Access-Control-Allow-Credentials being true.`, context);
|
|
92
|
+
// Note: Access-Control-Allow-Origin is NOT set to "null".
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
// For any other allowed, non-null origin, reflect it.
|
|
96
|
+
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
97
|
+
}
|
|
98
|
+
// These headers are set for any allowed & originated request.
|
|
99
|
+
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
100
|
+
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Last-Event-ID, Authorization");
|
|
101
|
+
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
102
|
+
}
|
|
103
|
+
else if (allowed && !origin && isLocalhostBinding) {
|
|
104
|
+
// Case: No origin header, but server is localhost-bound (e.g., same-origin, curl).
|
|
105
|
+
// 'allowed' is true. We can allow credentials. ACAO is not strictly needed for non-browser or same-origin.
|
|
106
|
+
// If it's a browser in a weird state sending no origin but expecting CORS for credentials,
|
|
107
|
+
// it will likely fail without ACAO, which is fine.
|
|
108
|
+
logger.debug(`No origin header, but request allowed due to localhost binding. Setting Access-Control-Allow-Credentials to true.`, context);
|
|
86
109
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
|
|
87
110
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type, Mcp-Session-Id, Last-Event-ID, Authorization");
|
|
88
111
|
res.setHeader("Access-Control-Allow-Credentials", "true");
|
|
89
112
|
}
|
|
90
113
|
else if (!allowed && origin) {
|
|
114
|
+
// Origin was present but not allowed by any rule.
|
|
91
115
|
logger.warning(`Origin denied: ${origin}`, context);
|
|
92
116
|
}
|
|
117
|
+
// If !allowed and !origin, no specific CORS headers needed, request proceeds to be potentially denied by other logic or auth.
|
|
93
118
|
logger.debug(`Origin check result: ${allowed}`, { ...context, allowed });
|
|
94
119
|
return allowed;
|
|
95
120
|
}
|
|
@@ -218,6 +243,48 @@ export async function startHttpTransport(createServerInstanceFn, parentContext)
|
|
|
218
243
|
});
|
|
219
244
|
logger.debug("Setting up Express app for HTTP transport...", transportContext);
|
|
220
245
|
app.use(express.json());
|
|
246
|
+
// Rate Limiting Middleware
|
|
247
|
+
// Apply this before more expensive operations like auth or request processing.
|
|
248
|
+
const httpRateLimitMiddleware = (req, res, next) => {
|
|
249
|
+
// Determine a reliable key for rate limiting. Prioritize req.ip,
|
|
250
|
+
// then fall back to req.socket.remoteAddress, and finally to a default string.
|
|
251
|
+
const rateLimitKey = req.ip || req.socket.remoteAddress || "unknown_ip_for_rate_limit";
|
|
252
|
+
const context = requestContextService.createRequestContext({
|
|
253
|
+
operation: "httpRateLimitCheck",
|
|
254
|
+
ipAddress: rateLimitKey, // Log the actual key being used
|
|
255
|
+
method: req.method,
|
|
256
|
+
path: req.path,
|
|
257
|
+
});
|
|
258
|
+
try {
|
|
259
|
+
rateLimiter.check(rateLimitKey, context); // Use the guaranteed string key
|
|
260
|
+
logger.debug("Rate limit check passed.", context);
|
|
261
|
+
next();
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
if (error instanceof McpError && error.code === BaseErrorCode.RATE_LIMITED) {
|
|
265
|
+
logger.warning(`Rate limit exceeded for IP: ${rateLimitKey}`, {
|
|
266
|
+
...context,
|
|
267
|
+
errorMessage: error.message,
|
|
268
|
+
details: error.details,
|
|
269
|
+
});
|
|
270
|
+
res.status(429).json({
|
|
271
|
+
jsonrpc: "2.0",
|
|
272
|
+
error: { code: -32000, message: "Too Many Requests" }, // Generic JSON-RPC error for rate limit
|
|
273
|
+
id: req.body?.id || null,
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
// For other errors, pass them to the default error handler
|
|
278
|
+
logger.error("Unexpected error in rate limit middleware", {
|
|
279
|
+
...context,
|
|
280
|
+
error: error instanceof Error ? error.message : String(error),
|
|
281
|
+
});
|
|
282
|
+
next(error);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
// Apply rate limiter to the MCP endpoint for all methods
|
|
287
|
+
app.use(MCP_ENDPOINT_PATH, httpRateLimitMiddleware);
|
|
221
288
|
app.options(MCP_ENDPOINT_PATH, (req, res) => {
|
|
222
289
|
const optionsContext = requestContextService.createRequestContext({
|
|
223
290
|
...transportContext,
|
|
@@ -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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
-
|
|
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").
|
|
@@ -60,10 +60,18 @@ export class IdGenerator {
|
|
|
60
60
|
* @returns The generated random string.
|
|
61
61
|
*/
|
|
62
62
|
generateRandomString(length = IdGenerator.DEFAULT_LENGTH, charset = IdGenerator.DEFAULT_CHARSET) {
|
|
63
|
-
const bytes = randomBytes(length);
|
|
64
63
|
let result = "";
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
// Determine the largest multiple of charset.length that is less than or equal to 256
|
|
65
|
+
// This is the threshold for rejection sampling to avoid bias.
|
|
66
|
+
const maxValidByteValue = Math.floor(256 / charset.length) * charset.length;
|
|
67
|
+
while (result.length < length) {
|
|
68
|
+
const byteBuffer = randomBytes(1); // Get one random byte
|
|
69
|
+
const byte = byteBuffer[0];
|
|
70
|
+
// If the byte is within the valid range (i.e., it won't introduce bias),
|
|
71
|
+
// use it to select a character from the charset. Otherwise, discard and try again.
|
|
72
|
+
if (byte < maxValidByteValue) {
|
|
73
|
+
result += charset[byte % charset.length];
|
|
74
|
+
}
|
|
67
75
|
}
|
|
68
76
|
return result;
|
|
69
77
|
}
|
|
@@ -101,16 +109,21 @@ export class IdGenerator {
|
|
|
101
109
|
* @param id - The ID string to validate.
|
|
102
110
|
* @param entityType - The expected entity type of the ID.
|
|
103
111
|
* @param options - Optional parameters used during generation for validation consistency.
|
|
112
|
+
* The `charset` from these options will be used for validation.
|
|
104
113
|
* @returns `true` if the ID is valid, `false` otherwise.
|
|
105
114
|
*/
|
|
106
115
|
isValid(id, entityType, options = {}) {
|
|
107
116
|
const prefix = this.entityPrefixes[entityType];
|
|
108
|
-
const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR,
|
|
117
|
+
const { length = IdGenerator.DEFAULT_LENGTH, separator = IdGenerator.DEFAULT_SEPARATOR, charset = IdGenerator.DEFAULT_CHARSET, // Use charset from options or default
|
|
118
|
+
} = options;
|
|
109
119
|
if (!prefix) {
|
|
110
120
|
return false;
|
|
111
121
|
}
|
|
112
|
-
//
|
|
113
|
-
|
|
122
|
+
// Build regex character class from the charset
|
|
123
|
+
// Escape characters that have special meaning inside a regex character class `[]`
|
|
124
|
+
const escapedCharsetForClass = charset.replace(/[[\]\\^-]/g, "\\$&");
|
|
125
|
+
const charsetRegexPart = `[${escapedCharsetForClass}]`;
|
|
126
|
+
const pattern = new RegExp(`^${this.escapeRegex(prefix)}${this.escapeRegex(separator)}${charsetRegexPart}{${length}}$`);
|
|
114
127
|
return pattern.test(id);
|
|
115
128
|
}
|
|
116
129
|
/**
|
|
@@ -153,7 +166,9 @@ export class IdGenerator {
|
|
|
153
166
|
}
|
|
154
167
|
/**
|
|
155
168
|
* Normalizes an entity ID to ensure the prefix matches the registered case
|
|
156
|
-
* and the random part is uppercase.
|
|
169
|
+
* and the random part is uppercase. Note: This assumes the charset characters
|
|
170
|
+
* have a meaningful uppercase version if case-insensitivity is desired for the random part.
|
|
171
|
+
* For default charset (A-Z0-9), this is fine. For custom charsets, behavior might vary.
|
|
157
172
|
* @param id - The ID to normalize (e.g., "proj_a6b3j0").
|
|
158
173
|
* @param separator - The separator used in the ID. Defaults to `IdGenerator.DEFAULT_SEPARATOR`.
|
|
159
174
|
* @returns The normalized ID (e.g., "PROJ_A6B3J0").
|
|
@@ -164,6 +179,8 @@ export class IdGenerator {
|
|
|
164
179
|
const registeredPrefix = this.entityPrefixes[entityType];
|
|
165
180
|
const idParts = id.split(separator);
|
|
166
181
|
const randomPart = idParts.slice(1).join(separator);
|
|
182
|
+
// Consider if randomPart.toUpperCase() is always correct for custom charsets.
|
|
183
|
+
// For now, maintaining existing behavior.
|
|
167
184
|
return `${registeredPrefix}${separator}${randomPart.toUpperCase()}`;
|
|
168
185
|
}
|
|
169
186
|
}
|
|
@@ -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.
|
|
@@ -204,8 +204,11 @@ export class Sanitization {
|
|
|
204
204
|
})) {
|
|
205
205
|
throw new Error("Invalid URL format or protocol not in allowed list.");
|
|
206
206
|
}
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
const lowercasedInput = trimmedInput.toLowerCase();
|
|
208
|
+
if (lowercasedInput.startsWith("javascript:") ||
|
|
209
|
+
lowercasedInput.startsWith("data:") ||
|
|
210
|
+
lowercasedInput.startsWith("vbscript:")) {
|
|
211
|
+
throw new Error("Disallowed pseudo-protocol (javascript:, data:, or vbscript:) in URL.");
|
|
209
212
|
}
|
|
210
213
|
return trimmedInput;
|
|
211
214
|
}
|
|
@@ -377,6 +380,17 @@ export class Sanitization {
|
|
|
377
380
|
* Sanitizes input for logging by redacting sensitive fields.
|
|
378
381
|
* Creates a deep clone and replaces values of fields matching `this.sensitiveFields`
|
|
379
382
|
* (case-insensitive substring match) with "[REDACTED]".
|
|
383
|
+
*
|
|
384
|
+
* It uses `structuredClone` if available for a high-fidelity deep clone.
|
|
385
|
+
* If `structuredClone` is not available (e.g., in older Node.js environments),
|
|
386
|
+
* it falls back to `JSON.parse(JSON.stringify(input))`. This fallback has limitations:
|
|
387
|
+
* - `Date` objects are converted to ISO date strings.
|
|
388
|
+
* - `undefined` values within objects are removed.
|
|
389
|
+
* - `Map`, `Set`, `RegExp` objects are converted to empty objects (`{}`).
|
|
390
|
+
* - Functions are removed.
|
|
391
|
+
* - `BigInt` values will throw an error during `JSON.stringify` unless a `toJSON` method is provided.
|
|
392
|
+
* - Circular references will cause `JSON.stringify` to throw an error.
|
|
393
|
+
*
|
|
380
394
|
* @param input - The input data to sanitize for logging.
|
|
381
395
|
* @returns A sanitized (deep cloned) version of the input, safe for logging.
|
|
382
396
|
* 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.
|
|
4
|
+
"version": "1.4.6",
|
|
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": [
|