diff --git a/CITATION.cff b/CITATION.cff index ed0f5172..d71af617 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 264 pre-built adapters, with a focus on ERP and e-commerce + It ships 265 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 b1282721..f5dc8f6e 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: 264 Connectors, 21 davon ohne API-Schlüssel. + AnythingMCP macht ERP-, E-Commerce-, REST-, SOAP- und SQL-Systeme zu MCP-Tools für Claude und ChatGPT: 265 Connectors, 21 davon ohne API-Schlüssel.

AnythingMCP

@@ -10,7 +10,7 @@

Mach aus jeder REST-/OpenAPI-, SOAP-, GraphQL- oder SQL-API MCP-Tools für Claude, ChatGPT und Copilot.
- Selbst gehosteter MCP-Server und MCP-Gateway, ohne Code. 264 fertige Adapter, auch für ERP und E-Commerce: SAP Business One, Xentral, weclapp, Shopware, WooCommerce, Amazon Seller, Kaufland und viele mehr. + Selbst gehosteter MCP-Server und MCP-Gateway, ohne Code. 265 fertige Adapter, auch für ERP und E-Commerce: SAP Business One, Xentral, weclapp, Shopware, WooCommerce, Amazon Seller, Kaufland und viele mehr.

@@ -40,7 +40,7 @@ docker compose up -d # → http://localhost:3000 Drei Begriffe tauchen immer wieder auf und bezeichnen unterschiedliche Dinge: -- Ein **Adapter** ist eine der 264 JSON-Definitionen in diesem Repository — SAP Business One, Odoo, weclapp, Xentral, Shopware, WooCommerce, Amazon Seller, DHL und viele weitere. 21 davon benötigen überhaupt keinen API-Schlüssel; bei den anderen gibst du deine Zugangsdaten beim Import an. +- Ein **Adapter** ist eine der 265 JSON-Definitionen in diesem Repository — SAP Business One, Odoo, weclapp, Xentral, Shopware, WooCommerce, Amazon Seller, DHL und viele weitere. 21 davon benötigen überhaupt keinen API-Schlüssel; bei den anderen gibst du deine Zugangsdaten beim Import an. - Ein **Connector** entsteht, wenn du einen Adapter oder deine eigene OpenAPI-Spezifikation, Postman-Collection, WSDL, einen GraphQL-Endpunkt oder eine Datenbank in deinem Workspace konfigurierst. In wenigen Minuten lässt sich so eine Verbindung einrichten, ohne einen MCP-Server zu programmieren. - Ein **MCP-Server** ist die URL, die du Claude übergibst. Er stellt ausschließlich die Connectors bereit, die du ihm zuweist. @@ -334,7 +334,7 @@ KI-Clients sprechen MCP, deine Systeme dagegen REST, SOAP, GraphQL und SQL. Eine ## Der Adapterkatalog -264 Adapter mit mehr als 2.400 Tools. **21 benötigen keinen API-Schlüssel**. Bei den übrigen gibst du deine Zugangsdaten beim Import an; danach stehen die Tools sofort bereit. Für jeden Adapter gibt es auf [anythingmcp.com/guides](https://anythingmcp.com/guides) eine Einrichtungsanleitung in sieben Sprachen. +265 Adapter mit mehr als 2.400 Tools. **21 benötigen keinen API-Schlüssel**. Bei den übrigen gibst du deine Zugangsdaten beim Import an; danach stehen die Tools sofort bereit. Für jeden Adapter gibt es auf [anythingmcp.com/guides](https://anythingmcp.com/guides) eine Einrichtungsanleitung in sieben Sprachen. | Kategorie | Beispiele | |---|---| diff --git a/README.ja.md b/README.ja.md index 454b5c97..a693b21b 100644 --- a/README.ja.md +++ b/README.ja.md @@ -1,5 +1,5 @@

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

AnythingMCP

@@ -10,7 +10,7 @@

REST/OpenAPI、SOAP、GraphQL、SQL のあらゆる API を、Claude、ChatGPT、Copilot 用の MCP ツールに変換します。
- コード不要のセルフホスト型 MCP サーバー兼ゲートウェイです。SAP Business One、Odoo、Xentral、weclapp、Shopware、WooCommerce、Amazon Seller、Kaufland など、ERP や E コマースを含む 264 種類の既製アダプターを用意しています。 + コード不要のセルフホスト型 MCP サーバー兼ゲートウェイです。SAP Business One、Odoo、Xentral、weclapp、Shopware、WooCommerce、Amazon Seller、Kaufland など、ERP や E コマースを含む 265 種類の既製アダプターを用意しています。

