Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions src/bin/elasticsearch-core-mcp-server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,14 @@
// specific language governing permissions and limitations
// under the License.

use std::io::ErrorKind;
use clap::Parser;
use elasticsearch_core_mcp_server::cli::Cli;
use std::io::ErrorKind;
use tracing_subscriber::EnvFilter;
// To test with stdio, use npx @modelcontextprotocol/inspector cargo run -p elastic-mcp

#[tokio::main]
async fn main() -> anyhow::Result<()> {

// Also accept .env files
match dotenvy::dotenv() {
Err(dotenvy::Error::Io(io_err)) if io_err.kind() == ErrorKind::NotFound => {}
Expand Down
14 changes: 8 additions & 6 deletions src/bin/start_http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,14 @@ use elasticsearch_core_mcp_server::run_http;
pub async fn main() -> anyhow::Result<()> {
println!("Current directory: {:?}", std::env::current_dir()?);

run_http(HttpCommand {
config: Some("elastic-mcp.json5".parse()?),
address: None,
sse: true,
},
false)
run_http(
HttpCommand {
config: Some("elastic-mcp.json5".parse()?),
address: None,
sse: true,
},
false,
)
.await?;

Ok(())
Expand Down
2 changes: 1 addition & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ use std::path::PathBuf;
#[command(version)]
pub struct Cli {
/// Container mode: change default http address, rewrite localhost to the host's address
#[clap(global=true, long, env = "CONTAINER_MODE")]
#[clap(global = true, long, env = "CONTAINER_MODE")]
pub container_mode: bool,

#[clap(subcommand)]
Expand Down
5 changes: 4 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,10 @@ pub async fn run_http(cmd: HttpCommand, container_mode: bool) -> anyhow::Result<
Ok(())
}

pub async fn setup_services(config: &Option<PathBuf>, container_mode: bool) -> anyhow::Result<impl Service<RoleServer> + Clone> {
pub async fn setup_services(
config: &Option<PathBuf>,
container_mode: bool,
) -> anyhow::Result<impl Service<RoleServer> + Clone> {
// Read config file and expand variables

let config = if let Some(path) = config {
Expand Down
23 changes: 21 additions & 2 deletions src/servers/elasticsearch/base_tools.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,24 @@ struct SearchParams {

/// Complete Elasticsearch query DSL object that can include query, size, from, sort, etc.
query_body: Map<String, Value>, // note: just Value doesn't work, as Claude would send a string

/// Query timeout (e.g., "30s", "2m", "1m30s"). Defaults to "120s"
#[serde(default = "default_timeout")]
timeout: String,
}

fn default_timeout() -> String {
"120s".to_string()
}

#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
struct EsqlQueryParams {
/// Complete Elasticsearch ES|QL query
query: String,

/// Query timeout (e.g., "30s", "2m", "1m30s"). Defaults to "120s"
#[serde(default = "default_timeout")]
timeout: String,
}

#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
Expand Down Expand Up @@ -158,6 +170,7 @@ impl EsBaseTools {
index,
fields,
query_body,
timeout,
}): Parameters<SearchParams>,
) -> Result<CallToolResult, rmcp::Error> {
let es_client = self.es_client.get(req_ctx);
Expand All @@ -178,6 +191,7 @@ impl EsBaseTools {
let response = es_client
.search(SearchParts::Index(&[&index]))
.body(query_body)
.timeout(&timeout)
.send()
.await;

Expand Down Expand Up @@ -226,11 +240,14 @@ impl EsBaseTools {
async fn esql(
&self,
req_ctx: RequestContext<RoleServer>,
Parameters(EsqlQueryParams { query }): Parameters<EsqlQueryParams>,
Parameters(EsqlQueryParams { query, timeout }): Parameters<EsqlQueryParams>,
) -> Result<CallToolResult, rmcp::Error> {
let es_client = self.es_client.get(req_ctx);

let request = EsqlQueryRequest { query };
let request = EsqlQueryRequest {
query,
query_timeout: Some(timeout),
};

let response = es_client.esql().query().body(request).send().await;
let response: EsqlQueryResponse = read_json(response).await?;
Expand Down Expand Up @@ -382,6 +399,8 @@ pub struct MappingProperty {
#[derive(Serialize, Deserialize)]
pub struct EsqlQueryRequest {
pub query: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub query_timeout: Option<String>,
}

#[derive(Serialize, Deserialize)]
Expand Down
15 changes: 11 additions & 4 deletions src/servers/elasticsearch/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,10 @@ pub enum SearchTemplate {
pub struct ElasticsearchMcp {}

impl ElasticsearchMcp {
pub fn new_with_config(config: ElasticsearchMcpConfig, container_mode: bool) -> anyhow::Result<base_tools::EsBaseTools> {
pub fn new_with_config(
config: ElasticsearchMcpConfig,
container_mode: bool,
) -> anyhow::Result<base_tools::EsBaseTools> {
let creds = if let Some(api_key) = config.api_key.clone() {
Some(Credentials::EncodedApiKey(api_key))
} else if let Some(username) = config.username.clone() {
Expand Down Expand Up @@ -224,13 +227,17 @@ impl ElasticsearchMcp {
fn rewrite_localhost(url: &mut Url) -> anyhow::Result<()> {
use std::net::ToSocketAddrs;
let aliases = &[
"host.docker.internal", // Docker
"host.docker.internal", // Docker
"host.containers.internal", // Podman, maybe others
];

if let Some(host) = url.host_str() && host == "localhost" {
if let Some(host) = url.host_str()
&& host == "localhost"
{
for alias in aliases {
if let Ok(mut alias_add) = (*alias, 80).to_socket_addrs() && alias_add.next().is_some() {
if let Ok(mut alias_add) = (*alias, 80).to_socket_addrs()
&& alias_add.next().is_some()
{
url.set_host(Some(alias))?;
tracing::info!("Container mode: using '{alias}' instead of 'localhost'");
return Ok(());
Expand Down