latitude-mcp-server 1.0.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.
Files changed (51) hide show
  1. package/.releaserc.json +34 -0
  2. package/README.md +687 -0
  3. package/dist/cli/index.d.ts +7 -0
  4. package/dist/cli/index.js +43 -0
  5. package/dist/cli/latitude.cli.d.ts +10 -0
  6. package/dist/cli/latitude.cli.js +286 -0
  7. package/dist/controllers/latitude.controller.d.ts +115 -0
  8. package/dist/controllers/latitude.controller.js +287 -0
  9. package/dist/index.d.ts +6 -0
  10. package/dist/index.js +166 -0
  11. package/dist/resources/latitude.resource.d.ts +12 -0
  12. package/dist/resources/latitude.resource.js +145 -0
  13. package/dist/services/vendor.latitude.service.d.ts +49 -0
  14. package/dist/services/vendor.latitude.service.js +294 -0
  15. package/dist/tools/latitude.tool.d.ts +6 -0
  16. package/dist/tools/latitude.tool.js +517 -0
  17. package/dist/types/common.types.d.ts +20 -0
  18. package/dist/types/common.types.js +7 -0
  19. package/dist/types/latitude.types.d.ts +487 -0
  20. package/dist/types/latitude.types.js +311 -0
  21. package/dist/utils/cli.test.util.d.ts +34 -0
  22. package/dist/utils/cli.test.util.js +143 -0
  23. package/dist/utils/config.util.d.ts +43 -0
  24. package/dist/utils/config.util.js +145 -0
  25. package/dist/utils/config.util.test.d.ts +1 -0
  26. package/dist/utils/constants.util.d.ts +26 -0
  27. package/dist/utils/constants.util.js +29 -0
  28. package/dist/utils/error-handler.util.d.ts +54 -0
  29. package/dist/utils/error-handler.util.js +202 -0
  30. package/dist/utils/error-handler.util.test.d.ts +1 -0
  31. package/dist/utils/error.util.d.ts +73 -0
  32. package/dist/utils/error.util.js +174 -0
  33. package/dist/utils/error.util.test.d.ts +1 -0
  34. package/dist/utils/formatter.util.d.ts +36 -0
  35. package/dist/utils/formatter.util.js +116 -0
  36. package/dist/utils/jest.setup.d.ts +5 -0
  37. package/dist/utils/jest.setup.js +36 -0
  38. package/dist/utils/jq.util.d.ts +34 -0
  39. package/dist/utils/jq.util.js +87 -0
  40. package/dist/utils/logger.util.d.ts +78 -0
  41. package/dist/utils/logger.util.js +344 -0
  42. package/dist/utils/toon.util.d.ts +15 -0
  43. package/dist/utils/toon.util.js +65 -0
  44. package/dist/utils/transport.util.d.ts +49 -0
  45. package/dist/utils/transport.util.js +162 -0
  46. package/eslint.config.mjs +46 -0
  47. package/openapi.json +12592 -0
  48. package/package.json +118 -0
  49. package/scripts/ensure-executable.js +38 -0
  50. package/scripts/package.json +3 -0
  51. package/scripts/update-version.js +204 -0