@@ -40,7 +40,7 @@ docker compose up -d # → http://localhost:3000 この文書で繰り返し使う 3 つの用語は、それぞれ異なるものを指します。 -- **アダプター(adapter)**は、このリポジトリに含まれる 264 個の JSON 定義のいずれかです。SAP Business One、Odoo、weclapp、Xentral、Shopware、WooCommerce、Amazon Seller、DHL などがあります。そのうち 21 個は API キーを一切必要とせず、それ以外はインポート時に認証情報を設定します。 +- **アダプター(adapter)**は、このリポジトリに含まれる 265 個の JSON 定義のいずれかです。SAP Business One、Odoo、weclapp、Xentral、Shopware、WooCommerce、Amazon Seller、DHL などがあります。そのうち 21 個は API キーを一切必要とせず、それ以外はインポート時に認証情報を設定します。 - **コネクター(connector)**は、アダプター、または独自の OpenAPI 仕様・Postman コレクション・WSDL・GraphQL エンドポイント・データベースを、ワークスペース内で設定したものです。接続先を指定すれば、MCP サーバーを書くことなく数分で設定できます。 - **MCP サーバー**は、Claude に渡す URL です。そのサーバーに割り当てたコネクターだけを公開します。 @@ -334,7 +334,7 @@ AI クライアントは MCP を使いますが、業務システムは REST、S ## アダプターカタログ -264 個のアダプターで、2,400 以上のツールを公開できます。**21 個は API キーが不要**です。それ以外はインポート時に認証情報を設定すれば、すぐにツールを利用できます。各アダプターには [anythingmcp.com/guides](https://anythingmcp.com/guides) で 7 言語のセットアップガイドを用意しています。 +265 個のアダプターで、2,400 以上のツールを公開できます。**21 個は API キーが不要**です。それ以外はインポート時に認証情報を設定すれば、すぐにツールを利用できます。各アダプターには [anythingmcp.com/guides](https://anythingmcp.com/guides) で 7 言語のセットアップガイドを用意しています。 | カテゴリー | 例 | |---|---| diff --git a/README.md b/README.md index f7d40b8f..bc355c54 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: 264 connectors, 21 of them with no API key. + AnythingMCP turns ERP, e-commerce, REST, SOAP and SQL systems into MCP tools for Claude and ChatGPT: 265 connectors, 21 of them with no API key.

AnythingMCP

@@ -10,7 +10,7 @@

Turn any REST/OpenAPI, SOAP, GraphQL or SQL API into MCP tools for Claude, ChatGPT and Copilot.
- A self-hosted MCP server and gateway, no code, 264 ready connectors including ERP and e-commerce: SAP Business One, Odoo, Xentral, weclapp, Shopware, WooCommerce, Amazon Seller, Kaufland and more. + A self-hosted MCP server and gateway, no code, 265 ready connectors including ERP and e-commerce: SAP Business One, Odoo, Xentral, weclapp, Shopware, WooCommerce, Amazon Seller, Kaufland and more.

@@ -43,7 +43,7 @@ docker compose up -d # → http://localhost:3000 Three words appear throughout and mean three different things: -- an **adapter** is one of the 264 JSON definitions that ship in this repo — SAP Business One, Odoo, weclapp, Xentral, Shopware, WooCommerce, Amazon Seller, DHL and the rest. 21 of them need no API key at all; the others ask for your credentials at import. +- an **adapter** is one of the 265 JSON definitions that ship in this repo — SAP Business One, Odoo, weclapp, Xentral, Shopware, WooCommerce, Amazon Seller, DHL and the rest. 21 of them need no API key at all; the others ask for your credentials at import. - a **connector** is an adapter, or your own OpenAPI spec / Postman collection / WSDL / GraphQL endpoint / database, once you have configured it in your workspace. Anything you can point at, in minutes, without writing an MCP server. - an **MCP server** is the URL you hand to Claude. It exposes the connectors you assign to it, and nothing else. @@ -335,7 +335,7 @@ AI clients speak MCP, but your systems speak REST, SOAP, GraphQL and SQL. Writin ## The adapter catalog -264 adapters, exposing 2,400+ tools. **21 need no API key**; the rest ask for your credentials at import and the tools are available immediately. Every one has a setup guide on [anythingmcp.com/guides](https://anythingmcp.com/guides), in seven languages. +265 adapters, exposing 2,400+ tools. **21 need no API key**; the rest ask for your credentials at import and the tools are available immediately. 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 0fe66500..9ab18086 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,5 +1,5 @@

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

AnythingMCP

@@ -10,7 +10,7 @@

