From ac694086e594e140a3f89cc7999d3feda35a1c4c Mon Sep 17 00:00:00 2001 From: Robert Tidball Date: Tue, 6 Oct 2026 10:02:04 +1100 Subject: [PATCH] feat(adapters): add FXMacroData adapter for macro releases, calendars and FX rates --- CITATION.cff | 2 +- README.de.md | 6 +- README.ja.md | 6 +- README.md | 6 +- README.zh-CN.md | 6 +- glama.json | 2 +- package.json | 2 +- packages/backend/src/adapters/catalog.ts | 2 + .../src/adapters/intl/fxmacrodata.json | 391 ++++++++++++++++++ .../adapters/intl/fxmacrodata.live.spec.ts | 266 ++++++++++++ .../public/logos/connectors/fxmacrodata.svg | 1 + server.json | 2 +- 12 files changed, 676 insertions(+), 16 deletions(-) create mode 100644 packages/backend/src/adapters/intl/fxmacrodata.json create mode 100644 packages/backend/src/adapters/intl/fxmacrodata.live.spec.ts create mode 100644 packages/frontend/public/logos/connectors/fxmacrodata.svg diff --git a/CITATION.cff b/CITATION.cff index 4e2e8fb8..5694e3e3 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -13,7 +13,7 @@ abstract: > server that turns REST/OpenAPI, SOAP/WSDL and GraphQL APIs, SQL/NoSQL databases and other MCP servers into tools for AI clients such as Claude, ChatGPT, Google Gemini, GitHub Copilot and Cursor, without writing code. - It ships 262 pre-built adapters, with a focus on ERP and e-commerce + It ships 263 pre-built adapters, with a focus on ERP and e-commerce systems (SAP Business One, Odoo, Xentral, JTL-Wawi, weclapp, Shopware, WooCommerce, Amazon Seller, Kaufland, OTTO, etc.), a per-workspace knowledge graph served over MCP, per-tool response mapping, diff --git a/README.de.md b/README.de.md index cb4e624d..c4bfd9f2 100644 --- a/README.de.md +++ b/README.de.md @@ -1,5 +1,5 @@

- AnythingMCP macht ERP-, E-Commerce-, REST-, SOAP- und SQL-Systeme zu MCP-Tools für Claude und ChatGPT: 262 Connectors, 15 davon ohne API-Schlüssel. + AnythingMCP macht ERP-, E-Commerce-, REST-, SOAP- und SQL-Systeme zu MCP-Tools für Claude und ChatGPT: 263 Connectors, 16 davon ohne API-Schlüssel.

AnythingMCP: selbst gehostetes MCP-Gateway

@@ -19,7 +19,7 @@

AnythingMCP ist ein quelloffenes, selbst gehostetes MCP-Gateway, das jedes REST-/OpenAPI-, SOAP-, GraphQL-, OData- oder SQL-System in MCP-Tools für Claude, ChatGPT und Copilot verwandelt, ohne dass du einen MCP-Server programmierst.
- Es bringt 262 fertige Adapter mit, darunter SAP, Etsy, weclapp und Amazon Seller; 15 davon kommen ohne API-Schlüssel aus. + Es bringt 263 fertige Adapter mit, darunter SAP, Etsy, weclapp und Amazon Seller; 16 davon kommen ohne API-Schlüssel aus.

@@ -111,7 +111,7 @@ Tools werden zur Laufzeit registriert, ohne Neustart. `{{VAR}}`-Werte pro Connec ## Connector-Katalog -262 Adapter mit über 2.400 Tools. Zu jedem gibt es eine Einrichtungsanleitung auf [anythingmcp.com/de/guides](https://anythingmcp.com/de/guides), in sieben Sprachen. +263 Adapter mit über 2.400 Tools. Zu jedem gibt es eine Einrichtungsanleitung auf [anythingmcp.com/de/guides](https://anythingmcp.com/de/guides), in sieben Sprachen. | Kategorie | Beispiele | |---|---| diff --git a/README.ja.md b/README.ja.md index 8c280202..6e95ed4b 100644 --- a/README.ja.md +++ b/README.ja.md @@ -1,5 +1,5 @@

- AnythingMCP は ERP、E コマース、REST、SOAP、SQL の各システムを Claude と ChatGPT 用の MCP ツールに変換します。262 のコネクター、うち 15 は API キー不要。 + AnythingMCP は ERP、E コマース、REST、SOAP、SQL の各システムを Claude と ChatGPT 用の MCP ツールに変換します。263 のコネクター、うち 16 は API キー不要。

AnythingMCP:セルフホスト型 MCP ゲートウェイ

@@ -19,7 +19,7 @@

