1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
use anyhow::Result;
use clap::Parser;
use mcp_postgres::{Args, config, http, metrics, pool, server};
use tracing::info;
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
#[tokio::main]
async fn main() -> Result<()> {
// NOTE: mimalloc reads its `MIMALLOC_*` tuning env vars once, at allocator
// init, which happens before `main` runs (the `#[global_allocator]` is live
// from the first allocation). Setting them here had no effect, so the block
// was removed. To tune mimalloc, export the vars in the process environment
// before launch, or configure them via the mimalloc crate's build features.
let args = Args::parse();
// Install the rustls `ring` crypto provider as the process default up front.
// Postgres TLS installs it lazily (src/tls.rs), but the data-import HTTP
// client (reqwest, built with `rustls-no-provider`) relies on the process
// default for any HTTPS fetch that happens without a prior Postgres TLS
// connection. Idempotent — the first install wins.
let _ = rustls::crypto::ring::default_provider().install_default();
// Initialize logging
init_tracing(&args.log_level)?;
info!("Starting MCP PostgreSQL Server");
info!("Version: {}", env!("CARGO_PKG_VERSION"));
log_mimalloc_version();
// Load configuration
let config = config::Config::from_args(&args)?;
// Security: refuse to expose a network transport without authentication
// when bound to a non-loopback address. Loopback-only binds remain open
// for local development; stdio mode is a trusted local pipe.
if !args.stdio
&& config.server.auth_token.is_none()
&& !mcp_postgres::auth::is_loopback_host(&config.server.host)
{
anyhow::bail!(
"Refusing to bind to non-loopback host '{}' without an auth token. \
Set --auth-token or the MCP_AUTH_TOKEN env var, or bind to a loopback address.",
config.server.host
);
}
if config.server.auth_token.is_some() {
info!("Transport authentication: ENABLED (token required on TCP and HTTP)");
}
// Initialize metrics if enabled
if args.enable_metrics {
metrics::init_metrics(args.metrics_port)?;
info!("Metrics enabled on port {}", args.metrics_port);
}
// Create connection pool. The server's request_timeout is enforced at the
// database as a per-connection statement_timeout so no single query can pin
// a pooled connection indefinitely. In restricted mode, connections are also
// set read-only so writes are rejected at the database, not just by tool name.
let read_only = config.server.access_mode == config::AccessMode::Restricted;
let pool = std::sync::Arc::new(
pool::ConnectionPool::with_session_setup(
&config.database.url,
config.pool.clone(),
config.server.request_timeout,
read_only,
)
.await?,
);
info!(
"Connection pool initialized: min={}, max={}",
config.pool.min_size, config.pool.max_size
);
// Create server
let mcp_server = server::MCPServer::new(config.clone(), pool.clone());
info!("Server initialized successfully");
// Run server (TCP, HTTP, or stdio mode)
if args.stdio {
info!("Running in stdio mode");
mcp_server.run_stdio().await?;
} else {
// Start both TCP and HTTP servers in parallel
info!("Starting TCP server on port {}", args.port);
info!("Starting HTTP/2 server on port {}", args.http_port);
let tcp_handle = tokio::spawn(async move {
if let Err(e) = mcp_server.run().await {
eprintln!("TCP server error: {}", e);
}
});
let http_config = config.clone();
let http_pool = pool.clone();
let http_port = args.http_port;
let http_handle = tokio::spawn(async move {
if let Err(e) = http::create_http_server(http_pool, http_config, http_port).await {
eprintln!("HTTP server error: {}", e);
}
});
// Wait for either server to exit
tokio::select! {
_ = tcp_handle => info!("TCP server exited"),
_ = http_handle => info!("HTTP server exited"),
}
}
info!("Server shutdown complete");
Ok(())
}
/// Log the linked mimalloc version and warn if it is not v3.
///
/// v3 is selected by the absence of `libmimalloc-sys/v2`. Cargo feature
/// unification is additive, so a transitive `v2` feature would silently
/// downgrade the allocator — this surfaces that instead of hiding it.
fn log_mimalloc_version() {
// SAFETY: mi_version() is a pure read of a compile-time constant.
let v = unsafe { libmimalloc_sys::mi_version() };
info!(
"mimalloc version: {}.{}.{}",
v / 10000,
(v / 100) % 100,
v % 100
);
if v < 30000 {
tracing::warn!(
"Expected mimalloc v3 but linked v{v}; a dependency enabled the `v2` feature"
);
}
}
fn init_tracing(log_level: &str) -> Result<()> {
use tracing_subscriber::{EnvFilter, fmt, prelude::*};
let env_filter = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new(log_level))
.unwrap_or_else(|_| EnvFilter::new("info"));
tracing_subscriber::registry()
.with(env_filter)
.with(fmt::layer().with_writer(std::io::stderr))
.init();
Ok(())
}