@aashari/boilerplate-mcp-server 1.16.0 → 1.17.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,22 @@
1
+ # [1.17.0](https://github.com/aashari/boilerplate-mcp-server/compare/v1.16.1...v1.17.0) (2025-12-03)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * regenerate package-lock.json to sync with package.json ([aeb3d63](https://github.com/aashari/boilerplate-mcp-server/commit/aeb3d63234a2deaa1257f17a3db6a6538d5345b1))
7
+
8
+
9
+ ### Features
10
+
11
+ * add raw response logging with truncation for large API responses ([3ed7b19](https://github.com/aashari/boilerplate-mcp-server/commit/3ed7b191fc51ff2562f9f5ad2d6dc46ff0a22714))
12
+
13
+ ## [1.16.1](https://github.com/aashari/boilerplate-mcp-server/compare/v1.16.0...v1.16.1) (2025-12-01)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * **deps:** resolve security vulnerabilities in body-parser and js-yaml ([abd1c08](https://github.com/aashari/boilerplate-mcp-server/commit/abd1c08d9c777ffb04dc851c3c14eeb8a4a09e1c))
19
+
1
20
  # [1.16.0](https://github.com/aashari/boilerplate-mcp-server/compare/v1.15.0...v1.16.0) (2025-12-01)
2
21
 
3
22
 
@@ -1,13 +1,8 @@
1
+ import { ControllerResponse } from '../types/common.types.js';
1
2
  /**
2
3
  * Output format type
3
4
  */
4
5
  type OutputFormat = 'toon' | 'json';
5
- /**
6
- * Controller response type
7
- */
8
- interface ControllerResponse {
9
- content: string;
10
- }
11
6
  /**
12
7
  * @namespace IpAddressController
13
8
  * @description Controller responsible for handling IP address lookup logic.
@@ -74,9 +74,12 @@ async function get(args = {}) {
74
74
  isTestEnvironment,
75
75
  });
76
76
  let data;
77
+ let rawResponsePath = null;
77
78
  try {
78
79
  // Call the service with ipAddress and the mapped serviceOptions
79
- data = await vendor_ip_api_com_service_js_1.default.get(args.ipAddress, serviceOptions);
80
+ const response = await vendor_ip_api_com_service_js_1.default.get(args.ipAddress, serviceOptions);
81
+ data = response.data;
82
+ rawResponsePath = response.rawResponsePath;
80
83
  methodLogger.debug(`Got the response from the service`, data);
81
84
  }
82
85
  catch (error) {
@@ -88,10 +91,12 @@ async function get(args = {}) {
88
91
  error.message.includes('Access denied'))) {
89
92
  methodLogger.warn('HTTPS request failed, falling back to HTTP');
90
93
  // Try again with HTTP
91
- data = await vendor_ip_api_com_service_js_1.default.get(args.ipAddress, {
94
+ const fallbackResponse = await vendor_ip_api_com_service_js_1.default.get(args.ipAddress, {
92
95
  ...serviceOptions,
93
96
  useHttps: false,
94
97
  });
98
+ data = fallbackResponse.data;
99
+ rawResponsePath = fallbackResponse.rawResponsePath;
95
100
  methodLogger.debug(`Got the response from HTTP fallback`, data);
96
101
  }
97
102
  else {
@@ -105,7 +110,7 @@ async function get(args = {}) {
105
110
  const useToon = args.outputFormat !== 'json';
106
111
  // Format the output
107
112
  const content = await (0, jq_util_js_1.toOutputString)(filteredData, useToon);
108
- return { content };
113
+ return { content, rawResponsePath };
109
114
  }
110
115
  catch (error) {
111
116
  throw (0, error_handler_util_js_1.handleControllerError)(error, (0, error_handler_util_js_2.buildErrorContext)('IP Address', 'get', 'controllers/ipaddress.controller.ts@get', args.ipAddress || 'current device', { args }));
@@ -1,4 +1,11 @@
1
1
  import { IPDetail, IPApiRequestOptions } from './vendor.ip-api.com.types.js';
2
+ /**
3
+ * Service response wrapper that includes the data and the path to the raw response file
4
+ */
5
+ export interface ServiceResponse<T> {
6
+ data: T;
7
+ rawResponsePath: string | null;
8
+ }
2
9
  /**
3
10
  * @namespace VendorIpApiService
4
11
  * @description Service layer for interacting directly with the ip-api.com vendor API.
@@ -12,7 +19,7 @@ import { IPDetail, IPApiRequestOptions } from './vendor.ip-api.com.types.js';
12
19
  * @memberof VendorIpApiService
13
20
  * @param {string} [ipAddress] - Optional IP address to look up. If omitted, fetches details for the current device's public IP.
14
21
  * @param {IPApiRequestOptions} [options={}] - Optional request options for the ip-api.com service, such as `useHttps`, `fields`, and `lang`.
15
- * @returns {Promise<IPDetail>} A promise that resolves to the detailed IP information if the API call is successful.
22
+ * @returns {Promise<ServiceResponse<IPDetail>>} A promise that resolves to the detailed IP information and raw response path if the API call is successful.
16
23
  * @throws {McpError} Throws an `McpError` (specifically `ApiError` or `UnexpectedError`) if:
17
24
  * - The `fetchIpApi` call fails (network error, non-2xx response).
18
25
  * - The ip-api.com response status is not 'success'.
@@ -23,7 +30,7 @@ import { IPDetail, IPApiRequestOptions } from './vendor.ip-api.com.types.js';
23
30
  * // Get extended details using HTTPS
24
31
  * const extendedDetails = await get('1.1.1.1', { useHttps: true, fields: [...] });
25
32
  */
26
- declare function get(ipAddress?: string, options?: IPApiRequestOptions): Promise<IPDetail>;
33
+ declare function get(ipAddress?: string, options?: IPApiRequestOptions): Promise<ServiceResponse<IPDetail>>;
27
34
  declare const _default: {
28
35
  get: typeof get;
29
36
  };
@@ -22,7 +22,7 @@ serviceLogger.debug('IP API service initialized');
22
22
  * @memberof VendorIpApiService
23
23
  * @param {string} [ipAddress] - Optional IP address to look up. If omitted, fetches details for the current device's public IP.
24
24
  * @param {IPApiRequestOptions} [options={}] - Optional request options for the ip-api.com service, such as `useHttps`, `fields`, and `lang`.
25
- * @returns {Promise<IPDetail>} A promise that resolves to the detailed IP information if the API call is successful.
25
+ * @returns {Promise<ServiceResponse<IPDetail>>} A promise that resolves to the detailed IP information and raw response path if the API call is successful.
26
26
  * @throws {McpError} Throws an `McpError` (specifically `ApiError` or `UnexpectedError`) if:
27
27
  * - The `fetchIpApi` call fails (network error, non-2xx response).
28
28
  * - The ip-api.com response status is not 'success'.
@@ -39,11 +39,13 @@ async function get(ipAddress, options = {}) {
39
39
  try {
40
40
  // Make the API call with correctly typed response
41
41
  // Use a more specific type here since we know the API returns at least status + potential message
42
- const rawData = await (0, transport_util_js_1.fetchIpApi)(ipAddress || '', {
42
+ const response = await (0, transport_util_js_1.fetchIpApi)(ipAddress || '', {
43
43
  useHttps: options.useHttps,
44
44
  fields: options.fields,
45
45
  lang: options.lang,
46
46
  });
47
+ // Extract data and rawResponsePath from TransportResponse
48
+ const { data: rawData, rawResponsePath } = response;
47
49
  // First check API-level success/failure before Zod validation
48
50
  // This avoids unnecessary validation errors for known API errors
49
51
  if (rawData.status !== 'success') {
@@ -63,7 +65,7 @@ async function get(ipAddress, options = {}) {
63
65
  // Validate with Zod schema and return
64
66
  const validatedData = vendor_ip_api_com_types_js_1.IPDetailSchema.parse(rawData);
65
67
  methodLogger.debug(`Received and validated successful data from IP API`);
66
- return validatedData;
68
+ return { data: validatedData, rawResponsePath };
67
69
  }
68
70
  catch (error) {
69
71
  methodLogger.error(`Service error fetching IP data`, error);
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const logger_util_js_1 = require("../utils/logger.util.js");
7
7
  const ipaddress_types_js_1 = require("./ipaddress.types.js");
8
8
  const error_util_js_1 = require("../utils/error.util.js");
9
+ const formatter_util_js_1 = require("../utils/formatter.util.js");
9
10
  const zod_1 = require("zod");
10
11
  const ipaddress_controller_js_1 = __importDefault(require("../controllers/ipaddress.controller.js"));
11
12
  /**
@@ -35,12 +36,12 @@ async function handleGetIpDetails(args) {
35
36
  // Pass args directly to the controller
36
37
  const result = await ipaddress_controller_js_1.default.get(args);
37
38
  methodLogger.debug(`Got the response from the controller`, result);
38
- // Format the response for the MCP tool
39
+ // Format the response for the MCP tool, applying truncation for large responses
39
40
  return {
40
41
  content: [
41
42
  {
42
43
  type: 'text',
43
- text: result.content,
44
+ text: (0, formatter_util_js_1.truncateForAI)(result.content, result.rawResponsePath),
44
45
  },
45
46
  ],
46
47
  };
@@ -3,6 +3,34 @@
3
3
  * These types provide a standard interface for controller interactions.
4
4
  * Centralized here to ensure consistency across the codebase.
5
5
  */
6
+ /**
7
+ * Common pagination information for API responses.
8
+ * This is used for providing consistent pagination details to clients.
9
+ * Note: This is now only used internally by controllers.
10
+ * The formatted pagination information will be integrated into the content string.
11
+ */
12
+ export interface ResponsePagination {
13
+ /**
14
+ * Cursor for the next page of results, if available.
15
+ * This should be passed to subsequent requests to retrieve the next page.
16
+ */
17
+ nextCursor?: string;
18
+ /**
19
+ * Whether more results are available beyond the current page.
20
+ * When true, clients should use the nextCursor to retrieve more results.
21
+ */
22
+ hasMore: boolean;
23
+ /**
24
+ * The number of items in the current result set.
25
+ * This helps clients track how many items they've received.
26
+ */
27
+ count?: number;
28
+ /**
29
+ * The total number of items available.
30
+ * This helps clients understand the total scope of their results.
31
+ */
32
+ total?: number;
33
+ }
6
34
  /**
7
35
  * Common response structure for controller operations.
8
36
  * All controller methods should return this structure.
@@ -17,4 +45,9 @@ export interface ControllerResponse {
17
45
  * including pagination details and any additional metadata.
18
46
  */
19
47
  content: string;
48
+ /**
49
+ * Optional path to the raw API response file.
50
+ * When the response is truncated, this path allows AI to access the full data.
51
+ */
52
+ rawResponsePath?: string | null;
20
53
  }
@@ -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.16.0";
11
+ export declare const VERSION = "1.17.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.16.0';
14
+ exports.VERSION = '1.17.0';
15
15
  /**
16
16
  * Package name with scope
17
17
  * Used for initialization and identification
@@ -34,3 +34,15 @@ export declare function formatBulletList(items: Record<string, unknown>, keyForm
34
34
  * @returns Separator line
35
35
  */
36
36
  export declare function formatSeparator(): string;
37
+ /**
38
+ * Truncate content for AI consumption and add guidance if truncated
39
+ *
40
+ * When responses exceed the token limit, this function truncates the content
41
+ * and appends guidance for the AI to either access the full response from
42
+ * the raw log file or refine the request with better filtering.
43
+ *
44
+ * @param content - The formatted response content
45
+ * @param rawResponsePath - Optional path to the raw response file in /tmp/mcp/
46
+ * @returns Truncated content with guidance if needed, or original content if within limits
47
+ */
48
+ export declare function truncateForAI(content: string, rawResponsePath?: string | null): string;
@@ -9,6 +9,7 @@ exports.formatUrl = formatUrl;
9
9
  exports.formatHeading = formatHeading;
10
10
  exports.formatBulletList = formatBulletList;
11
11
  exports.formatSeparator = formatSeparator;
12
+ exports.truncateForAI = truncateForAI;
12
13
  /**
13
14
  * Format a date in a standardized way: YYYY-MM-DD HH:MM:SS UTC
14
15
  * @param dateString - ISO date string or Date object
@@ -114,3 +115,50 @@ function formatValue(value) {
114
115
  function formatSeparator() {
115
116
  return '---';
116
117
  }
118
+ /**
119
+ * Maximum character limit for AI responses (~10k tokens)
120
+ * 1 token ≈ 4 characters, so 10k tokens ≈ 40,000 characters
121
+ */
122
+ const MAX_RESPONSE_CHARS = 40000;
123
+ /**
124
+ * Truncate content for AI consumption and add guidance if truncated
125
+ *
126
+ * When responses exceed the token limit, this function truncates the content
127
+ * and appends guidance for the AI to either access the full response from
128
+ * the raw log file or refine the request with better filtering.
129
+ *
130
+ * @param content - The formatted response content
131
+ * @param rawResponsePath - Optional path to the raw response file in /tmp/mcp/
132
+ * @returns Truncated content with guidance if needed, or original content if within limits
133
+ */
134
+ function truncateForAI(content, rawResponsePath) {
135
+ if (content.length <= MAX_RESPONSE_CHARS) {
136
+ return content;
137
+ }
138
+ // Truncate at a reasonable boundary (try to find a newline near the limit)
139
+ let truncateAt = MAX_RESPONSE_CHARS;
140
+ const searchStart = Math.max(0, MAX_RESPONSE_CHARS - 500);
141
+ const lastNewline = content.lastIndexOf('\n', MAX_RESPONSE_CHARS);
142
+ if (lastNewline > searchStart) {
143
+ truncateAt = lastNewline;
144
+ }
145
+ const truncatedContent = content.substring(0, truncateAt);
146
+ const originalSize = content.length;
147
+ const truncatedSize = truncatedContent.length;
148
+ const percentShown = Math.round((truncatedSize / originalSize) * 100);
149
+ // Build guidance section
150
+ const guidance = [
151
+ '',
152
+ formatSeparator(),
153
+ formatHeading('Response Truncated', 2),
154
+ '',
155
+ `This response was truncated to ~${Math.round(truncatedSize / 4000)}k tokens (${percentShown}% of original ${Math.round(originalSize / 1000)}k chars).`,
156
+ '',
157
+ '**To access the complete data:**',
158
+ ];
159
+ if (rawResponsePath) {
160
+ guidance.push(`- The full raw API response is saved at: \`${rawResponsePath}\``);
161
+ }
162
+ guidance.push('- Consider refining your request with more specific filters or selecting fewer fields', '- For paginated data, use smaller page sizes or specific identifiers', '- When searching, use more targeted queries to reduce result sets');
163
+ return truncatedContent + guidance.join('\n');
164
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Save raw API response to a file in /tmp/mcp/<project-name>/
3
+ *
4
+ * @param url The URL that was called
5
+ * @param method The HTTP method used
6
+ * @param requestBody The request body (if any)
7
+ * @param responseData The raw response data
8
+ * @param statusCode The HTTP status code
9
+ * @param durationMs The request duration in milliseconds
10
+ * @returns The path to the saved file, or null if saving failed
11
+ */
12
+ export declare function saveRawResponse(url: string, method: string, requestBody: unknown, responseData: unknown, statusCode: number, durationMs: number): string | null;
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.saveRawResponse = saveRawResponse;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const crypto = __importStar(require("crypto"));
40
+ const logger_util_js_1 = require("./logger.util.js");
41
+ const constants_util_js_1 = require("./constants.util.js");
42
+ // Create a contextualized logger for this file
43
+ const responseLogger = logger_util_js_1.Logger.forContext('utils/response.util.ts');
44
+ /**
45
+ * Get the project name from PACKAGE_NAME, stripping the scope prefix
46
+ * e.g., "@aashari/boilerplate-mcp-server" -> "boilerplate-mcp-server"
47
+ */
48
+ function getProjectName() {
49
+ const name = constants_util_js_1.PACKAGE_NAME.replace(/^@[^/]+\//, '');
50
+ return name;
51
+ }
52
+ /**
53
+ * Generate a unique filename with timestamp and random string
54
+ * Format: <timestamp>-<random>.txt
55
+ */
56
+ function generateFilename() {
57
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
58
+ const randomStr = crypto.randomBytes(4).toString('hex');
59
+ return `${timestamp}-${randomStr}.txt`;
60
+ }
61
+ /**
62
+ * Ensure the directory exists, creating it if necessary
63
+ */
64
+ function ensureDirectoryExists(dirPath) {
65
+ if (!fs.existsSync(dirPath)) {
66
+ fs.mkdirSync(dirPath, { recursive: true });
67
+ responseLogger.debug(`Created directory: ${dirPath}`);
68
+ }
69
+ }
70
+ /**
71
+ * Save raw API response to a file in /tmp/mcp/<project-name>/
72
+ *
73
+ * @param url The URL that was called
74
+ * @param method The HTTP method used
75
+ * @param requestBody The request body (if any)
76
+ * @param responseData The raw response data
77
+ * @param statusCode The HTTP status code
78
+ * @param durationMs The request duration in milliseconds
79
+ * @returns The path to the saved file, or null if saving failed
80
+ */
81
+ function saveRawResponse(url, method, requestBody, responseData, statusCode, durationMs) {
82
+ const methodLogger = logger_util_js_1.Logger.forContext('utils/response.util.ts', 'saveRawResponse');
83
+ try {
84
+ const projectName = getProjectName();
85
+ const dirPath = path.join('/tmp', 'mcp', projectName);
86
+ const filename = generateFilename();
87
+ const filePath = path.join(dirPath, filename);
88
+ ensureDirectoryExists(dirPath);
89
+ // Build the content
90
+ const content = buildResponseContent(url, method, requestBody, responseData, statusCode, durationMs);
91
+ // Write to file
92
+ fs.writeFileSync(filePath, content, 'utf8');
93
+ methodLogger.debug(`Saved raw response to: ${filePath}`);
94
+ return filePath;
95
+ }
96
+ catch (error) {
97
+ methodLogger.error('Failed to save raw response', error);
98
+ return null;
99
+ }
100
+ }
101
+ /**
102
+ * Build the content string for the response file
103
+ */
104
+ function buildResponseContent(url, method, requestBody, responseData, statusCode, durationMs) {
105
+ const timestamp = new Date().toISOString();
106
+ const separator = '='.repeat(80);
107
+ let content = `${separator}
108
+ RAW API RESPONSE LOG
109
+ ${separator}
110
+
111
+ Timestamp: ${timestamp}
112
+ URL: ${url}
113
+ Method: ${method}
114
+ Status Code: ${statusCode}
115
+ Duration: ${durationMs.toFixed(2)}ms
116
+
117
+ ${separator}
118
+ REQUEST BODY
119
+ ${separator}
120
+ `;
121
+ if (requestBody) {
122
+ content +=
123
+ typeof requestBody === 'string'
124
+ ? requestBody
125
+ : JSON.stringify(requestBody, null, 2);
126
+ }
127
+ else {
128
+ content += '(no request body)';
129
+ }
130
+ content += `
131
+
132
+ ${separator}
133
+ RESPONSE DATA
134
+ ${separator}
135
+ `;
136
+ if (responseData !== undefined && responseData !== null) {
137
+ content +=
138
+ typeof responseData === 'string'
139
+ ? responseData
140
+ : JSON.stringify(responseData, null, 2);
141
+ }
142
+ else {
143
+ content += '(no response data)';
144
+ }
145
+ content += `
146
+ ${separator}
147
+ `;
148
+ return content;
149
+ }
@@ -13,3 +13,19 @@
13
13
  * const output = await toToonOrJson(data, json);
14
14
  */
15
15
  export declare function toToonOrJson(data: unknown, jsonFallback: string): Promise<string>;
16
+ /**
17
+ * Synchronous TOON conversion with JSON fallback.
18
+ *
19
+ * Uses cached encoder if available, otherwise returns JSON fallback.
20
+ * Prefer toToonOrJson for first-time conversion.
21
+ *
22
+ * @param data - The data to convert
23
+ * @param jsonFallback - The JSON string to return if TOON is unavailable
24
+ * @returns TOON formatted string, or JSON fallback
25
+ */
26
+ export declare function toToonOrJsonSync(data: unknown, jsonFallback: string): string;
27
+ /**
28
+ * Pre-load the TOON encoder for synchronous usage later.
29
+ * Call this during server initialization.
30
+ */
31
+ export declare function preloadToonEncoder(): Promise<boolean>;
@@ -1,6 +1,8 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.toToonOrJson = toToonOrJson;
4
+ exports.toToonOrJsonSync = toToonOrJsonSync;
5
+ exports.preloadToonEncoder = preloadToonEncoder;
4
6
  const logger_util_js_1 = require("./logger.util.js");
5
7
  const logger = logger_util_js_1.Logger.forContext('utils/toon.util.ts');
6
8
  /**
@@ -63,3 +65,37 @@ async function toToonOrJson(data, jsonFallback) {
63
65
  return jsonFallback;
64
66
  }
65
67
  }
68
+ /**
69
+ * Synchronous TOON conversion with JSON fallback.
70
+ *
71
+ * Uses cached encoder if available, otherwise returns JSON fallback.
72
+ * Prefer toToonOrJson for first-time conversion.
73
+ *
74
+ * @param data - The data to convert
75
+ * @param jsonFallback - The JSON string to return if TOON is unavailable
76
+ * @returns TOON formatted string, or JSON fallback
77
+ */
78
+ function toToonOrJsonSync(data, jsonFallback) {
79
+ const methodLogger = logger.forMethod('toToonOrJsonSync');
80
+ if (!toonEncode) {
81
+ methodLogger.debug('TOON encoder not loaded, using JSON fallback');
82
+ return jsonFallback;
83
+ }
84
+ try {
85
+ const toonResult = toonEncode(data, { indent: 2 });
86
+ methodLogger.debug('Successfully converted to TOON format');
87
+ return toonResult;
88
+ }
89
+ catch (error) {
90
+ methodLogger.error('TOON conversion failed, using JSON fallback', error);
91
+ return jsonFallback;
92
+ }
93
+ }
94
+ /**
95
+ * Pre-load the TOON encoder for synchronous usage later.
96
+ * Call this during server initialization.
97
+ */
98
+ async function preloadToonEncoder() {
99
+ const encode = await loadToonEncoder();
100
+ return encode !== null;
101
+ }
@@ -13,6 +13,13 @@ export interface RequestOptions {
13
13
  headers?: Record<string, string>;
14
14
  body?: unknown;
15
15
  }
16
+ /**
17
+ * Transport response wrapper that includes the data and the path to the raw response file
18
+ */
19
+ export interface TransportResponse<T> {
20
+ data: T;
21
+ rawResponsePath: string | null;
22
+ }
16
23
  /**
17
24
  * Retrieves IP API credentials from configuration.
18
25
  * Specifically checks for IPAPI_API_TOKEN.
@@ -29,21 +36,21 @@ export declare function getIpApiCredentials(): IpApiCredentials;
29
36
  * @param options.useHttps - Use HTTPS (requires paid plan for ip-api.com). Defaults to false.
30
37
  * @param options.fields - Specific fields to request from ip-api.com.
31
38
  * @param options.lang - Language code for response data.
32
- * @returns The response data parsed as type T.
39
+ * @returns Transport response with data and raw response path.
33
40
  * @throws {McpError} If the request fails, including network errors, API errors, or parsing issues.
34
41
  */
35
42
  export declare function fetchIpApi<T>(path: string, options?: RequestOptions & {
36
43
  useHttps?: boolean;
37
44
  fields?: string[];
38
45
  lang?: string;
39
- }): Promise<T>;
46
+ }): Promise<TransportResponse<T>>;
40
47
  /**
41
48
  * Generic and reusable function to fetch data from any API endpoint.
42
49
  * Handles standard HTTP request setup, response checking, basic error handling, and logging.
43
50
  *
44
51
  * @param url The full URL to fetch data from.
45
52
  * @param options Request options including method, headers, and body.
46
- * @returns The response data parsed as type T.
53
+ * @returns Transport response with data and raw response path.
47
54
  * @throws {McpError} If the request fails, including network errors, non-OK HTTP status, or JSON parsing issues.
48
55
  */
49
- export declare function fetchApi<T>(url: string, options?: RequestOptions): Promise<T>;
56
+ export declare function fetchApi<T>(url: string, options?: RequestOptions): Promise<TransportResponse<T>>;
@@ -6,6 +6,7 @@ exports.fetchApi = fetchApi;
6
6
  const logger_util_js_1 = require("./logger.util.js");
7
7
  const config_util_js_1 = require("./config.util.js");
8
8
  const error_util_js_1 = require("./error.util.js");
9
+ const response_util_js_1 = require("./response.util.js");
9
10
  // Create a contextualized logger for this file
10
11
  const transportLogger = logger_util_js_1.Logger.forContext('utils/transport.util.ts');
11
12
  // Log transport utility initialization
@@ -37,7 +38,7 @@ function getIpApiCredentials() {
37
38
  * @param options.useHttps - Use HTTPS (requires paid plan for ip-api.com). Defaults to false.
38
39
  * @param options.fields - Specific fields to request from ip-api.com.
39
40
  * @param options.lang - Language code for response data.
40
- * @returns The response data parsed as type T.
41
+ * @returns Transport response with data and raw response path.
41
42
  * @throws {McpError} If the request fails, including network errors, API errors, or parsing issues.
42
43
  */
43
44
  async function fetchIpApi(path, options = {}) {
@@ -86,7 +87,7 @@ async function fetchIpApi(path, options = {}) {
86
87
  *
87
88
  * @param url The full URL to fetch data from.
88
89
  * @param options Request options including method, headers, and body.
89
- * @returns The response data parsed as type T.
90
+ * @returns Transport response with data and raw response path.
90
91
  * @throws {McpError} If the request fails, including network errors, non-OK HTTP status, or JSON parsing issues.
91
92
  */
92
93
  async function fetchApi(url, options = {}) {
@@ -134,7 +135,9 @@ async function fetchApi(url, options = {}) {
134
135
  try {
135
136
  const responseData = await response.json();
136
137
  methodLogger.debug('Response body successfully parsed as JSON.');
137
- return responseData;
138
+ // Save raw response to file and capture the path
139
+ const rawResponsePath = (0, response_util_js_1.saveRawResponse)(url, requestOptions.method || 'GET', options.body, responseData, response.status, parseFloat(duration));
140
+ return { data: responseData, rawResponsePath };
138
141
  }
139
142
  catch (parseError) {
140
143
  methodLogger.error('Failed to parse API response JSON:', parseError);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aashari/boilerplate-mcp-server",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "TypeScript MCP server boilerplate with STDIO and HTTP transport support, CLI tools, and extensible architecture",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -19,15 +19,23 @@
19
19
  "clean": "rm -rf dist coverage",
20
20
  "test": "jest",
21
21
  "test:coverage": "jest --coverage",
22
+ "test:cli": "jest src/cli/.*\\.cli\\.test\\.ts --runInBand --testTimeout=60000",
22
23
  "lint": "eslint src --ext .ts --config eslint.config.mjs",
23
- "update:deps": "npx npm-check-updates -u && npm install --legacy-peer-deps",
24
24
  "format": "prettier --write 'src/**/*.ts' 'scripts/**/*.js'",
25
- "cli": "npm run build && node dist/index.js",
26
- "mcp:stdio": "npm run build && TRANSPORT_MODE=stdio node dist/index.js",
27
- "mcp:http": "npm run build && TRANSPORT_MODE=http node dist/index.js",
28
- "mcp:inspect": "npm run build && (TRANSPORT_MODE=http node dist/index.js &) && sleep 2 && npx @modelcontextprotocol/inspector http://localhost:3000/mcp",
25
+ "publish:npm": "npm publish",
26
+ "update:check": "npx npm-check-updates",
27
+ "update:deps": "npx npm-check-updates -u && npm install --legacy-peer-deps",
28
+ "update:version": "node scripts/update-version.js",
29
+ "mcp:stdio": "TRANSPORT_MODE=stdio npm run build && node dist/index.js",
30
+ "mcp:http": "TRANSPORT_MODE=http npm run build && node dist/index.js",
31
+ "mcp:inspect": "TRANSPORT_MODE=http npm run build && (node dist/index.js &) && sleep 2 && npx @modelcontextprotocol/inspector http://localhost:3000/mcp",
29
32
  "dev:stdio": "npm run build && npx @modelcontextprotocol/inspector -e TRANSPORT_MODE=stdio -e DEBUG=true node dist/index.js",
30
- "dev:http": "npm run build && DEBUG=true TRANSPORT_MODE=http node dist/index.js"
33
+ "dev:http": "DEBUG=true TRANSPORT_MODE=http npm run build && node dist/index.js",
34
+ "dev:server": "DEBUG=true npm run build && npx @modelcontextprotocol/inspector -e DEBUG=true node dist/index.js",
35
+ "dev:cli": "DEBUG=true npm run build && DEBUG=true node dist/index.js",
36
+ "start:server": "npm run build && npx @modelcontextprotocol/inspector node dist/index.js",
37
+ "start:cli": "npm run build && node dist/index.js",
38
+ "cli": "npm run build && node dist/index.js"
31
39
  },
32
40
  "keywords": [
33
41
  "mcp",
@@ -66,11 +74,15 @@
66
74
  "@typescript-eslint/parser": "^8.48.0",
67
75
  "eslint": "^9.39.1",
68
76
  "eslint-config-prettier": "^10.1.8",
77
+ "eslint-plugin-filenames": "^1.3.2",
69
78
  "eslint-plugin-prettier": "^5.5.4",
70
79
  "jest": "^30.2.0",
80
+ "nodemon": "^3.1.11",
81
+ "npm-check-updates": "^19.1.2",
71
82
  "prettier": "^3.7.3",
72
83
  "semantic-release": "^25.0.2",
73
84
  "ts-jest": "^29.4.5",
85
+ "ts-node": "^10.9.2",
74
86
  "typescript": "^5.9.3",
75
87
  "typescript-eslint": "^8.48.0"
76
88
  },
@@ -100,7 +112,17 @@
100
112
  "collectCoverageFrom": [
101
113
  "src/**/*.ts",
102
114
  "!src/**/*.test.ts",
103
- "!src/utils/jest.setup.ts"
115
+ "!src/**/*.spec.ts"
116
+ ],
117
+ "coveragePathIgnorePatterns": [
118
+ "/node_modules/",
119
+ "/dist/",
120
+ "/coverage/"
121
+ ],
122
+ "coverageReporters": [
123
+ "text",
124
+ "lcov",
125
+ "json-summary"
104
126
  ],
105
127
  "transform": {
106
128
  "^.+\\.tsx?$": [
@@ -115,6 +137,14 @@
115
137
  },
116
138
  "extensionsToTreatAsEsm": [
117
139
  ".ts"
140
+ ],
141
+ "moduleFileExtensions": [
142
+ "ts",
143
+ "tsx",
144
+ "js",
145
+ "jsx",
146
+ "json",
147
+ "node"
118
148
  ]
119
149
  }
120
150
  }
package/package.json.bak CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aashari/boilerplate-mcp-server",
3
- "version": "1.15.0",
3
+ "version": "1.16.1",
4
4
  "description": "TypeScript MCP server boilerplate with STDIO and HTTP transport support, CLI tools, and extensible architecture",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -19,15 +19,23 @@
19
19
  "clean": "rm -rf dist coverage",
20
20
  "test": "jest",
21
21
  "test:coverage": "jest --coverage",
22
+ "test:cli": "jest src/cli/.*\\.cli\\.test\\.ts --runInBand --testTimeout=60000",
22
23
  "lint": "eslint src --ext .ts --config eslint.config.mjs",
23
- "update:deps": "npx npm-check-updates -u && npm install --legacy-peer-deps",
24
24
  "format": "prettier --write 'src/**/*.ts' 'scripts/**/*.js'",
25
- "cli": "npm run build && node dist/index.js",
26
- "mcp:stdio": "npm run build && TRANSPORT_MODE=stdio node dist/index.js",
27
- "mcp:http": "npm run build && TRANSPORT_MODE=http node dist/index.js",
28
- "mcp:inspect": "npm run build && (TRANSPORT_MODE=http node dist/index.js &) && sleep 2 && npx @modelcontextprotocol/inspector http://localhost:3000/mcp",
25
+ "publish:npm": "npm publish",
26
+ "update:check": "npx npm-check-updates",
27
+ "update:deps": "npx npm-check-updates -u && npm install --legacy-peer-deps",
28
+ "update:version": "node scripts/update-version.js",
29
+ "mcp:stdio": "TRANSPORT_MODE=stdio npm run build && node dist/index.js",
30
+ "mcp:http": "TRANSPORT_MODE=http npm run build && node dist/index.js",
31
+ "mcp:inspect": "TRANSPORT_MODE=http npm run build && (node dist/index.js &) && sleep 2 && npx @modelcontextprotocol/inspector http://localhost:3000/mcp",
29
32
  "dev:stdio": "npm run build && npx @modelcontextprotocol/inspector -e TRANSPORT_MODE=stdio -e DEBUG=true node dist/index.js",
30
- "dev:http": "npm run build && DEBUG=true TRANSPORT_MODE=http node dist/index.js"
33
+ "dev:http": "DEBUG=true TRANSPORT_MODE=http npm run build && node dist/index.js",
34
+ "dev:server": "DEBUG=true npm run build && npx @modelcontextprotocol/inspector -e DEBUG=true node dist/index.js",
35
+ "dev:cli": "DEBUG=true npm run build && DEBUG=true node dist/index.js",
36
+ "start:server": "npm run build && npx @modelcontextprotocol/inspector node dist/index.js",
37
+ "start:cli": "npm run build && node dist/index.js",
38
+ "cli": "npm run build && node dist/index.js"
31
39
  },
32
40
  "keywords": [
33
41
  "mcp",
@@ -66,11 +74,15 @@
66
74
  "@typescript-eslint/parser": "^8.48.0",
67
75
  "eslint": "^9.39.1",
68
76
  "eslint-config-prettier": "^10.1.8",
77
+ "eslint-plugin-filenames": "^1.3.2",
69
78
  "eslint-plugin-prettier": "^5.5.4",
70
79
  "jest": "^30.2.0",
80
+ "nodemon": "^3.1.11",
81
+ "npm-check-updates": "^19.1.2",
71
82
  "prettier": "^3.7.3",
72
83
  "semantic-release": "^25.0.2",
73
84
  "ts-jest": "^29.4.5",
85
+ "ts-node": "^10.9.2",
74
86
  "typescript": "^5.9.3",
75
87
  "typescript-eslint": "^8.48.0"
76
88
  },
@@ -100,7 +112,17 @@
100
112
  "collectCoverageFrom": [
101
113
  "src/**/*.ts",
102
114
  "!src/**/*.test.ts",
103
- "!src/utils/jest.setup.ts"
115
+ "!src/**/*.spec.ts"
116
+ ],
117
+ "coveragePathIgnorePatterns": [
118
+ "/node_modules/",
119
+ "/dist/",
120
+ "/coverage/"
121
+ ],
122
+ "coverageReporters": [
123
+ "text",
124
+ "lcov",
125
+ "json-summary"
104
126
  ],
105
127
  "transform": {
106
128
  "^.+\\.tsx?$": [
@@ -115,6 +137,14 @@
115
137
  },
116
138
  "extensionsToTreatAsEsm": [
117
139
  ".ts"
140
+ ],
141
+ "moduleFileExtensions": [
142
+ "ts",
143
+ "tsx",
144
+ "js",
145
+ "jsx",
146
+ "json",
147
+ "node"
118
148
  ]
119
149
  }
120
150
  }