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
JavaScript
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;
}
}