将任意 REST/OpenAPI、SOAP、GraphQL 或 SQL API 转化为 Claude、ChatGPT 和 Copilot 可用的 MCP 工具。
- 自行托管的 MCP 服务器与网关,无需编写代码,264 个现成适配器开箱即用,涵盖 ERP 和电子商务:SAP Business One、Odoo、Xentral、weclapp、Shopware、WooCommerce、Amazon Seller、Kaufland 等。 + 自行托管的 MCP 服务器与网关,无需编写代码,265 个现成适配器开箱即用,涵盖 ERP 和电子商务:SAP Business One、Odoo、Xentral、weclapp、Shopware、WooCommerce、Amazon Seller、Kaufland 等。

@@ -40,7 +40,7 @@ docker compose up -d # → http://localhost:3000 本文反复使用的三个术语,分别指不同的概念: -- **适配器(adapter)**:本仓库随附的 264 个 JSON 定义之一,例如 SAP Business One、Odoo、weclapp、Xentral、Shopware、WooCommerce、Amazon Seller、DHL 等。其中 21 个完全不需要 API 密钥,其余适配器会在导入时要求你提供相应凭据。 +- **适配器(adapter)**:本仓库随附的 265 个 JSON 定义之一,例如 SAP Business One、Odoo、weclapp、Xentral、Shopware、WooCommerce、Amazon Seller、DHL 等。其中 21 个完全不需要 API 密钥,其余适配器会在导入时要求你提供相应凭据。 - **连接器(connector)**:在工作区中配置好的适配器,或你自己的 OpenAPI 规范、Postman 集合、WSDL、GraphQL 端点或数据库。只要有可连接的目标,就能在几分钟内完成配置,无需编写 MCP 服务器。 - **MCP 服务器**:你提供给 Claude 的那个 URL。它只公开分配给它的连接器,不会公开其他连接器。 @@ -334,7 +334,7 @@ AI 客户端使用 MCP,而你的系统使用 REST、SOAP、GraphQL 和 SQL。 ## 适配器目录 -264 个适配器,提供 2,400 多个工具。**其中 21 个不需要 API 密钥**,其余适配器会在导入时要求提供凭据,导入后工具即可立即使用。每个适配器都在 [anythingmcp.com/guides](https://anythingmcp.com/guides) 上提供七种语言的设置指南。 +265 个适配器,提供 2,400 多个工具。**其中 21 个不需要 API 密钥**,其余适配器会在导入时要求提供凭据,导入后工具即可立即使用。每个适配器都在 [anythingmcp.com/guides](https://anythingmcp.com/guides) 上提供七种语言的设置指南。 | 分类 | 示例 | |---|---| diff --git a/glama.json b/glama.json index 9c394f71..22d16b5a 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. 264 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. 265 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 e2e00aab..2af75d72 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "anythingmcp", "version": "0.14.1", - "description": "Turn any REST/OpenAPI, SOAP/WSDL, GraphQL, OData or SQL API into MCP tools for Claude, ChatGPT and Copilot, no code. 264 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. 265 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 a7e622a5..da015732 100644 --- a/packages/backend/src/adapters/catalog.ts +++ b/packages/backend/src/adapters/catalog.ts @@ -108,6 +108,7 @@ import * as etsy from './intl/etsy.json'; import * as fathom from './intl/fathom.json'; import * as fhir from './intl/fhir.json'; import * as fillout from './intl/fillout.json'; +import * as firma from './intl/firma.json'; import * as flutterwave from './intl/flutterwave.json'; import * as folk from './intl/folk.json'; import * as freshbooks from './intl/freshbooks.json'; @@ -530,6 +531,7 @@ const RAW_ADAPTERS: AdapterDefinition[] = [ fathom as unknown as AdapterDefinition, fhir as unknown as AdapterDefinition, fillout as unknown as AdapterDefinition, + firma as unknown as AdapterDefinition, flutterwave as unknown as AdapterDefinition, folk as unknown as AdapterDefinition, freshbooks as unknown as AdapterDefinition, diff --git a/packages/backend/src/adapters/intl/firma.json b/packages/backend/src/adapters/intl/firma.json new file mode 100644 index 00000000..664628b4 --- /dev/null +++ b/packages/backend/src/adapters/intl/firma.json @@ -0,0 +1,486 @@ +{ + "slug": "firma", + "name": "Firma.dev", + "description": "Firma.dev e-signature from any AI agent: templates, signing requests from a template, sending and reminders, recipient status, audit trail and signed PDFs. 13 tools, API-key auth, with free test keys.", + "instructions": "This connector drives **Firma.dev** (api.firma.dev), a developer-first e-signature API: templates, signing requests (envelopes), their recipients, audit trail and signed documents. 13 tools.\n\n**Setup**\n1. Sign up at https://firma.dev (free, no card) and open your workspace in the dashboard.\n2. Copy an API key into `FIRMA_API_KEY`. Every workspace has two: the **test** key creates watermarked test requests and consumes **no credits**, the **live** key sends real, legally binding requests and consumes one credit per envelope (about €0.049). Start with the test key.\n3. The key is sent as the `Authorization` header, without a Bearer prefix. It is stored encrypted.\n\nA workspace key only sees that workspace's templates and requests. The account's main key (protected workspace) sees the company account too.\n\n**The usual flow: a document from a template**\n1. `firma_list_templates` (filter by `name`) and `firma_get_template` to find the template.\n2. `firma_list_template_recipients` for its recipient slots: each has an `id` (use it as `template_user_id`), a `designation` (Signer, Approver, CC) and an `order`.\n3. `firma_create_signing_request_from_template` with the real people for those slots. This creates a **draft**: nothing is emailed and no credit is used.\n4. Show the user the draft (name, recipients, emails) and ask for confirmation. Then `firma_send_signing_request`: this emails every signer and, on a live key, consumes one credit. `firma_get_company` shows the remaining `credits`.\n5. Track it with `firma_get_signing_request` (status flags and timestamps), `firma_list_signing_request_recipients` (who signed, who declined and why, who is still missing fields) and `firma_get_signing_request_audit`.\n6. When it is finished, `firma_get_signed_document_url` returns a short-lived download URL for the signed PDF.\n\n**Rules**\n- Never send, resend or cancel without the user's explicit go-ahead: these email real people.\n- `firma_resend_signing_request` only reaches recipients who are eligible to sign now (with a signing order, only the current step) and never people who already signed.\n- `firma_cancel_signing_request` works only on sent requests that are not finished or cancelled; set `notify_signers` to tell them.\n- Status values for filtering: `not_sent`, `in_progress`, `finished`, `cancelled`, `declined`, `expired` (comma-separated for several).\n- Dates are ISO 8601. Lists are paginated: `pagination.total_pages` tells you whether to ask for the next `page`.\n\n**Errors**: 401 `INVALID_API_KEY` = wrong or revoked key; 402 or a credits message = the account needs credits (or use the test key); 429 = rate limit (200 reads and 120 writes per minute per key), wait for `Retry-After`.", + "region": "intl", + "category": "e-signature", + "icon": "firma", + "docsUrl": "https://docs.firma.dev", + "requiredEnvVars": [ + "FIRMA_API_KEY" + ], + "connector": { + "name": "Firma.dev", + "type": "REST", + "baseUrl": "https://api.firma.dev/functions/v1/signing-request-api", + "authType": "API_KEY", + "authConfig": { + "headerName": "Authorization", + "apiKey": "{{FIRMA_API_KEY}}" + } + }, + "tools": [ + { + "name": "firma_get_company", + "description": "The Firma.dev company account behind the key: name, owner, language and the remaining envelope credits. Use it before sending to check credits. Needs the account's main key; a workspace key may get 403.", + "parameters": { + "type": "object", + "properties": {} + }, + "endpointMapping": { + "method": "GET", + "path": "/company" + }, + "annotations": { + "readOnlyHint": true + } + }, + { + "name": "firma_list_templates", + "description": "List the workspace's templates (reusable documents with recipient slots and fields), newest first unless sorted otherwise. Filter by name to find the one to send.", + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Part of the template name, case-insensitive." + }, + "page": { + "type": "integer", + "minimum": 1, + "description": "Page number, starting at 1." + }, + "page_size": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Items per page (default 20)." + }, + "sort_by": { + "type": "string", + "enum": [ + "name", + "created_on", + "last_changed_on" + ], + "description": "Field to sort by." + }, + "sort_order": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "Sort direction." + } + } + }, + "endpointMapping": { + "method": "GET", + "path": "/templates", + "queryParams": { + "name": "$name", + "page": "$page", + "page_size": "$page_size", + "sort_by": "$sort_by", + "sort_order": "$sort_order" + } + }, + "annotations": { + "readOnlyHint": true + } + }, + { + "name": "firma_get_template", + "description": "One template in full: its settings, document, fields and reminders, as it would be copied into a signing request.", + "parameters": { + "type": "object", + "properties": { + "template_id": { + "type": "string", + "description": "The template id, as returned by the list tools." + } + }, + "required": [ + "template_id" + ] + }, + "endpointMapping": { + "method": "GET", + "path": "/templates/{template_id}" + }, + "annotations": { + "readOnlyHint": true + } + }, + { + "name": "firma_list_template_recipients", + "description": "The recipient slots of a template: id (pass it as template_user_id when creating a signing request), designation (Signer, Approver, CC), signing order and any preset name or email. Call it before firma_create_signing_request_from_template.", + "parameters": { + "type": "object", + "properties": { + "template_id": { + "type": "string", + "description": "The template id, as returned by the list tools." + } + }, + "required": [ + "template_id" + ] + }, + "endpointMapping": { + "method": "GET", + "path": "/templates/{template_id}/users" + }, + "annotations": { + "readOnlyHint": true + } + }, + { + "name": "firma_list_signing_requests", + "description": "List signing requests (envelopes) with their status and dates, filtered by status, name, signer or creation date. Returns a trimmed view; use firma_get_signing_request for one request in full.", + "parameters": { + "type": "object", + "properties": { + "status": { + "type": "string", + "description": "not_sent, in_progress, finished, cancelled, declined or expired; comma-separate several, e.g. 'in_progress,declined'." + }, + "name": { + "type": "string", + "description": "Part of the request name, case-insensitive." + }, + "signer_email": { + "type": "string", + "description": "Exact email of a signer." + }, + "signer_name": { + "type": "string", + "description": "Part of a signer's name." + }, + "created_after": { + "type": "string", + "description": "ISO 8601 date or timestamp." + }, + "created_before": { + "type": "string", + "description": "ISO 8601 date or timestamp." + }, + "page": { + "type": "integer", + "minimum": 1, + "description": "Page number, starting at 1." + }, + "page_size": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Items per page (default 20)." + }, + "sort_by": { + "type": "string", + "enum": [ + "name", + "created_on", + "expiration_hours", + "sent_on", + "finished_on" + ], + "description": "Field to sort by." + }, + "sort_order": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "Sort direction." + } + } + }, + "endpointMapping": { + "method": "GET", + "path": "/signing-requests", + "queryParams": { + "status": "$status", + "name": "$name", + "signer_email": "$signer_email", + "signer_name": "$signer_name", + "created_after": "$created_after", + "created_before": "$created_before", + "page": "$page", + "page_size": "$page_size", + "sort_by": "$sort_by", + "sort_order": "$sort_order" + } + }, + "responseMapping": { + "transform": { + "mode": "jmespath", + "expression": "{results: results[].{id: id, name: name, status: status, template_id: template_id, credit_cost: credit_cost, created_date: created_date, sent_date: sent_date, finished_date: finished_date, cancelled_date: cancelled_date, declined_date: declined_date, expires_at: expires_at, recipients: recipients[].{name: name, email: email, designation: designation, finished_on: finished_on, declined_on: declined_on}}, pagination: pagination}" + } + }, + "annotations": { + "readOnlyHint": true + } + }, + { + "name": "firma_get_signing_request", + "description": "One signing request in full: status flags (sent, finished, cancelled, declined, expired), timestamps, expiry, credit cost and settings.", + "parameters": { + "type": "object", + "properties": { + "signing_request_id": { + "type": "string", + "description": "The signing request id, as returned by the list tools." + } + }, + "required": [ + "signing_request_id" + ] + }, + "endpointMapping": { + "method": "GET", + "path": "/signing-requests/{signing_request_id}" + }, + "annotations": { + "readOnlyHint": true + } + }, + { + "name": "firma_list_signing_request_recipients", + "description": "The recipients of a signing request with where each one stands: finished_on (signed), declined_on and decline_reason, signing order, and missing_fields / ready_to_send before sending. Use it to answer 'who still has to sign'.", + "parameters": { + "type": "object", + "properties": { + "signing_request_id": { + "type": "string", + "description": "The signing request id, as returned by the list tools." + } + }, + "required": [ + "signing_request_id" + ] + }, + "endpointMapping": { + "method": "GET", + "path": "/signing-requests/{signing_request_id}/users" + }, + "annotations": { + "readOnlyHint": true + } + }, + { + "name": "firma_get_signing_request_audit", + "description": "The audit trail of a signing request in chronological order: created, edited, sent and cancelled by the sender; viewed, signed, declined and downloaded by each signer.", + "parameters": { + "type": "object", + "properties": { + "signing_request_id": { + "type": "string", + "description": "The signing request id, as returned by the list tools." + } + }, + "required": [ + "signing_request_id" + ] + }, + "endpointMapping": { + "method": "GET", + "path": "/signing-requests/{signing_request_id}/audit" + }, + "annotations": { + "readOnlyHint": true + } + }, + { + "name": "firma_get_signed_document_url", + "description": "A short-lived download URL for the signed PDF of a finished signing request (or the partially signed PDF when partial download is enabled). Give the user the URL; it expires at expires_at.", + "parameters": { + "type": "object", + "properties": { + "signing_request_id": { + "type": "string", + "description": "The signing request id, as returned by the list tools." + } + }, + "required": [ + "signing_request_id" + ] + }, + "endpointMapping": { + "method": "GET", + "path": "/signing-requests/{signing_request_id}/download" + }, + "annotations": { + "readOnlyHint": true + } + }, + { + "name": "firma_create_signing_request_from_template", + "description": "Create a DRAFT signing request from a template, with the real recipients filled into the template's slots. Nothing is emailed and no credit is used until firma_send_signing_request. Get the slot ids with firma_list_template_recipients.", + "parameters": { + "type": "object", + "properties": { + "template_id": { + "type": "string", + "description": "The template id, as returned by the list tools." + }, + "name": { + "type": "string", + "description": "Name of the signing request, e.g. 'NDA - Acme GmbH'. Defaults to the template's name." + }, + "recipients": { + "type": "array", + "description": "One entry per template slot you fill: {\"template_user_id\": \"\", \"first_name\": \"Ada\", \"last_name\": \"Lovelace\", \"email\": \"ada@example.com\", \"designation\": \"Signer\"}. designation is Signer, Approver or CC; first_name, email and designation are required. Omit to keep the template's own recipients.", + "items": { + "type": "object" + } + }, + "expiration_hours": { + "type": "integer", + "minimum": 1, + "description": "Hours until the request expires once sent (default 168 = 7 days)." + }, + "description": { + "type": "string", + "description": "Optional description shown to recipients." + } + }, + "required": [ + "template_id" + ] + }, + "endpointMapping": { + "method": "POST", + "path": "/signing-requests", + "bodyMapping": { + "template_id": "$template_id", + "name": "$name", + "recipients": "$recipients", + "expiration_hours": "$expiration_hours", + "description": "$description" + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": false + } + }, + { + "name": "firma_send_signing_request", + "description": "Send a draft signing request: every signer gets the signing email. On a live key this consumes one credit and cannot be undone (only cancelled). Ask the user to confirm the recipients first.", + "parameters": { + "type": "object", + "properties": { + "signing_request_id": { + "type": "string", + "description": "The signing request id, as returned by the list tools." + } + }, + "required": [ + "signing_request_id" + ] + }, + "endpointMapping": { + "method": "POST", + "path": "/signing-requests/{signing_request_id}/send" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "firma_resend_signing_request", + "description": "Email the signing link again to recipients who can sign now and have not signed yet, optionally with a short message. Recipient ids come from firma_list_signing_request_recipients.", + "parameters": { + "type": "object", + "properties": { + "signing_request_id": { + "type": "string", + "description": "The signing request id, as returned by the list tools." + }, + "recipient_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Ids of the recipients to remind." + }, + "custom_message": { + "type": "string", + "description": "Optional message added to the reminder email." + } + }, + "required": [ + "signing_request_id", + "recipient_ids" + ] + }, + "endpointMapping": { + "method": "POST", + "path": "/signing-requests/{signing_request_id}/resend", + "bodyMapping": { + "recipient_ids": "$recipient_ids", + "custom_message": "$custom_message" + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "firma_cancel_signing_request", + "description": "Cancel a sent signing request that is not finished yet; signers can no longer sign it. Only with the user's explicit go-ahead.", + "parameters": { + "type": "object", + "properties": { + "signing_request_id": { + "type": "string", + "description": "The signing request id, as returned by the list tools." + }, + "reason": { + "type": "string", + "description": "Optional reason, shown to signers when they are notified." + }, + "notify_signers": { + "type": "boolean", + "description": "Email the signers about the cancellation." + } + }, + "required": [ + "signing_request_id" + ] + }, + "endpointMapping": { + "method": "POST", + "path": "/signing-requests/{signing_request_id}/cancel", + "bodyMapping": { + "reason": "$reason", + "notify_signers": "$notify_signers" + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": true, + "openWorldHint": true + } + } + ], + "probe": { + "tool": "firma_list_templates", + "params": { + "page_size": 1 + } + } +} diff --git a/packages/backend/src/adapters/intl/firma.live.spec.ts b/packages/backend/src/adapters/intl/firma.live.spec.ts new file mode 100644 index 00000000..4bc6de31 --- /dev/null +++ b/packages/backend/src/adapters/intl/firma.live.spec.ts @@ -0,0 +1,201 @@ +import * as adapter from './firma.json'; +import axios 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 { applyResponseTransform } from '../../connectors/response-transform.util'; +import { deriveToolAnnotations } from '../../mcp-server/tool-annotations'; + +/** + * Firma.dev adapter. + * + * 1. Static — always runs: the key goes in `Authorization` with no Bearer + * prefix, every tool reaches the right path with the right body, the + * list view is trimmed, and the tools that email real people or spend a + * credit are not advertised as read-only. + * + * 2. Live — skipped unless RUN_FIRMA_LIVE is set. Reads only, so a test key + * is enough and nothing is sent or charged: + * + * RUN_FIRMA_LIVE=1 FIRMA_API_KEY= \ + * npx jest src/adapters/intl/firma.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, + }; +}); +const mockedAxios = axios as unknown as jest.Mock & { __actual: typeof axios }; + +type Tool = { + name: string; + description: string; + parameters: { properties?: Record; required?: string[] }; + endpointMapping: { method: string; path: string; bodyMapping?: Record }; + responseMapping?: { transform: Record }; + annotations?: Record; +}; +const a = adapter as unknown as { + instructions: string; + requiredEnvVars: string[]; + connector: { baseUrl: string; authType: string; authConfig: Record }; + tools: Tool[]; +}; +const tool = (name: string) => { + const t = a.tools.find((x) => x.name === name); + if (!t) throw new Error(`no tool ${name}`); + return t; +}; + +const engine = () => + new RestEngine({} as OAuth2TokenService, {} as LoginTokenService); +const config = (key = 'firma_test_key') => ({ + baseUrl: a.connector.baseUrl, + authType: a.connector.authType, + authConfig: { ...a.connector.authConfig, apiKey: key }, +}); +const BASE = 'https://api.firma.dev/functions/v1/signing-request-api'; + +const call = async (name: string, params: Record) => { + mockedAxios.mockResolvedValue({ data: {} }); + await engine().execute(config(), tool(name).endpointMapping as any, params); + return mockedAxios.mock.calls[mockedAxios.mock.calls.length - 1][0]; +}; + +describe('firma adapter — static spec conformance', () => { + beforeEach(() => mockedAxios.mockReset()); + + it('sends the key as a bare Authorization header', async () => { + expect(a.requiredEnvVars).toEqual(['FIRMA_API_KEY']); + expect(a.connector.authType).toBe('API_KEY'); + expect(a.connector.authConfig).toEqual({ + headerName: 'Authorization', + apiKey: '{{FIRMA_API_KEY}}', + }); + const req = await call('firma_get_company', {}); + expect(req.headers.Authorization).toBe('firma_test_key'); + }); + + it.each([ + ['firma_get_company', {}, 'GET', '/company'], + ['firma_list_templates', { name: 'NDA' }, 'GET', '/templates'], + ['firma_get_template', { template_id: 't1' }, 'GET', '/templates/t1'], + ['firma_list_template_recipients', { template_id: 't1' }, 'GET', '/templates/t1/users'], + ['firma_list_signing_requests', { status: 'in_progress' }, 'GET', '/signing-requests'], + ['firma_get_signing_request', { signing_request_id: 's1' }, 'GET', '/signing-requests/s1'], + ['firma_list_signing_request_recipients', { signing_request_id: 's1' }, 'GET', '/signing-requests/s1/users'], + ['firma_get_signing_request_audit', { signing_request_id: 's1' }, 'GET', '/signing-requests/s1/audit'], + ['firma_get_signed_document_url', { signing_request_id: 's1' }, 'GET', '/signing-requests/s1/download'], + ['firma_create_signing_request_from_template', { template_id: 't1' }, 'POST', '/signing-requests'], + ['firma_send_signing_request', { signing_request_id: 's1' }, 'POST', '/signing-requests/s1/send'], + ['firma_resend_signing_request', { signing_request_id: 's1', recipient_ids: ['u1'] }, 'POST', '/signing-requests/s1/resend'], + ['firma_cancel_signing_request', { signing_request_id: 's1' }, 'POST', '/signing-requests/s1/cancel'], + ])('%s calls %s %s', async (name, params, method, path) => { + const req = await call(name, params); + expect(req.method).toBe(method); + expect(req.url).toBe(BASE + path); + }); + + it('covers every tool in the table above', () => { + expect(a.tools).toHaveLength(13); + expect(a.instructions).toContain('13 tools'); + }); + + it('creates a draft from a template with the recipients in their slots', async () => { + const recipients = [ + { template_user_id: 'slot-1', first_name: 'Ada', email: 'ada@example.com', designation: 'Signer' }, + ]; + const req = await call('firma_create_signing_request_from_template', { + template_id: 't1', + name: 'NDA - Acme', + recipients, + expiration_hours: 72, + }); + expect(req.data).toEqual({ template_id: 't1', name: 'NDA - Acme', recipients, expiration_hours: 72 }); + }); + + it('sends only what was given on resend and cancel', async () => { + expect((await call('firma_resend_signing_request', { signing_request_id: 's1', recipient_ids: ['u1'] })).data) + .toEqual({ recipient_ids: ['u1'] }); + expect((await call('firma_cancel_signing_request', { signing_request_id: 's1', notify_signers: true })).data) + .toEqual({ notify_signers: true }); + }); + + it('trims the list of signing requests to what a model needs', () => { + const raw = { + results: [ + { + id: 's1', name: 'NDA', status: 'in_progress', template_id: 't1', credit_cost: 1, + created_date: '2026-09-28', sent_date: '2026-09-28', document_url: 'https://signed.example/x.pdf', + fields: [{ id: 'f1' }], settings: { allow_download: true }, + recipients: [{ name: 'Ada', email: 'ada@example.com', designation: 'Signer', finished_on: null, phone_number: '+49' }], + }, + ], + pagination: { current_page: 1, page_size: 20, total_count: 1, total_pages: 1 }, + }; + const out = applyResponseTransform(raw, tool('firma_list_signing_requests').responseMapping as any).value as any; + expect(out.pagination.total_pages).toBe(1); + expect(out.results[0]).toMatchObject({ id: 's1', status: 'in_progress', credit_cost: 1 }); + expect(out.results[0]).not.toHaveProperty('document_url'); + expect(out.results[0]).not.toHaveProperty('fields'); + expect(out.results[0].recipients[0]).toEqual({ + name: 'Ada', email: 'ada@example.com', designation: 'Signer', finished_on: null, declined_on: null, + }); + }); + + it('marks reads read-only and never calls a send or cancel read-only', () => { + const ann = (t: Tool) => + deriveToolAnnotations({ name: t.name, connectorType: 'REST', endpointMapping: t.endpointMapping, annotations: t.annotations }); + for (const t of a.tools.filter((x) => x.endpointMapping.method === 'GET')) { + expect({ tool: t.name, ro: ann(t).readOnlyHint }).toEqual({ tool: t.name, ro: true }); + } + expect(ann(tool('firma_send_signing_request'))).toMatchObject({ readOnlyHint: false, openWorldHint: true }); + expect(ann(tool('firma_resend_signing_request'))).toMatchObject({ readOnlyHint: false, openWorldHint: true }); + expect(ann(tool('firma_cancel_signing_request'))).toMatchObject({ readOnlyHint: false, destructiveHint: true }); + expect(ann(tool('firma_create_signing_request_from_template'))).toMatchObject({ readOnlyHint: false, destructiveHint: false }); + }); + + it('exposes no workspace tool: those responses carry API keys', () => { + for (const t of a.tools) expect(t.endpointMapping.path).not.toMatch(/workspace/); + }); + + it('only names tools that exist', () => { + const names = new Set(a.tools.map((t) => t.name)); + const mentioned = [ + ...a.instructions.matchAll(/\bfirma_[a-z_]+/g), + ...a.tools.flatMap((t) => [...t.description.matchAll(/\bfirma_[a-z_]+/g)]), + ].map((m) => m[0]); + expect(mentioned.length).toBeGreaterThan(10); + for (const n of mentioned) expect(names).toContain(n); + }); +}); + +const live = process.env.RUN_FIRMA_LIVE ? describe : describe.skip; +live('firma adapter — live API (reads only)', () => { + beforeAll(() => { + if (!process.env.FIRMA_API_KEY) throw new Error('Set FIRMA_API_KEY (a test key is enough)'); + mockedAxios.mockImplementation((cfg: unknown) => mockedAxios.__actual(cfg as any)); + }); + const run = (name: string, params: Record = {}) => + engine().execute(config(process.env.FIRMA_API_KEY), tool(name).endpointMapping as any, params) as Promise; + + it('lists templates', async () => { + const out = await run('firma_list_templates', { page_size: 5 }); + expect(Array.isArray(out.results)).toBe(true); + }, 30_000); + + it('lists signing requests and reads the first one', async () => { + const out = await run('firma_list_signing_requests', { page_size: 5 }); + expect(out.pagination).toBeDefined(); + if (out.results.length) { + const id = out.results[0].id; + expect((await run('firma_get_signing_request', { signing_request_id: id })).id).toBe(id); + expect(Array.isArray((await run('firma_list_signing_request_recipients', { signing_request_id: id })).results)).toBe(true); + } + }, 30_000); +}); diff --git a/packages/frontend/public/logos/connectors/firma.svg b/packages/frontend/public/logos/connectors/firma.svg new file mode 100644 index 00000000..beb55d87 --- /dev/null +++ b/packages/frontend/public/logos/connectors/firma.svg @@ -0,0 +1 @@ +Firma.dev diff --git a/server.json b/server.json index c77f4a46..71ba1091 100644 --- a/server.json +++ b/server.json @@ -1,7 +1,7 @@ { "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", "name": "io.github.HelpCode-ai/anythingmcp", - "description": "Any REST/SOAP/GraphQL/OData/SQL API as MCP tools for Claude & ChatGPT. 264 connectors: SAP, ERP.", + "description": "Any REST/SOAP/GraphQL/OData/SQL API as MCP tools for Claude & ChatGPT. 265 connectors: SAP, ERP.", "repository": { "url": "https://github.com/HelpCode-ai/anythingmcp", "source": "github"