AnythingMCP は、オープンソースのセルフホスト型 MCP ゲートウェイです。MCP サーバーを書かずに、REST/OpenAPI、SOAP、GraphQL、OData、SQL のあらゆるシステムを Claude、ChatGPT、Copilot 用の MCP ツールに変換します。
- SAP、Etsy、weclapp、Amazon Seller などを含む 262 種類の既製アダプターを同梱しており、うち 15 は API キー不要です。 + SAP、Etsy、weclapp、Amazon Seller などを含む 263 種類の既製アダプターを同梱しており、うち 16 は API キー不要です。

@@ -111,7 +111,7 @@ amd64 ではイメージの取得に約 30 秒、その 24 秒後に API が利 ## コネクターカタログ -262 個のアダプターで 2,400 以上のツールを提供しています。どのアダプターにも [anythingmcp.com/ja/guides](https://anythingmcp.com/ja/guides) に 7 言語の設定ガイドがあります。 +263 個のアダプターで 2,400 以上のツールを提供しています。どのアダプターにも [anythingmcp.com/ja/guides](https://anythingmcp.com/ja/guides) に 7 言語の設定ガイドがあります。 | カテゴリー | 例 | |---|---| diff --git a/README.md b/README.md index 9c882b6f..551233ef 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- AnythingMCP turns ERP, e-commerce, REST, SOAP and SQL systems into MCP tools for Claude and ChatGPT: 262 connectors, 15 of them with no API key. + AnythingMCP turns ERP, e-commerce, REST, SOAP and SQL systems into MCP tools for Claude and ChatGPT: 263 connectors, 16 of them with no API key.

AnythingMCP: self-hosted MCP gateway

@@ -19,7 +19,7 @@

AnythingMCP is an open-source, self-hosted MCP gateway that turns any REST/OpenAPI, SOAP, GraphQL, OData or SQL system into MCP tools for Claude, ChatGPT and Copilot, without writing an MCP server.
- It ships 262 ready connectors, among them SAP, Etsy, weclapp and Amazon Seller, and 15 of them need no API key. + It ships 263 ready connectors, among them SAP, Etsy, weclapp and Amazon Seller, and 16 of them need no API key.

@@ -103,7 +103,7 @@ Tools register at runtime, without a restart. Per-connector `{{VAR}}` values are ## Connector catalog -262 adapters, exposing 2,400+ tools. Every one has a setup guide on [anythingmcp.com/guides](https://anythingmcp.com/guides), in seven languages. +263 adapters, exposing 2,400+ tools. Every one has a setup guide on [anythingmcp.com/guides](https://anythingmcp.com/guides), in seven languages. | Category | Examples | |---|---| diff --git a/README.zh-CN.md b/README.zh-CN.md index 95b18ac5..4135d421 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,5 +1,5 @@

- AnythingMCP 将 ERP、电子商务、REST、SOAP 和 SQL 系统转化为 Claude 和 ChatGPT 可用的 MCP 工具:262 个连接器,其中 15 个无需 API 密钥。 + AnythingMCP 将 ERP、电子商务、REST、SOAP 和 SQL 系统转化为 Claude 和 ChatGPT 可用的 MCP 工具:263 个连接器,其中 16 个无需 API 密钥。

AnythingMCP:自行托管的 MCP 网关

@@ -19,7 +19,7 @@

AnythingMCP 是一个开源、可自行托管的 MCP 网关,无需编写 MCP 服务器,即可将任意 REST/OpenAPI、SOAP、GraphQL、OData 或 SQL 系统转化为 Claude、ChatGPT 和 Copilot 可用的 MCP 工具。
- 它自带 262 个现成适配器,涵盖 SAP、Etsy、weclapp 和 Amazon Seller 等,其中 15 个无需 API 密钥。 + 它自带 263 个现成适配器,涵盖 SAP、Etsy、weclapp 和 Amazon Seller 等,其中 16 个无需 API 密钥。

