Skip to content

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

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

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

Conversation

@ilya-bogin-keenable

Copy link
Copy Markdown

Summary

Adds Keenable web search in two places: a native keenable provider for the built-in web_search tool, and a bundled MCP integration (openfang add keenable) for the hosted Keenable MCP server. Keenable works without an API key, so this is the first provider here that returns API results on a fresh install; KEENABLE_API_KEY is optional and only lifts the per-IP rate limit. I work at Keenable.

Default provider is unchanged (auto). In auto mode Keenable runs after every keyed provider (Tavily, Brave, Perplexity) and SearXNG, and before the DuckDuckGo HTML scrape, so anyone with a paid key configured sees no change in which provider answers first. The practical change is for installs with no search key at all: they get a search API instead of the scraper, and the scraper stays as the last fallback if Keenable fails.

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.
  • crates/openfang-runtime/src/web_search.rs: search_keenable. 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. Added to all_providers() and to the auto cascade before DuckDuckGo.
  • crates/openfang-runtime/src/tool_runner.rs: web_search tool description lists Keenable.
  • 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.
  • docs/configuration.md, docs/mcp-a2a.md, docs/security.md, docs/cli-reference.md: provider table, [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 386 passed
  • cargo fmt --all -- --check clean
  • Live integration tested: WebSearchEngine::search with search_provider = "keenable" and no key in the environment, query rust programming language: 5 results, 5 non-empty snippets, 3,390 chars of output, 330 ms. 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. 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 provider is selected or reached in the auto cascade, or when the keenable integration is installed. 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.
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