@@ -0,0 +1,344 @@
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.Logger = void 0;
37
+ const fs = __importStar(require("fs"));
38
+ const path = __importStar(require("path"));
39
+ const os = __importStar(require("os"));
40
+ const crypto = __importStar(require("crypto"));
41
+ /**
42
+ * Format a timestamp for logging
43
+ * @returns Formatted timestamp [HH:MM:SS]
44
+ */
45
+ function getTimestamp() {
46
+ const now = new Date();
47
+ return `[${now.toISOString().split('T')[1].split('.')[0]}]`;
48
+ }
49
+ /**
50
+ * Safely convert object to string with size limits
51
+ * @param obj Object to stringify
52
+ * @param maxLength Maximum length of the resulting string
53
+ * @returns Safely stringified object
54
+ */
55
+ function safeStringify(obj, maxLength = 1000) {
56
+ try {
57
+ const str = JSON.stringify(obj);
58
+ if (str.length <= maxLength) {
59
+ return str;
60
+ }
61
+ return `${str.substring(0, maxLength)}... (truncated, ${str.length} chars total)`;
62
+ }
63
+ catch {
64
+ return '[Object cannot be stringified]';
65
+ }
66
+ }
67
+ /**
68
+ * Extract essential values from larger objects for logging
69
+ * @param obj The object to extract values from
70
+ * @param keys Keys to extract (if available)
71
+ * @returns Object containing only the specified keys
72
+ */
73
+ function extractEssentialValues(obj, keys) {
74
+ const result = {};
75
+ keys.forEach((key) => {
76
+ if (Object.prototype.hasOwnProperty.call(obj, key)) {
77
+ result[key] = obj[key];
78
+ }
79
+ });
80
+ return result;
81
+ }
82
+ /**
83
+ * Format source path consistently using the standardized format:
84
+ * [module/file.ts@function] or [module/file.ts]
85
+ *
86
+ * @param filePath File path (with or without src/ prefix)
87
+ * @param functionName Optional function name
88
+ * @returns Formatted source path according to standard pattern
89
+ */
90
+ function formatSourcePath(filePath, functionName) {
91
+ // Always strip 'src/' prefix for consistency
92
+ const normalizedPath = filePath.replace(/^src\//, '');
93
+ return functionName
94
+ ? `[${normalizedPath}@${functionName}]`
95
+ : `[${normalizedPath}]`;
96
+ }
97
+ /**
98
+ * Check if debug logging is enabled for a specific module
99
+ *
100
+ * This function parses the DEBUG environment variable to determine if a specific
101
+ * module should have debug logging enabled. The DEBUG variable can be:
102
+ * - 'true' or '1': Enable all debug logging
103
+ * - Comma-separated list of modules: Enable debug only for those modules
104
+ * - Module patterns with wildcards: e.g., 'controllers/*' enables all controllers
105
+ *
106
+ * Examples:
107
+ * - DEBUG=true
108
+ * - DEBUG=controllers/*,services/aws.sso.auth.service.ts
109
+ * - DEBUG=transport,utils/formatter*
110
+ *
111
+ * @param modulePath The module path to check against DEBUG patterns
112
+ * @returns true if debug is enabled for this module, false otherwise
113
+ */
114
+ function isDebugEnabledForModule(modulePath) {
115
+ const debugEnv = process.env.DEBUG;
116
+ if (!debugEnv) {
117
+ return false;
118
+ }
119
+ // If debug is set to true or 1, enable all debug logging
120
+ if (debugEnv === 'true' || debugEnv === '1') {
121
+ return true;
122
+ }
123
+ // Parse comma-separated debug patterns
124
+ const debugPatterns = debugEnv.split(',').map((p) => p.trim());
125
+ // Check if the module matches any pattern
126
+ return debugPatterns.some((pattern) => {
127
+ // Convert glob-like patterns to regex
128
+ // * matches anything within a path segment
129
+ // ** matches across path segments
130
+ const regexPattern = pattern
131
+ .replace(/\*/g, '.*') // Convert * to regex .*
132
+ .replace(/\?/g, '.'); // Convert ? to regex .
133
+ const regex = new RegExp(`^${regexPattern}$`);
134
+ return (regex.test(modulePath) ||
135
+ // Check for pattern matches without the 'src/' prefix
136
+ regex.test(modulePath.replace(/^src\//, '')));
137
+ });
138
+ }
139
+ // Generate a unique session ID for this process
140
+ const SESSION_ID = crypto.randomUUID();
141
+ // Get the package name from environment variables or default to 'mcp-server'
142
+ const getPkgName = () => {
143
+ try {
144
+ // Try to get it from package.json first if available
145
+ const packageJsonPath = path.resolve(process.cwd(), 'package.json');
146
+ if (fs.existsSync(packageJsonPath)) {
147
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
148
+ if (packageJson.name) {
149
+ // Extract the last part of the name if it's scoped
150
+ const match = packageJson.name.match(/(@[\w-]+\/)?(.+)/);
151
+ return match ? match[2] : packageJson.name;
152
+ }
153
+ }
154
+ }
155
+ catch {
156
+ // Silently fail and use default
157
+ }
158
+ // Fallback to environment variable or default
159
+ return process.env.PACKAGE_NAME || 'mcp-server';
160
+ };
161
+ // MCP logs directory setup
162
+ const HOME_DIR = os.homedir();
163
+ const MCP_DATA_DIR = path.join(HOME_DIR, '.mcp', 'data');
164
+ const CLI_NAME = getPkgName();
165
+ // Ensure the MCP data directory exists
166
+ if (!fs.existsSync(MCP_DATA_DIR)) {
167
+ fs.mkdirSync(MCP_DATA_DIR, { recursive: true });
168
+ }
169
+ // Create the log file path with session ID
170
+ const LOG_FILENAME = `${CLI_NAME}.${SESSION_ID}.log`;
171
+ const LOG_FILEPATH = path.join(MCP_DATA_DIR, LOG_FILENAME);
172
+ // Write initial log header
173
+ fs.writeFileSync(LOG_FILEPATH, `# ${CLI_NAME} Log Session\n` +
174
+ `Session ID: ${SESSION_ID}\n` +
175
+ `Started: ${new Date().toISOString()}\n` +
176
+ `Process ID: ${process.pid}\n` +
177
+ `Working Directory: ${process.cwd()}\n` +
178
+ `Command: ${process.argv.join(' ')}\n\n` +
179
+ `## Log Entries\n\n`, 'utf8');
180
+ // Logger singleton to track initialization
181
+ let isLoggerInitialized = false;
182
+ /**
183
+ * Logger class for consistent logging across the application.
184
+ *
185
+ * RECOMMENDED USAGE:
186
+ *
187
+ * 1. Create a file-level logger using the static forContext method:
188
+ * ```
189
+ * const logger = Logger.forContext('controllers/myController.ts');
190
+ * ```
191
+ *
192
+ * 2. For method-specific logging, create a method logger:
193
+ * ```
194
+ * const methodLogger = Logger.forContext('controllers/myController.ts', 'myMethod');
195
+ * ```
196
+ *
197
+ * 3. Avoid using raw string prefixes in log messages. Instead, use contextualized loggers.
198
+ *
199
+ * 4. For debugging objects, use the debugResponse method to log only essential properties.
200
+ *
201
+ * 5. Set DEBUG environment variable to control which modules show debug logs:
202
+ * - DEBUG=true (enable all debug logs)
203
+ * - DEBUG=controllers/*,services/* (enable for specific module groups)
204
+ * - DEBUG=transport,utils/formatter* (enable specific modules, supports wildcards)
205
+ */
206
+ class Logger {
207
+ constructor(context, modulePath = '') {
208
+ this.context = context;
209
+ this.modulePath = modulePath;
210
+ // Log initialization message only once
211
+ if (!isLoggerInitialized) {
212
+ this.info(`Logger initialized with session ID: ${Logger.sessionId}`);
213
+ this.info(`Logs will be saved to: ${Logger.logFilePath}`);
214
+ isLoggerInitialized = true;
215
+ }
216
+ }
217
+ /**
218
+ * Create a contextualized logger for a specific file or component.
219
+ * This is the preferred method for creating loggers.
220
+ *
221
+ * @param filePath The file path (e.g., 'controllers/aws.sso.auth.controller.ts')
222
+ * @param functionName Optional function name for more specific context
223
+ * @returns A new Logger instance with the specified context
224
+ *
225
+ * @example
226
+ * // File-level logger
227
+ * const logger = Logger.forContext('controllers/myController.ts');
228
+ *
229
+ * // Method-level logger
230
+ * const methodLogger = Logger.forContext('controllers/myController.ts', 'myMethod');
231
+ */
232
+ static forContext(filePath, functionName) {
233
+ return new Logger(formatSourcePath(filePath, functionName), filePath);
234
+ }
235
+ /**
236
+ * Create a method level logger from a context logger
237
+ * @param method Method name
238
+ * @returns A new logger with the method context
239
+ */
240
+ forMethod(method) {
241
+ return Logger.forContext(this.modulePath, method);
242
+ }
243
+ _formatMessage(message) {
244
+ return this.context ? `${this.context} ${message}` : message;
245
+ }
246
+ _formatArgs(args) {
247
+ // If the first argument is an object and not an Error, safely stringify it
248
+ if (args.length > 0 &&
249
+ typeof args[0] === 'object' &&
250
+ args[0] !== null &&
251
+ !(args[0] instanceof Error)) {
252
+ args[0] = safeStringify(args[0]);
253
+ }
254
+ return args;
255
+ }
256
+ _log(level, message, ...args) {
257
+ // Skip debug messages if not enabled for this module
258
+ if (level === 'debug' && !isDebugEnabledForModule(this.modulePath)) {
259
+ return;
260
+ }
261
+ const timestamp = getTimestamp();
262
+ const prefix = `${timestamp} [${level.toUpperCase()}]`;
263
+ let logMessage = `${prefix} ${this._formatMessage(message)}`;
264
+ const formattedArgs = this._formatArgs(args);
265
+ if (formattedArgs.length > 0) {
266
+ // Handle errors specifically
267
+ if (formattedArgs[0] instanceof Error) {
268
+ const error = formattedArgs[0];
269
+ logMessage += ` Error: ${error.message}`;
270
+ if (error.stack) {
271
+ logMessage += `\n${error.stack}`;
272
+ }
273
+ // If there are more args, add them after the error
274
+ if (formattedArgs.length > 1) {
275
+ logMessage += ` ${formattedArgs
276
+ .slice(1)
277
+ .map((arg) => typeof arg === 'string' ? arg : safeStringify(arg))
278
+ .join(' ')}`;
279
+ }
280
+ }
281
+ else {
282
+ logMessage += ` ${formattedArgs
283
+ .map((arg) => typeof arg === 'string' ? arg : safeStringify(arg))
284
+ .join(' ')}`;
285
+ }
286
+ }
287
+ // Write to log file
288
+ try {
289
+ fs.appendFileSync(Logger.logFilePath, `${logMessage}\n`, 'utf8');
290
+ }
291
+ catch (err) {
292
+ // If we can't write to the log file, log the error to console
293
+ console.error(`Failed to write to log file: ${err}`);
294
+ }
295
+ // Only output to console if not in test environment or if debug is enabled
296
+ if (process.env.NODE_ENV !== 'test' || process.env.DEBUG === 'true') {
297
+ if (process.env.NODE_ENV === 'test') {
298
+ console[level](logMessage);
299
+ }
300
+ else {
301
+ console.error(logMessage);
302
+ }
303
+ }
304
+ }
305
+ info(message, ...args) {
306
+ this._log('info', message, ...args);
307
+ }
308
+ warn(message, ...args) {
309
+ this._log('warn', message, ...args);
310
+ }
311
+ error(message, ...args) {
312
+ this._log('error', message, ...args);
313
+ }
314
+ debug(message, ...args) {
315
+ this._log('debug', message, ...args);
316
+ }
317
+ /**
318
+ * Log essential information about an API response
319
+ * @param message Log message
320
+ * @param response API response object
321
+ * @param essentialKeys Keys to extract from the response
322
+ */
323
+ debugResponse(message, response, essentialKeys) {
324
+ const essentialInfo = extractEssentialValues(response, essentialKeys);
325
+ this.debug(message, essentialInfo);
326
+ }
327
+ /**
328
+ * Get the current session ID
329
+ * @returns The UUID for the current logging session
330
+ */
331
+ static getSessionId() {
332
+ return Logger.sessionId;
333
+ }
334
+ /**
335
+ * Get the current log file path
336
+ * @returns The path to the current log file
337
+ */
338
+ static getLogFilePath() {
339
+ return Logger.logFilePath;
340
+ }
341
+ }
342
+ exports.Logger = Logger;
343
+ Logger.sessionId = SESSION_ID;
344
+ Logger.logFilePath = LOG_FILEPATH;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Convert data to TOON format with JSON fallback.
3
+ *
4
+ * TOON (Token-Oriented Object Notation) is 30-60% more token-efficient than JSON
5
+ * for tabular data, making it ideal for LLM responses.
6
+ *
7
+ * @param data - The data to convert
8
+ * @param jsonFallback - JSON string to return if TOON encoding fails
9
+ * @returns TOON formatted string, or JSON fallback on failure
10
+ *
11
+ * @example
12
+ * const json = JSON.stringify(data, null, 2);
13
+ * const output = await toToonOrJson(data, json);
14
+ */
15
+ export declare function toToonOrJson(data: unknown, jsonFallback: string): Promise<string>;
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toToonOrJson = toToonOrJson;
4
+ const logger_util_js_1 = require("./logger.util.js");
5
+ const logger = logger_util_js_1.Logger.forContext('utils/toon.util.ts');
6
+ /**
7
+ * Cached TOON encoder to avoid repeated dynamic imports
8
+ */
9
+ let toonEncode = null;
10
+ /**
11
+ * Dynamically loads the TOON encoder module.
12
+ * Uses dynamic import because @toon-format/toon is an ESM-only package.
13
+ *
14
+ * @returns Promise resolving to the encode function or null if loading fails
15
+ */
16
+ async function loadToonEncoder() {
17
+ const methodLogger = logger.forMethod('loadToonEncoder');
18
+ // Return cached encoder if available
19
+ if (toonEncode) {
20
+ return toonEncode;
21
+ }
22
+ try {
23
+ methodLogger.debug('Loading TOON encoder module...');
24
+ // Dynamic import for ESM module in CommonJS project
25
+ const toon = await import('@toon-format/toon');
26
+ toonEncode = toon.encode;
27
+ methodLogger.debug('TOON encoder loaded successfully');
28
+ return toonEncode;
29
+ }
30
+ catch (error) {
31
+ methodLogger.error('Failed to load TOON encoder', error);
32
+ return null;
33
+ }
34
+ }
35
+ /**
36
+ * Convert data to TOON format with JSON fallback.
37
+ *
38
+ * TOON (Token-Oriented Object Notation) is 30-60% more token-efficient than JSON
39
+ * for tabular data, making it ideal for LLM responses.
40
+ *
41
+ * @param data - The data to convert
42
+ * @param jsonFallback - JSON string to return if TOON encoding fails
43
+ * @returns TOON formatted string, or JSON fallback on failure
44
+ *
45
+ * @example
46
+ * const json = JSON.stringify(data, null, 2);
47
+ * const output = await toToonOrJson(data, json);
48
+ */
49
+ async function toToonOrJson(data, jsonFallback) {
50
+ const methodLogger = logger.forMethod('toToonOrJson');
51
+ try {
52
+ const encode = await loadToonEncoder();
53
+ if (!encode) {
54
+ methodLogger.debug('TOON encoder not available, using JSON fallback');
55
+ return jsonFallback;
56
+ }
57
+ const result = encode(data, { indent: 2 });
58
+ methodLogger.debug('Successfully converted to TOON format');
59
+ return result;
60
+ }
61
+ catch (error) {
62
+ methodLogger.error('TOON encoding failed, using JSON fallback', error);
63
+ return jsonFallback;
64
+ }
65
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Interface for IP API credentials.
3
+ * Note: API token is optional for the free tier.
4
+ */
5
+ export interface IpApiCredentials {
6
+ apiToken?: string;
7
+ }
8
+ /**
9
+ * Interface for HTTP request options
10
+ */
11
+ export interface RequestOptions {
12
+ method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
13
+ headers?: Record<string, string>;
14
+ body?: unknown;
15
+ }
16
+ /**
17
+ * Retrieves IP API credentials from configuration.
18
+ * Specifically checks for IPAPI_API_TOKEN.
19
+ * @returns IpApiCredentials object containing the API token if found.
20
+ */
21
+ export declare function getIpApiCredentials(): IpApiCredentials;
22
+ /**
23
+ * Fetches data specifically from the ip-api.com endpoint.
24
+ * Handles URL construction, authentication (if token provided), and query parameters.
25
+ * Relies on the generic fetchApi function for the actual HTTP request.
26
+ *
27
+ * @param path The specific IP address or path component (e.g., "8.8.8.8"). Empty string for current IP.
28
+ * @param options Additional options like HTTP method, headers, body, and ip-api specific params.
29
+ * @param options.useHttps - Use HTTPS (requires paid plan for ip-api.com). Defaults to false.
30
+ * @param options.fields - Specific fields to request from ip-api.com.
31
+ * @param options.lang - Language code for response data.
32
+ * @returns The response data parsed as type T.
33
+ * @throws {McpError} If the request fails, including network errors, API errors, or parsing issues.
34
+ */
35
+ export declare function fetchIpApi<T>(path: string, options?: RequestOptions & {
36
+ useHttps?: boolean;
37
+ fields?: string[];
38
+ lang?: string;
39
+ }): Promise<T>;
40
+ /**
41
+ * Generic and reusable function to fetch data from any API endpoint.
42
+ * Handles standard HTTP request setup, response checking, basic error handling, and logging.
43
+ *
44
+ * @param url The full URL to fetch data from.
45
+ * @param options Request options including method, headers, and body.
46
+ * @returns The response data parsed as type T.
47
+ * @throws {McpError} If the request fails, including network errors, non-OK HTTP status, or JSON parsing issues.
48
+ */
49
+ export declare function fetchApi<T>(url: string, options?: RequestOptions): Promise<T>;
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getIpApiCredentials = getIpApiCredentials;
4
+ exports.fetchIpApi = fetchIpApi;
5
+ exports.fetchApi = fetchApi;
6
+ const logger_util_js_1 = require("./logger.util.js");
7
+ const config_util_js_1 = require("./config.util.js");
8
+ const error_util_js_1 = require("./error.util.js");
9
+ // Create a contextualized logger for this file
10
+ const transportLogger = logger_util_js_1.Logger.forContext('utils/transport.util.ts');
11
+ // Log transport utility initialization
12
+ transportLogger.debug('Transport utility initialized');
13
+ /**
14
+ * Retrieves IP API credentials from configuration.
15
+ * Specifically checks for IPAPI_API_TOKEN.
16
+ * @returns IpApiCredentials object containing the API token if found.
17
+ */
18
+ function getIpApiCredentials() {
19
+ const methodLogger = logger_util_js_1.Logger.forContext('utils/transport.util.ts', 'getIpApiCredentials');
20
+ const apiToken = config_util_js_1.config.get('IPAPI_API_TOKEN');
21
+ if (!apiToken) {
22
+ methodLogger.debug('No IP API token found (IPAPI_API_TOKEN). Using free tier.');
23
+ return {}; // Return empty object if no token
24
+ }
25
+ else {
26
+ methodLogger.debug('Using IP API token from configuration.');
27
+ return { apiToken };
28
+ }
29
+ }
30
+ /**
31
+ * Fetches data specifically from the ip-api.com endpoint.
32
+ * Handles URL construction, authentication (if token provided), and query parameters.
33
+ * Relies on the generic fetchApi function for the actual HTTP request.
34
+ *
35
+ * @param path The specific IP address or path component (e.g., "8.8.8.8"). Empty string for current IP.
36
+ * @param options Additional options like HTTP method, headers, body, and ip-api specific params.
37
+ * @param options.useHttps - Use HTTPS (requires paid plan for ip-api.com). Defaults to false.
38
+ * @param options.fields - Specific fields to request from ip-api.com.
39
+ * @param options.lang - Language code for response data.
40
+ * @returns The response data parsed as type T.
41
+ * @throws {McpError} If the request fails, including network errors, API errors, or parsing issues.
42
+ */
43
+ async function fetchIpApi(path, options = {}) {
44
+ const methodLogger = logger_util_js_1.Logger.forContext('utils/transport.util.ts', 'fetchIpApi');
45
+ // Get credentials (token might be undefined)
46
+ const credentials = getIpApiCredentials();
47
+ // Determine protocol based on options
48
+ const protocol = options.useHttps ? 'https' : 'http';
49
+ const baseUrl = `${protocol}://ip-api.com/json`;
50
+ // Format path for URL
51
+ const normalizedPath = path ? `/${path}` : '';
52
+ let url = `${baseUrl}${normalizedPath}`;
53
+ // Build query parameters
54
+ const queryParams = new URLSearchParams();
55
+ // Add API token if present
56
+ if (credentials.apiToken) {
57
+ queryParams.set('key', credentials.apiToken);
58
+ methodLogger.debug('API token added to query parameters.');
59
+ }
60
+ // Add fields parameter
61
+ if (options.fields?.length) {
62
+ queryParams.set('fields', options.fields.join(','));
63
+ methodLogger.debug(`Requesting fields: ${options.fields.join(',')}`);
64
+ }
65
+ // Add language parameter
66
+ if (options.lang) {
67
+ queryParams.set('lang', options.lang);
68
+ methodLogger.debug(`Requesting language: ${options.lang}`);
69
+ }
70
+ // Append query string if needed
71
+ const queryString = queryParams.toString();
72
+ if (queryString) {
73
+ url += `?${queryString}`;
74
+ }
75
+ methodLogger.debug(`Constructed URL: ${url}`);
76
+ // Delegate the actual fetch call to the generic fetchApi
77
+ return fetchApi(url, {
78
+ method: options.method,
79
+ headers: options.headers,
80
+ body: options.body,
81
+ });
82
+ }
83
+ /**
84
+ * Generic and reusable function to fetch data from any API endpoint.
85
+ * Handles standard HTTP request setup, response checking, basic error handling, and logging.
86
+ *
87
+ * @param url The full URL to fetch data from.
88
+ * @param options Request options including method, headers, and body.
89
+ * @returns The response data parsed as type T.
90
+ * @throws {McpError} If the request fails, including network errors, non-OK HTTP status, or JSON parsing issues.
91
+ */
92
+ async function fetchApi(url, options = {}) {
93
+ const methodLogger = logger_util_js_1.Logger.forContext('utils/transport.util.ts', 'fetchApi');
94
+ // Prepare standard request options
95
+ const requestOptions = {
96
+ method: options.method || 'GET',
97
+ headers: {
98
+ // Standard headers, allow overrides via options.headers
99
+ 'Content-Type': 'application/json',
100
+ Accept: 'application/json',
101
+ ...options.headers,
102
+ },
103
+ body: options.body ? JSON.stringify(options.body) : undefined,
104
+ };
105
+ methodLogger.debug(`Executing API call: ${requestOptions.method} ${url}`);
106
+ const startTime = performance.now(); // Track performance
107
+ try {
108
+ const response = await fetch(url, requestOptions);
109
+ const endTime = performance.now();
110
+ const duration = (endTime - startTime).toFixed(2);
111
+ methodLogger.debug(`API call completed in ${duration}ms with status: ${response.status} ${response.statusText}`, { url, status: response.status });
112
+ // Check if the response status is OK (2xx)
113
+ if (!response.ok) {
114
+ const errorText = await response.text(); // Get error body for context
115
+ methodLogger.error(`API error response (${response.status}):`, errorText);
116
+ // Classify standard HTTP errors
117
+ if (response.status === 401) {
118
+ // Use createAuthInvalidError for consistency, even if ip-api uses keys
119
+ throw (0, error_util_js_1.createAuthInvalidError)('Authentication failed. Check API token if required.');
120
+ }
121
+ else if (response.status === 403) {
122
+ // Use createAuthInvalidError or a more specific permission error if needed
123
+ throw (0, error_util_js_1.createAuthInvalidError)('Permission denied for the requested resource.');
124
+ }
125
+ else if (response.status === 404) {
126
+ throw (0, error_util_js_1.createApiError)('Resource not found at the specified URL.', response.status, errorText);
127
+ }
128
+ else {
129
+ // Generic API error for other non-2xx statuses
130
+ throw (0, error_util_js_1.createApiError)(`API request failed with status ${response.status}: ${response.statusText}`, response.status, errorText);
131
+ }
132
+ }
133
+ // Attempt to parse the response body as JSON
134
+ try {
135
+ const responseData = await response.json();
136
+ methodLogger.debug('Response body successfully parsed as JSON.');
137
+ return responseData;
138
+ }
139
+ catch (parseError) {
140
+ methodLogger.error('Failed to parse API response JSON:', parseError);
141
+ // Throw a specific error for JSON parsing failure
142
+ throw (0, error_util_js_1.createApiError)(`Failed to parse API response JSON: ${parseError instanceof Error ? parseError.message : String(parseError)}`, response.status, // Include original status for context
143
+ parseError);
144
+ }
145
+ }
146
+ catch (error) {
147
+ const endTime = performance.now();
148
+ const duration = (endTime - startTime).toFixed(2);
149
+ methodLogger.error(`API call failed after ${duration}ms for ${url}:`, error);
150
+ // Rethrow if it's already an McpError (e.g., from status checks or parsing)
151
+ if (error instanceof error_util_js_1.McpError) {
152
+ throw error;
153
+ }
154
+ // Handle potential network errors (TypeError in fetch)
155
+ if (error instanceof TypeError) {
156
+ throw (0, error_util_js_1.createApiError)(`Network error during API call: ${error.message}`, undefined, // No specific HTTP status for network errors
157
+ error);
158
+ }
159
+ // Wrap any other unexpected errors
160
+ throw (0, error_util_js_1.createUnexpectedError)(`Unexpected error during API call: ${error instanceof Error ? error.message : String(error)}`, error);
161
+ }
162
+ }
@@ -0,0 +1,46 @@
1
+ import eslint from '@eslint/js';
2
+ import tseslint from 'typescript-eslint';
3
+ import prettierPlugin from 'eslint-plugin-prettier';
4
+ import eslintConfigPrettier from 'eslint-config-prettier';
5
+
6
+ export default tseslint.config(
7
+ {
8
+ ignores: ['node_modules/**', 'dist/**', 'examples/**'],
9
+ },
10
+ eslint.configs.recommended,
11
+ ...tseslint.configs.recommended,
12
+ {
13
+ plugins: {
14
+ prettier: prettierPlugin,
15
+ },
16
+ rules: {
17
+ 'prettier/prettier': 'error',
18
+ indent: ['error', 'tab', { SwitchCase: 1 }],
19
+ '@typescript-eslint/no-unused-vars': [
20
+ 'error',
21
+ { argsIgnorePattern: '^_' },
22
+ ],
23
+ },
24
+ languageOptions: {
25
+ parserOptions: {
26
+ ecmaVersion: 'latest',
27
+ sourceType: 'module',
28
+ },
29
+ globals: {
30
+ node: 'readonly',
31
+ jest: 'readonly',
32
+ },
33
+ },
34
+ },
35
+ // Special rules for test files
36
+ {
37
+ files: ['**/*.test.ts'],
38
+ rules: {
39
+ '@typescript-eslint/no-explicit-any': 'off',
40
+ '@typescript-eslint/no-require-imports': 'off',
41
+ '@typescript-eslint/no-unsafe-function-type': 'off',
42
+ '@typescript-eslint/no-unused-vars': 'off',
43
+ },
44
+ },
45
+ eslintConfigPrettier,
46
+ );