Skip to content

Commit a7958d9

Browse files
authored
Add container-mode to rename localhost to the container host name (#172)
1 parent 1f63e1b commit a7958d9

File tree

9 files changed

+52
-31
lines changed

9 files changed

+52
-31
lines changed

Cargo.lock

Lines changed: 0 additions & 10 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ clap = { version = "4", features = ["derive", "env"] }
2626
dotenvy = "0.15"
2727
serde-aux = "4"
2828
serde_json5 = "0.2"
29-
is-container = "0.1.2"
3029

3130
# Logging
3231
tracing = "0.1"

Dockerfile

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ WORKDIR /app
1111
COPY Cargo.toml Cargo.lock ./
1212

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

@@ -27,5 +25,7 @@ FROM cgr.dev/chainguard/wolfi-base:latest
2725

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

28+
ENV CONTAINER_MODE=true
29+
3030
EXPOSE 8080/tcp
3131
ENTRYPOINT ["/usr/local/bin/elasticsearch-core-mcp-server"]

Dockerfile-8000

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,7 @@ WORKDIR /app
1212
COPY Cargo.toml Cargo.lock ./
1313

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

@@ -36,6 +34,7 @@ EOF
3634

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

37+
ENV CONTAINER_MODE=true
3938
ENV HTTP_ADDRESS="0.0.0.0:8000"
4039

4140
EXPOSE 8000/tcp

src/bin/start_http.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ pub async fn main() -> anyhow::Result<()> {
2828
config: Some("elastic-mcp.json5".parse()?),
2929
address: None,
3030
sse: true,
31-
})
31+
},
32+
false)
3233
.await?;
3334

3435
Ok(())

src/cli.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@ use std::path::PathBuf;
2525
/// Elastic MCP server
2626
#[derive(Debug, Parser)]
2727
pub struct Cli {
28+
/// Container mode: change default http address, rewrite localhost to the host's address
29+
#[clap(global=true, long, env = "CONTAINER_MODE")]
30+
pub container_mode: bool,
31+
2832
#[clap(subcommand)]
2933
pub command: Command,
3034
}
@@ -51,7 +55,7 @@ pub struct HttpCommand {
5155
pub sse: bool,
5256
}
5357

