use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::mcp::error::McpError;
use crate::mcp::expose::ExposeResolver;
use crate::mcp::schema::{ExampleCall, FieldInfo, SchemaCatalog};
#[derive(Debug, Default, Deserialize, JsonSchema)]
pub struct ListSourcesRequest {}
#[derive(Debug, Serialize)]
pub struct ListSourcesResponse {
pub data_sources: Vec<DataSourceInfo>,
}
#[derive(Debug, Serialize)]
pub struct DataSourceInfo {
pub name: String,
pub kind: String,
pub description: String,
pub searchable: bool,
pub containers: Vec<String>,
pub schema: Vec<FieldInfo>,
pub examples: Vec<ExampleCall>,
}
pub async fn run(
expose: &ExposeResolver,
schema: &SchemaCatalog,
) -> Result<ListSourcesResponse, McpError> {
let mut out = Vec::new();
for (name, resolved) in expose.list_all() {
let kind_info = schema.lookup(&resolved.kind).ok_or_else(|| {
McpError::Internal(format!("schema for kind '{}' not found", resolved.kind))
})?;
out.push(DataSourceInfo {
name: name.clone(),
kind: resolved.kind.clone(),
description: kind_info.description.clone(),
searchable: kind_info.searchable,
containers: resolved
.backed_by
.iter()
.map(|b| b.container.clone())
.collect(),
schema: kind_info.fields.clone(),
examples: kind_info.examples.clone(),
});
}
out.sort_by(|a, b| a.name.cmp(&b.name));
Ok(ListSourcesResponse { data_sources: out })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mcp::schema::SchemaCatalog;
use crate::mcp::tools::test_helpers::{build_expose, build_expose_jira_issues};
#[tokio::test]
async fn list_sources_returns_only_exposed_data_sources() {
let expose = build_expose_jira_issues(); let schema = SchemaCatalog::new();
let resp = run(&expose, &schema).await.unwrap();
assert_eq!(resp.data_sources.len(), 1);
assert_eq!(resp.data_sources[0].name, "jira_issues");
}
#[tokio::test]
async fn list_sources_includes_kind_and_schema() {
let expose = build_expose_jira_issues();
let schema = SchemaCatalog::new();
let resp = run(&expose, &schema).await.unwrap();
let ds = &resp.data_sources[0];
assert_eq!(ds.kind, "jira_issue");
assert!(!ds.schema.is_empty(), "schema should have fields");
assert!(ds.schema.iter().any(|f| f.field == "key"));
}
#[tokio::test]
async fn list_sources_marks_searchable() {
let expose = build_expose_jira_issues();
let schema = SchemaCatalog::new();
let resp = run(&expose, &schema).await.unwrap();
let ds = &resp.data_sources[0];
assert!(ds.searchable);
}
#[tokio::test]
async fn list_sources_marks_non_searchable() {
let expose = build_expose(&[("jira_sprints", "jira_sprint", "jira-sprints")]);
let schema = SchemaCatalog::new();
let resp = run(&expose, &schema).await.unwrap();
let ds = &resp.data_sources[0];
assert!(!ds.searchable);
}
#[tokio::test]
async fn list_sources_sorted_by_name() {
let expose = build_expose(&[
("jira_sprints", "jira_sprint", "jira-sprints"),
("jira_issues", "jira_issue", "jira-issues"),
("confluence_pages", "confluence_page", "confluence-pages"),
]);
let schema = SchemaCatalog::new();
let resp = run(&expose, &schema).await.unwrap();
let names: Vec<&str> = resp.data_sources.iter().map(|d| d.name.as_str()).collect();
assert_eq!(
names,
vec!["confluence_pages", "jira_issues", "jira_sprints"]
);
}
#[tokio::test]
async fn list_sources_includes_containers() {
let expose = build_expose_jira_issues();
let schema = SchemaCatalog::new();
let resp = run(&expose, &schema).await.unwrap();
let ds = &resp.data_sources[0];
assert_eq!(ds.containers, vec!["jira-issues"]);
}
#[tokio::test]
async fn list_sources_includes_examples() {
let expose = build_expose_jira_issues();
let schema = SchemaCatalog::new();
let resp = run(&expose, &schema).await.unwrap();
let ds = &resp.data_sources[0];
assert!(!ds.examples.is_empty(), "jira_issue should have examples");
}
}