From 8f50448a4a5c2524eb473dcedb6765cba1f4e61f Mon Sep 17 00:00:00 2001 From: OddsRelay <330405695+oddsrelay@users.noreply.github.com> Date: Fri, 9 Oct 2026 19:59:09 +0100 Subject: [PATCH 1/3] Add OddsRelay connector Co-Authored-By: Claude Opus 5.5 --- 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 + .../backend/src/adapters/gb/oddsrelay.json | 296 ++++++++++++++++++ .../src/adapters/gb/oddsrelay.live.spec.ts | 30 ++ .../public/logos/connectors/oddsrelay.svg | 1 + server.json | 2 +- 12 files changed, 345 insertions(+), 16 deletions(-) create mode 100644 packages/backend/src/adapters/gb/oddsrelay.json create mode 100644 packages/backend/src/adapters/gb/oddsrelay.live.spec.ts create mode 100644 packages/frontend/public/logos/connectors/oddsrelay.svg diff --git a/CITATION.cff b/CITATION.cff index 1376e1832..c64c34d06 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 331 pre-built adapters, with a focus on ERP and e-commerce + It ships 332 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 8a7617339..0e89a1952 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: 331 Connectors, 17 davon ohne API-Schlüssel. + AnythingMCP macht ERP-, E-Commerce-, REST-, SOAP- und SQL-Systeme zu MCP-Tools für Claude und ChatGPT: 332 Connectors, 17 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 331 fertige Adapter mit, darunter SAP, Etsy, weclapp und Amazon Seller; 17 davon kommen ohne API-Schlüssel aus. + Es bringt 332 fertige Adapter mit, darunter SAP, Etsy, weclapp und Amazon Seller; 17 davon kommen ohne API-Schlüssel aus.

@@ -111,7 +111,7 @@ Tools werden zur Laufzeit registriert, ohne Neustart. `{{VAR}}`-Werte pro Connec ## Connector-Katalog -331 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. +332 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 1ef473861..3e0732164 100644 --- a/README.ja.md +++ b/README.ja.md @@ -1,5 +1,5 @@

- AnythingMCP は ERP、E コマース、REST、SOAP、SQL の各システムを Claude と ChatGPT 用の MCP ツールに変換します。331 のコネクター、うち 17 は API キー不要。 + AnythingMCP は ERP、E コマース、REST、SOAP、SQL の各システムを Claude と ChatGPT 用の MCP ツールに変換します。332 のコネクター、うち 17 は 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 などを含む 331 種類の既製アダプターを同梱しており、うち 17 は API キー不要です。 + SAP、Etsy、weclapp、Amazon Seller などを含む 332 種類の既製アダプターを同梱しており、うち 17 は API キー不要です。

@@ -111,7 +111,7 @@ amd64 ではイメージの取得に約 30 秒、その 24 秒後に API が利 ## コネクターカタログ -331 個のアダプターで 2,400 以上のツールを提供しています。どのアダプターにも [anythingmcp.com/ja/guides](https://anythingmcp.com/ja/guides) に 7 言語の設定ガイドがあります。 +332 個のアダプターで 2,400 以上のツールを提供しています。どのアダプターにも [anythingmcp.com/ja/guides](https://anythingmcp.com/ja/guides) に 7 言語の設定ガイドがあります。 | カテゴリー | 例 | |---|---| diff --git a/README.md b/README.md index 0ce5eb007..8706cf8f7 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: 331 connectors, 17 of them with no API key. + AnythingMCP turns ERP, e-commerce, REST, SOAP and SQL systems into MCP tools for Claude and ChatGPT: 332 connectors, 17 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 331 ready connectors, among them SAP, Etsy, weclapp and Amazon Seller, and 17 of them need no API key. + It ships 332 ready connectors, among them SAP, Etsy, weclapp and Amazon Seller, and 17 of them need no API key.

@@ -103,7 +103,7 @@ Tools register at runtime, without a restart. Per-connector `{{VAR}}` values are ## Connector catalog -331 adapters, exposing 2,400+ tools. Every one has a setup guide on [anythingmcp.com/guides](https://anythingmcp.com/guides), in seven languages. +332 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 7062f8ab6..5fb6db6dc 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,5 +1,5 @@

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

AnythingMCP:自行托管的 MCP 网关

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

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