@@ -111,7 +111,7 @@ docker compose up -d ## 连接器目录 -共 262 个适配器,提供 2,400 多个工具。每个适配器都在 [anythingmcp.com/zh/guides](https://anythingmcp.com/zh/guides) 上提供七种语言的配置指南。 +共 263 个适配器,提供 2,400 多个工具。每个适配器都在 [anythingmcp.com/zh/guides](https://anythingmcp.com/zh/guides) 上提供七种语言的配置指南。 | 类别 | 示例 | |---|---| diff --git a/glama.json b/glama.json index b58001db..d4c5409b 100644 --- a/glama.json +++ b/glama.json @@ -3,5 +3,5 @@ "maintainers": [ "keysersoft" ], - "description": "Turn any REST/OpenAPI, SOAP/WSDL, GraphQL, OData or SQL API into MCP tools for Claude, ChatGPT and Copilot, no code. 262 pre-built adapters for ERP and e-commerce (SAP S/4HANA, SAP Business One, Odoo, Xentral, JTL-Wawi, weclapp, Shopware, WooCommerce, Amazon Seller, Kaufland, OTTO\u2026) and more. Self-hosted, knowledge graph, per-tool response mapping, OAuth2/RBAC/SSO/audit. Open source under AGPL-3.0." + "description": "Turn any REST/OpenAPI, SOAP/WSDL, GraphQL, OData or SQL API into MCP tools for Claude, ChatGPT and Copilot, no code. 263 pre-built adapters for ERP and e-commerce (SAP S/4HANA, SAP Business One, Odoo, Xentral, JTL-Wawi, weclapp, Shopware, WooCommerce, Amazon Seller, Kaufland, OTTO\u2026) and more. Self-hosted, knowledge graph, per-tool response mapping, OAuth2/RBAC/SSO/audit. Open source under AGPL-3.0." } diff --git a/package.json b/package.json index 358968d4..453bdbb6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "anythingmcp", "version": "0.18.1", - "description": "Turn any REST/OpenAPI, SOAP/WSDL, GraphQL, OData or SQL API into MCP tools for Claude, ChatGPT and Copilot, no code. 262 pre-built adapters for ERP, e-commerce and more (SAP S/4HANA, SAP Business One, Odoo, Xentral, JTL-Wawi, Shopware, WooCommerce, Amazon Seller). Self-hosted, open source (AGPL-3.0).", + "description": "Turn any REST/OpenAPI, SOAP/WSDL, GraphQL, OData or SQL API into MCP tools for Claude, ChatGPT and Copilot, no code. 263 pre-built adapters for ERP, e-commerce and more (SAP S/4HANA, SAP Business One, Odoo, Xentral, JTL-Wawi, Shopware, WooCommerce, Amazon Seller). Self-hosted, open source (AGPL-3.0).", "private": true, "license": "AGPL-3.0-only", "engines": { diff --git a/packages/backend/src/adapters/catalog.ts b/packages/backend/src/adapters/catalog.ts index e540cc62..d21c6845 100644 --- a/packages/backend/src/adapters/catalog.ts +++ b/packages/backend/src/adapters/catalog.ts @@ -117,6 +117,7 @@ import * as freshchat from './intl/freshchat.json'; import * as freshdesk from './intl/freshdesk.json'; import * as freshservice from './intl/freshservice.json'; import * as front from './intl/front.json'; +import * as fxmacrodata from './intl/fxmacrodata.json'; import * as georgianRailway from './intl/georgian-railway.json'; import * as ghost from './intl/ghost.json'; import * as gitbook from './intl/gitbook.json'; @@ -559,6 +560,7 @@ const RAW_ADAPTERS: AdapterDefinition[] = [ freshdesk as unknown as AdapterDefinition, freshservice as unknown as AdapterDefinition, front as unknown as AdapterDefinition, + fxmacrodata as unknown as AdapterDefinition, georgianRailway as unknown as AdapterDefinition, ghost as unknown as AdapterDefinition, gitbook as unknown as AdapterDefinition, diff --git a/packages/backend/src/adapters/intl/fxmacrodata.json b/packages/backend/src/adapters/intl/fxmacrodata.json new file mode 100644 index 00000000..b77d034d --- /dev/null +++ b/packages/backend/src/adapters/intl/fxmacrodata.json @@ -0,0 +1,391 @@ +{ + "slug": "fxmacrodata", + "name": "FXMacroData", + "description": "Official macroeconomic releases, central-bank policy rates, release calendars and FX rates for 22 currencies via the FXMacroData API. 5 read-only tools; the data catalogue, the USD calendar and recent USD releases work without an API key.", + "instructions": "This connector reads the FXMacroData REST API (api.fxmacrodata.com/v1): macroeconomic releases, central-bank policy rates, release calendars and FX rates for 22 currencies (AUD, BRL, CAD, CHF, CNH, CNY, DKK, EUR, GBP, HUF, ILS, JPY, KRW, MYR, NGN, NOK, NZD, PEN, SEK, THB, TWD, USD). Each value comes from the official publisher (BLS, ECB, RBA, ONS and so on) and is timestamped at publication. Every tool is a read-only GET.\n\n**Setup**: nothing is required to start. Without a key the API serves:\n- the data catalogue for every currency;\n- the USD release calendar;\n- USD announcements for the most recent 90 days, each release readable 15 minutes after it is published.\n\nOther currencies, FX rates, full history and real-time releases need a key: subscribe at https://fxmacrodata.com/subscribe and set `FXMACRODATA_API_KEY`. The key is sent as the `X-API-Key` header, and only when it is set; a wrong key is answered with 401 `invalid_api_key` even on the free routes. Keyless traffic has a fair-use allowance of 100 requests a day.\n\n**Free-tier answers say so**: a keyless announcement response carries `freemium_window` (history cut to 90 days) and `freemium_delay` (15-minute delay, `withheld_count`, `message`). When `freemium_delay.withheld_count` is above zero, a newer release has been published that the free tier cannot show yet: say so, and do not present the older value as the current one. A route that needs a key answers 401 with `code: api_key_required`; the data exists behind a subscription, it is not missing.\n\n**Tools**\n- `fxmacrodata_data_catalogue`: start here. Lists every indicator slug a currency publishes (`inflation`, `policy_rate`, `unemployment`, `gdp`, `non_farm_payrolls`, bond yields...) with unit, frequency, source and coverage. Use its slugs in the other tools.\n- `fxmacrodata_latest_announcements`: the most recent release of every indicator for one currency.\n- `fxmacrodata_indicator_history`: the release history of one indicator, filtered by `start_date`/`end_date`.\n- `fxmacrodata_release_calendar`: scheduled releases for one currency.\n- `fxmacrodata_fx_rates`: daily FX rates for a pair of supported currencies (key required).\n\n**Reading the data**\n- `date` on an announcement or calendar row is the reference period the value describes (the month, for CPI), NOT the day it was released. Release timing is `announcement_datetime` (Unix seconds, UTC) and `announcement_datetime_local` (publisher time zone). Sort and compare releases by `announcement_datetime`.\n- A row with `release_time_assumed: true` has a release time derived from the series' usual publication lag rather than captured from the release.\n- `val` is the value; `source_url` links the official publication. Credit the publisher (for example \"Bureau of Labor Statistics, via FXMacroData\").\n- List endpoints return 20 rows by default and take `limit` up to 100. To read a full range, repeat the call with `offset` = `pagination.next_offset` while `pagination.has_more` is true.\n- Dates are `YYYY-MM-DD`. Currency codes are lowercase.\n\nDocs: https://fxmacrodata.com/documentation", + "region": "intl", + "category": "finance", + "icon": "fxmacrodata", + "docsUrl": "https://fxmacrodata.com/documentation", + "requiredEnvVars": [], + "optionalEnvVars": [ + "FXMACRODATA_API_KEY" + ], + "envVarMeta": { + "FXMACRODATA_API_KEY": { + "label": "API key (optional)", + "kind": "credential", + "secret": true, + "help": "Leave empty for the free tier (data catalogue, USD calendar, recent USD releases). A key unlocks every currency, FX rates, full history and real-time releases.", + "link": "https://fxmacrodata.com/subscribe", + "pattern": "^\\S+$", + "patternMessage": "The key is a single token with no spaces or line breaks." + } + }, + "probe": { + "tool": "fxmacrodata_data_catalogue", + "params": { + "currency": "usd" + } + }, + "connector": { + "name": "FXMacroData API", + "type": "REST", + "baseUrl": "https://api.fxmacrodata.com/v1", + "authType": "NONE" + }, + "tools": [ + { + "name": "fxmacrodata_data_catalogue", + "description": "List every indicator FXMacroData publishes for one currency: slug, name, unit, frequency, official source and coverage (earliest/latest available date, freshness). Call this first to find the indicator slugs used by the other tools. Works without an API key for every currency.", + "parameters": { + "type": "object", + "properties": { + "currency": { + "type": "string", + "enum": [ + "aud", + "brl", + "cad", + "chf", + "cnh", + "cny", + "dkk", + "eur", + "gbp", + "huf", + "ils", + "jpy", + "krw", + "myr", + "ngn", + "nok", + "nzd", + "pen", + "sek", + "thb", + "twd", + "usd" + ], + "description": "Lowercase ISO currency code, e.g. 'usd', 'eur', 'jpy'." + } + }, + "required": [ + "currency" + ] + }, + "endpointMapping": { + "method": "GET", + "path": "/data_catalogue/{currency}", + "encodePathParams": true, + "headers": { + "Accept": "application/json", + "X-API-Key": "${FXMACRODATA_API_KEY}" + } + } + }, + { + "name": "fxmacrodata_latest_announcements", + "description": "The most recent official release of every indicator for one currency (policy rate, CPI, GDP, unemployment, payrolls, bond yields...), each with val, the previous value, the reference period in `date` and the release time in `announcement_datetime`. USD works without an API key on a 15-minute delay (see freemium_delay in the response); other currencies need FXMACRODATA_API_KEY.", + "parameters": { + "type": "object", + "properties": { + "currency": { + "type": "string", + "enum": [ + "aud", + "brl", + "cad", + "chf", + "cnh", + "cny", + "dkk", + "eur", + "gbp", + "huf", + "ils", + "jpy", + "krw", + "myr", + "ngn", + "nok", + "nzd", + "pen", + "sek", + "thb", + "twd", + "usd" + ], + "description": "Lowercase ISO currency code, e.g. 'usd', 'eur', 'gbp'." + } + }, + "required": [ + "currency" + ] + }, + "endpointMapping": { + "method": "GET", + "path": "/announcements/{currency}/latest", + "encodePathParams": true, + "headers": { + "Accept": "application/json", + "X-API-Key": "${FXMACRODATA_API_KEY}" + } + } + }, + { + "name": "fxmacrodata_indicator_history", + "description": "Release history of one indicator for one currency, with val, the reference period in `date` and the release time in `announcement_datetime`. Get the indicator slug from fxmacrodata_data_catalogue. Paginated: up to 100 rows per call; call again with offset = pagination.next_offset while pagination.has_more is true. Without an API key only USD works, limited to the most recent 90 days (see freemium_window and freemium_delay in the response).", + "parameters": { + "type": "object", + "properties": { + "currency": { + "type": "string", + "enum": [ + "aud", + "brl", + "cad", + "chf", + "cnh", + "cny", + "dkk", + "eur", + "gbp", + "huf", + "ils", + "jpy", + "krw", + "myr", + "ngn", + "nok", + "nzd", + "pen", + "sek", + "thb", + "twd", + "usd" + ], + "description": "Lowercase ISO currency code, e.g. 'usd', 'aud'." + }, + "indicator": { + "type": "string", + "description": "Indicator slug from fxmacrodata_data_catalogue, e.g. 'inflation', 'policy_rate', 'unemployment', 'non_farm_payrolls'." + }, + "start_date": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", + "description": "Earliest reference date, YYYY-MM-DD." + }, + "end_date": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", + "description": "Latest reference date, YYYY-MM-DD. Must not be before start_date." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Rows per page, 1-100. The API returns 20 when omitted." + }, + "offset": { + "type": "integer", + "minimum": 0, + "description": "Rows to skip. To read a whole range, call again with offset = pagination.next_offset while pagination.has_more is true." + } + }, + "required": [ + "currency", + "indicator" + ] + }, + "endpointMapping": { + "method": "GET", + "path": "/announcements/{currency}/{indicator}", + "encodePathParams": true, + "queryParams": { + "start_date": "$start_date", + "end_date": "$end_date", + "limit": "$limit", + "offset": "$offset" + }, + "headers": { + "Accept": "application/json", + "X-API-Key": "${FXMACRODATA_API_KEY}" + } + } + }, + { + "name": "fxmacrodata_release_calendar", + "description": "Scheduled releases for one currency: indicator (`release`), name, importance and the scheduled time in `announcement_datetime` (Unix seconds, UTC) and `announcement_datetime_utc`. If a row carries `date`, that is the reference period, not the release day: time releases by announcement_datetime. USD works without an API key; other currencies need FXMACRODATA_API_KEY.", + "parameters": { + "type": "object", + "properties": { + "currency": { + "type": "string", + "enum": [ + "aud", + "brl", + "cad", + "chf", + "cnh", + "cny", + "dkk", + "eur", + "gbp", + "huf", + "ils", + "jpy", + "krw", + "myr", + "ngn", + "nok", + "nzd", + "pen", + "sek", + "thb", + "twd", + "usd" + ], + "description": "Lowercase ISO currency code, e.g. 'usd', 'eur'." + }, + "indicator": { + "type": "string", + "description": "Optional indicator slug to keep only that release, e.g. 'policy_rate'." + }, + "start_date": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", + "description": "Earliest release date to include, YYYY-MM-DD." + }, + "end_date": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", + "description": "Latest release date to include, YYYY-MM-DD. Must not be before start_date." + } + }, + "required": [ + "currency" + ] + }, + "endpointMapping": { + "method": "GET", + "path": "/calendar/{currency}", + "encodePathParams": true, + "queryParams": { + "indicator": "$indicator", + "start_date": "$start_date", + "end_date": "$end_date" + }, + "headers": { + "Accept": "application/json", + "X-API-Key": "${FXMACRODATA_API_KEY}" + } + } + }, + { + "name": "fxmacrodata_fx_rates", + "description": "Daily FX rates for a currency pair where both legs are supported currencies, e.g. base 'eur' and quote 'usd' for EUR/USD. Always needs FXMACRODATA_API_KEY; without one the API answers 401 api_key_required. Paginated: up to 100 rows per call; call again with offset = pagination.next_offset while pagination.has_more is true.", + "parameters": { + "type": "object", + "properties": { + "base": { + "type": "string", + "enum": [ + "aud", + "brl", + "cad", + "chf", + "cnh", + "cny", + "dkk", + "eur", + "gbp", + "huf", + "ils", + "jpy", + "krw", + "myr", + "ngn", + "nok", + "nzd", + "pen", + "sek", + "thb", + "twd", + "usd" + ], + "description": "Base currency, lowercase, e.g. 'eur'." + }, + "quote": { + "type": "string", + "enum": [ + "aud", + "brl", + "cad", + "chf", + "cnh", + "cny", + "dkk", + "eur", + "gbp", + "huf", + "ils", + "jpy", + "krw", + "myr", + "ngn", + "nok", + "nzd", + "pen", + "sek", + "thb", + "twd", + "usd" + ], + "description": "Quote currency, lowercase, e.g. 'usd'." + }, + "start_date": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", + "description": "First date, YYYY-MM-DD." + }, + "end_date": { + "type": "string", + "pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$", + "description": "Last date, YYYY-MM-DD. Must not be before start_date." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Rows per page, 1-100. The API returns 20 when omitted." + }, + "offset": { + "type": "integer", + "minimum": 0, + "description": "Rows to skip. To read a whole range, call again with offset = pagination.next_offset while pagination.has_more is true." + } + }, + "required": [ + "base", + "quote" + ] + }, + "endpointMapping": { + "method": "GET", + "path": "/forex/{base}/{quote}", + "encodePathParams": true, + "queryParams": { + "start_date": "$start_date", + "end_date": "$end_date", + "limit": "$limit", + "offset": "$offset" + }, + "headers": { + "Accept": "application/json", + "X-API-Key": "${FXMACRODATA_API_KEY}" + } + } + } + ] +} diff --git a/packages/backend/src/adapters/intl/fxmacrodata.live.spec.ts b/packages/backend/src/adapters/intl/fxmacrodata.live.spec.ts new file mode 100644 index 00000000..c30a521c --- /dev/null +++ b/packages/backend/src/adapters/intl/fxmacrodata.live.spec.ts @@ -0,0 +1,266 @@ +import * as adapter from './fxmacrodata.json'; +import axios, { AxiosError, AxiosHeaders } from 'axios'; +import { RestEngine } from '../../connectors/engines/rest.engine'; +import { OAuth2TokenService } from '../../connectors/engines/oauth2-token.service'; +import { LoginTokenService } from '../../connectors/engines/login-token.service'; +import { deriveToolAnnotations } from '../../mcp-server/tool-annotations'; + +/** + * Two layers of verification for the FXMacroData adapter: + * + * 1. Static - always runs, no network. Pins what makes the connector work + * with no setup and keeps its one optional secret where it belongs: + * - the key is optional. The free routes answer a request with no key, + * but FXMacroData answers a wrong or empty X-API-Key with 401 + * invalid_api_key, so an unset key must mean no header at all. + * - every tool is a read-only GET on api.fxmacrodata.com/v1, with path + * values percent-encoded and list sizes capped at 100. + * + * 2. Live - skipped unless RUN_FXMACRODATA_LIVE is set. Without a key it + * calls the free routes (catalogue, USD releases, USD calendar) and + * checks that FX rates are refused; with FXMACRODATA_API_KEY it also + * reads EUR/USD: + * + * RUN_FXMACRODATA_LIVE=1 npx jest src/adapters/intl/fxmacrodata.live.spec.ts + */ + +jest.mock('axios', () => { + const actual = jest.requireActual('axios'); + const mocked = jest.fn(); + return { + __esModule: true, + default: Object.assign(mocked, { __actual: actual.default }), + AxiosError: actual.AxiosError, + AxiosHeaders: actual.AxiosHeaders, + }; +}); +const mockedAxios = axios as unknown as jest.Mock & { __actual: typeof axios }; + +type Property = { type: string; enum?: string[]; minimum?: number; maximum?: number; pattern?: string }; +type Tool = { + name: string; + description: string; + parameters: { properties: Record; required?: string[] }; + endpointMapping: { + method: string; + path: string; + encodePathParams?: boolean; + queryParams?: Record; + headers?: Record; + }; +}; + +const a = adapter as unknown as { + instructions: string; + requiredEnvVars: string[]; + optionalEnvVars: string[]; + envVarMeta: Record; + probe: { tool: string; params: Record }; + connector: { baseUrl: string; authType: string; authConfig?: unknown }; + tools: Tool[]; +}; + +const CURRENCIES = [ + 'aud', 'brl', 'cad', 'chf', 'cnh', 'cny', 'dkk', 'eur', 'gbp', 'huf', 'ils', + 'jpy', 'krw', 'myr', 'ngn', 'nok', 'nzd', 'pen', 'sek', 'thb', 'twd', 'usd', +]; + +const tool = (name: string) => { + const found = a.tools.find((t) => t.name === name); + if (!found) throw new Error(`no tool ${name}`); + return found; +}; + +const engine = () => new RestEngine({} as OAuth2TokenService, {} as LoginTokenService); +const config = () => ({ baseUrl: a.connector.baseUrl, authType: a.connector.authType }); + +const lastRequest = () => + mockedAxios.mock.calls[mockedAxios.mock.calls.length - 1][0] as { + method: string; + url: string; + params?: Record; + headers: Record; + }; + +const SAMPLE_ARGS: Record> = { + fxmacrodata_data_catalogue: { currency: 'usd' }, + fxmacrodata_latest_announcements: { currency: 'usd' }, + fxmacrodata_indicator_history: { currency: 'usd', indicator: 'inflation' }, + fxmacrodata_release_calendar: { currency: 'usd' }, + fxmacrodata_fx_rates: { base: 'eur', quote: 'usd' }, +}; + +describe('fxmacrodata adapter - static spec conformance', () => { + beforeEach(() => mockedAxios.mockReset()); + + it('installs with no credentials and probes a free route', () => { + expect(a.connector.baseUrl).toBe('https://api.fxmacrodata.com/v1'); + expect(a.connector.authType).toBe('NONE'); + expect(a.connector.authConfig).toBeUndefined(); + expect(a.requiredEnvVars).toEqual([]); + expect(a.optionalEnvVars).toEqual(['FXMACRODATA_API_KEY']); + expect(a.probe).toEqual({ tool: 'fxmacrodata_data_catalogue', params: { currency: 'usd' } }); + }); + + it('treats the key as a secret and rejects whitespace in it at setup', () => { + const meta = a.envVarMeta.FXMACRODATA_API_KEY; + expect(meta.kind).toBe('credential'); + expect(meta.secret).toBe(true); + const pattern = new RegExp(meta.pattern); + expect(pattern.test('test-key')).toBe(true); + expect(pattern.test('test-key\n')).toBe(false); + expect(pattern.test(' test-key')).toBe(false); + }); + + it.each(Object.keys(SAMPLE_ARGS))('%s sends X-API-Key only when the key is set', async (name) => { + mockedAxios.mockResolvedValue({ data: {} }); + const mapping = tool(name).endpointMapping; + const args = SAMPLE_ARGS[name]; + + await engine().execute(config(), mapping, { ...args }); + expect(lastRequest().headers).not.toHaveProperty('X-API-Key'); + + await engine().execute(config(), mapping, { ...args, FXMACRODATA_API_KEY: '' }); + expect(lastRequest().headers).not.toHaveProperty('X-API-Key'); + + await engine().execute(config(), mapping, { ...args, FXMACRODATA_API_KEY: 'test-key' }); + expect(lastRequest().headers['X-API-Key']).toBe('test-key'); + expect(lastRequest().url).toMatch(/^https:\/\/api\.fxmacrodata\.com\/v1\//); + expect(lastRequest().params ?? {}).not.toHaveProperty('api_key'); + }); + + it.each([ + ['fxmacrodata_data_catalogue', { currency: 'eur' }, 'https://api.fxmacrodata.com/v1/data_catalogue/eur', {}], + ['fxmacrodata_latest_announcements', { currency: 'usd' }, 'https://api.fxmacrodata.com/v1/announcements/usd/latest', {}], + [ + 'fxmacrodata_indicator_history', + { currency: 'usd', indicator: 'inflation', start_date: '2026-01-01', end_date: '2026-06-30', limit: 100, offset: 20 }, + 'https://api.fxmacrodata.com/v1/announcements/usd/inflation', + { start_date: '2026-01-01', end_date: '2026-06-30', limit: '100', offset: '20' }, + ], + [ + 'fxmacrodata_release_calendar', + { currency: 'usd', indicator: 'policy_rate', start_date: '2026-10-01', end_date: '2026-12-31' }, + 'https://api.fxmacrodata.com/v1/calendar/usd', + { indicator: 'policy_rate', start_date: '2026-10-01', end_date: '2026-12-31' }, + ], + [ + 'fxmacrodata_fx_rates', + { base: 'eur', quote: 'usd', start_date: '2026-09-01', limit: 5 }, + 'https://api.fxmacrodata.com/v1/forex/eur/usd', + { start_date: '2026-09-01', limit: '5' }, + ], + ])('%s builds the request URL and query', async (name, args, url, params) => { + mockedAxios.mockResolvedValue({ data: {} }); + await engine().execute(config(), tool(name as string).endpointMapping, args as Record); + expect(lastRequest().method).toBe('GET'); + expect(lastRequest().url).toBe(url); + expect(Object.fromEntries(Object.entries(lastRequest().params ?? {}).map(([k, v]) => [k, String(v)]))).toEqual(params); + }); + + it('percent-encodes path values, so a slug cannot reach another route', async () => { + mockedAxios.mockResolvedValue({ data: {} }); + await engine().execute(config(), tool('fxmacrodata_indicator_history').endpointMapping, { + currency: 'usd', + indicator: '../../forex/eur/usd', + }); + expect(lastRequest().url).toBe( + 'https://api.fxmacrodata.com/v1/announcements/usd/..%2F..%2Fforex%2Feur%2Fusd', + ); + }); + + it('raises a 401 from the API instead of returning it as data', async () => { + const response = { + status: 401, + statusText: 'Unauthorized', + headers: {}, + config: { headers: new AxiosHeaders() }, + data: { error: 'api_key_required', code: 'api_key_required' }, + }; + mockedAxios.mockRejectedValue( + new AxiosError('Request failed with status code 401', 'ERR_BAD_REQUEST', response.config, null, response), + ); + await expect( + engine().execute(config(), tool('fxmacrodata_fx_rates').endpointMapping, { base: 'eur', quote: 'usd' }), + ).rejects.toMatchObject({ response: { status: 401 } }); + }); + + it('constrains inputs: supported currencies, YYYY-MM-DD dates, limit 1-100', () => { + for (const t of a.tools) { + const properties = t.parameters.properties; + for (const [key, property] of Object.entries(properties)) { + if (['currency', 'base', 'quote'].includes(key)) expect(property.enum).toEqual(CURRENCIES); + if (key.endsWith('_date')) expect(property.pattern).toBe('^[0-9]{4}-[0-9]{2}-[0-9]{2}$'); + if (key === 'limit') expect([property.minimum, property.maximum]).toEqual([1, 100]); + } + } + }); + + it('is read-only everywhere', () => { + for (const t of a.tools) { + expect(t.endpointMapping.method).toBe('GET'); + expect( + deriveToolAnnotations({ name: t.name, connectorType: 'REST', endpointMapping: t.endpointMapping }).readOnlyHint, + ).toBe(true); + } + }); + + it('documents the keyless scope, the free-tier markers and release timing', () => { + expect(a.instructions).toMatch(/data catalogue for every currency/); + expect(a.instructions).toMatch(/USD release calendar/); + expect(a.instructions).toMatch(/most recent 90 days/); + expect(a.instructions).toMatch(/FX rates/); + expect(a.instructions).toMatch(/freemium_delay/); + expect(a.instructions).toMatch(/`date`.*reference period/); + expect(a.instructions).toMatch(/announcement_datetime/); + const names = new Set(a.tools.map((t) => t.name)); + for (const m of a.instructions.matchAll(/\bfxmacrodata_[a-z_]+/g)) expect(names).toContain(m[0]); + }); +}); + +const live = process.env.RUN_FXMACRODATA_LIVE ? describe : describe.skip; + +live('fxmacrodata adapter - live API', () => { + beforeAll(() => { + mockedAxios.mockImplementation((cfg: unknown) => mockedAxios.__actual(cfg as any)); + }); + + const apiKey = process.env.FXMACRODATA_API_KEY; + const run = (name: string, args: Record, key?: string) => + engine().execute(config(), tool(name).endpointMapping, key ? { ...args, FXMACRODATA_API_KEY: key } : args) as Promise; + + it('reads the USD catalogue without a key', async () => { + const out = await run('fxmacrodata_data_catalogue', { currency: 'usd' }); + expect(out.inflation).toEqual(expect.objectContaining({ name: expect.any(String) })); + }, 30_000); + + it('reads USD releases without a key, marked as delayed', async () => { + const out = await run('fxmacrodata_latest_announcements', { currency: 'usd' }); + expect(out.data.length).toBeGreaterThan(0); + expect(out.freemium_delay).toEqual(expect.objectContaining({ applied: true, message: expect.any(String) })); + }, 30_000); + + it('pages USD CPI history without a key', async () => { + const out = await run('fxmacrodata_indicator_history', { currency: 'usd', indicator: 'inflation', limit: 2 }); + expect(out.pagination).toEqual(expect.objectContaining({ limit: 2, offset: 0, has_more: expect.any(Boolean) })); + expect(out.freemium_window).toEqual(expect.objectContaining({ applied: true })); + for (const row of out.data) expect(typeof row.announcement_datetime).toBe('number'); + }, 30_000); + + it('reads the USD calendar without a key', async () => { + const out = await run('fxmacrodata_release_calendar', { currency: 'usd' }); + expect(out.data.length).toBeGreaterThan(0); + expect(typeof out.data[0].announcement_datetime).toBe('number'); + }, 30_000); + + it('refuses FX rates without a key', async () => { + await expect(run('fxmacrodata_fx_rates', { base: 'eur', quote: 'usd', limit: 2 })).rejects.toMatchObject({ + response: { status: 401 }, + }); + }, 30_000); + + (apiKey ? it : it.skip)('reads EUR/USD with a key', async () => { + const out = await run('fxmacrodata_fx_rates', { base: 'eur', quote: 'usd', limit: 2 }, apiKey); + expect(out.data.length).toBeGreaterThan(0); + }, 30_000); +}); diff --git a/packages/frontend/public/logos/connectors/fxmacrodata.svg b/packages/frontend/public/logos/connectors/fxmacrodata.svg new file mode 100644 index 00000000..f832a252 --- /dev/null +++ b/packages/frontend/public/logos/connectors/fxmacrodata.svg @@ -0,0 +1 @@ +FXMacroData diff --git a/server.json b/server.json index d6cbb600..ceb242c7 100644 --- a/server.json +++ b/server.json @@ -2,7 +2,7 @@ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "io.github.HelpCode-ai/anythingmcp", "title": "AnythingMCP", - "description": "Any REST/SOAP/GraphQL/OData/SQL API as MCP tools for Claude & ChatGPT. 262 connectors: SAP, ERP.", + "description": "Any REST/SOAP/GraphQL/OData/SQL API as MCP tools for Claude & ChatGPT. 263 connectors: SAP, ERP.", "repository": { "url": "https://github.com/HelpCode-ai/anythingmcp", "source": "github"