@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 +19 -0
- package/dist/controllers/ipaddress.controller.d.ts +1 -6
- package/dist/controllers/ipaddress.controller.js +8 -3
- package/dist/services/vendor.ip-api.com.service.d.ts +9 -2
- package/dist/services/vendor.ip-api.com.service.js +5 -3
- package/dist/tools/ipaddress.tool.js +3 -2
- package/dist/types/common.types.d.ts +33 -0
- package/dist/utils/constants.util.d.ts +1 -1
- package/dist/utils/constants.util.js +1 -1
- package/dist/utils/formatter.util.d.ts +12 -0
- package/dist/utils/formatter.util.js +48 -0
- package/dist/utils/response.util.d.ts +12 -0
- package/dist/utils/response.util.js +149 -0
- package/dist/utils/toon.util.d.ts +16 -0
- package/dist/utils/toon.util.js +36 -0
- package/dist/utils/transport.util.d.ts +11 -4
- package/dist/utils/transport.util.js +6 -3
- package/package.json +38 -8
- package/package.json.bak +38 -8
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
}
|
|
@@ -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.
|
|
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>;
|
package/dist/utils/toon.util.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
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 &&
|
|
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
|
|
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.
|
|
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
|
-
"
|
|
26
|
-
"
|
|
27
|
-
"
|
|
28
|
-
"
|
|
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 &&
|
|
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
|
|
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
|
}
|