From 19f9ad31bb48959f829a659ee315c8ba05212ab1 Mon Sep 17 00:00:00 2001 From: Shouryamaan Jain Date: Thu, 8 Oct 2026 20:59:12 +0000 Subject: [PATCH] Add Cohesivity MCP bridge; keep top-level oneOf tool parameters Cohesivity's provision_resource lists its arguments under a top-level oneOf. The gateway read only top-level properties, so it advertised the tool with no parameters and stripped every argument. Both schema converters now merge the branches. --- 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/intl/cohesivity.json | 748 ++++++++++++++++++ .../src/adapters/intl/cohesivity.live.spec.ts | 81 ++ .../src/mcp-server/mcp-server.service.ts | 4 +- .../src/mcp-server/tool-schema.util.spec.ts | 82 ++ .../src/mcp-server/tool-schema.util.ts | 42 +- .../public/logos/connectors/cohesivity.svg | 1 + server.json | 2 +- 15 files changed, 972 insertions(+), 20 deletions(-) create mode 100644 packages/backend/src/adapters/intl/cohesivity.json create mode 100644 packages/backend/src/adapters/intl/cohesivity.live.spec.ts create mode 100644 packages/backend/src/mcp-server/tool-schema.util.spec.ts create mode 100644 packages/frontend/public/logos/connectors/cohesivity.svg diff --git a/CITATION.cff b/CITATION.cff index 14b808c6..e1964621 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 325 pre-built adapters, with a focus on ERP and e-commerce + It ships 326 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 e1fb2d71..0588c9e4 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: 325 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: 326 Connectors, 18 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 325 fertige Adapter mit, darunter SAP, Etsy, weclapp und Amazon Seller; 17 davon kommen ohne API-Schlüssel aus. + Es bringt 326 fertige Adapter mit, darunter SAP, Etsy, weclapp und Amazon Seller; 18 davon kommen ohne API-Schlüssel aus.

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

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

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

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

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

AnythingMCP:自行托管的 MCP 网关

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

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

