UNPKG

mcp-banka

Version:

Model Context Protocol Server for Turkish Currency and Exchange Rate data from the Central Bank of The Republic of Turkey (TCMB) (TCMB)

257 lines (256 loc) 11.8 kB
export class TcmbApiClient { baseUrl = 'https://www.tcmb.gov.tr/kurlar/'; /** * Generates a valid URL for TCMB currency rates API * Automatically adjusts dates to handle weekends (uses Friday's data) * @param date - Optional date to get rates for (defaults to today) * @returns Formatted URL string for the TCMB API */ generateUrl(date = new Date()) { // Clone the date to avoid modifying the original const targetDate = new Date(date); // Adjust for weekends (0 = Sunday, 6 = Saturday) const dayOfWeek = targetDate.getDay(); if (dayOfWeek === 0) { // Sunday -> use Friday's data (subtract 2 days) targetDate.setDate(targetDate.getDate() - 2); } else if (dayOfWeek === 6) { // Saturday -> use Friday's data (subtract 1 day) targetDate.setDate(targetDate.getDate() - 1); } // Format the date components const year = targetDate.getFullYear(); const month = String(targetDate.getMonth() + 1).padStart(2, '0'); const day = String(targetDate.getDate()).padStart(2, '0'); // Create year-month folder format (YYYYMM) const yearMonth = `${year}${month}`; // Create date format for filename (DDMMYYYY) const formattedDate = `${day}${month}${year}`; return `${this.baseUrl}${yearMonth}/${formattedDate}.xml`; } /** * Fetches raw XML currency data from TCMB * Handles weekend date adjustments automatically * @param date - Optional date to get rates for (defaults to today) * @returns Promise containing the raw XML string response */ async fetchData(date) { const url = this.generateUrl(date); try { const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch data: ${response.status} ${response.statusText}`); } const xmlText = await response.text(); return xmlText; } catch (error) { throw new Error(`Error fetching TCMB data: ${error instanceof Error ? error.message : String(error)}`); } } /** * Fetches currency data and converts XML to JSON * @param date - Optional date to get rates for (defaults to today) * @returns Promise containing the parsed JSON response */ async fetchDataAsJson(date) { const xmlText = await this.fetchData(date); return this.xmlToJson(xmlText); } /** * Gets today's exchange rate for a specific currency * @param currencyCode - Optional currency code (e.g., 'USD', 'EUR') * @returns Promise containing the response and currency code */ async getTodayExchangeRate(currencyCode) { const data = await this.fetchDataAsJson(); return { response: data, currencyCode: currencyCode || "TRY" }; } /** * Gets exchange rate for a specific currency on a specific date * @param currencyCode - Currency code to fetch (e.g., 'USD', 'EUR') * @param dateString - Date string in YYYY-MM-DD format * @returns Promise containing either a single currency or all currencies if the requested one isn't found */ async getExchangeRateHistory(currencyCode, dateString) { const date = new Date(dateString); if (isNaN(date.getTime())) { throw new Error(`Invalid date format: ${dateString}. Please use YYYY-MM-DD format.`); } const data = await this.fetchDataAsJson(date); return this.extractCurrencyData(data, currencyCode); } /** * Extract specific currency data from the full response * If the requested currency code isn't found, returns all currencies * @param data - The TcmbResponse object to extract from * @param currencyCode - Optional currency code to extract * @returns Either the specific currency data or all currencies */ extractCurrencyData(data, currencyCode) { try { // The structure might vary based on TCMB's API, adjust as needed const tarihDate = data['Tarih_Date']; const currencies = Array.isArray(tarihDate.Currency) ? tarihDate.Currency : [tarihDate.Currency]; // If no currency code is provided, return all currencies if (!currencyCode) { return currencies.map((c) => ({ date: tarihDate['@Date'] || tarihDate['@Tarih'] || '', currency: c['@Kod'], ...c })); } // Find specific currency if code is provided const currency = currencies.find((c) => c['@Kod'] === currencyCode.toUpperCase()); if (!currency) { // If currency code not found, return all currencies instead of throwing an error console.warn(`Currency code ${currencyCode} not found in data. Returning all currencies.`); return currencies.map((c) => ({ date: tarihDate['@Date'] || tarihDate['@Tarih'] || '', currency: c['@Kod'], ...c })); } // Add date information to the response return { date: tarihDate['@Date'] || tarihDate['@Tarih'] || '', currency: currencyCode.toUpperCase(), ...currency }; } catch (error) { throw new Error(`Error extracting currency data: ${error instanceof Error ? error.message : String(error)}`); } } /** * Converts XML string to JSON object * Handles both browser and Node.js environments * @param xml - Raw XML string to convert * @returns Parsed JSON object */ xmlToJson(xml) { // Check if running in Node.js environment if (typeof window === 'undefined' || !window.DOMParser) { // Use a simplified approach for Node.js to avoid recursion issues try { // NOTE: This is a simplified non-recursive approach specifically tailored for TCMB XML format. // Previous implementations caused "Maximum call stack size exceeded" errors due to recursive parsing. // This implementation avoids recursion completely and extracts only the needed information. // For a more robust solution in production, consider using a dedicated XML parsing library like xml2js. const result = { Tarih_Date: {} }; // Extract Tarih_Date attributes const tarihDateRegex = /<Tarih_Date([^>]*)>/; const tarihDateMatch = xml.match(tarihDateRegex); if (tarihDateMatch && tarihDateMatch[1]) { const attrStr = tarihDateMatch[1]; const dateMatch = attrStr.match(/Date="([^"]*)"/); const tarihMatch = attrStr.match(/Tarih="([^"]*)"/); if (dateMatch) result.Tarih_Date['@Date'] = dateMatch[1]; if (tarihMatch) result.Tarih_Date['@Tarih'] = tarihMatch[1]; } // Extract Currency elements using a simpler approach that doesn't use recursion const currencies = []; const currencySections = xml.split('<Currency '); // Skip the first section (before the first Currency tag) for (let i = 1; i < currencySections.length; i++) { const section = currencySections[i]; const endIndex = section.indexOf('</Currency>'); if (endIndex === -1) continue; const currencyXml = '<Currency ' + section.substring(0, endIndex + 10); const currency = {}; // Extract Currency attributes const kodMatch = currencyXml.match(/Kod="([^"]*)"/); const nameMatch = currencyXml.match(/CurrencyName="([^"]*)"/); if (kodMatch) currency['@Kod'] = kodMatch[1]; if (nameMatch) currency['@CurrencyName'] = nameMatch[1]; // Extract common fields const fields = [ 'Isim', 'ForexBuying', 'ForexSelling', 'BanknoteBuying', 'BanknoteSelling', 'CrossRateUSD', 'CrossRateOther' ]; for (const field of fields) { const fieldRegex = new RegExp(`<${field}>([^<]*)<\/${field}>`); const fieldMatch = currencyXml.match(fieldRegex); if (fieldMatch && fieldMatch[1]) { currency[field] = fieldMatch[1].trim(); } } currencies.push(currency); } // Add currencies to the result if (currencies.length === 0) { console.warn('No currencies found in XML'); result.Tarih_Date.Currency = []; } else if (currencies.length === 1) { result.Tarih_Date.Currency = currencies[0]; } else { result.Tarih_Date.Currency = currencies; } return result; } catch (error) { console.error('XML parsing error:', error); // Provide a minimal fallback structure return { Tarih_Date: { '@Date': new Date().toISOString().split('T')[0], Currency: [] } }; } } else { // Browser environment - use DOMParser const parser = new DOMParser(); const xmlDoc = parser.parseFromString(xml, "text/xml"); const rootElement = xmlDoc.documentElement; const result = {}; result[rootElement.nodeName] = this.convertNodeToJson(rootElement); return result; } } /** * Converts DOM Element to JSON object (browser environment only) * @param node - DOM Element to convert * @returns Parsed JSON object */ convertNodeToJson(node) { const obj = {}; // Add attributes as properties Array.from(node.attributes).forEach(attr => { obj[`@${attr.name}`] = attr.value; }); // Process child nodes Array.from(node.childNodes).forEach(child => { if (child.nodeType === Node.ELEMENT_NODE) { const childElement = child; const childName = childElement.nodeName; // If child has no children and no attributes, treat as simple value if (childElement.childNodes.length === 1 && childElement.childNodes[0].nodeType === Node.TEXT_NODE && childElement.attributes.length === 0) { obj[childName] = childElement.textContent; } else { // Handle existing properties (convert to array if needed) if (obj[childName]) { if (!Array.isArray(obj[childName])) { obj[childName] = [obj[childName]]; } obj[childName].push(this.convertNodeToJson(childElement)); } else { obj[childName] = this.convertNodeToJson(childElement); } } } }); return obj; } }