Skip to content

feat(web): add Keenable search provider and bundled MCP integration - #1286

Open
ilya-bogin-keenable wants to merge 2 commits into
RightNow-AI:mainfrom
keenableai:feat/keenable-web-search
Open

ilya-bogin-keenable wants to merge 2 commits into
RightNow-AI:mainfrom
keenableai:feat/keenable-web-search

Conversation

@ilya-bogin-keenable

@ilya-bogin-keenable ilya-bogin-keenable commented Sep 7, 2026 •

Copy link
Copy Markdown

Summary

Adds Keenable web search in two places, both opt-in: a native keenable provider for the built-in web_search tool, used only when [web] search_provider = "keenable" is set, and a bundled MCP integration that is enabled only by openfang add keenable. Keenable works without an API key, so selecting it gives search API results on an install with no keys configured; KEENABLE_API_KEY is optional and only lifts the per-IP rate limit. I work at Keenable.

Nothing changes for anyone who does not select it. The default provider stays auto, and the auto cascade is untouched (Tavily, Brave, Perplexity, SearXNG, DuckDuckGo): it never picks Keenable, with or without a key.

Changes

  • crates/openfang-types/src/config.rs: SearchProvider::Keenable, [web.keenable] (KeenableSearchConfig: api_key_env default KEENABLE_API_KEY, max_results 5, snippet_max_length 500), no validation warning when the key is unset since it is optional.
  • crates/openfang-runtime/src/web_search.rs: search_keenable, reached only from the explicit SearchProvider::Keenable arm. No key: POST https://api.keenable.ai/v1/search/public with the required X-Keenable-Title: openfang header. Key set: POST /v1/search with X-API-Key. Body is {query, max_results (clamped 1..50), snippet_max_length}. Reads snippet, falls back to description. 429 is an error that names the env var; other non-2xx follow the Brave/Tavily shape. Output goes through wrap_external_content("keenable-search", ...) like the other providers. Listed in all_providers(); search_auto is unchanged.
  • crates/openfang-extensions/integrations/keenable.toml + bundled.rs / registry.rs / installer.rs / lib.rs: bundled template for https://api.keenable.ai/mcp (Streamable HTTP, no required_env, no OAuth). Counts 25 to 26; the transport test now accepts HTTPS remote endpoints. The template exposes mcp_keenable_search_web_pages and mcp_keenable_fetch_page_content. Nothing is installed by default; every bundled template still lists as Available until the user adds it.
  • docs/configuration.md, docs/mcp-a2a.md, docs/security.md, docs/cli-reference.md: provider table (keenable marked opt-in, auto row unchanged), [web.keenable] section, env var tables, and a bundled-integration example.

No new dependencies. Cargo.lock untouched.

Testing

  • cargo clippy -p openfang-types -p openfang-runtime -p openfang-extensions --all-targets -- -D warnings passes (the three touched crates; the full --workspace run was not done locally because of disk space, CI covers it)
  • cargo test -p openfang-runtime web_search 12 passed; cargo test -p openfang-extensions 55 passed; cargo test -p openfang-types 387 passed
  • cargo fmt --all -- --check clean
  • Live integration tested, no keys in the environment, query rust programming language: WebSearchEngine::search with search_provider = "keenable" returned 5 results from Keenable's public endpoint in 321 ms. The same query with the default auto was answered by DuckDuckGo and never reached Keenable. The hosted MCP endpoint answers anonymous initialize and tools/list (JSON, no session header required) with the two tools above.

New tests: parser (snippet first, description fallback, missing URL skipped, max honoured, malformed bodies), and four request-shape tests against a local one-shot TCP server: keyless hits /v1/search/public with X-Keenable-Title and no X-API-Key; keyed hits /v1/search with X-API-Key in the header and not in the body, max_results clamped to 50; 429 returns an error naming the env var; empty results returns the "No results" error. Config: test_web_search_keenable_is_opt_in (the default stays auto; search_provider = "keenable" parses and raises no missing-key warning). Extensions: install_keenable_without_credentials (install is Ready with no key, survives a registry reload as an Http entry with empty env/headers, removes cleanly).

Security

  • No new unsafe code
  • No secrets or API keys in diff (test fixtures use placeholder strings)
  • User input validated at boundaries (max_results clamped to the API range; provider output wrapped as external content like the other providers)

New egress: api.keenable.ai, only when the user selects it (search_provider = "keenable") or installs the keenable integration. The keyless request carries the query, max_results, snippet_max_length, and a fixed app title; the API key, when set, travels only in a request header via the existing Zeroizing resolve_api_key path.

Keenable (https://keenable.ai) works without an API key, so it is the
first web_search provider here that returns API results on a fresh
install. Two pieces:

- Native provider in web_search.rs: `search_provider = "keenable"`.
  Without KEENABLE_API_KEY the request goes to /v1/search/public with
  the required X-Keenable-Title header; with the key it goes to
  /v1/search with X-API-Key. The key only lifts the per-IP rate limit.
  In auto mode Keenable runs after every keyed provider and SearXNG,
  and before the DuckDuckGo HTML scrape, so paid keys keep priority.
  429 surfaces as an error (naming the env var), never as an empty
  result. Snippets are read from `snippet` with `description` as the
  fallback, and `snippet_max_length` (default 500) is sent as a hint.
- Bundled MCP integration `keenable` pointing at the hosted server
  https://api.keenable.ai/mcp over Streamable HTTP. No credentials, so
  `openfang add keenable` is Ready immediately.

Default provider is unchanged (auto). Tests cover the parser, keyless
and keyed request shape (endpoint, headers, body), 429 and empty
responses via a local one-shot HTTP server, plus the bundled template
count and a credential-free install round trip.
Keenable is now opt-in: the web_search tool uses it only when
`[web] search_provider = "keenable"` is set. The auto cascade is back
to Tavily, Brave, Perplexity, SearXNG, DuckDuckGo and never selects
Keenable, with or without a key. Selected explicitly it still works
without an API key.

Add a config test that the default stays `auto` and that
`search_provider = "keenable"` parses with no missing-key warning.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant