Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "elasticsearch-core-mcp-server"
version = "0.4.2"
version = "0.4.3"
edition = "2024"
authors = ["Elastic.co"]
license-file = "LICENSE"
Expand All @@ -22,7 +22,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1"

# CLI, config
clap = { version = "4", features = ["derive"] }
clap = { version = "4", features = ["derive", "env"] }
dotenvy = "0.15"
serde-aux = "4"
serde_json5 = "0.2"
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -27,5 +27,5 @@ FROM cgr.dev/chainguard/wolfi-base:latest

COPY --from=builder /app/target/release/elasticsearch-core-mcp-server /usr/local/bin/elasticsearch-core-mcp-server

EXPOSE 8000/tcp
EXPOSE 8080/tcp
ENTRYPOINT ["/usr/local/bin/elasticsearch-core-mcp-server"]
37 changes: 37 additions & 0 deletions Dockerfile-8000
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright Elasticsearch B.V. and contributors
# SPDX-License-Identifier: Apache-2.0

# Custom image to start
# To create a multi-arch image, run:
# docker buildx build --platform linux/amd64,linux/arm64 --tag elasticsearch-core-mcp-server .

FROM rust:1.88 AS builder

WORKDIR /app

COPY Cargo.toml Cargo.lock ./

# Cache dependencies
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/app/target \
mkdir -p ./src/bin && \
echo "pub fn main() {}" > ./src/bin/elasticsearch-core-mcp-server.rs && \
cargo build --release

COPY src ./src/

RUN cargo build --release

#--------------------------------------------------------------------------------------------------

FROM debian:stable-slim

RUN apt-get update && apt install -y libssl-dev && apt clean && rm -rf /var/lib/apt/lists/*

COPY --from=builder /app/target/release/elasticsearch-core-mcp-server /usr/local/bin/elasticsearch-core-mcp-server

ENV HTTP_ADDRESS="0.0.0.0:8000"

EXPOSE 8000/tcp
ENTRYPOINT ["/usr/local/bin/elasticsearch-core-mcp-server"]
CMD ["http"]
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,13 @@ cluster's API key
The MCP server is started in http mode with this command:

```bash
docker run --rm -e ES_URL -p 8000:8000 docker.elastic.co/mcp/elasticsearch http
docker run --rm -e ES_URL -p 8080:8080 docker.elastic.co/mcp/elasticsearch http
```

If for some reason your execution environment doesn't allow passing parameters to the container, they can be passed
using the `CLI_ARGS` environment variable: `docker run --rm -e ES_URL -e CLI_ARGS=http -p 8000:8000...`
using the `CLI_ARGS` environment variable: `docker run --rm -e ES_URL -e CLI_ARGS=http -p 8080:8080...`

The streamable-HTTP endpoint is at `http:<host>:8000/mcp`. There's also a health check at `http:<host>:8000/ping`
The streamable-HTTP endpoint is at `http:<host>:8080/mcp`. There's also a health check at `http:<host>:8080/ping`

Configuration for Claude Desktop (free edition that only supports the stdio protocol).

Expand Down
11 changes: 10 additions & 1 deletion src/bin/elasticsearch-core-mcp-server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,22 @@
// specific language governing permissions and limitations
// under the License.

use std::io::ErrorKind;
use clap::Parser;
use elasticsearch_core_mcp_server::cli::Cli;
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 => {}
Err(err) => return Err(err)?,
Ok(_) => {}
}

let env_args = std::env::vars().find(|(k, _v)| k == "CLI_ARGS").map(|(_k, v)| v);

let cli = if let Some(env_args) = env_args {
Expand All @@ -43,7 +52,7 @@ async fn main() -> anyhow::Result<()> {
.with_ansi(false)
.init();

tracing::info!("Starting MCP server");
tracing::info!("Elasticsearch MCP server, version {}", env!("CARGO_PKG_VERSION"));

cli.run().await
}
4 changes: 2 additions & 2 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ pub struct HttpCommand {
#[clap(short, long)]
pub config: Option<PathBuf>,

/// Address to listen to [default: 127.0.0.1:8000]
#[clap(long, value_name = "IP_ADDRESS:PORT")]
/// Address to listen to [default: 127.0.0.1:8080]
#[clap(long, value_name = "IP_ADDRESS:PORT", env = "HTTP_ADDRESS")]
pub address: Option<std::net::SocketAddr>,

/// Also start an SSE server on '/sse'
Expand Down
15 changes: 6 additions & 9 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ use is_container::is_container;
use rmcp::transport::stdio;
use rmcp::transport::streamable_http_server::session::never::NeverSessionManager;
use rmcp::{RoleServer, Service, ServiceExt};
use std::io::ErrorKind;
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
use std::path::PathBuf;
use std::sync::Arc;
Expand All @@ -45,6 +44,7 @@ impl Cli {
}

pub async fn run_stdio(cmd: StdioCommand) -> anyhow::Result<()> {
tracing::info!("Starting stdio server");
let handler = setup_services(&cmd.config).await?;
let service = handler.serve(stdio()).await.inspect_err(|e| {
tracing::error!("serving error: {:?}", e);
Expand All @@ -64,9 +64,9 @@ pub async fn run_http(cmd: HttpCommand) -> anyhow::Result<()> {
let address: SocketAddr = if let Some(addr) = cmd.address {
addr
} else if is_container() {
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8000)
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8080)
} else {
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8000)
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080)
};

let ct = HttpProtocol::serve_with_config(
Expand All @@ -82,18 +82,15 @@ pub async fn run_http(cmd: HttpCommand) -> anyhow::Result<()> {
)
.await?;

tracing::info!("Starting http server at address {}", address);

tokio::signal::ctrl_c().await?;
ct.cancel();
Ok(())
}

pub async fn setup_services(config: &Option<PathBuf>) -> anyhow::Result<impl Service<RoleServer> + Clone> {
// Read config file and expand variables, also accepting .env files
match dotenvy::dotenv() {
Err(dotenvy::Error::Io(io_err)) if io_err.kind() == ErrorKind::NotFound => {}
Err(err) => return Err(err)?,
Ok(_) => {}
}
// Read config file and expand variables

let config = if let Some(path) = config {
std::fs::read_to_string(path)?
Expand Down