diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index e859816..a52c053 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -1,8 +1,14 @@ { "name": "connector-hub", - "version": "2.0.0", - "tagline": "One hub for every external service — spec-driven, type-safe, auditable.", - "tags": ["connectors", "api", "mcp", "automation", "devops"], + "version": "2.1.0", + "tagline": "One hub for every external service — 24 providers, 301 operations, spec-driven and auditable.", + "tags": [ + "connectors", + "api", + "mcp", + "automation", + "devops" + ], "icon": "plug", "homepage": "https://github.com/CodeWithJuber/connector-hub", "repository": "https://github.com/CodeWithJuber/connector-hub" diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json index fc88313..5b67f46 100644 --- a/.claude-plugin/plugin.json +++ b/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "connector-hub", - "version": "2.0.0", - "description": "Spec-driven connector orchestration — 21 providers, 278 operations, type-safe execution contract with audit ledger.", + "version": "2.1.0", + "description": "Spec-driven connector orchestration — 24 providers, 301 operations, type-safe execution contract with audit ledger.", "author": { "name": "Connector Hub contributors", "url": "https://github.com/CodeWithJuber" @@ -13,7 +13,11 @@ "longDescription": "Runs the Connector Hub MCP server with spec-driven connectors covering AI, email, hosting, cloud, chat, GitHub, and operations — behind one interface with a type-safe execution contract, permission model, and hash-chained audit ledger.", "developerName": "Connector Hub contributors", "category": "Developer Tools", - "capabilities": ["Interactive", "Read", "Write"], + "capabilities": [ + "Interactive", + "Read", + "Write" + ], "defaultPrompt": [ "List all providers and their operation counts.", "Search for operations matching a query.", diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index fc88313..5b67f46 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,7 +1,7 @@ { "name": "connector-hub", - "version": "2.0.0", - "description": "Spec-driven connector orchestration — 21 providers, 278 operations, type-safe execution contract with audit ledger.", + "version": "2.1.0", + "description": "Spec-driven connector orchestration — 24 providers, 301 operations, type-safe execution contract with audit ledger.", "author": { "name": "Connector Hub contributors", "url": "https://github.com/CodeWithJuber" @@ -13,7 +13,11 @@ "longDescription": "Runs the Connector Hub MCP server with spec-driven connectors covering AI, email, hosting, cloud, chat, GitHub, and operations — behind one interface with a type-safe execution contract, permission model, and hash-chained audit ledger.", "developerName": "Connector Hub contributors", "category": "Developer Tools", - "capabilities": ["Interactive", "Read", "Write"], + "capabilities": [ + "Interactive", + "Read", + "Write" + ], "defaultPrompt": [ "List all providers and their operation counts.", "Search for operations matching a query.", diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 0000000..9c98404 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,13 @@ +{ + "mcpServers": { + "connector-hub": { + "command": "${CLAUDE_PLUGIN_ROOT}/crates/target/release/connector-hub", + "args": [ + "mcp" + ], + "env": { + "CONNECTOR_HUB_SPECS_DIR": "${CLAUDE_PLUGIN_ROOT}/specs" + } + } + } +} diff --git a/README.md b/README.md index 981b4d2..7c4fbb4 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,18 @@ connector-hub mcp connector-hub validate ``` +### Specs resolution + +Provider specs are located in this order: + +1. `CONNECTOR_HUB_SPECS_DIR` — explicit override. +2. `./specs` relative to the working directory. +3. `specs/` found by walking up from the executable. + +Rule 3 lets an installed binary find its specs without the caller setting a +working directory — MCP hosts launch servers with an arbitrary cwd, so a hub +installed globally would otherwise start with an empty catalogue. + ## Architecture Connectors are **data, not code**. Provider specs (OpenAPI 3.x or Google diff --git a/crates/connector-hub/src/main.rs b/crates/connector-hub/src/main.rs index 82df4e8..64661e4 100644 --- a/crates/connector-hub/src/main.rs +++ b/crates/connector-hub/src/main.rs @@ -109,12 +109,15 @@ async fn main() -> anyhow::Result<()> { println!("Validating installation...\n"); // 1. Check specs directory - let specs_dir = std::path::Path::new("specs"); - if !specs_dir.exists() { - errors.push("specs/ directory not found".into()); + let specs_dir = specs_dir(); + if !specs_dir.is_dir() { + errors.push(format!( + "specs/ directory not found at {}", + specs_dir.display() + )); } else { let mut spec_count = 0; - for entry in std::fs::read_dir(specs_dir)? { + for entry in std::fs::read_dir(&specs_dir)? { let entry = entry?; let path = entry.path(); if path.extension().is_some_and(|e| e == "json") { @@ -259,11 +262,46 @@ async fn main() -> anyhow::Result<()> { Ok(()) } +/// Resolve the provider specs directory. +/// +/// Resolution order: +/// 1. `CONNECTOR_HUB_SPECS_DIR` — explicit override. +/// 2. `./specs` relative to the working directory — repo-root invocation. +/// 3. `specs/` found by walking up from the executable — lets an installed +/// binary (e.g. `crates/target/release/connector-hub`) locate the specs +/// without the caller having to set a working directory. MCP hosts launch +/// servers with an arbitrary cwd, so this is the common case. +/// +/// Falls back to `./specs` so callers keep a stable "not found" path to report. +fn specs_dir() -> std::path::PathBuf { + if let Ok(dir) = std::env::var("CONNECTOR_HUB_SPECS_DIR") { + return std::path::PathBuf::from(dir); + } + + let cwd_specs = std::path::PathBuf::from("specs"); + if cwd_specs.is_dir() { + return cwd_specs; + } + + if let Ok(exe) = std::env::current_exe() { + let mut ancestor = exe.parent(); + while let Some(dir) = ancestor { + let candidate = dir.join("specs"); + if candidate.is_dir() { + return candidate; + } + ancestor = dir.parent(); + } + } + + cwd_specs +} + fn build_catalogue() -> anyhow::Result { let mut catalogue = hub_core::Catalogue::new(); - let specs_dir = std::path::Path::new("specs"); - if specs_dir.exists() { + let specs_dir = specs_dir(); + if specs_dir.is_dir() { for entry in std::fs::read_dir(specs_dir)? { let entry = entry?; let path = entry.path(); diff --git a/crates/hub-net/src/client.rs b/crates/hub-net/src/client.rs index f14233f..3fbe114 100644 --- a/crates/hub-net/src/client.rs +++ b/crates/hub-net/src/client.rs @@ -65,13 +65,12 @@ impl NetClient { // For methods that take a body, send args as JSON match method.to_uppercase().as_str() { - "POST" | "PUT" | "PATCH" => { - if !args.is_null() { - request = request - .header("Content-Type", "application/json") - .json(args); - } + "POST" | "PUT" | "PATCH" if !args.is_null() => { + request = request + .header("Content-Type", "application/json") + .json(args); } + "POST" | "PUT" | "PATCH" => {} "GET" | "HEAD" | "DELETE" => { // GET/HEAD/DELETE: if args has query-like params, append as query string if let Some(obj) = args.as_object() { diff --git a/kimi.plugin.json b/kimi.plugin.json index e3c7d60..f12d967 100644 --- a/kimi.plugin.json +++ b/kimi.plugin.json @@ -1,7 +1,7 @@ { "name": "connector-hub", - "version": "2.0.0", - "description": "Spec-driven connector orchestration — 21 providers, 278 operations, type-safe execution contract with audit ledger.", + "version": "2.1.0", + "description": "Spec-driven connector orchestration — 24 providers, 301 operations, type-safe execution contract with audit ledger.", "author": { "name": "Connector Hub contributors", "url": "https://github.com/CodeWithJuber" diff --git a/skills/connector-hub/SKILL.md b/skills/connector-hub/SKILL.md new file mode 100644 index 0000000..c4f8597 --- /dev/null +++ b/skills/connector-hub/SKILL.md @@ -0,0 +1,82 @@ +--- +name: connector-hub +description: Call external services (AI providers, Gmail, GitHub, Hetzner/Linode/Contabo/OVH VPS, cPanel/WHM/WHMCS panels, Cloudflare, tawk.to, SSH and network ops) through the Connector Hub MCP server. Use whenever a task needs a real API call to one of these providers, when you need to discover which operation exists for a provider, or when a destructive operation needs confirmation. Triggers include "list my servers", "send this email", "create a DNS record", "suspend that hosting account", "reboot the VPS", "what can I do with ". +--- + +# Connector Hub + +One MCP surface over 24 providers and 301 operations. The tool surface is fixed +and small; the reachable API surface is complete. Never guess an operation ID — +discover it. + +## The four tools + +| Tool | Use it for | +|---|---| +| `list_providers` | What providers exist and how many operations each has | +| `search_operations` | Find the operation ID for an intent — free text, optional `provider` filter | +| `describe_operation` | Exact JSON Schema for one operation before calling it | +| `call_operation` | Execute — `id`, `args`, optional `account`, `dry_run`, confirmation token | + +## Workflow + +Always in this order: + +1. **Search** — `search_operations({query: "delete server", provider: "hetzner"})`. + Search by what you want to do, not by the endpoint name you imagine. +2. **Describe** — `describe_operation({id: "hetzner.servers.delete"})`. Read the + schema. Do not construct `args` from memory of the provider's REST API. +3. **Dry run** — for anything mutating, call with `dry_run: true` first and show + the user the `would_execute` payload and `mutation_class`. +4. **Call** — only after the user has seen what will happen. + +## Reading the result + +Every result is one of five states. Only one of them means something happened: + +- `Succeeded { executed: true, data }` — real output, the only executed state. +- `DryRun { would_execute, mutation_class }` — nothing ran. +- `ConfirmationRequired { provider, operation, token_format }` — destructive op. + Relay the request to the user; pass the token they give back on the retry. + Never mint or guess a confirmation token. +- `ConfigurationRequired { provider, missing }` — credentials absent. Tell the + user exactly which env vars are missing; do not retry, and do not attempt the + same call through curl, a raw HTTP client, or a different tool. +- `PermissionDenied` — policy refused. Report it and stop. + +A non-`Succeeded` state never means "probably worked". Do not report success +unless you saw `executed: true`. + +## Safety rules + +- 32 of the 301 operations are destructive (server deletes, account suspends, + Gmail deletes, SSH commands). Confirm with the user in plain language — name + the resource and the provider — before requesting a confirmation token. +- Credentials live in environment variables or the encrypted store. Never put a + key in `args`, never echo one back to the user, never write one into a file + the user did not ask for. +- `ops_ssh`, `ops_network`, and `ops_security` need a `HUB_SECURITY_POLICY` + capability grant. If they are denied, that is the policy working — surface it + rather than routing around it. +- Every policy decision is appended to a BLAKE3 hash-chained audit ledger. + Verify with `connector-hub audit-verify audit.jsonl`. + +## Provider map + +- **AI** — `claude`, `openai`, `kimi` +- **Mail** — `gmail` (79 ops), `email` (IMAP/SMTP) +- **Code** — `github` (25 ops) +- **Cloud / VPS** — `hetzner` (72 ops), `linode`, `contabo`, `ovh`, + `oneprovider`, `cloudflare` +- **Hosting panels** — `cpanel`, `whm`, `whmcs`, `ultrahost` +- **Chat** — `tawk` +- **Affiliate** — `iherb_apify`, `iherb_impact`, `iherb_partnerize` +- **Local ops** — `ops_ssh`, `ops_network`, `ops_security`, `ops_browser` + +## Troubleshooting + +- **Zero providers listed** — the specs directory was not found. Set + `CONNECTOR_HUB_SPECS_DIR` to the repo's `specs/` folder, or launch with the + repo root as the working directory. +- **Provider missing from `list_providers`** — its spec file failed to parse; + run `connector-hub validate`.