mcp-ts-template 1.6.1 → 1.6.3

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.13.0-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.6.1-blue.svg)](./CHANGELOG.md)
6
+ [![Version](https://img.shields.io/badge/Version-1.6.2-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)
@@ -177,7 +177,6 @@ Begin the task. Your response must be only the JSON object.`;
177
177
  { role: "system", content: systemPrompt },
178
178
  { role: "user", content: initialPrompt },
179
179
  ];
180
- // eslint-disable-next-line no-constant-condition
181
180
  while (true) {
182
181
  const llmParams = {
183
182
  messages,
@@ -191,7 +190,7 @@ Begin the task. Your response must be only the JSON object.`;
191
190
  try {
192
191
  commandJson = jsonParser.parse(llmResponse);
193
192
  }
194
- catch (e) {
193
+ catch (_e) {
195
194
  logger.warning("LLM response was not valid JSON. Treating as a conversational message.", { ...runContext, llmResponse });
196
195
  if (onStreamChunk) {
197
196
  onStreamChunk(`\n[AGENT_NOTE]: The AI responded with conversational text instead of a command. I will remind it of the protocol.\n[AI]: ${llmResponse}\n`);
@@ -205,6 +204,9 @@ Begin the task. Your response must be only the JSON object.`;
205
204
  const { command, arguments: args } = commandJson;
206
205
  if (command === "mcp_tool_call") {
207
206
  const toolResult = await this._executeToolCall(args, runContext);
207
+ if (typeof args.name !== "string") {
208
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Tool call name is missing or not a string.", runContext);
209
+ }
208
210
  const toolMessage = {
209
211
  role: "tool",
210
212
  tool_call_id: args.name,
@@ -315,6 +317,10 @@ Begin the task. Your response must be only the JSON object.`;
315
317
  args,
316
318
  });
317
319
  const client = await this.mcpClientManager.connectMcpClient(serverName, context);
320
+ if (args !== undefined &&
321
+ (typeof args !== "object" || args === null || Array.isArray(args))) {
322
+ throw new McpError(BaseErrorCode.VALIDATION_ERROR, `Tool arguments for '${toolName}' must be a plain object or undefined.`, context);
323
+ }
318
324
  const toolResult = await client.callTool({
319
325
  name: toolName,
320
326
  arguments: args,
@@ -49,7 +49,8 @@ try {
49
49
  projectRoot = findProjectRoot(currentModuleDir);
50
50
  }
51
51
  catch (error) {
52
- console.error(`FATAL: Error determining project root: ${error.message}`);
52
+ const errorMessage = error instanceof Error ? error.message : String(error);
53
+ console.error(`FATAL: Error determining project root: ${errorMessage}`);
53
54
  // Fallback to process.cwd() if project root cannot be determined.
54
55
  // This might happen in unusual execution environments.
55
56
  projectRoot = process.cwd();
@@ -215,7 +216,8 @@ const ensureDirectory = (dirPath, rootDir, dirName) => {
215
216
  }
216
217
  catch (statError) {
217
218
  if (process.stdout.isTTY) {
218
- console.error(`Error accessing ${dirName} path ${resolvedDirPath}: ${statError.message}`);
219
+ const statErrorMessage = statError instanceof Error ? statError.message : String(statError);
220
+ console.error(`Error accessing ${dirName} path ${resolvedDirPath}: ${statErrorMessage}`);
219
221
  }
220
222
  return null;
221
223
  }
package/dist/index.js CHANGED
@@ -178,7 +178,7 @@ const start = async () => {
178
178
  logger.error("FATAL: Uncaught exception detected. This indicates a bug or unexpected state. Initiating shutdown...", errorContext);
179
179
  await shutdown("uncaughtException");
180
180
  });
181
- process.on("unhandledRejection", async (reason, promise) => {
181
+ process.on("unhandledRejection", async (reason, _promise) => {
182
182
  const rejectionContext = {
183
183
  ...startupContext,
184
184
  triggerEvent: "unhandledRejection",
@@ -68,8 +68,16 @@ export async function establishNewMcpConnection(serverName, operationContext, di
68
68
  // These handlers are crucial for reacting to errors and closures.
69
69
  // They will call the main disconnectMcpClient from clientManager.ts for cleanup.
70
70
  client.onerror = (clientError) => {
71
- const errorCode = clientError.code;
72
- const errorData = clientError.data;
71
+ const errorCode = typeof clientError === "object" &&
72
+ clientError !== null &&
73
+ "code" in clientError
74
+ ? clientError.code
75
+ : "UNKNOWN";
76
+ const errorData = typeof clientError === "object" &&
77
+ clientError !== null &&
78
+ "data" in clientError
79
+ ? clientError.data
80
+ : undefined;
73
81
  logger.error(`MCP SDK Client error for server ${serverName}`, {
74
82
  ...operationContext,
75
83
  error: clientError.message,
@@ -59,7 +59,7 @@ export declare class McpClientManager {
59
59
  * @param parentContext - The context of the calling operation.
60
60
  * @returns A promise that resolves to a map where keys are tool names and values are their definitions.
61
61
  */
62
- getAllTools(parentContext?: RequestContext | null): Promise<Map<string, any>>;
62
+ getAllTools(parentContext?: RequestContext | null): Promise<Map<string, unknown>>;
63
63
  /**
64
64
  * Finds the server name for a given tool from the cached tool map.
65
65
  * This is a synchronous method and relies on `getAllTools` having been called first.
@@ -67,5 +67,5 @@ export declare class McpClientManager {
67
67
  * @param allTools - The map of all available tools.
68
68
  * @returns The server name, or null if the tool is not found.
69
69
  */
70
- getServerForTool(toolName: string, allTools: Map<string, any>): string | null;
70
+ getServerForTool(toolName: string, allTools: Map<string, unknown>): string | null;
71
71
  }
@@ -166,11 +166,18 @@ export class McpClientManager {
166
166
  const toolPromises = Array.from(this.connectedClients.entries()).map(async ([serverName, client]) => {
167
167
  try {
168
168
  const result = await client.listTools();
169
- const tools = result?.tools;
169
+ const tools = result && typeof result === "object" && "tools" in result
170
+ ? result.tools
171
+ : [];
170
172
  if (Array.isArray(tools)) {
171
173
  logger.debug(`Successfully fetched ${tools.length} tools from server: ${serverName}`, { ...context, serverName });
172
174
  for (const tool of tools) {
173
- allTools.set(tool.name, { ...tool, server: serverName });
175
+ if (tool && typeof tool === "object" && "name" in tool) {
176
+ allTools.set(tool.name, {
177
+ ...tool,
178
+ server: serverName,
179
+ });
180
+ }
174
181
  }
175
182
  }
176
183
  else {
@@ -198,6 +205,9 @@ export class McpClientManager {
198
205
  */
199
206
  getServerForTool(toolName, allTools) {
200
207
  const tool = allTools.get(toolName);
201
- return tool?.server || null;
208
+ if (tool && typeof tool === "object" && "server" in tool) {
209
+ return tool.server;
210
+ }
211
+ return null;
202
212
  }
203
213
  }
@@ -18,8 +18,13 @@ export function registerFetchImageTestTool(server) {
18
18
  });
19
19
  ErrorHandler.tryCatch(async () => {
20
20
  server.tool(toolName, toolDescription, FetchImageTestInputSchema.shape, async (input, mcpProvidedContext) => {
21
+ const parentRequestId = mcpProvidedContext &&
22
+ typeof mcpProvidedContext === "object" &&
23
+ "requestId" in mcpProvidedContext
24
+ ? mcpProvidedContext.requestId
25
+ : registrationContext.requestId;
21
26
  const handlerContext = requestContextService.createRequestContext({
22
- parentRequestId: mcpProvidedContext?.requestId || registrationContext.requestId,
27
+ parentRequestId,
23
28
  operation: "fetchImageTestToolHandler",
24
29
  toolName: toolName,
25
30
  input,
@@ -25,7 +25,7 @@ import { randomUUID } from "node:crypto";
25
25
  import { config } from "../../config/index.js";
26
26
  import { BaseErrorCode, McpError } from "../../types-global/errors.js";
27
27
  import { logger, rateLimiter, requestContextService, } from "../../utils/index.js";
28
- import { jwtAuthMiddleware, oauthMiddleware, } from "./auth/index.js";
28
+ import { jwtAuthMiddleware, oauthMiddleware } from "./auth/index.js";
29
29
  import { httpErrorHandler } from "./httpErrorHandler.js";
30
30
  const HTTP_PORT = config.mcpHttpPort;
31
31
  const HTTP_HOST = config.mcpHttpHost;
@@ -37,13 +37,7 @@ const MAX_PORT_RETRIES = 15;
37
37
  // For a scalable deployment, this would need to be replaced with a distributed
38
38
  // store like Redis or Memcached.
39
39
  const transports = {};
40
- async function isPortInUse(port, host, parentContext) {
41
- const checkContext = requestContextService.createRequestContext({
42
- ...parentContext,
43
- operation: "isPortInUse",
44
- port,
45
- host,
46
- });
40
+ async function isPortInUse(port, host, _parentContext) {
47
41
  return new Promise((resolve) => {
48
42
  const tempServer = http.createServer();
49
43
  tempServer
@@ -61,40 +55,48 @@ function startHttpServerWithRetry(app, initialPort, host, maxRetries, parentCont
61
55
  ...parentContext,
62
56
  operation: "startHttpServerWithRetry",
63
57
  });
64
- return new Promise(async (resolve, reject) => {
65
- for (let i = 0; i <= maxRetries; i++) {
66
- const currentPort = initialPort + i;
67
- const attemptContext = {
68
- ...startContext,
69
- port: currentPort,
70
- attempt: i + 1,
71
- };
72
- if (await isPortInUse(currentPort, host, attemptContext)) {
73
- logger.warning(`Port ${currentPort} is in use, retrying...`, attemptContext);
74
- continue;
75
- }
76
- try {
77
- const serverInstance = serve({ fetch: app.fetch, port: currentPort, hostname: host }, (info) => {
78
- const serverAddress = `http://${info.address}:${info.port}${MCP_ENDPOINT_PATH}`;
79
- logger.info(`HTTP transport listening at ${serverAddress}`, {
80
- ...attemptContext,
81
- address: serverAddress,
82
- });
83
- if (process.stdout.isTTY) {
84
- console.log(`\n🚀 MCP Server running at: ${serverAddress}\n`);
85
- }
86
- });
87
- resolve(serverInstance);
58
+ return new Promise((resolve, reject) => {
59
+ const tryBind = (port, attempt) => {
60
+ if (attempt > maxRetries + 1) {
61
+ reject(new Error("Failed to bind to any port after multiple retries."));
88
62
  return;
89
63
  }
90
- catch (err) {
91
- if (err.code !== "EADDRINUSE") {
92
- reject(err);
64
+ const attemptContext = { ...startContext, port, attempt };
65
+ isPortInUse(port, host, attemptContext)
66
+ .then((inUse) => {
67
+ if (inUse) {
68
+ logger.warning(`Port ${port} is in use, retrying...`, attemptContext);
69
+ setTimeout(() => tryBind(port + 1, attempt + 1), 50); // Small delay
93
70
  return;
94
71
  }
95
- }
96
- }
97
- reject(new Error("Failed to bind to any port after multiple retries."));
72
+ try {
73
+ const serverInstance = serve({ fetch: app.fetch, port, hostname: host }, (info) => {
74
+ const serverAddress = `http://${info.address}:${info.port}${MCP_ENDPOINT_PATH}`;
75
+ logger.info(`HTTP transport listening at ${serverAddress}`, {
76
+ ...attemptContext,
77
+ address: serverAddress,
78
+ });
79
+ if (process.stdout.isTTY) {
80
+ console.log(`\n🚀 MCP Server running at: ${serverAddress}\n`);
81
+ }
82
+ });
83
+ resolve(serverInstance);
84
+ }
85
+ catch (err) {
86
+ if (err &&
87
+ typeof err === "object" &&
88
+ "code" in err &&
89
+ err.code !== "EADDRINUSE") {
90
+ reject(err);
91
+ }
92
+ else {
93
+ setTimeout(() => tryBind(port + 1, attempt + 1), 50);
94
+ }
95
+ }
96
+ })
97
+ .catch((err) => reject(err));
98
+ };
99
+ tryBind(initialPort, 1);
98
100
  });
99
101
  }
100
102
  export async function startHttpTransport(createServerInstanceFn, parentContext) {
@@ -7,9 +7,9 @@ import { DuckDBQueryResult } from "./types.js";
7
7
  export declare class DuckDBQueryExecutor {
8
8
  private dbConnection;
9
9
  constructor(connection: duckdb.DuckDBConnection);
10
- run(sql: string, params?: duckdb.DuckDBValue[] | Record<string, duckdb.DuckDBValue>): Promise<void>;
11
- query<T = Record<string, unknown>>(sql: string, params?: duckdb.DuckDBValue[] | Record<string, duckdb.DuckDBValue>): Promise<DuckDBQueryResult<T>>;
12
- stream(sql: string, params?: duckdb.DuckDBValue[] | Record<string, duckdb.DuckDBValue>): Promise<duckdb.DuckDBPendingResult>;
10
+ run(sql: string, params?: duckdb.DuckDBValue[]): Promise<void>;
11
+ query<T = Record<string, unknown>>(sql: string, params?: duckdb.DuckDBValue[]): Promise<DuckDBQueryResult<T>>;
12
+ stream(sql: string, params?: duckdb.DuckDBValue[]): Promise<duckdb.DuckDBResult>;
13
13
  prepare(sql: string): Promise<duckdb.DuckDBPreparedStatement>;
14
14
  beginTransaction(): Promise<void>;
15
15
  commitTransaction(): Promise<void>;
@@ -18,11 +18,8 @@ export class DuckDBQueryExecutor {
18
18
  if (params === undefined) {
19
19
  await this.dbConnection.run(sql);
20
20
  }
21
- else if (Array.isArray(params)) {
22
- await this.dbConnection.run(sql, params); // Pass array directly
23
- }
24
21
  else {
25
- await this.dbConnection.run(sql, params); // Cast to any for object
22
+ await this.dbConnection.run(sql, params);
26
23
  }
27
24
  }, {
28
25
  operation: "DuckDBQueryExecutor.run",
@@ -38,32 +35,11 @@ export class DuckDBQueryExecutor {
38
35
  });
39
36
  return ErrorHandler.tryCatch(async () => {
40
37
  logger.debug(`Executing SQL (query): ${sql}`, { ...context, params });
41
- let resultObject; // Corresponds to 'result' in docs after connection.stream()
42
- if (params === undefined) {
43
- resultObject = await this.dbConnection.stream(sql);
44
- }
45
- else if (Array.isArray(params)) {
46
- resultObject = await this.dbConnection.stream(sql,
47
- // According to docs, params for stream are passed as subsequent arguments
48
- // However, the DuckDBConnection type definition might expect a single array/object.
49
- // The original code passed `params` directly, which is typical for many drivers
50
- // if the method signature is `stream(sql: string, params?: any[])`
51
- // Let's stick to the original way params were passed to stream, as the docs
52
- // show `connection.stream(sql, values, types)` where values could be an array/object.
53
- // The most robust way for array params is often `stream(sql, ...params_array)`.
54
- // Let's assume the type defs are `stream(sql: string, ...args: any[])` for array params.
55
- // Or `stream(sql: string, namedParams: object)` for named.
56
- // The previous code `params` directly for array, and `params as any` for object.
57
- // This seems reasonable.
58
- params);
59
- }
60
- else {
61
- resultObject = await this.dbConnection.stream(sql, params); // Cast to any for object
62
- }
63
- const rows = (await resultObject.getRows()); // Use getRows() as per docs, and it's async
64
- const columnNames = resultObject.columnNames(); // Sync as per docs
38
+ const resultObject = await this.stream(sql, params);
39
+ const rows = (await resultObject.getRows());
40
+ const columnNames = resultObject.columnNames();
65
41
  const columnTypes = resultObject
66
- .columnTypes() // Sync as per docs
42
+ .columnTypes()
67
43
  .map((ct) => ct.typeId);
68
44
  return {
69
45
  rows: rows,
@@ -88,9 +64,6 @@ export class DuckDBQueryExecutor {
88
64
  if (params === undefined) {
89
65
  return this.dbConnection.stream(sql);
90
66
  }
91
- else if (Array.isArray(params)) {
92
- return this.dbConnection.stream(sql, params);
93
- }
94
67
  else {
95
68
  return this.dbConnection.stream(sql, params);
96
69
  }
@@ -11,9 +11,10 @@ export declare class DuckDBService implements IDuckDBService {
11
11
  constructor();
12
12
  initialize(config?: DuckDBServiceConfig): Promise<void>;
13
13
  private ensureInitialized;
14
- run(sql: string, params?: unknown[] | Record<string, unknown>): Promise<void>;
15
- query<T = Record<string, unknown>>(sql: string, params?: unknown[] | Record<string, unknown>): Promise<DuckDBQueryResult<T>>;
16
- stream(sql: string, params?: unknown[] | Record<string, unknown>): Promise<duckdb.DuckDBPendingResult>;
14
+ private validateParams;
15
+ run(sql: string, params?: unknown[]): Promise<void>;
16
+ query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<DuckDBQueryResult<T>>;
17
+ stream(sql: string, params?: unknown[]): Promise<duckdb.DuckDBResult>;
17
18
  prepare(sql: string): Promise<duckdb.DuckDBPreparedStatement>;
18
19
  beginTransaction(): Promise<void>;
19
20
  commitTransaction(): Promise<void>;
@@ -42,14 +42,23 @@ export class DuckDBService {
42
42
  throw new McpError(BaseErrorCode.SERVICE_NOT_INITIALIZED, "DuckDBQueryExecutor not available. DuckDBService may not be fully initialized.", context);
43
43
  }
44
44
  }
45
+ validateParams(params, context) {
46
+ if (params === undefined) {
47
+ return undefined;
48
+ }
49
+ if (Array.isArray(params)) {
50
+ return params;
51
+ }
52
+ throw new McpError(BaseErrorCode.INVALID_INPUT, "DuckDB service only supports array-style parameters, not named objects.", context);
53
+ }
45
54
  async run(sql, params) {
46
55
  const context = requestContextService.createRequestContext({
47
56
  operation: "DuckDBService.run",
48
57
  initialData: { sql, params },
49
58
  });
50
59
  this.ensureInitialized(context);
51
- // Type assertion for params needed due to DuckDBQueryExecutor's specific type
52
- return this.queryExecutor.run(sql, params);
60
+ const validatedParams = this.validateParams(params, context);
61
+ return this.queryExecutor.run(sql, validatedParams);
53
62
  }
54
63
  async query(sql, params) {
55
64
  const context = requestContextService.createRequestContext({
@@ -57,7 +66,8 @@ export class DuckDBService {
57
66
  initialData: { sql, params },
58
67
  });
59
68
  this.ensureInitialized(context);
60
- return this.queryExecutor.query(sql, params);
69
+ const validatedParams = this.validateParams(params, context);
70
+ return this.queryExecutor.query(sql, validatedParams);
61
71
  }
62
72
  async stream(sql, params) {
63
73
  const context = requestContextService.createRequestContext({
@@ -65,7 +75,8 @@ export class DuckDBService {
65
75
  initialData: { sql, params },
66
76
  });
67
77
  this.ensureInitialized(context);
68
- return this.queryExecutor.stream(sql, params);
78
+ const validatedParams = this.validateParams(params, context);
79
+ return this.queryExecutor.stream(sql, validatedParams);
69
80
  }
70
81
  async prepare(sql) {
71
82
  const context = requestContextService.createRequestContext({
@@ -30,7 +30,7 @@ export interface DuckDBServiceConfig {
30
30
  */
31
31
  export interface DuckDBQuery {
32
32
  sql: string;
33
- params?: unknown[] | Record<string, unknown>;
33
+ params?: unknown[];
34
34
  }
35
35
  /**
36
36
  * Represents the result of a query execution.
@@ -56,31 +56,31 @@ export interface IDuckDBService {
56
56
  /**
57
57
  * Executes a SQL query that does not return a large result set (e.g., CREATE, INSERT, UPDATE, DELETE).
58
58
  * @param {string} sql - The SQL query string.
59
- * @param {unknown[] | Record<string, unknown>} [params] - Optional parameters for the query.
59
+ * @param {unknown[]} [params] - Optional parameters for the query.
60
60
  * @returns {Promise<void>}
61
61
  * @throws {McpError} If the query fails or the service is not initialized.
62
62
  */
63
- run(sql: string, params?: unknown[] | Record<string, unknown>): Promise<void>;
63
+ run(sql: string, params?: unknown[]): Promise<void>;
64
64
  /**
65
65
  * Executes a SQL query and returns all resulting rows.
66
66
  * Suitable for queries that return a manageable number of rows.
67
67
  * @template T - The expected type of the row objects.
68
68
  * @param {string} sql - The SQL query string.
69
- * @param {unknown[] | Record<string, unknown>} [params] - Optional parameters for the query.
69
+ * @param {unknown[]} [params] - Optional parameters for the query.
70
70
  * @returns {Promise<DuckDBQueryResult<T>>} The query result.
71
71
  * @throws {McpError} If the query fails or the service is not initialized.
72
72
  */
73
- query<T = Record<string, unknown>>(sql: string, params?: unknown[] | Record<string, unknown>): Promise<DuckDBQueryResult<T>>;
73
+ query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<DuckDBQueryResult<T>>;
74
74
  /**
75
75
  * Executes a SQL query and provides a streaming result reader.
76
76
  * Suitable for queries that return very large result sets.
77
77
  * The caller is responsible for closing the stream.
78
78
  * @param {string} sql - The SQL query string.
79
- * @param {unknown[] | Record<string, unknown>} [params] - Optional parameters for the query.
79
+ * @param {unknown[]} [params] - Optional parameters for the query.
80
80
  * @returns {Promise<duckdb.DuckDBStreamingResult>} A streaming result object.
81
81
  * @throws {McpError} If the query fails or the service is not initialized.
82
82
  */
83
- stream(sql: string, params?: unknown[] | Record<string, unknown>): Promise<duckdb.DuckDBPendingResult>;
83
+ stream(sql: string, params?: unknown[]): Promise<duckdb.DuckDBResult>;
84
84
  /**
85
85
  * Creates a prepared statement.
86
86
  * @param {string} sql - The SQL query string for the prepared statement.
@@ -19,7 +19,7 @@ export type OpenRouterChatParams = (ChatCompletionCreateParamsNonStreaming | Cha
19
19
  transforms?: string[];
20
20
  models?: string[];
21
21
  route?: "fallback";
22
- provider?: Record<string, any>;
22
+ provider?: Record<string, unknown>;
23
23
  };
24
24
  declare class OpenRouterProvider {
25
25
  private client?;
@@ -65,9 +65,13 @@ function _prepareApiParameters(params) {
65
65
  if (extraBody.min_p === undefined && config.llmDefaultMinP !== undefined) {
66
66
  extraBody.min_p = config.llmDefaultMinP;
67
67
  }
68
- if (extraBody.provider && typeof extraBody.provider === "object") {
69
- if (!extraBody.provider.sort)
70
- extraBody.provider.sort = "throughput";
68
+ if (extraBody.provider &&
69
+ typeof extraBody.provider === "object" &&
70
+ extraBody.provider !== null) {
71
+ const provider = extraBody.provider;
72
+ if (!provider.sort) {
73
+ provider.sort = "throughput";
74
+ }
71
75
  }
72
76
  else if (extraBody.provider === undefined) {
73
77
  extraBody.provider = { sort: "throughput" };
@@ -108,7 +112,8 @@ async function _openRouterChatCompletionLogic(client, params, context) {
108
112
  });
109
113
  return response;
110
114
  }
111
- catch (error) {
115
+ catch (e) {
116
+ const error = e;
112
117
  logger.logInteraction("OpenRouterError", {
113
118
  context,
114
119
  error: {
@@ -164,7 +169,8 @@ class OpenRouterProvider {
164
169
  this.status = "ready";
165
170
  logger.info("OpenRouter Service Initialized and Ready", opContext);
166
171
  }
167
- catch (error) {
172
+ catch (e) {
173
+ const error = e;
168
174
  this.status = "error";
169
175
  this.initializationError = error;
170
176
  logger.error("Failed to initialize OpenRouter client", {
@@ -7,7 +7,7 @@
7
7
  * @module src/services/supabase/supabaseClient
8
8
  */
9
9
  import { SupabaseClient } from "@supabase/supabase-js";
10
- type Database = any;
10
+ type Database = Record<string, unknown>;
11
11
  /**
12
12
  * Returns the singleton Supabase client instance.
13
13
  * Throws an McpError if the client is not initialized.
@@ -23,6 +23,8 @@ export declare enum BaseErrorCode {
23
23
  CONFLICT = "CONFLICT",
24
24
  /** The request failed due to invalid input parameters or data. */
25
25
  VALIDATION_ERROR = "VALIDATION_ERROR",
26
+ /** The provided input is invalid for the operation. */
27
+ INVALID_INPUT = "INVALID_INPUT",
26
28
  /** An error occurred while parsing input data (e.g., date string, JSON). */
27
29
  PARSING_ERROR = "PARSING_ERROR",
28
30
  /** The request was rejected because the client has exceeded rate limits. */
@@ -24,6 +24,8 @@ export var BaseErrorCode;
24
24
  BaseErrorCode["CONFLICT"] = "CONFLICT";
25
25
  /** The request failed due to invalid input parameters or data. */
26
26
  BaseErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR";
27
+ /** The provided input is invalid for the operation. */
28
+ BaseErrorCode["INVALID_INPUT"] = "INVALID_INPUT";
27
29
  /** An error occurred while parsing input data (e.g., date string, JSON). */
28
30
  BaseErrorCode["PARSING_ERROR"] = "PARSING_ERROR";
29
31
  /** The request was rejected because the client has exceeded rate limits. */
@@ -126,7 +126,7 @@ function getErrorMessage(error) {
126
126
  try {
127
127
  return `Non-Error object encountered: ${JSON.stringify(error)}`;
128
128
  }
129
- catch (stringifyError) {
129
+ catch {
130
130
  return `Unstringifyable non-Error object encountered (constructor: ${error.constructor?.name || "Unknown"})`;
131
131
  }
132
132
  }
@@ -17,7 +17,7 @@ export interface McpLogPayload {
17
17
  message: string;
18
18
  stack?: string;
19
19
  };
20
- [key: string]: any;
20
+ [key: string]: unknown;
21
21
  }
22
22
  /**
23
23
  * Type for the `data` parameter of the `McpNotificationSender` function.
@@ -139,7 +139,7 @@ export declare class Logger {
139
139
  * @param interactionName - A name for the interaction type (e.g., 'OpenRouterIO').
140
140
  * @param data - The structured data to log.
141
141
  */
142
- logInteraction(interactionName: string, data: Record<string, any>): void;
142
+ logInteraction(interactionName: string, data: Record<string, unknown>): void;
143
143
  }
144
144
  /**
145
145
  * The singleton instance of the Logger.
@@ -16,7 +16,7 @@ import { RequestContext } from "../index.js";
16
16
  * @throws {McpError} If an unexpected error occurs during parsing.
17
17
  * @private
18
18
  */
19
- declare function parseDateString(text: string, context: RequestContext, refDate?: Date): Promise<Date | null>;
19
+ export declare function parseDateString(text: string, context: RequestContext, refDate?: Date): Promise<Date | null>;
20
20
  /**
21
21
  * Parses a natural language date string and returns detailed parsing results.
22
22
  * Provides more information than just the Date object, including matched text and components.
@@ -28,7 +28,7 @@ declare function parseDateString(text: string, context: RequestContext, refDate?
28
28
  * @throws {McpError} If an unexpected error occurs during parsing.
29
29
  * @private
30
30
  */
31
- declare function parseDateStringDetailed(text: string, context: RequestContext, refDate?: Date): Promise<chrono.ParsedResult[]>;
31
+ export declare function parseDateStringDetailed(text: string, context: RequestContext, refDate?: Date): Promise<chrono.ParsedResult[]>;
32
32
  /**
33
33
  * An object providing date parsing functionalities.
34
34
  *
@@ -70,4 +70,3 @@ export declare const dateParser: {
70
70
  */
71
71
  parseDate: typeof parseDateString;
72
72
  };
73
- export {};
@@ -17,7 +17,7 @@ import { ErrorHandler, logger } from "../index.js";
17
17
  * @throws {McpError} If an unexpected error occurs during parsing.
18
18
  * @private
19
19
  */
20
- async function parseDateString(text, context, refDate) {
20
+ export async function parseDateString(text, context, refDate) {
21
21
  const operation = "parseDateString";
22
22
  const logContext = { ...context, operation, inputText: text, refDate };
23
23
  logger.debug(`Attempting to parse date string: "${text}"`, logContext);
@@ -49,7 +49,7 @@ async function parseDateString(text, context, refDate) {
49
49
  * @throws {McpError} If an unexpected error occurs during parsing.
50
50
  * @private
51
51
  */
52
- async function parseDateStringDetailed(text, context, refDate) {
52
+ export async function parseDateStringDetailed(text, context, refDate) {
53
53
  const operation = "parseDateStringDetailed";
54
54
  const logContext = { ...context, operation, inputText: text, refDate };
55
55
  logger.debug(`Attempting detailed parse of date string: "${text}"`, logContext);
@@ -56,7 +56,7 @@ export declare class JsonParser {
56
56
  * @returns The parsed JavaScript value.
57
57
  * @throws {McpError} If the string is empty after processing or if `partial-json` fails.
58
58
  */
59
- parse<T = any>(jsonString: string, allowPartial?: number, context?: RequestContext): T;
59
+ parse<T = unknown>(jsonString: string, allowPartial?: number, context?: RequestContext): T;
60
60
  }
61
61
  /**
62
62
  * Singleton instance of the `JsonParser`.
@@ -83,7 +83,8 @@ export class JsonParser {
83
83
  try {
84
84
  return parsePartialJson(stringToParse, allowPartial);
85
85
  }
86
- catch (error) {
86
+ catch (e) {
87
+ const error = e;
87
88
  const errorLogContext = context ||
88
89
  requestContextService.createRequestContext({
89
90
  operation: "JsonParser.parseError",
@@ -4,7 +4,7 @@
4
4
  * defining, starting, stopping, and listing recurring tasks within the application.
5
5
  * @module src/utils/scheduling/scheduler
6
6
  */
7
- import cron from "node-cron";
7
+ import cron, { createTask } from "node-cron";
8
8
  import { logger } from "../internal/index.js";
9
9
  import { requestContextService } from "../internal/requestContext.js";
10
10
  /**
@@ -45,7 +45,7 @@ export class SchedulerService {
45
45
  if (!cron.validate(schedule)) {
46
46
  throw new Error(`Invalid cron schedule: ${schedule}`);
47
47
  }
48
- const task = cron.schedule(schedule, async () => {
48
+ const task = createTask(schedule, async () => {
49
49
  const job = this.jobs.get(id);
50
50
  if (job && job.isRunning) {
51
51
  logger.warning(`Job '${id}' is already running. Skipping this execution.`, {
@@ -74,8 +74,6 @@ export class SchedulerService {
74
74
  job.isRunning = false;
75
75
  }
76
76
  }
77
- }, {
78
- scheduled: false, // Do not start immediately
79
77
  });
80
78
  const newJob = {
81
79
  id,
@@ -233,7 +233,6 @@ export class Sanitization {
233
233
  rootDir: options.rootDir ? path.resolve(options.rootDir) : undefined,
234
234
  };
235
235
  let wasAbsoluteInitially = false;
236
- let convertedToRelative = false;
237
236
  try {
238
237
  if (!input || typeof input !== "string")
239
238
  throw new Error("Invalid path input: must be a non-empty string.");
@@ -263,7 +262,6 @@ export class Sanitization {
263
262
  if (path.isAbsolute(normalized)) {
264
263
  if (!effectiveOptions.allowAbsolute) {
265
264
  finalSanitizedPath = normalized.replace(/^(?:[A-Za-z]:)?[/\\]+/, "");
266
- convertedToRelative = true;
267
265
  }
268
266
  else {
269
267
  finalSanitizedPath = normalized;
@@ -355,7 +353,7 @@ export class Sanitization {
355
353
  throw new McpError(BaseErrorCode.VALIDATION_ERROR, "Invalid number value (NaN or Infinity).", { input });
356
354
  }
357
355
  let clamped = false;
358
- let originalValueForLog = value;
356
+ const originalValueForLog = value;
359
357
  if (min !== undefined && value < min) {
360
358
  value = min;
361
359
  clamped = true;
@@ -399,8 +397,8 @@ export class Sanitization {
399
397
  try {
400
398
  if (!input || typeof input !== "object")
401
399
  return input;
402
- const clonedInput = typeof structuredClone === "function"
403
- ? structuredClone(input)
400
+ const clonedInput = typeof globalThis.structuredClone === "function"
401
+ ? globalThis.structuredClone(input)
404
402
  : JSON.parse(JSON.stringify(input));
405
403
  this.redactSensitiveFields(clonedInput);
406
404
  return clonedInput;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-ts-template",
3
- "version": "1.6.1",
3
+ "version": "1.6.3",
4
4
  "description": "Jumpstart Model Context Protocol (MCP) development with this production-ready TypeScript template. Build robust MCP servers and clients with built-in utilities, authentication, and service integrations. Agent framework utilizing MCP Client included.",
5
5
  "main": "dist/index.js",
6
6
  "files": [
@@ -29,6 +29,8 @@
29
29
  "rebuild": "ts-node --esm scripts/clean.ts && npm run build",
30
30
  "docs:generate": "typedoc --tsconfig ./tsconfig.typedoc.json",
31
31
  "depcheck": "npx depcheck",
32
+ "lint": "eslint .",
33
+ "lint:fix": "eslint . --fix",
32
34
  "tree": "ts-node --esm scripts/tree.ts",
33
35
  "fetch-spec": "ts-node --esm scripts/fetch-openapi-spec.ts",
34
36
  "format": "prettier --write \"**/*.{ts,js,json,md,html,css}\"",
@@ -44,22 +46,24 @@
44
46
  "@types/sanitize-html": "^2.16.0",
45
47
  "@types/validator": "13.15.2",
46
48
  "chrono-node": "^2.8.0",
47
- "dotenv": "^16.5.0",
49
+ "dotenv": "^16.6.1",
50
+ "eslint": "^9.30.1",
48
51
  "hono": "^4.8.4",
49
52
  "ignore": "^7.0.5",
50
53
  "jose": "^6.0.11",
54
+ "js-yaml": "^4.1.0",
55
+ "node-cron": "^4.2.0",
51
56
  "openai": "^5.8.2",
52
57
  "partial-json": "^0.1.7",
53
58
  "sanitize-html": "^2.17.0",
54
59
  "tiktoken": "^1.0.21",
55
60
  "ts-node": "^10.9.2",
56
61
  "typescript": "^5.8.3",
62
+ "typescript-eslint": "^8.35.1",
57
63
  "validator": "13.15.15",
58
64
  "winston": "^3.17.0",
59
65
  "winston-transport": "^4.9.0",
60
- "node-cron": "^4.2.0",
61
- "zod": "^3.25.74",
62
- "js-yaml": "^4.1.0"
66
+ "zod": "^3.25.74"
63
67
  },
64
68
  "keywords": [
65
69
  "typescript",
@@ -101,10 +105,12 @@
101
105
  "node": ">=20.0.0"
102
106
  },
103
107
  "devDependencies": {
108
+ "@eslint/js": "^9.30.1",
104
109
  "@types/js-yaml": "^4.0.9",
105
110
  "@types/node-cron": "^3.0.11",
106
111
  "axios": "^1.10.0",
107
112
  "depcheck": "^1.4.7",
113
+ "globals": "^16.3.0",
108
114
  "prettier": "^3.6.2",
109
115
  "typedoc": "^0.28.7"
110
116
  }