@@ -111,7 +111,7 @@ docker compose up -d ## 连接器目录 -共 325 个适配器,提供 2,400 多个工具。每个适配器都在 [anythingmcp.com/zh/guides](https://anythingmcp.com/zh/guides) 上提供七种语言的配置指南。 +共 326 个适配器,提供 2,400 多个工具。每个适配器都在 [anythingmcp.com/zh/guides](https://anythingmcp.com/zh/guides) 上提供七种语言的配置指南。 | 类别 | 示例 | |---|---| diff --git a/glama.json b/glama.json index 35dd2f13..b60bdefe 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. 325 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. 326 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 784340b5..824e958f 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. 325 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. 326 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 9c7ba1ef..97f0e1ba 100644 --- a/packages/backend/src/adapters/catalog.ts +++ b/packages/backend/src/adapters/catalog.ts @@ -109,6 +109,7 @@ import * as clickup from './intl/clickup.json'; import * as clockify from './intl/clockify.json'; import * as close from './intl/close.json'; import * as coda from './intl/coda.json'; +import * as cohesivity from './intl/cohesivity.json'; import * as coingecko from './intl/coingecko.json'; import * as convertkit from './intl/convertkit.json'; import * as copper from './intl/copper.json'; @@ -647,6 +648,7 @@ const RAW_ADAPTERS: AdapterDefinition[] = [ clockify as unknown as AdapterDefinition, close as unknown as AdapterDefinition, coda as unknown as AdapterDefinition, + cohesivity as unknown as AdapterDefinition, coingecko as unknown as AdapterDefinition, convertkit as unknown as AdapterDefinition, copper as unknown as AdapterDefinition, diff --git a/packages/backend/src/adapters/intl/cohesivity.json b/packages/backend/src/adapters/intl/cohesivity.json new file mode 100644 index 00000000..f344d4a1 --- /dev/null +++ b/packages/backend/src/adapters/intl/cohesivity.json @@ -0,0 +1,748 @@ +{ + "slug": "cohesivity", + "name": "Cohesivity", + "description": "Set up a backend for the project an AI agent is building: Postgres, hosting, email, storage and more, from Claude, ChatGPT or Copilot through Cohesivity's hosted MCP server. No account or API key needed.", + "instructions": "This connector bridges **Cohesivity's hosted MCP server** (`https://cohesivity.ai/mcp`). Cohesivity gives an AI agent a backend for the project it is building: Postgres, Redis, object and vector storage, hosting, an email inbox, auth and social login, realtime, cron, and AI model APIs, all behind one tenant. The server needs no account or API key, so the connector installs with nothing to fill in.\n\n**The tools come from the server.** At install AnythingMCP asks Cohesivity's server which tools it has. When Cohesivity adds tools, open the connector and click **Discover tools** to pick them up.\n\n**Tenants**: `create_tenant` creates a new ephemeral tenant on every confirmed call and returns its `tenant_id`, a management key and an application key. Every other tool takes the `tenant_id` and the management key. An ephemeral tenant expires after 72 hours; `claim_tenant` returns an approval link a person opens to keep it in their Cohesivity account. Tenant creation is limited to 10 calls per 60 seconds per IP address, and every user of this connector shares the gateway's address, so reuse a tenant instead of creating one per conversation.\n\n**Safety**:\n- The tenant keys pass through the conversation as tool results and arguments, so they are stored in AnythingMCP's audit log with the rest of each call. Limit who can use this connector and read its audit log with roles, and claim the tenant before relying on it.\n- An ephemeral tenant runs under fixed usage caps and pauses when it reaches one. A claimed tenant moves to its owner's Cohesivity plan and wallet, where provisioned services can cost money.\n- `give_feedback` installs switched off, because its text goes to Cohesivity.\n\n**Working with it**: start with `get_cohesivity_documentation` (`offerings`, then `offering` for the service you need), then `create_tenant` once, then `provision_resource` for each service and `tenant_status` to check what is provisioned. The application code then calls the provisioned services over HTTPS with the application key, as the offering documents describe.", + "region": "intl", + "category": "infrastructure", + "icon": "cohesivity", + "docsUrl": "https://cohesivity.ai/docs?ref=gh-anythingmcp", + "requiredEnvVars": [], + "prerequisites": "- Nothing to set up for a first tenant: Cohesivity's server needs no account or API key.\n- A Cohesivity account to keep a tenant past 72 hours; `claim_tenant` returns the link that adds it to one.\n- Network access from the AnythingMCP server to cohesivity.ai over HTTPS.", + "connector": { + "name": "Cohesivity", + "type": "MCP", + "baseUrl": "https://cohesivity.ai/mcp", + "authType": "NONE", + "config": { + "mcpToolPrefix": "cohesivity_" + } + }, + "tools": [ + { + "name": "cohesivity_get_cohesivity_documentation", + "description": "Read a current Cohesivity overview, reference, onboarding guide, pricing guide, offerings catalog, or individual offering document.", + "parameters": { + "type": "object", + "required": [ + "document" + ], + "properties": { + "document": { + "type": "string", + "enum": [ + "docs", + "quick-reference", + "full-reference", + "onboarding", + "pricing", + "offerings", + "offering" + ], + "description": "Which document to read: `offerings` lists every service, `offering` reads one service named in `offering`, and `docs`, `quick-reference`, `full-reference`, `onboarding` and `pricing` cover the platform." + }, + "offering": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "pattern": "^[a-z0-9][a-z0-9-]*$", + "description": "Offering slug. Required only when document is \"offering\"." + } + }, + "additionalProperties": false + }, + "endpointMapping": { + "method": "get_cohesivity_documentation", + "path": "/mcp" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + } + }, + { + "name": "cohesivity_create_tenant", + "description": "Create an ephemeral Cohesivity tenant without login. Call this first when the project has no .cohesivity file. Each confirmed call creates a new tenant; do not retry automatically when the outcome is unknown. Write credentials_file.content verbatim to a private (0600), gitignored .cohesivity file. Never overwrite a different tenant without approval, print the contents, or commit them. Use its tenant_id and coh_management_key for follow-up tools. If the client cannot write files, say so. This response contains project secrets and may be retained in client tool history. Sign in before creating new tenants to have them owned by your account without a separate claim.", + "parameters": { + "type": "object", + "properties": { + "confirmed": { + "type": "boolean", + "enum": [ + true + ], + "description": "Set to true only when the current user request explicitly authorized this exact mutation." + } + }, + "additionalProperties": false, + "required": [ + "confirmed" + ] + }, + "endpointMapping": { + "method": "create_tenant", + "path": "/mcp" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "cohesivity_provision_resource", + "description": "Provision one resource with resource/configuration, or several with resources/configurations. Requires an existing tenant: pass its tenant_id, and if the project has no .cohesivity file, call create_tenant first. Fetch every requested offering's live documentation and obtain any required user consent before calling.", + "parameters": { + "type": "object", + "oneOf": [ + { + "type": "object", + "required": [ + "tenant_id", + "resource", + "confirmed", + "coh_management_key" + ], + "properties": { + "tenant_id": { + "type": "string", + "minLength": 1, + "maxLength": 40, + "pattern": "^[A-Za-z0-9][A-Za-z0-9-]*$", + "description": "Required explicit tenant id. Use the tenant_id that create_tenant returned, or the one in the project .cohesivity file. Account connections are not bound to a tenant; access is checked for every call." + }, + "resource": { + "type": "string", + "enum": [ + "openweather-api", + "google-geocoding-api", + "openai-api", + "ai-gateway", + "deepgram-api", + "exa-api", + "steel-browser", + "redis", + "object-storage", + "railway-hosting", + "cloudflare-workers" + ] + }, + "confirmed": { + "type": "boolean", + "enum": [ + true + ], + "description": "Set to true only when the current user request explicitly authorized this exact mutation." + }, + "coh_management_key": { + "type": "string", + "minLength": 9, + "maxLength": 256, + "pattern": "^coh_man_[A-Za-z0-9_-]+$", + "writeOnly": true, + "description": "Secret management key from the tenant .cohesivity file. Authorizes this tenant only. Never print it in chat or logs or commit it." + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "tenant_id", + "resource", + "confirmed", + "coh_management_key" + ], + "properties": { + "tenant_id": { + "type": "string", + "minLength": 1, + "maxLength": 40, + "pattern": "^[A-Za-z0-9][A-Za-z0-9-]*$", + "description": "Required explicit tenant id. Use the tenant_id that create_tenant returned, or the one in the project .cohesivity file. Account connections are not bound to a tenant; access is checked for every call." + }, + "resource": { + "type": "string", + "const": "postgres" + }, + "configuration": { + "type": "object", + "properties": { + "region": { + "type": "string", + "enum": [ + "apac", + "aws-us-east-1", + "aws-us-east-2", + "aws-us-west-2", + "aws-eu-central-1", + "aws-eu-west-2", + "aws-ap-southeast-1", + "aws-ap-southeast-2", + "aws-sa-east-1" + ] + } + }, + "additionalProperties": false + }, + "confirmed": { + "type": "boolean", + "enum": [ + true + ], + "description": "Set to true only when the current user request explicitly authorized this exact mutation." + }, + "coh_management_key": { + "type": "string", + "minLength": 9, + "maxLength": 256, + "pattern": "^coh_man_[A-Za-z0-9_-]+$", + "writeOnly": true, + "description": "Secret management key from the tenant .cohesivity file. Authorizes this tenant only. Never print it in chat or logs or commit it." + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "tenant_id", + "resource", + "confirmed", + "coh_management_key" + ], + "properties": { + "tenant_id": { + "type": "string", + "minLength": 1, + "maxLength": 40, + "pattern": "^[A-Za-z0-9][A-Za-z0-9-]*$", + "description": "Required explicit tenant id. Use the tenant_id that create_tenant returned, or the one in the project .cohesivity file. Account connections are not bound to a tenant; access is checked for every call." + }, + "resource": { + "type": "string", + "const": "inbox" + }, + "configuration": { + "type": "object", + "properties": { + "webhook_url": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + }, + "additionalProperties": false + }, + "confirmed": { + "type": "boolean", + "enum": [ + true + ], + "description": "Set to true only when the current user request explicitly authorized this exact mutation." + }, + "coh_management_key": { + "type": "string", + "minLength": 9, + "maxLength": 256, + "pattern": "^coh_man_[A-Za-z0-9_-]+$", + "writeOnly": true, + "description": "Secret management key from the tenant .cohesivity file. Authorizes this tenant only. Never print it in chat or logs or commit it." + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "tenant_id", + "resource", + "confirmed", + "coh_management_key" + ], + "properties": { + "tenant_id": { + "type": "string", + "minLength": 1, + "maxLength": 40, + "pattern": "^[A-Za-z0-9][A-Za-z0-9-]*$", + "description": "Required explicit tenant id. Use the tenant_id that create_tenant returned, or the one in the project .cohesivity file. Account connections are not bound to a tenant; access is checked for every call." + }, + "resource": { + "type": "string", + "const": "realtime" + }, + "configuration": { + "type": "object", + "properties": { + "write_region": { + "type": "string", + "enum": [ + "wnam", + "enam", + "weur", + "eeur", + "apac", + "oc" + ] + } + }, + "additionalProperties": false + }, + "confirmed": { + "type": "boolean", + "enum": [ + true + ], + "description": "Set to true only when the current user request explicitly authorized this exact mutation." + }, + "coh_management_key": { + "type": "string", + "minLength": 9, + "maxLength": 256, + "pattern": "^coh_man_[A-Za-z0-9_-]+$", + "writeOnly": true, + "description": "Secret management key from the tenant .cohesivity file. Authorizes this tenant only. Never print it in chat or logs or commit it." + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "tenant_id", + "resource", + "configuration", + "confirmed", + "coh_management_key" + ], + "properties": { + "tenant_id": { + "type": "string", + "minLength": 1, + "maxLength": 40, + "pattern": "^[A-Za-z0-9][A-Za-z0-9-]*$", + "description": "Required explicit tenant id. Use the tenant_id that create_tenant returned, or the one in the project .cohesivity file. Account connections are not bound to a tenant; access is checked for every call." + }, + "resource": { + "type": "string", + "const": "social-login" + }, + "configuration": { + "type": "object", + "required": [ + "callback_urls" + ], + "properties": { + "callback_urls": { + "type": "array", + "minItems": 1, + "maxItems": 50, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + } + }, + "additionalProperties": false + }, + "confirmed": { + "type": "boolean", + "enum": [ + true + ], + "description": "Set to true only when the current user request explicitly authorized this exact mutation." + }, + "coh_management_key": { + "type": "string", + "minLength": 9, + "maxLength": 256, + "pattern": "^coh_man_[A-Za-z0-9_-]+$", + "writeOnly": true, + "description": "Secret management key from the tenant .cohesivity file. Authorizes this tenant only. Never print it in chat or logs or commit it." + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "tenant_id", + "resource", + "configuration", + "confirmed", + "coh_management_key" + ], + "properties": { + "tenant_id": { + "type": "string", + "minLength": 1, + "maxLength": 40, + "pattern": "^[A-Za-z0-9][A-Za-z0-9-]*$", + "description": "Required explicit tenant id. Use the tenant_id that create_tenant returned, or the one in the project .cohesivity file. Account connections are not bound to a tenant; access is checked for every call." + }, + "resource": { + "type": "string", + "const": "vector-database" + }, + "configuration": { + "type": "object", + "required": [ + "dimensions" + ], + "properties": { + "dimensions": { + "type": "integer", + "enum": [ + 384, + 768, + 1024, + 1536, + 3072 + ] + }, + "metric": { + "type": "string", + "enum": [ + "cosine", + "euclidean", + "dotproduct" + ] + } + }, + "additionalProperties": false + }, + "confirmed": { + "type": "boolean", + "enum": [ + true + ], + "description": "Set to true only when the current user request explicitly authorized this exact mutation." + }, + "coh_management_key": { + "type": "string", + "minLength": 9, + "maxLength": 256, + "pattern": "^coh_man_[A-Za-z0-9_-]+$", + "writeOnly": true, + "description": "Secret management key from the tenant .cohesivity file. Authorizes this tenant only. Never print it in chat or logs or commit it." + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "tenant_id", + "resources", + "confirmed", + "coh_management_key" + ], + "properties": { + "tenant_id": { + "type": "string", + "minLength": 1, + "maxLength": 40, + "pattern": "^[A-Za-z0-9][A-Za-z0-9-]*$", + "description": "Required explicit tenant id. Use the tenant_id that create_tenant returned, or the one in the project .cohesivity file. Account connections are not bound to a tenant; access is checked for every call." + }, + "resources": { + "type": "array", + "minItems": 1, + "maxItems": 16, + "uniqueItems": true, + "items": { + "type": "string", + "enum": [ + "openweather-api", + "google-geocoding-api", + "openai-api", + "ai-gateway", + "deepgram-api", + "exa-api", + "steel-browser", + "inbox", + "postgres", + "redis", + "object-storage", + "vector-database", + "railway-hosting", + "cloudflare-workers", + "social-login", + "realtime" + ] + } + }, + "configurations": { + "type": "object", + "properties": { + "postgres": { + "type": "object", + "properties": { + "region": { + "type": "string", + "enum": [ + "apac", + "aws-us-east-1", + "aws-us-east-2", + "aws-us-west-2", + "aws-eu-central-1", + "aws-eu-west-2", + "aws-ap-southeast-1", + "aws-ap-southeast-2", + "aws-sa-east-1" + ] + } + }, + "additionalProperties": false + }, + "inbox": { + "type": "object", + "properties": { + "webhook_url": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + }, + "additionalProperties": false + }, + "realtime": { + "type": "object", + "properties": { + "write_region": { + "type": "string", + "enum": [ + "wnam", + "enam", + "weur", + "eeur", + "apac", + "oc" + ] + } + }, + "additionalProperties": false + }, + "social-login": { + "type": "object", + "required": [ + "callback_urls" + ], + "properties": { + "callback_urls": { + "type": "array", + "minItems": 1, + "maxItems": 50, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 2048 + } + } + }, + "additionalProperties": false + }, + "vector-database": { + "type": "object", + "required": [ + "dimensions" + ], + "properties": { + "dimensions": { + "type": "integer", + "enum": [ + 384, + 768, + 1024, + 1536, + 3072 + ] + }, + "metric": { + "type": "string", + "enum": [ + "cosine", + "euclidean", + "dotproduct" + ] + } + }, + "additionalProperties": false + } + }, + "additionalProperties": false + }, + "confirmed": { + "type": "boolean", + "enum": [ + true + ], + "description": "Set to true only when the current user request explicitly authorized this exact mutation." + }, + "coh_management_key": { + "type": "string", + "minLength": 9, + "maxLength": 256, + "pattern": "^coh_man_[A-Za-z0-9_-]+$", + "writeOnly": true, + "description": "Secret management key from the tenant .cohesivity file. Authorizes this tenant only. Never print it in chat or logs or commit it." + } + }, + "additionalProperties": false + } + ] + }, + "endpointMapping": { + "method": "provision_resource", + "path": "/mcp" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "cohesivity_tenant_status", + "description": "Read current lifecycle, limits, resources, runtime state, and notifications for a tenant you can manage.", + "parameters": { + "type": "object", + "required": [ + "tenant_id", + "coh_management_key" + ], + "properties": { + "tenant_id": { + "type": "string", + "minLength": 1, + "maxLength": 40, + "pattern": "^[A-Za-z0-9][A-Za-z0-9-]*$", + "description": "Required explicit tenant id. Use the tenant_id that create_tenant returned, or the one in the project .cohesivity file. Account connections are not bound to a tenant; access is checked for every call." + }, + "coh_management_key": { + "type": "string", + "minLength": 9, + "maxLength": 256, + "pattern": "^coh_man_[A-Za-z0-9_-]+$", + "writeOnly": true, + "description": "Secret management key from the tenant .cohesivity file. Authorizes this tenant only. Never print it in chat or logs or commit it." + } + }, + "additionalProperties": false + }, + "endpointMapping": { + "method": "tenant_status", + "path": "/mcp" + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": false + } + }, + { + "name": "cohesivity_claim_tenant", + "description": "Start the claim handoff for an ephemeral tenant you can manage. Claiming is a consent gate; call only after explicit user approval.", + "parameters": { + "type": "object", + "required": [ + "tenant_id", + "confirmed", + "coh_management_key" + ], + "properties": { + "tenant_id": { + "type": "string", + "minLength": 1, + "maxLength": 40, + "pattern": "^[A-Za-z0-9][A-Za-z0-9-]*$", + "description": "Required explicit tenant id. Use the tenant_id that create_tenant returned, or the one in the project .cohesivity file. Account connections are not bound to a tenant; access is checked for every call." + }, + "confirmed": { + "type": "boolean", + "enum": [ + true + ], + "description": "Set to true only when the current user request explicitly authorized this exact mutation." + }, + "coh_management_key": { + "type": "string", + "minLength": 9, + "maxLength": 256, + "pattern": "^coh_man_[A-Za-z0-9_-]+$", + "writeOnly": true, + "description": "Secret management key from the tenant .cohesivity file. Authorizes this tenant only. Never print it in chat or logs or commit it." + } + }, + "additionalProperties": false + }, + "endpointMapping": { + "method": "claim_tenant", + "path": "/mcp" + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "cohesivity_give_feedback", + "description": "Submit feedback on Cohesivity and its services anytime; no user confirmation is needed. Exclude personal information and secrets.", + "parameters": { + "type": "object", + "required": [ + "tenant_id", + "feedback", + "coh_management_key" + ], + "properties": { + "tenant_id": { + "type": "string", + "minLength": 1, + "maxLength": 40, + "pattern": "^[A-Za-z0-9][A-Za-z0-9-]*$", + "description": "Required explicit tenant id. Use the tenant_id that create_tenant returned, or the one in the project .cohesivity file. Account connections are not bound to a tenant; access is checked for every call." + }, + "feedback": { + "type": "string", + "minLength": 1, + "maxLength": 20000, + "pattern": "\\S", + "description": "The feedback text, sent to Cohesivity. Leave out personal information and secrets." + }, + "coh_management_key": { + "type": "string", + "minLength": 9, + "maxLength": 256, + "pattern": "^coh_man_[A-Za-z0-9_-]+$", + "writeOnly": true, + "description": "Secret management key from the tenant .cohesivity file. Authorizes this tenant only. Never print it in chat or logs or commit it." + } + }, + "additionalProperties": false + }, + "endpointMapping": { + "method": "give_feedback", + "path": "/mcp" + }, + "enabled": false, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + } + ] +} diff --git a/packages/backend/src/adapters/intl/cohesivity.live.spec.ts b/packages/backend/src/adapters/intl/cohesivity.live.spec.ts new file mode 100644 index 00000000..05241361 --- /dev/null +++ b/packages/backend/src/adapters/intl/cohesivity.live.spec.ts @@ -0,0 +1,81 @@ +import * as adapter from './cohesivity.json'; +import { localMcpToolName } from '../../connectors/mcp-connector-config.util'; +import { McpClientEngine } from '../../connectors/engines/mcp-client.engine'; +import { OAuth2TokenService } from '../../connectors/engines/oauth2-token.service'; + +/** Live: RUN_COHESIVITY_LIVE=1 npx jest src/adapters/intl/cohesivity.live.spec.ts */ + +type Tool = { + name: string; + enabled?: boolean; + endpointMapping: { method: string; path: string }; +}; +const a = adapter as unknown as { + instructions: string; + category: string; + requiredEnvVars: string[]; + connector: { type: string; baseUrl: string; authType: string; config: { mcpToolPrefix: string } }; + tools: Tool[]; +}; +const remoteNames = [ + 'get_cohesivity_documentation', + 'create_tenant', + 'provision_resource', + 'tenant_status', + 'claim_tenant', + 'give_feedback', +]; + +describe('cohesivity adapter: static spec conformance', () => { + it("bridges Cohesivity's hosted MCP server with no credentials", () => { + expect(a.connector.type).toBe('MCP'); + expect(a.connector.baseUrl).toBe('https://cohesivity.ai/mcp'); + expect(a.connector.authType).toBe('NONE'); + expect(a.connector.config).toEqual({ mcpToolPrefix: 'cohesivity_' }); + expect(a.requiredEnvVars).toEqual([]); + expect(a.category).toBe('infrastructure'); + }); + + it('names each tool with the prefix and calls it by its remote name on /mcp', () => { + for (const t of a.tools) { + expect(t.name).toBe(localMcpToolName(t.endpointMapping.method, a.connector.config.mcpToolPrefix)); + expect(t.name.length).toBeLessThanOrEqual(64); + expect(t.endpointMapping.path).toBe('/mcp'); + } + expect(a.tools.map((t) => t.endpointMapping.method).sort()).toEqual([...remoteNames].sort()); + }); + + it('ships the feedback tool switched off', () => { + expect(a.tools.filter((t) => t.enabled === false).map((t) => t.endpointMapping.method)).toEqual(['give_feedback']); + }); + + it('documents the tenant keys, the audit log and the creation rate limit in the instructions', () => { + expect(a.instructions).toContain('audit log'); + expect(a.instructions).toContain('72 hours'); + expect(a.instructions).toContain('10 calls per 60 seconds'); + }); + + it('uses no em or en dashes', () => { + expect(JSON.stringify(adapter)).not.toMatch(/[–—]/); + }); +}); + +// Read-only on purpose: every create_tenant call creates a tenant and counts +// against the per-IP creation limit. +const maybe = process.env.RUN_COHESIVITY_LIVE ? describe : describe.skip; +maybe('cohesivity adapter - live', () => { + const engine = new McpClientEngine({} as OAuth2TokenService); + const config = { baseUrl: a.connector.baseUrl, authType: 'NONE' }; + + it('lists every tool of the snapshot without credentials', async () => { + const tools = await engine.listTools(config); + expect(tools.map((t) => t.name)).toEqual(expect.arrayContaining(remoteNames)); + }, 30000); + + it('reads the offerings catalog without a tenant', async () => { + const doc = a.tools.find((t) => t.endpointMapping.method === 'get_cohesivity_documentation')!; + const res: any = await engine.execute(config, doc.endpointMapping, { document: 'offerings' }); + expect(res.isError).toBeFalsy(); + expect(res.content[0].text).toContain('postgres'); + }, 30000); +}); diff --git a/packages/backend/src/mcp-server/mcp-server.service.ts b/packages/backend/src/mcp-server/mcp-server.service.ts index a139dc0b..f69174a8 100644 --- a/packages/backend/src/mcp-server/mcp-server.service.ts +++ b/packages/backend/src/mcp-server/mcp-server.service.ts @@ -9,6 +9,7 @@ import { decrypt } from '../common/crypto/encryption.util'; import { getRequiredSecret } from '../common/secrets.util'; import { ToolRegistry } from './tool-registry'; import { DynamicMcpTools } from './dynamic-mcp-tools'; +import { toolSchemaProperties } from './tool-schema.util'; import { RolesService } from '../roles/roles.service'; import { McpServersService } from '../mcp-servers/mcp-servers.service'; import { McpSessionManager } from '../mcp-servers/mcp-session.manager'; @@ -583,10 +584,9 @@ export class McpServerService implements OnModuleInit { * validation signal where it matters. */ private jsonSchemaToZod(schema: Record): any { - const properties = schema?.properties as Record | undefined; + const { properties, required } = toolSchemaProperties(schema); if (!properties) return z.object({}); - const required = (schema?.required as string[]) || []; const shape: Record = {}; for (const [key, prop] of Object.entries(properties)) { diff --git a/packages/backend/src/mcp-server/tool-schema.util.spec.ts b/packages/backend/src/mcp-server/tool-schema.util.spec.ts new file mode 100644 index 00000000..4e995d3d --- /dev/null +++ b/packages/backend/src/mcp-server/tool-schema.util.spec.ts @@ -0,0 +1,82 @@ +import { z } from 'zod'; +import { jsonSchemaToZodShape, toolSchemaProperties } from './tool-schema.util'; + +// One resource from a list, one with its own configuration, or several at once: +// the shape of a vendor MCP server that puts its alternatives under `oneOf`. +const provision = { + type: 'object', + oneOf: [ + { + type: 'object', + required: ['tenant_id', 'resource', 'confirmed'], + properties: { + tenant_id: { type: 'string', description: 'Tenant id.' }, + resource: { type: 'string', enum: ['redis', 'object-storage'] }, + confirmed: { type: 'boolean' }, + }, + }, + { + type: 'object', + required: ['tenant_id', 'resource', 'confirmed'], + properties: { + tenant_id: { type: 'string', description: 'Tenant id.' }, + resource: { type: 'string', const: 'postgres' }, + configuration: { type: 'object' }, + confirmed: { type: 'boolean' }, + }, + }, + { + type: 'object', + required: ['tenant_id', 'resources', 'confirmed'], + properties: { + tenant_id: { type: 'string', description: 'Tenant id.' }, + resources: { type: 'array', items: { type: 'string' } }, + confirmed: { type: 'boolean' }, + }, + }, + ], +}; + +describe('toolSchemaProperties', () => { + it('returns the properties of a plain object schema unchanged', () => { + const schema = { type: 'object', properties: { q: { type: 'string' } }, required: ['q'] }; + expect(toolSchemaProperties(schema)).toEqual({ properties: schema.properties, required: ['q'] }); + }); + + it('merges the branches of a top-level oneOf', () => { + const { properties, required } = toolSchemaProperties(provision); + expect(Object.keys(properties!).sort()).toEqual( + ['configuration', 'confirmed', 'resource', 'resources', 'tenant_id'].sort(), + ); + expect(properties!.resource.enum).toEqual(['redis', 'object-storage', 'postgres']); + expect(properties!.tenant_id.description).toBe('Tenant id.'); + expect(required.sort()).toEqual(['confirmed', 'tenant_id']); + }); + + it('drops the value list when one branch leaves the property open', () => { + const { properties } = toolSchemaProperties({ + anyOf: [ + { properties: { mode: { type: 'string', enum: ['a'] } } }, + { properties: { mode: { type: 'string' } } }, + ], + }); + expect(properties!.mode).toEqual({ type: 'string' }); + }); +}); + +describe('jsonSchemaToZodShape with a top-level oneOf', () => { + it('keeps the arguments of every branch instead of stripping them', () => { + const parsed = z.object(jsonSchemaToZodShape(provision)).parse({ + tenant_id: 't-1', + resource: 'postgres', + configuration: { region: 'eu' }, + confirmed: true, + }); + expect(parsed).toEqual({ + tenant_id: 't-1', + resource: 'postgres', + configuration: { region: 'eu' }, + confirmed: true, + }); + }); +}); diff --git a/packages/backend/src/mcp-server/tool-schema.util.ts b/packages/backend/src/mcp-server/tool-schema.util.ts index 7351699b..9df9bb9b 100644 --- a/packages/backend/src/mcp-server/tool-schema.util.ts +++ b/packages/backend/src/mcp-server/tool-schema.util.ts @@ -1,5 +1,44 @@ import { z } from 'zod'; +/** + * The properties a tool accepts and the ones it requires. A schema with no + * `properties` of its own that lists alternatives under a top-level `oneOf` or + * `anyOf` (as some vendors' MCP servers do) accepts every branch's properties; + * one is required only when every branch requires it. Without this the tool was + * advertised with no parameters and every argument was stripped. The upstream + * server still checks which combination is valid. + */ +export function toolSchemaProperties(schema: Record): { + properties?: Record; + required: string[]; +} { + const properties = schema?.properties as Record | undefined; + const branches = (schema?.oneOf ?? schema?.anyOf) as Record[] | undefined; + if (properties || !Array.isArray(branches) || branches.length === 0) { + return { properties, required: (schema?.required as string[]) || [] }; + } + + const merged: Record = {}; + for (const branch of branches) { + for (const [key, prop] of Object.entries((branch?.properties ?? {}) as Record)) { + const seen = merged[key]; + if (!seen) { + merged[key] = prop; + continue; + } + // Keep a value list only when every branch restricts the property to one. + const values = (p: any) => p?.enum ?? (p?.const !== undefined ? [p.const] : undefined); + const [a, b] = [values(seen), values(prop)]; + const { const: _const, enum: _enum, ...rest } = seen; + merged[key] = a && b ? { ...rest, enum: [...new Set([...a, ...b])] } : rest; + } + } + const required = Object.keys(merged).filter((key) => + branches.every((b) => ((b?.required as string[]) ?? []).includes(key)), + ); + return { properties: Object.keys(merged).length ? merged : undefined, required }; +} + /** * Convert a tool's JSON Schema to a Zod raw shape, for McpServer registration * and for validating the arguments of a tool run through the shared endpoint. @@ -7,10 +46,9 @@ import { z } from 'zod'; export function jsonSchemaToZodShape( schema: Record, ): Record { - const properties = schema?.properties as Record | undefined; + const { properties, required } = toolSchemaProperties(schema); if (!properties) return {}; - const required = (schema?.required as string[]) || []; const shape: Record = {}; for (const [key, prop] of Object.entries(properties)) { diff --git a/packages/frontend/public/logos/connectors/cohesivity.svg b/packages/frontend/public/logos/connectors/cohesivity.svg new file mode 100644 index 00000000..364cf8b9 --- /dev/null +++ b/packages/frontend/public/logos/connectors/cohesivity.svg @@ -0,0 +1 @@ +Cohesivity diff --git a/server.json b/server.json index 45bd44e7..2247584e 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. 325 connectors: SAP, ERP.", + "description": "Any REST/SOAP/GraphQL/OData/SQL API as MCP tools for Claude & ChatGPT. 326 connectors: SAP, ERP.", "repository": { "url": "https://github.com/HelpCode-ai/anythingmcp", "source": "github"