54-
/// Start a stdio server
58+
/// Start an stdio server
5559
#[derive(Debug, Args)]
5660
pub struct StdioCommand {
5761
/// Config file

src/lib.rs

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ use crate::cli::{Cli, Command, Configuration, HttpCommand, StdioCommand};
2424
use crate::protocol::http::{HttpProtocol, HttpServerConfig};
2525
use crate::servers::elasticsearch;
2626
use crate::utils::interpolator;
27-
use is_container::is_container;
2827
use rmcp::transport::stdio;
2928
use rmcp::transport::streamable_http_server::session::never::NeverSessionManager;
3029
use rmcp::{RoleServer, Service, ServiceExt};
@@ -37,15 +36,15 @@ use tokio_util::sync::CancellationToken;
3736
impl Cli {
3837
pub async fn run(self) -> anyhow::Result<()> {
3938
match self.command {
40-
Command::Stdio(cmd) => run_stdio(cmd).await,
41-
Command::Http(cmd) => run_http(cmd).await,
39+
Command::Stdio(cmd) => run_stdio(cmd, self.container_mode).await,
40+
Command::Http(cmd) => run_http(cmd, self.container_mode).await,
4241
}
4342
}
4443
}
4544

46-
pub async fn run_stdio(cmd: StdioCommand) -> anyhow::Result<()> {
45+
pub async fn run_stdio(cmd: StdioCommand, container_mode: bool) -> anyhow::Result<()> {
4746
tracing::info!("Starting stdio server");
48-
let handler = setup_services(&cmd.config).await?;
47+
let handler = setup_services(&cmd.config, container_mode).await?;
4948
let service = handler.serve(stdio()).await.inspect_err(|e| {
5049
tracing::error!("serving error: {:?}", e);
5150
})?;
@@ -58,12 +57,12 @@ pub async fn run_stdio(cmd: StdioCommand) -> anyhow::Result<()> {
5857
Ok(())
5958
}
6059

61-
pub async fn run_http(cmd: HttpCommand) -> anyhow::Result<()> {
62-
let handler = setup_services(&cmd.config).await?;
60+
pub async fn run_http(cmd: HttpCommand, container_mode: bool) -> anyhow::Result<()> {
61+
let handler = setup_services(&cmd.config, container_mode).await?;
6362
let server_provider = move || handler.clone();
6463
let address: SocketAddr = if let Some(addr) = cmd.address {
6564
addr
66-
} else if is_container() {
65+
} else if container_mode {
6766
SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 8080)
6867
} else {
6968
SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), 8080)
@@ -89,7 +88,7 @@ pub async fn run_http(cmd: HttpCommand) -> anyhow::Result<()> {
8988
Ok(())
9089
}
9190

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

9594
let config = if let Some(path) = config {
@@ -123,6 +122,6 @@ pub async fn setup_services(config: &Option<PathBuf>) -> anyhow::Result<impl Ser
123122
Err(err) => return Err(err)?,
124123
};
125124

126-
let handler = elasticsearch::ElasticsearchMcp::new_with_config(config.elasticsearch)?;
125+
let handler = elasticsearch::ElasticsearchMcp::new_with_config(config.elasticsearch, container_mode)?;
127126
Ok(handler)
128127
}

src/servers/elasticsearch/mod.rs

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ pub enum SearchTemplate {
175175
pub struct ElasticsearchMcp {}
176176

177177
impl ElasticsearchMcp {
178-
pub fn new_with_config(config: ElasticsearchMcpConfig) -> anyhow::Result<base_tools::EsBaseTools> {
178+
pub fn new_with_config(config: ElasticsearchMcpConfig, container_mode: bool) -> anyhow::Result<base_tools::EsBaseTools> {
179179
let creds = if let Some(api_key) = config.api_key.clone() {
180180
Some(Credentials::EncodedApiKey(api_key))
181181
} else if let Some(login) = config.login.clone() {
@@ -190,7 +190,10 @@ impl ElasticsearchMcp {
190190
return Err(anyhow::Error::msg("Elasticsearch URL is empty"));
191191
}
192192

193-
let url = Url::parse(url)?;
193+
let mut url = Url::parse(url)?;
194+
if container_mode {
195+
rewrite_localhost(&mut url)?;
196+
}
194197

195198
let pool = elasticsearch::http::transport::SingleNodeConnectionPool::new(url.clone());
196199
let mut transport = elasticsearch::http::transport::TransportBuilder::new(pool);
@@ -214,6 +217,30 @@ impl ElasticsearchMcp {
214217
//------------------------------------------------------------------------------------------------
215218
// Utilities
216219

220+
/// Rewrite urls targeting `localhost` to a hostname that maps to the container host, if possible.
221+
///
222+
/// The host name for the container host depends on the OCI runtime used. This is useful to accept
223+
/// Elasticsearch URLs like `http://localhost:9200`.
224+
fn rewrite_localhost(url: &mut Url) -> anyhow::Result<()> {
225+
use std::net::ToSocketAddrs;
226+
let aliases = &[
227+
"host.docker.internal", // Docker
228+
"host.containers.internal", // Podman, maybe others
229+
];
230+
231+
if let Some(host) = url.host_str() && host == "localhost" {
232+
for alias in aliases {
233+
if let Ok(mut alias_add) = (*alias, 80).to_socket_addrs() && alias_add.next().is_some() {
234+
url.set_host(Some(alias))?;
235+
tracing::info!("Container mode: using '{alias}' instead of 'localhost'");
236+
return Ok(());
237+
}
238+
}
239+
}
240+
tracing::warn!("Container mode: cound not find a replacement for 'localhost'");
241+
Ok(())
242+
}
243+
217244
/// Map any error to an internal error of the MCP server
218245
pub fn internal_error(e: impl std::error::Error) -> rmcp::Error {
219246
rmcp::Error::internal_error(e.to_string(), None)

tests/http_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ async fn http_tool_list() -> anyhow::Result<()> {
3636
let addr = find_address()?;
3737

3838
let cli = cli::Cli {
39+
container_mode: false,
3940
command: cli::Command::Http(cli::HttpCommand {
4041
config: None,
4142
address: Some(addr),
@@ -116,6 +117,7 @@ async fn end_to_end() -> anyhow::Result<()> {
116117
// Start an http MCP server
117118
let addr = find_address()?;
118119
let cli = cli::Cli {
120+
container_mode: false,
119121
command: cli::Command::Http(cli::HttpCommand {
120122
config: None,
121123
address: Some(addr),

0 commit comments

Comments
 (0)