@aashari/boilerplate-mcp-server 1.5.9 → 1.6.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [1.6.0](https://github.com/aashari/boilerplate-mcp-server/compare/v1.5.10...v1.6.0) (2025-05-15)
2
+
3
+
4
+ ### Features
5
+
6
+ * enhanced error handling across the application ([75aa905](https://github.com/aashari/boilerplate-mcp-server/commit/75aa90528e615d1c1a9a411ddd1bf1931edfde61))
7
+
8
+ ## [1.5.10](https://github.com/aashari/boilerplate-mcp-server/compare/v1.5.9...v1.5.10) (2025-05-14)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * remove Dockerfile and smithery.yaml ([582e9f9](https://github.com/aashari/boilerplate-mcp-server/commit/582e9f9e66087fd2211d6fcf1aaa79c9ee54a123))
14
+
1
15
  ## [1.5.9](https://github.com/aashari/boilerplate-mcp-server/compare/v1.5.8...v1.5.9) (2025-05-13)
2
16
 
3
17
 
@@ -9,6 +9,7 @@ const ipaddress_formatter_js_1 = require("./ipaddress.formatter.js");
9
9
  const error_handler_util_js_1 = require("../utils/error-handler.util.js");
10
10
  const config_util_js_1 = require("../utils/config.util.js");
11
11
  const error_util_js_1 = require("../utils/error.util.js");
12
+ const error_handler_util_js_2 = require("../utils/error-handler.util.js");
12
13
  /**
13
14
  * @namespace IpAddressController
14
15
  * @description Controller responsible for handling IP address lookup logic.
@@ -97,12 +98,7 @@ async function get(ipAddress, options = {
97
98
  }
98
99
  }
99
100
  catch (error) {
100
- throw (0, error_handler_util_js_1.handleControllerError)(error, {
101
- entityType: 'IP Address',
102
- operation: 'get',
103
- source: 'controllers/ipaddress.controller.ts@get',
104
- additionalInfo: { ipAddress, options },
105
- });
101
+ throw (0, error_handler_util_js_1.handleControllerError)(error, (0, error_handler_util_js_2.buildErrorContext)('IP Address', 'get', 'controllers/ipaddress.controller.ts@get', ipAddress || 'current device', { options }));
106
102
  }
107
103
  }
108
104
  /** Helper to define all fields for extended data */
@@ -47,26 +47,23 @@ async function get(ipAddress, options = {}) {
47
47
  // First check API-level success/failure before Zod validation
48
48
  // This avoids unnecessary validation errors for known API errors
49
49
  if (rawData.status !== 'success') {
50
- throw (0, error_util_js_1.createApiError)(`IP API error: ${rawData.message || 'Unknown error'}`);
51
- }
52
- // Now parse with Zod for successful responses
53
- try {
54
- const validatedData = vendor_ip_api_com_types_js_1.IPDetailSchema.parse(rawData);
55
- methodLogger.debug(`Received and validated successful data from IP API`);
56
- return validatedData;
57
- }
58
- catch (zodError) {
59
- // Throw error on validation failure
60
- methodLogger.error('Zod validation failed', zodError);
61
- if (zodError instanceof zod_1.z.ZodError) {
62
- throw (0, error_util_js_1.createApiError)(`API response validation failed: ${zodError.errors
63
- .map((e) => `${e.path.join('.')}: ${e.message}`)
64
- .join(', ')}`, undefined, // No specific HTTP status for validation errors
65
- zodError);
50
+ // Handle specific ip-api.com error responses
51
+ if (rawData.message) {
52
+ if (rawData.message.includes('private range')) {
53
+ throw (0, error_util_js_1.createApiError)(`Private IP addresses are not supported: ${rawData.message}`, 400, rawData);
54
+ }
55
+ else if (rawData.message.includes('reserved range')) {
56
+ throw (0, error_util_js_1.createApiError)(`Reserved IP addresses are not supported: ${rawData.message}`, 400, rawData);
57
+ }
66
58
  }
67
- // Rethrow if it's not a ZodError (shouldn't happen here)
68
- throw zodError;
59
+ throw (0, error_util_js_1.createApiError)(`IP API error: ${rawData.message || 'Unknown error'}`, 400, // Use 400 for client errors from ip-api.com
60
+ rawData);
69
61
  }
62
+ // Now parse with Zod for successful responses
63
+ // Validate with Zod schema and return
64
+ const validatedData = vendor_ip_api_com_types_js_1.IPDetailSchema.parse(rawData);
65
+ methodLogger.debug(`Received and validated successful data from IP API`);
66
+ return validatedData;
70
67
  }
71
68
  catch (error) {
72
69
  methodLogger.error(`Service error fetching IP data`, error);
@@ -74,7 +71,7 @@ async function get(ipAddress, options = {}) {
74
71
  if (error instanceof zod_1.z.ZodError) {
75
72
  throw (0, error_util_js_1.createApiError)(`API response validation failed: ${error.errors
76
73
  .map((e) => `${e.path.join('.')}: ${e.message}`)
77
- .join(', ')}`, undefined, // No specific HTTP status for validation errors
74
+ .join(', ')}`, 500, // Use 500 for validation errors as it's a server-side issue
78
75
  error);
79
76
  }
80
77
  // Rethrow other McpErrors
@@ -8,7 +8,7 @@
8
8
  * Current application version
9
9
  * This should match the version in package.json
10
10
  */
11
- export declare const VERSION = "1.5.9";
11
+ export declare const VERSION = "1.6.0";
12
12
  /**
13
13
  * Package name with scope
14
14
  * Used for initialization and identification
@@ -11,7 +11,7 @@ exports.CLI_NAME = exports.PACKAGE_NAME = exports.VERSION = void 0;
11
11
  * Current application version
12
12
  * This should match the version in package.json
13
13
  */
14
- exports.VERSION = '1.5.9';
14
+ exports.VERSION = '1.6.0';
15
15
  /**
16
16
  * Package name with scope
17
17
  * Used for initialization and identification
@@ -6,7 +6,11 @@ export declare enum ErrorCode {
6
6
  INVALID_CURSOR = "INVALID_CURSOR",
7
7
  ACCESS_DENIED = "ACCESS_DENIED",
8
8
  VALIDATION_ERROR = "VALIDATION_ERROR",
9
- UNEXPECTED_ERROR = "UNEXPECTED_ERROR"
9
+ UNEXPECTED_ERROR = "UNEXPECTED_ERROR",
10
+ NETWORK_ERROR = "NETWORK_ERROR",
11
+ RATE_LIMIT_ERROR = "RATE_LIMIT_ERROR",
12
+ PRIVATE_IP_ERROR = "PRIVATE_IP_ERROR",
13
+ RESERVED_RANGE_ERROR = "RESERVED_RANGE_ERROR"
10
14
  }
11
15
  /**
12
16
  * Context information for error handling
@@ -33,6 +37,16 @@ export interface ErrorContext {
33
37
  */
34
38
  additionalInfo?: Record<string, unknown>;
35
39
  }
40
+ /**
41
+ * Helper function to create a consistent error context object
42
+ * @param entityType Type of entity being processed
43
+ * @param operation Operation being performed
44
+ * @param source Source of the error (typically file path and function)
45
+ * @param entityId Optional identifier of the entity
46
+ * @param additionalInfo Optional additional information for debugging
47
+ * @returns A formatted ErrorContext object
48
+ */
49
+ export declare function buildErrorContext(entityType: string, operation: string, source: string, entityId?: string | Record<string, string>, additionalInfo?: Record<string, unknown>): ErrorContext;
36
50
  /**
37
51
  * Detect specific error types from raw errors
38
52
  * @param error The error to analyze
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ErrorCode = void 0;
4
+ exports.buildErrorContext = buildErrorContext;
4
5
  exports.detectErrorType = detectErrorType;
5
6
  exports.createUserFriendlyErrorMessage = createUserFriendlyErrorMessage;
6
7
  exports.handleControllerError = handleControllerError;
@@ -16,7 +17,29 @@ var ErrorCode;
16
17
  ErrorCode["ACCESS_DENIED"] = "ACCESS_DENIED";
17
18
  ErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR";
18
19
  ErrorCode["UNEXPECTED_ERROR"] = "UNEXPECTED_ERROR";
20
+ ErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
21
+ ErrorCode["RATE_LIMIT_ERROR"] = "RATE_LIMIT_ERROR";
22
+ ErrorCode["PRIVATE_IP_ERROR"] = "PRIVATE_IP_ERROR";
23
+ ErrorCode["RESERVED_RANGE_ERROR"] = "RESERVED_RANGE_ERROR";
19
24
  })(ErrorCode || (exports.ErrorCode = ErrorCode = {}));
25
+ /**
26
+ * Helper function to create a consistent error context object
27
+ * @param entityType Type of entity being processed
28
+ * @param operation Operation being performed
29
+ * @param source Source of the error (typically file path and function)
30
+ * @param entityId Optional identifier of the entity
31
+ * @param additionalInfo Optional additional information for debugging
32
+ * @returns A formatted ErrorContext object
33
+ */
34
+ function buildErrorContext(entityType, operation, source, entityId, additionalInfo) {
35
+ return {
36
+ entityType,
37
+ operation,
38
+ source,
39
+ ...(entityId && { entityId }),
40
+ ...(additionalInfo && { additionalInfo }),
41
+ };
42
+ }
20
43
  /**
21
44
  * Detect specific error types from raw errors
22
45
  * @param error The error to analyze
@@ -30,6 +53,51 @@ function detectErrorType(error, context = {}) {
30
53
  const statusCode = error instanceof Error && 'statusCode' in error
31
54
  ? error.statusCode
32
55
  : undefined;
56
+ // Network error detection
57
+ if (errorMessage.includes('network error') ||
58
+ errorMessage.includes('fetch failed') ||
59
+ errorMessage.includes('ECONNREFUSED') ||
60
+ errorMessage.includes('ENOTFOUND') ||
61
+ errorMessage.includes('Failed to fetch') ||
62
+ errorMessage.includes('Network request failed')) {
63
+ return { code: ErrorCode.NETWORK_ERROR, statusCode: 500 };
64
+ }
65
+ // Rate limiting detection
66
+ if (errorMessage.includes('rate limit') ||
67
+ errorMessage.includes('too many requests') ||
68
+ statusCode === 429) {
69
+ return { code: ErrorCode.RATE_LIMIT_ERROR, statusCode: 429 };
70
+ }
71
+ // ip-api.com specific error detection
72
+ if (errorMessage.includes('private range') ||
73
+ errorMessage.includes('private IP')) {
74
+ return { code: ErrorCode.PRIVATE_IP_ERROR, statusCode: 400 };
75
+ }
76
+ if (errorMessage.includes('reserved range')) {
77
+ return { code: ErrorCode.RESERVED_RANGE_ERROR, statusCode: 400 };
78
+ }
79
+ // Check for ip-api.com status="fail" in originalError
80
+ if (error instanceof Error &&
81
+ 'originalError' in error &&
82
+ error.originalError &&
83
+ typeof error.originalError === 'object') {
84
+ const originalError = error.originalError;
85
+ if (originalError.status === 'fail') {
86
+ const apiMessage = originalError.message
87
+ ? String(originalError.message)
88
+ : '';
89
+ if (apiMessage.includes('private')) {
90
+ return { code: ErrorCode.PRIVATE_IP_ERROR, statusCode: 400 };
91
+ }
92
+ if (apiMessage.includes('reserved')) {
93
+ return {
94
+ code: ErrorCode.RESERVED_RANGE_ERROR,
95
+ statusCode: 400,
96
+ };
97
+ }
98
+ return { code: ErrorCode.VALIDATION_ERROR, statusCode: 400 };
99
+ }
100
+ }
33
101
  // Not Found detection
34
102
  if (errorMessage.includes('not found') ||
35
103
  errorMessage.includes('does not exist') ||
@@ -110,6 +178,18 @@ function createUserFriendlyErrorMessage(code, context = {}, originalMessage) {
110
178
  originalMessage ||
111
179
  `Invalid data provided for ${operation || 'operation'} ${entity.toLowerCase()}.`;
112
180
  break;
181
+ case ErrorCode.NETWORK_ERROR:
182
+ message = `Network error while ${operation || 'connecting to'} the service. Please check your internet connection and try again.`;
183
+ break;
184
+ case ErrorCode.RATE_LIMIT_ERROR:
185
+ message = `Rate limit exceeded. Please wait a moment and try again, or reduce the frequency of requests.`;
186
+ break;
187
+ case ErrorCode.PRIVATE_IP_ERROR:
188
+ message = `Private IP addresses are not supported. Please provide a public IP address.`;
189
+ break;
190
+ case ErrorCode.RESERVED_RANGE_ERROR:
191
+ message = `Reserved range IP addresses are not supported. Please provide a public IP address.`;
192
+ break;
113
193
  default:
114
194
  message = `An unexpected error occurred while ${operation || 'processing'} ${entity.toLowerCase()}.`;
115
195
  }
@@ -0,0 +1 @@
1
+ export {};
@@ -36,6 +36,12 @@ export declare function createUnexpectedError(message?: string, originalError?:
36
36
  * Ensure an error is an McpError
37
37
  */
38
38
  export declare function ensureMcpError(error: unknown): McpError;
39
+ /**
40
+ * Get the deepest original error from an error chain
41
+ * @param error The error to extract the original cause from
42
+ * @returns The deepest original error or the error itself
43
+ */
44
+ export declare function getDeepOriginalError(error: unknown): unknown;
39
45
  /**
40
46
  * Format error for MCP tool response
41
47
  */
@@ -44,6 +50,11 @@ export declare function formatErrorForMcpTool(error: unknown): {
44
50
  type: 'text';
45
51
  text: string;
46
52
  }>;
53
+ metadata?: {
54
+ errorType: ErrorType;
55
+ statusCode?: number;
56
+ errorDetails?: unknown;
57
+ };
47
58
  };
48
59
  /**
49
60
  * Format error for MCP resource response
@@ -57,6 +68,6 @@ export declare function formatErrorForMcpResource(error: unknown, uri: string):
57
68
  }>;
58
69
  };
59
70
  /**
60
- * Handle error in CLI context
71
+ * Handle error in CLI context with improved user feedback
61
72
  */
62
73
  export declare function handleCliError(error: unknown): never;
@@ -6,6 +6,7 @@ exports.createAuthInvalidError = createAuthInvalidError;
6
6
  exports.createApiError = createApiError;
7
7
  exports.createUnexpectedError = createUnexpectedError;
8
8
  exports.ensureMcpError = ensureMcpError;
9
+ exports.getDeepOriginalError = getDeepOriginalError;
9
10
  exports.formatErrorForMcpTool = formatErrorForMcpTool;
10
11
  exports.formatErrorForMcpResource = formatErrorForMcpResource;
11
12
  exports.handleCliError = handleCliError;
@@ -69,6 +70,27 @@ function ensureMcpError(error) {
69
70
  }
70
71
  return createUnexpectedError(String(error));
71
72
  }
73
+ /**
74
+ * Get the deepest original error from an error chain
75
+ * @param error The error to extract the original cause from
76
+ * @returns The deepest original error or the error itself
77
+ */
78
+ function getDeepOriginalError(error) {
79
+ if (!error) {
80
+ return error;
81
+ }
82
+ let current = error;
83
+ let depth = 0;
84
+ const maxDepth = 10; // Prevent infinite recursion
85
+ while (depth < maxDepth &&
86
+ current instanceof Error &&
87
+ 'originalError' in current &&
88
+ current.originalError) {
89
+ current = current.originalError;
90
+ depth++;
91
+ }
92
+ return current;
93
+ }
72
94
  /**
73
95
  * Format error for MCP tool response
74
96
  */
@@ -76,6 +98,12 @@ function formatErrorForMcpTool(error) {
76
98
  const methodLogger = logger_util_js_1.Logger.forContext('utils/error.util.ts', 'formatErrorForMcpTool');
77
99
  const mcpError = ensureMcpError(error);
78
100
  methodLogger.error(`${mcpError.type} error`, mcpError);
101
+ // Get the deep original error for additional context
102
+ const originalError = getDeepOriginalError(mcpError.originalError);
103
+ // Safely extract details from the original error
104
+ const errorDetails = originalError instanceof Error
105
+ ? { message: originalError.message }
106
+ : originalError;
79
107
  return {
80
108
  content: [
81
109
  {
@@ -83,6 +111,11 @@ function formatErrorForMcpTool(error) {
83
111
  text: `Error: ${mcpError.message}`,
84
112
  },
85
113
  ],
114
+ metadata: {
115
+ errorType: mcpError.type,
116
+ statusCode: mcpError.statusCode,
117
+ errorDetails,
118
+ },
86
119
  };
87
120
  }
88
121
  /**
@@ -104,12 +137,38 @@ function formatErrorForMcpResource(error, uri) {
104
137
  };
105
138
  }
106
139
  /**
107
- * Handle error in CLI context
140
+ * Handle error in CLI context with improved user feedback
108
141
  */
109
142
  function handleCliError(error) {
110
143
  const methodLogger = logger_util_js_1.Logger.forContext('utils/error.util.ts', 'handleCliError');
111
144
  const mcpError = ensureMcpError(error);
112
145
  methodLogger.error(`${mcpError.type} error`, mcpError);
146
+ // Get the deep original error for more context
147
+ const originalError = getDeepOriginalError(mcpError.originalError);
148
+ // Print the error message
113
149
  console.error(`Error: ${mcpError.message}`);
150
+ // Provide helpful context based on error type
151
+ if (mcpError.type === ErrorType.AUTH_MISSING) {
152
+ console.error('\nTip: Make sure to set up your API token in the configuration file or environment variables.');
153
+ }
154
+ else if (mcpError.type === ErrorType.AUTH_INVALID) {
155
+ console.error('\nTip: Check that your API token is correct and has not expired.');
156
+ }
157
+ else if (mcpError.type === ErrorType.API_ERROR) {
158
+ if (mcpError.statusCode === 429) {
159
+ console.error('\nTip: You may have exceeded your API rate limits. Try again later or upgrade your API plan.');
160
+ }
161
+ // Add ip-api.com specific context if available
162
+ if (originalError && typeof originalError === 'object') {
163
+ const origErr = originalError;
164
+ if (origErr.status === 'fail' && origErr.message) {
165
+ console.error(`\nAPI returned failure: ${String(origErr.message)}`);
166
+ }
167
+ }
168
+ }
169
+ // Display DEBUG tip
170
+ if (process.env.DEBUG !== 'mcp:*') {
171
+ console.error('\nFor more detailed error information, run with DEBUG=mcp:* environment variable.');
172
+ }
114
173
  process.exit(1);
115
174
  }
@@ -0,0 +1 @@
1
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aashari/boilerplate-mcp-server",
3
- "version": "1.5.9",
3
+ "version": "1.6.0",
4
4
  "description": "TypeScript Model Context Protocol (MCP) server boilerplate providing IP lookup tools/resources. Includes CLI support and extensible structure for connecting AI systems (LLMs) to external data sources like ip-api.com. Ideal template for creating new MCP integrations via Node.js.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/package.json.bak CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aashari/boilerplate-mcp-server",
3
- "version": "1.5.8",
3
+ "version": "1.5.10",
4
4
  "description": "TypeScript Model Context Protocol (MCP) server boilerplate providing IP lookup tools/resources. Includes CLI support and extensible structure for connecting AI systems (LLMs) to external data sources like ip-api.com. Ideal template for creating new MCP integrations via Node.js.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/Dockerfile DELETED
@@ -1,25 +0,0 @@
1
- # Generated by https://smithery.ai. See: https://smithery.ai/docs/config#dockerfile
2
- FROM node:lts-alpine
3
-
4
- # Create app directory
5
- WORKDIR /usr/src/app
6
-
7
- # Copy package files
8
- COPY package*.json ./
9
-
10
- # Install dependencies without running prepare scripts
11
- RUN npm install --ignore-scripts
12
-
13
- # Copy source code
14
- COPY . .
15
-
16
- # Build the TypeScript code
17
- RUN npm run build
18
-
19
- # Ensure the entrypoint is executable (already set in prepare, but reensure here)
20
- RUN chmod +x dist/index.js
21
-
22
- EXPOSE 3000
23
-
24
- # Start the MCP server
25
- CMD [ "npm", "start" ]
package/smithery.yaml DELETED
@@ -1,34 +0,0 @@
1
- # Smithery configuration file: https://smithery.ai/docs/config#smitheryyaml
2
-
3
- startCommand:
4
- type: stdio
5
- configSchema:
6
- # JSON Schema defining the configuration options for the MCP.
7
- type: object
8
- required: []
9
- properties:
10
- debug:
11
- type: boolean
12
- default: false
13
- description: Enable debug logging
14
- ipapiApiToken:
15
- type: string
16
- default: ""
17
- description: API token for the IP API service
18
- commandFunction:
19
- # A JS function that produces the CLI command based on the given config to start the MCP on stdio.
20
- |-
21
- (config) => {
22
- // Setup environment variables based on provided config
23
- const env = Object.assign({}, process.env);
24
- if (config.debug) {
25
- env.DEBUG = 'true';
26
- }
27
- if (config.ipapiApiToken) {
28
- env.IPAPI_API_TOKEN = config.ipapiApiToken;
29
- }
30
- return { command: 'node', args: ['dist/index.js'], env };
31
- }
32
- exampleConfig:
33
- debug: true
34
- ipapiApiToken: YOUR_API_TOKEN