Skip to main content

bitrouter_api/router/
tools.rs

1//! Warp filter for the `GET /v1/tools` endpoint.
2//!
3//! Returns all tools available through the router, including
4//! metadata such as name, description, and input schema.
5//!
6//! Supports optional query parameter filters:
7//!
8//! - `provider` — exact match on provider/server name
9//! - `id` — substring match on tool ID (case-insensitive)
10
11use std::sync::Arc;
12
13use bitrouter_core::routers::registry::ToolRegistry;
14use serde::Serialize;
15use warp::Filter;
16
17/// Query parameters for filtering the tool list.
18#[derive(Debug, Default)]
19struct ToolQuery {
20    /// Filter by provider/server name (exact match).
21    provider: Option<String>,
22    /// Filter by tool ID (substring match, case-insensitive).
23    id: Option<String>,
24}
25
26/// Creates a warp filter for `GET /v1/tools`.
27///
28/// Accepts `Option<Arc<T>>` — when `None` (no tool source configured), the
29/// endpoint returns 404.
30pub fn tools_filter<T>(
31    registry: Option<Arc<T>>,
32) -> impl Filter<Extract = (impl warp::Reply,), Error = warp::Rejection> + Clone
33where
34    T: ToolRegistry + 'static,
35{
36    warp::path!("v1" / "tools")
37        .and(warp::get())
38        .and(optional_raw_query())
39        .and(warp::any().map(move || registry.clone()))
40        .and_then(handle_list_tools)
41}
42
43/// Extracts the raw query string as `Option<String>`. Returns `None` when
44/// the request has no query component instead of rejecting.
45fn optional_raw_query()
46-> impl Filter<Extract = (Option<String>,), Error = std::convert::Infallible> + Clone {
47    warp::query::raw()
48        .map(Some)
49        .or(warp::any().map(|| None))
50        .unify()
51}
52
53#[derive(Serialize)]
54struct ToolResponse {
55    id: String,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    name: Option<String>,
58    provider: String,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    description: Option<String>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    input_schema: Option<serde_json::Value>,
63}
64
65fn parse_query(raw: &str) -> ToolQuery {
66    let mut query = ToolQuery::default();
67    for pair in raw.split('&') {
68        if let Some((key, value)) = pair.split_once('=') {
69            match key {
70                "provider" => query.provider = Some(value.to_owned()),
71                "id" => query.id = Some(value.to_owned()),
72                _ => {}
73            }
74        }
75    }
76    query
77}
78
79async fn handle_list_tools<T: ToolRegistry>(
80    raw_query: Option<String>,
81    registry: Option<Arc<T>>,
82) -> Result<impl warp::Reply, warp::Rejection> {
83    let Some(registry) = registry else {
84        return Err(warp::reject::not_found());
85    };
86    let query = raw_query.as_deref().map(parse_query).unwrap_or_default();
87    let entries = registry.list_tools().await;
88    let id_lower = query.id.as_deref().map(str::to_lowercase);
89
90    let tools: Vec<ToolResponse> = entries
91        .into_iter()
92        .filter(|e| {
93            if query.provider.as_deref().is_some_and(|p| e.provider != p) {
94                return false;
95            }
96            if id_lower
97                .as_deref()
98                .is_some_and(|s| !e.id.to_lowercase().contains(s))
99            {
100                return false;
101            }
102            true
103        })
104        .map(|e| {
105            let input_schema = e
106                .definition
107                .input_schema
108                .and_then(|s| serde_json::to_value(s).ok());
109            ToolResponse {
110                id: e.id,
111                name: Some(e.definition.name),
112                provider: e.provider,
113                description: e.definition.description,
114                input_schema,
115            }
116        })
117        .collect();
118    Ok(warp::reply::json(&serde_json::json!({ "tools": tools })))
119}