@@ -111,7 +111,7 @@ docker compose up -d ## 连接器目录 -共 331 个适配器,提供 2,400 多个工具。每个适配器都在 [anythingmcp.com/zh/guides](https://anythingmcp.com/zh/guides) 上提供七种语言的配置指南。 +共 332 个适配器,提供 2,400 多个工具。每个适配器都在 [anythingmcp.com/zh/guides](https://anythingmcp.com/zh/guides) 上提供七种语言的配置指南。 | 类别 | 示例 | |---|---| diff --git a/glama.json b/glama.json index 3e2d9e25f..9c0ca9491 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. 331 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. 332 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 b0402699a..4e9f40e9f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "anythingmcp", "version": "0.20.0", - "description": "Turn any REST/OpenAPI, SOAP/WSDL, GraphQL, OData or SQL API into MCP tools for Claude, ChatGPT and Copilot, no code. 331 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. 332 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 af0bb66d8..d9b5aa44b 100644 --- a/packages/backend/src/adapters/catalog.ts +++ b/packages/backend/src/adapters/catalog.ts @@ -66,6 +66,7 @@ import * as zalandoZds from './de/zalando-zds.json'; import * as zammad from './de/zammad.json'; import * as znuny from './de/znuny.json'; import * as companiesHouse from './gb/companies-house.json'; +import * as oddsrelay from './gb/oddsrelay.json'; import * as peoplehr from './gb/peoplehr.json'; import * as pinpoint from './gb/pinpoint.json'; import * as sage200 from './gb/sage-200.json'; @@ -610,6 +611,7 @@ const RAW_ADAPTERS: AdapterDefinition[] = [ zammad as unknown as AdapterDefinition, znuny as unknown as AdapterDefinition, companiesHouse as unknown as AdapterDefinition, + oddsrelay as unknown as AdapterDefinition, peoplehr as unknown as AdapterDefinition, pinpoint as unknown as AdapterDefinition, sage200 as unknown as AdapterDefinition, diff --git a/packages/backend/src/adapters/gb/oddsrelay.json b/packages/backend/src/adapters/gb/oddsrelay.json new file mode 100644 index 000000000..c2479c2d0 --- /dev/null +++ b/packages/backend/src/adapters/gb/oddsrelay.json @@ -0,0 +1,296 @@ +{ + "slug": "oddsrelay", + "name": "OddsRelay", + "description": "Pre-match odds from UK and Irish bookmakers, bet365 included, with Betfair, Smarkets, Matchbook and BETDAQ lay prices. Look up sports, events and bookmakers, then read one matched board or one event's prices.", + "instructions": "## OddsRelay\nThis connector reads the OddsRelay odds API (`https://api.oddsrelay.io/v2`): pre-match prices from UK and Irish bookmakers, bet365 included, beside Betfair, Smarkets, Matchbook and BETDAQ lay prices. Every tool only reads. Create an API key from your account at https://oddsrelay.io and paste it as `ODDSRELAY_KEY`: it is sent as a Bearer token on every call and is never a tool argument.\n\n**Tokens.** `oddsrelay_list_sports`, `oddsrelay_list_events`, `oddsrelay_list_bookmakers`, `oddsrelay_get_coverage`, `oddsrelay_get_usage` and the two quote tools cost nothing. `oddsrelay_get_board` and `oddsrelay_get_event_odds` spend tokens from the account behind the key, and this connector has no cap: it cannot refuse a call that costs more than the user expects. Before either one, call the matching quote tool with exactly the same arguments, tell the user the `cost` and `remaining` it returns, and go ahead only when they agree. Keep every call narrow: one competition key rather than a whole sport, only the bookmakers the user asked about, a `markets` filter and a start-time window.\n\n**Workflow.**\n1. `oddsrelay_get_coverage`: which bookmakers carry which sports on each board, and each board's market keys.\n2. `oddsrelay_list_sports`: competition keys. Each starts with its sport, such as `soccer`, and the sport alone covers all of its competitions.\n3. `oddsrelay_list_bookmakers`: bookmaker and exchange keys such as `william_hill` or `betfair_exchange`.\n4. `oddsrelay_quote_board`, then `oddsrelay_get_board` with `type`, `sport` and `bookmakers`.\n5. For one event: an `event_id` from a board, or `oddsrelay_list_events`; then `oddsrelay_quote_event_odds` and `oddsrelay_get_event_odds`. Event odds need a key that can read raw odds.\n\n**Paging.** `oddsrelay_list_sports` and `oddsrelay_list_events` return `meta.next_cursor`. Pass it back as `cursor`, with the same other arguments, until it is null. A cursor expires after a few minutes; start again from the first page when it does.\n\n**Errors** carry an `error.code` and a message:\n- 401: the key is missing or wrong.\n- 402 `insufficient_tokens`: the balance cannot cover this call; `oddsrelay_get_usage` shows the balance and when it resets.\n- 400 with `valid`: an unknown sport, bookmaker, exchange or market; pick a key from the `valid` list.\n- 403: the request reaches past what the key may read; pick from `valid` when it is given, otherwise tell the user.\n- 404 `unknown_event`: the event is no longer listed; find it again with `oddsrelay_list_events`.\n- 429: too many calls at once; wait for the `Retry-After` time, then try once more.\n- 503: not available right now; try again later.\n\n**Times** are ISO 8601 in UTC, such as `2026-10-10T12:00:00Z`.", + "region": "gb", + "category": "sports", + "icon": "oddsrelay", + "docsUrl": "https://oddsrelay.io/docs", + "prerequisites": "An OddsRelay account and an API key from it. Event odds need a key that can read raw odds.", + "requiredEnvVars": ["ODDSRELAY_KEY"], + "envVarMeta": { + "ODDSRELAY_KEY": { + "label": "API key", + "kind": "credential", + "secret": true, + "help": "Create a key from your account at oddsrelay.io, then paste it here.", + "link": "https://oddsrelay.io" + } + }, + "probe": { "tool": "oddsrelay_get_usage" }, + "connector": { + "name": "OddsRelay", + "type": "REST", + "baseUrl": "https://api.oddsrelay.io", + "authType": "BEARER_TOKEN", + "authConfig": { + "token": "{{ODDSRELAY_KEY}}" + }, + "headers": { + "Accept-Encoding": "gzip" + }, + "healthcheckPath": "/v2/usage" + }, + "tools": [ + { + "name": "oddsrelay_list_sports", + "description": "Lists the competition keys for the sport argument of the board and event tools. Each starts with its sport (such as soccer), which alone covers all of that sport's competitions. Costs no tokens. Paged: pass meta.next_cursor back as cursor until it is null.", + "parameters": { + "type": "object", + "properties": { + "cursor": { + "type": "string", + "description": "meta.next_cursor from the previous page. Leave out for the first page." + } + } + }, + "endpointMapping": { + "method": "GET", + "path": "/v2/sports", + "queryParams": { + "cursor": "$cursor" + } + } + }, + { + "name": "oddsrelay_list_events", + "description": "Lists the events of one sport with their event ids, teams and start times, for oddsrelay_get_event_odds. Costs no tokens. Paged: pass meta.next_cursor back as cursor until it is null.", + "parameters": { + "type": "object", + "properties": { + "sport": { + "type": "string", + "pattern": "^[a-z0-9_.-]+$", + "description": "One competition key from oddsrelay_list_sports, or a sport such as tennis for all of its competitions." + }, + "commenceTimeFrom": { + "type": "string", + "description": "Only events starting at or after this time, ISO 8601 in UTC." + }, + "commenceTimeTo": { + "type": "string", + "description": "Only events starting at or before this time, ISO 8601 in UTC." + }, + "cursor": { + "type": "string", + "description": "meta.next_cursor from the previous page, with the same other arguments." + } + }, + "required": ["sport"] + }, + "endpointMapping": { + "method": "GET", + "path": "/v2/events", + "queryParams": { + "sport": "$sport", + "commenceTimeFrom": "$commenceTimeFrom", + "commenceTimeTo": "$commenceTimeTo", + "cursor": "$cursor" + } + } + }, + { + "name": "oddsrelay_list_bookmakers", + "description": "Lists the bookmaker and exchange keys (such as william_hill or betfair_exchange) that the board tools take. Costs no tokens.", + "parameters": { + "type": "object", + "properties": {} + }, + "endpointMapping": { + "method": "GET", + "path": "/v2/bookmakers" + } + }, + { + "name": "oddsrelay_get_coverage", + "description": "Shows which bookmakers carry which sports on each matched board, and each board's market keys (the values the markets argument takes). Costs no tokens.", + "parameters": { + "type": "object", + "properties": {} + }, + "endpointMapping": { + "method": "GET", + "path": "/v2/coverage" + } + }, + { + "name": "oddsrelay_quote_board", + "description": "Returns what a matched board request would cost in tokens, and the tokens remaining, without reading the board. Costs no tokens. Call it with exactly the arguments you will pass to oddsrelay_get_board, and confirm the cost with the user first.", + "parameters": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["standard", "2up", "dutching", "each-way", "extra-place", "bog"], + "description": "Which matched board. oddsrelay_get_coverage lists the sports and market keys of each." + }, + "sport": { + "type": "string", + "pattern": "^[a-z0-9_.-]+$", + "description": "One competition key from oddsrelay_list_sports, or a sport such as soccer for all of its competitions. A competition key spends fewer tokens than a whole sport." + }, + "bookmakers": { + "type": "string", + "pattern": "^[a-z0-9_.-]+(,[a-z0-9_.-]+){0,4}$", + "description": "Bookmaker keys from oddsrelay_list_bookmakers, comma-separated, such as ladbrokes,william_hill. At most five keys; fewer keys spend fewer tokens." + }, + "exchanges": { + "type": "string", + "pattern": "^[a-z0-9_.-]+(,[a-z0-9_.-]+){0,3}$", + "description": "Optional exchange keys, comma-separated, such as betfair_exchange. Left out, every exchange on the board." + }, + "markets": { + "type": "string", + "pattern": "^[a-z0-9_.-]+(,[a-z0-9_.-]+){0,4}$", + "description": "Optional market keys for this board from oddsrelay_get_coverage, comma-separated, such as h2h. Left out, every market on the board." + }, + "commenceTimeFrom": { + "type": "string", + "description": "Optional: only events starting at or after this time, ISO 8601 in UTC." + }, + "commenceTimeTo": { + "type": "string", + "description": "Optional: only events starting at or before this time, ISO 8601 in UTC." + } + }, + "required": ["type", "sport", "bookmakers"] + }, + "endpointMapping": { + "method": "GET", + "path": "/v2/odds/{type}", + "queryParams": { + "quote": "true", + "sports": "$sport", + "bookmakers": "$bookmakers", + "exchanges": "$exchanges", + "markets": "$markets", + "commenceTimeFrom": "$commenceTimeFrom", + "commenceTimeTo": "$commenceTimeTo" + } + } + }, + { + "name": "oddsrelay_get_board", + "description": "Reads one matched board: bookmaker back prices beside exchange lay prices, for one sport at the bookmakers you name. Spends tokens, and this connector has no cap: run oddsrelay_quote_board with the same arguments first and confirm the cost with the user.", + "parameters": { + "type": "object", + "properties": { + "type": { + "type": "string", + "enum": ["standard", "2up", "dutching", "each-way", "extra-place", "bog"], + "description": "Which matched board. oddsrelay_get_coverage lists the sports and market keys of each." + }, + "sport": { + "type": "string", + "pattern": "^[a-z0-9_.-]+$", + "description": "One competition key from oddsrelay_list_sports, or a sport such as soccer for all of its competitions. A competition key spends fewer tokens than a whole sport." + }, + "bookmakers": { + "type": "string", + "pattern": "^[a-z0-9_.-]+(,[a-z0-9_.-]+){0,4}$", + "description": "Bookmaker keys from oddsrelay_list_bookmakers, comma-separated, such as ladbrokes,william_hill. At most five keys; fewer keys spend fewer tokens." + }, + "exchanges": { + "type": "string", + "pattern": "^[a-z0-9_.-]+(,[a-z0-9_.-]+){0,3}$", + "description": "Optional exchange keys, comma-separated, such as betfair_exchange. Left out, every exchange on the board." + }, + "markets": { + "type": "string", + "pattern": "^[a-z0-9_.-]+(,[a-z0-9_.-]+){0,4}$", + "description": "Optional market keys for this board from oddsrelay_get_coverage, comma-separated, such as h2h. Left out, every market on the board." + }, + "commenceTimeFrom": { + "type": "string", + "description": "Optional: only events starting at or after this time, ISO 8601 in UTC." + }, + "commenceTimeTo": { + "type": "string", + "description": "Optional: only events starting at or before this time, ISO 8601 in UTC." + } + }, + "required": ["type", "sport", "bookmakers"] + }, + "endpointMapping": { + "method": "GET", + "path": "/v2/odds/{type}", + "queryParams": { + "sports": "$sport", + "bookmakers": "$bookmakers", + "exchanges": "$exchanges", + "markets": "$markets", + "commenceTimeFrom": "$commenceTimeFrom", + "commenceTimeTo": "$commenceTimeTo" + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "oddsrelay_quote_event_odds", + "description": "Returns what one event's odds would cost in tokens, and the tokens remaining, without reading them. Costs no tokens. Call it with the event_id you will pass to oddsrelay_get_event_odds, and confirm the cost with the user first.", + "parameters": { + "type": "object", + "properties": { + "event_id": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]+$", + "description": "An event id such as or_evt_917dd44bce05, from a board or oddsrelay_list_events." + } + }, + "required": ["event_id"] + }, + "endpointMapping": { + "method": "GET", + "path": "/v2/odds/event/{event_id}", + "encodePathParams": true, + "queryParams": { + "quote": "true" + } + } + }, + { + "name": "oddsrelay_get_event_odds", + "description": "Reads one event's prices at every bookmaker and exchange carrying it, market by market. Needs a key that can read raw odds. Spends tokens, and this connector has no cap: run oddsrelay_quote_event_odds first and confirm the cost with the user.", + "parameters": { + "type": "object", + "properties": { + "event_id": { + "type": "string", + "pattern": "^[A-Za-z0-9_-]+$", + "description": "An event id such as or_evt_917dd44bce05, from a board or oddsrelay_list_events." + } + }, + "required": ["event_id"] + }, + "endpointMapping": { + "method": "GET", + "path": "/v2/odds/event/{event_id}", + "encodePathParams": true + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "oddsrelay_get_usage", + "description": "Shows the key's token balance and when it resets, and which boards the key can read. Costs no tokens, and is the connection check.", + "parameters": { + "type": "object", + "properties": {} + }, + "endpointMapping": { + "method": "GET", + "path": "/v2/usage" + } + } + ] +} diff --git a/packages/backend/src/adapters/gb/oddsrelay.live.spec.ts b/packages/backend/src/adapters/gb/oddsrelay.live.spec.ts new file mode 100644 index 000000000..bb29041a4 --- /dev/null +++ b/packages/backend/src/adapters/gb/oddsrelay.live.spec.ts @@ -0,0 +1,30 @@ +import * as adapter from './oddsrelay.json'; +const a = adapter as unknown as { + connector: { + baseUrl: string; + authType: string; + authConfig: Record; + headers: Record; + healthcheckPath: string; + }; + probe: { tool: string }; + tools: Array<{ name: string; annotations?: { readOnlyHint?: boolean } }>; +}; +describe('oddsrelay adapter — static spec conformance', () => { + it('api.oddsrelay.io, /v2 routes', () => expect(a.connector.baseUrl).toBe('https://api.oddsrelay.io')); + it('Bearer auth', () => { + expect(a.connector.authType).toBe('BEARER_TOKEN'); + expect(a.connector.authConfig.token).toBe('{{ODDSRELAY_KEY}}'); + }); + it('asks for gzip, which some boards require', () => { + expect(a.connector.headers['Accept-Encoding']).toBe('gzip'); + }); + it('checks the connection with the usage call, which spends no tokens', () => { + expect(a.probe.tool).toBe('oddsrelay_get_usage'); + expect(a.connector.healthcheckPath).toBe('/v2/usage'); + }); + it('marks only the two tools that spend tokens as not read-only', () => { + const spending = a.tools.filter((t) => t.annotations?.readOnlyHint === false).map((t) => t.name); + expect(spending.sort()).toEqual(['oddsrelay_get_board', 'oddsrelay_get_event_odds']); + }); +}); diff --git a/packages/frontend/public/logos/connectors/oddsrelay.svg b/packages/frontend/public/logos/connectors/oddsrelay.svg new file mode 100644 index 000000000..74d2662a5 --- /dev/null +++ b/packages/frontend/public/logos/connectors/oddsrelay.svg @@ -0,0 +1 @@ +OddsRelay \ No newline at end of file diff --git a/server.json b/server.json index 726ce45b1..d8453d0b5 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. 331 connectors: SAP, ERP.", + "description": "Any REST/SOAP/GraphQL/OData/SQL API as MCP tools for Claude & ChatGPT. 332 connectors: SAP, ERP.", "repository": { "url": "https://github.com/HelpCode-ai/anythingmcp", "source": "github" From 71987c3326e631f97dd47b0d09033ba779e6cf84 Mon Sep 17 00:00:00 2001 From: OddsRelay <330405695+oddsrelay@users.noreply.github.com> Date: Fri, 16 Oct 2026 15:12:25 +0100 Subject: [PATCH 2/3] Shrink OddsRelay coverage reply and add a live check - oddsrelay_get_coverage gets a JMESPath responseMapping. The connector always sends the key, so /v2/coverage answers with its keyed view (venues with prices now, per board, with event counts and last-seen times; it has no markets field). The mapping keeps, per board, each bookmaker's and exchange's key with its sport keys, and drops names, counts and times. - Tool and parameter descriptions no longer say coverage lists market keys; an unknown market key is refused with the valid keys listed. - The description and instructions say what the connector covers and leave the bookmaker list to oddsrelay_list_bookmakers. - oddsrelay.live.spec.ts pins the coverage mapping on a keyed fixture, and adds a block behind RUN_ODDSRELAY_LIVE=1 (key from ODDSRELAY_KEY) that calls usage and quotes one board, both free. It is skipped when either variable is unset. Co-Authored-By: Claude Opus 5.5 Signed-off-by: OddsRelay <330405695+oddsrelay@users.noreply.github.com> --- .../backend/src/adapters/gb/oddsrelay.json | 20 +++-- .../src/adapters/gb/oddsrelay.live.spec.ts | 75 ++++++++++++++++++- 2 files changed, 87 insertions(+), 8 deletions(-) diff --git a/packages/backend/src/adapters/gb/oddsrelay.json b/packages/backend/src/adapters/gb/oddsrelay.json index c2479c2d0..0c043a772 100644 --- a/packages/backend/src/adapters/gb/oddsrelay.json +++ b/packages/backend/src/adapters/gb/oddsrelay.json @@ -1,8 +1,8 @@ { "slug": "oddsrelay", "name": "OddsRelay", - "description": "Pre-match odds from UK and Irish bookmakers, bet365 included, with Betfair, Smarkets, Matchbook and BETDAQ lay prices. Look up sports, events and bookmakers, then read one matched board or one event's prices.", - "instructions": "## OddsRelay\nThis connector reads the OddsRelay odds API (`https://api.oddsrelay.io/v2`): pre-match prices from UK and Irish bookmakers, bet365 included, beside Betfair, Smarkets, Matchbook and BETDAQ lay prices. Every tool only reads. Create an API key from your account at https://oddsrelay.io and paste it as `ODDSRELAY_KEY`: it is sent as a Bearer token on every call and is never a tool argument.\n\n**Tokens.** `oddsrelay_list_sports`, `oddsrelay_list_events`, `oddsrelay_list_bookmakers`, `oddsrelay_get_coverage`, `oddsrelay_get_usage` and the two quote tools cost nothing. `oddsrelay_get_board` and `oddsrelay_get_event_odds` spend tokens from the account behind the key, and this connector has no cap: it cannot refuse a call that costs more than the user expects. Before either one, call the matching quote tool with exactly the same arguments, tell the user the `cost` and `remaining` it returns, and go ahead only when they agree. Keep every call narrow: one competition key rather than a whole sport, only the bookmakers the user asked about, a `markets` filter and a start-time window.\n\n**Workflow.**\n1. `oddsrelay_get_coverage`: which bookmakers carry which sports on each board, and each board's market keys.\n2. `oddsrelay_list_sports`: competition keys. Each starts with its sport, such as `soccer`, and the sport alone covers all of its competitions.\n3. `oddsrelay_list_bookmakers`: bookmaker and exchange keys such as `william_hill` or `betfair_exchange`.\n4. `oddsrelay_quote_board`, then `oddsrelay_get_board` with `type`, `sport` and `bookmakers`.\n5. For one event: an `event_id` from a board, or `oddsrelay_list_events`; then `oddsrelay_quote_event_odds` and `oddsrelay_get_event_odds`. Event odds need a key that can read raw odds.\n\n**Paging.** `oddsrelay_list_sports` and `oddsrelay_list_events` return `meta.next_cursor`. Pass it back as `cursor`, with the same other arguments, until it is null. A cursor expires after a few minutes; start again from the first page when it does.\n\n**Errors** carry an `error.code` and a message:\n- 401: the key is missing or wrong.\n- 402 `insufficient_tokens`: the balance cannot cover this call; `oddsrelay_get_usage` shows the balance and when it resets.\n- 400 with `valid`: an unknown sport, bookmaker, exchange or market; pick a key from the `valid` list.\n- 403: the request reaches past what the key may read; pick from `valid` when it is given, otherwise tell the user.\n- 404 `unknown_event`: the event is no longer listed; find it again with `oddsrelay_list_events`.\n- 429: too many calls at once; wait for the `Retry-After` time, then try once more.\n- 503: not available right now; try again later.\n\n**Times** are ISO 8601 in UTC, such as `2026-10-10T12:00:00Z`.", + "description": "Pre-match odds from UK and Irish bookmakers with exchange lay prices. Look up sports, events and bookmakers, then read one matched board or one event's prices.", + "instructions": "## OddsRelay\nThis connector reads the OddsRelay odds API (`https://api.oddsrelay.io/v2`): pre-match odds from UK and Irish bookmakers with exchange lay prices; `oddsrelay_list_bookmakers` lists the bookmakers and exchanges. Every tool only reads. Create an API key from your account at https://oddsrelay.io and paste it as `ODDSRELAY_KEY`: it is sent as a Bearer token on every call and is never a tool argument.\n\n**Tokens.** `oddsrelay_list_sports`, `oddsrelay_list_events`, `oddsrelay_list_bookmakers`, `oddsrelay_get_coverage`, `oddsrelay_get_usage` and the two quote tools cost nothing. `oddsrelay_get_board` and `oddsrelay_get_event_odds` spend tokens from the account behind the key, and this connector has no cap: it cannot refuse a call that costs more than the user expects. Before either one, call the matching quote tool with exactly the same arguments, tell the user the `cost` and `remaining` it returns, and go ahead only when they agree. Keep every call narrow: one competition key rather than a whole sport, only the bookmakers the user asked about, a `markets` filter and a start-time window.\n\n**Workflow.**\n1. `oddsrelay_get_coverage`: for each board the key can read, the bookmakers and exchanges with prices now and their sports.\n2. `oddsrelay_list_sports`: competition keys. Each starts with its sport, such as `soccer`, and the sport alone covers all of its competitions.\n3. `oddsrelay_list_bookmakers`: bookmaker and exchange keys such as `william_hill` or `betfair_exchange`.\n4. `oddsrelay_quote_board`, then `oddsrelay_get_board` with `type`, `sport` and `bookmakers`.\n5. For one event: an `event_id` from a board, or `oddsrelay_list_events`; then `oddsrelay_quote_event_odds` and `oddsrelay_get_event_odds`. Event odds need a key that can read raw odds.\n\n**Paging.** `oddsrelay_list_sports` and `oddsrelay_list_events` return `meta.next_cursor`. Pass it back as `cursor`, with the same other arguments, until it is null. A cursor expires after a few minutes; start again from the first page when it does.\n\n**Errors** carry an `error.code` and a message:\n- 401: the key is missing or wrong.\n- 402 `insufficient_tokens`: the balance cannot cover this call; `oddsrelay_get_usage` shows the balance and when it resets.\n- 400 with `valid`: an unknown sport, bookmaker, exchange or market; pick a key from the `valid` list.\n- 403: the request reaches past what the key may read; pick from `valid` when it is given, otherwise tell the user.\n- 404 `unknown_event`: the event is no longer listed; find it again with `oddsrelay_list_events`.\n- 429: too many calls at once; wait for the `Retry-After` time, then try once more.\n- 503: not available right now; try again later.\n\n**Times** are ISO 8601 in UTC, such as `2026-10-10T12:00:00Z`.", "region": "gb", "category": "sports", "icon": "oddsrelay", @@ -104,7 +104,7 @@ }, { "name": "oddsrelay_get_coverage", - "description": "Shows which bookmakers carry which sports on each matched board, and each board's market keys (the values the markets argument takes). Costs no tokens.", + "description": "Shows, for each matched board the key can read, which bookmakers and exchanges have prices now and for which sports. Costs no tokens.", "parameters": { "type": "object", "properties": {} @@ -112,6 +112,12 @@ "endpointMapping": { "method": "GET", "path": "/v2/coverage" + }, + "responseMapping": { + "transform": { + "mode": "jmespath", + "expression": "{generated_at: generated_at, products: from_items(items(products)[*].[@[0], {bookmakers: from_items(@[1].bookmakers[*].[key, keys(sports)]), exchanges: from_items(@[1].exchanges[*].[key, keys(sports)])}])}" + } } }, { @@ -123,7 +129,7 @@ "type": { "type": "string", "enum": ["standard", "2up", "dutching", "each-way", "extra-place", "bog"], - "description": "Which matched board. oddsrelay_get_coverage lists the sports and market keys of each." + "description": "Which matched board. oddsrelay_get_coverage lists the bookmakers and sports on each." }, "sport": { "type": "string", @@ -143,7 +149,7 @@ "markets": { "type": "string", "pattern": "^[a-z0-9_.-]+(,[a-z0-9_.-]+){0,4}$", - "description": "Optional market keys for this board from oddsrelay_get_coverage, comma-separated, such as h2h. Left out, every market on the board." + "description": "Optional market keys for this board, comma-separated, such as h2h. Left out, every market on the board. An unknown key is refused with the valid keys listed." }, "commenceTimeFrom": { "type": "string", @@ -179,7 +185,7 @@ "type": { "type": "string", "enum": ["standard", "2up", "dutching", "each-way", "extra-place", "bog"], - "description": "Which matched board. oddsrelay_get_coverage lists the sports and market keys of each." + "description": "Which matched board. oddsrelay_get_coverage lists the bookmakers and sports on each." }, "sport": { "type": "string", @@ -199,7 +205,7 @@ "markets": { "type": "string", "pattern": "^[a-z0-9_.-]+(,[a-z0-9_.-]+){0,4}$", - "description": "Optional market keys for this board from oddsrelay_get_coverage, comma-separated, such as h2h. Left out, every market on the board." + "description": "Optional market keys for this board, comma-separated, such as h2h. Left out, every market on the board. An unknown key is refused with the valid keys listed." }, "commenceTimeFrom": { "type": "string", diff --git a/packages/backend/src/adapters/gb/oddsrelay.live.spec.ts b/packages/backend/src/adapters/gb/oddsrelay.live.spec.ts index bb29041a4..419a73130 100644 --- a/packages/backend/src/adapters/gb/oddsrelay.live.spec.ts +++ b/packages/backend/src/adapters/gb/oddsrelay.live.spec.ts @@ -1,4 +1,11 @@ import * as adapter from './oddsrelay.json'; +import { RestEngine } from '../../connectors/engines/rest.engine'; +import { OAuth2TokenService } from '../../connectors/engines/oauth2-token.service'; +import { LoginTokenService } from '../../connectors/engines/login-token.service'; +import { applyResponseTransform } from '../../connectors/response-transform.util'; + +/** Live (spends no tokens): RUN_ODDSRELAY_LIVE=1 ODDSRELAY_KEY= npx jest src/adapters/gb/oddsrelay.live.spec.ts */ + const a = adapter as unknown as { connector: { baseUrl: string; @@ -8,8 +15,16 @@ const a = adapter as unknown as { healthcheckPath: string; }; probe: { tool: string }; - tools: Array<{ name: string; annotations?: { readOnlyHint?: boolean } }>; + tools: Array<{ + name: string; + parameters: { properties: Record }; + endpointMapping: { method: string; path: string; queryParams?: Record }; + responseMapping?: { transform: { mode: string; expression: string } }; + annotations?: { readOnlyHint?: boolean }; + }>; }; +const tool = (name: string) => a.tools.find((t) => t.name === name)!; + describe('oddsrelay adapter — static spec conformance', () => { it('api.oddsrelay.io, /v2 routes', () => expect(a.connector.baseUrl).toBe('https://api.oddsrelay.io')); it('Bearer auth', () => { @@ -27,4 +42,62 @@ describe('oddsrelay adapter — static spec conformance', () => { const spending = a.tools.filter((t) => t.annotations?.readOnlyHint === false).map((t) => t.name); expect(spending.sort()).toEqual(['oddsrelay_get_board', 'oddsrelay_get_event_odds']); }); + // The connector always sends the key, so /v2/coverage answers with its keyed view. + it('shrinks the keyed coverage reply to bookmaker and sport keys per board', () => { + const keyed = { + region: 'uk', + generated_at: '2026-10-16T12:00:00Z', + products: { + standard: { + bookmakers: [ + { key: 'ladbrokes', name: 'Ladbrokes', sports: { soccer: { events: 12, last_seen: '2026-10-16T11:59:50Z' } } }, + ], + exchanges: [{ key: 'betfair_exchange', name: 'Betfair Exchange', sports: { soccer: { events: 40, last_seen: null } } }], + }, + }, + }; + const out = applyResponseTransform(keyed, tool('oddsrelay_get_coverage').responseMapping); + expect(out.applied).toBe(true); + expect(out.value).toEqual({ + generated_at: '2026-10-16T12:00:00Z', + products: { standard: { bookmakers: { ladbrokes: ['soccer'] }, exchanges: { betfair_exchange: ['soccer'] } } }, + }); + }); +}); + +const live = process.env.RUN_ODDSRELAY_LIVE === '1' && !!process.env.ODDSRELAY_KEY; +(live ? describe : describe.skip)('oddsrelay adapter — live, spends no tokens', () => { + const engine = new RestEngine({} as OAuth2TokenService, {} as LoginTokenService); + const run = async (name: string, params: Record = {}): Promise => { + const t = tool(name); + const raw = await engine.execute( + { + baseUrl: a.connector.baseUrl, + authType: a.connector.authType, + authConfig: { token: process.env.ODDSRELAY_KEY }, + headers: a.connector.headers, + }, + t.endpointMapping, + params, + ); + return applyResponseTransform(raw, t.responseMapping).value; + }; + + it('usage answers with the balance and the key', async () => { + const usage = await run('oddsrelay_get_usage'); + expect(usage).toEqual( + expect.objectContaining({ rate_card: expect.any(String), tokens: expect.any(Object), key: expect.any(Object) }), + ); + }, 30000); + + it('quotes one board picked from the mapped coverage', async () => { + const coverage = await run('oddsrelay_get_coverage'); + const boards = tool('oddsrelay_quote_board').parameters.properties.type.enum!; + const type = boards.find((b) => Object.keys(coverage.products?.[b]?.bookmakers ?? {}).length > 0); + expect(type).toBeDefined(); + const [bookmaker, sports] = Object.entries(coverage.products[type!].bookmakers as Record)[0]; + const quote = await run('oddsrelay_quote_board', { type, sport: sports[0], bookmakers: bookmaker }); + expect(quote).toEqual(expect.objectContaining({ cost: expect.any(Number), rate_card: expect.any(String) })); + expect(quote).toHaveProperty('remaining'); + }, 30000); }); From c549a1386e78b4b623500802259f85419ef1f1f1 Mon Sep 17 00:00:00 2001 From: "helpcode.ai" Date: Sun, 18 Oct 2026 20:02:08 +0200 Subject: [PATCH 3/3] OddsRelay coverage: default missing bookmakers, exchanges and sports to empty --- packages/backend/src/adapters/gb/oddsrelay.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/adapters/gb/oddsrelay.json b/packages/backend/src/adapters/gb/oddsrelay.json index 0c043a772..f0c2245c2 100644 --- a/packages/backend/src/adapters/gb/oddsrelay.json +++ b/packages/backend/src/adapters/gb/oddsrelay.json @@ -116,7 +116,7 @@ "responseMapping": { "transform": { "mode": "jmespath", - "expression": "{generated_at: generated_at, products: from_items(items(products)[*].[@[0], {bookmakers: from_items(@[1].bookmakers[*].[key, keys(sports)]), exchanges: from_items(@[1].exchanges[*].[key, keys(sports)])}])}" + "expression": "{generated_at: generated_at, products: from_items(items(products || `{}`)[*].[@[0], {bookmakers: from_items((@[1].bookmakers || `[]`)[*].[key, keys(sports || `{}`)]), exchanges: from_items((@[1].exchanges || `[]`)[*].[key, keys(sports || `{}`)])}])}" } } },