use std::sync::Arc;
use pmcp::ServerBuilder;
use crate::config::ServerConfig;
use crate::error::Result;
use crate::sql::SqlConnector;
pub trait ServerBuilderExt: Sized {
fn tools_from_config(self, config: &ServerConfig) -> Self;
fn try_tools_from_config(self, config: &ServerConfig) -> Result<Self>;
fn tools_from_config_with_connector(
self,
config: &ServerConfig,
connector: Arc<dyn SqlConnector>,
) -> Self;
fn try_tools_from_config_with_connector(
self,
config: &ServerConfig,
connector: Arc<dyn SqlConnector>,
) -> Result<Self>;
fn code_mode_from_config(self, config: &ServerConfig) -> Self;
fn try_code_mode_from_config(self, config: &ServerConfig) -> Result<Self>;
fn try_code_mode_from_config_with_connector(
self,
config: &ServerConfig,
connector: Arc<dyn SqlConnector>,
) -> Result<Self>;
}
impl ServerBuilderExt for ServerBuilder {
fn tools_from_config(self, config: &ServerConfig) -> Self {
self.try_tools_from_config(config).expect(
"tools_from_config: synthesize_from_config returned an error — \
prefer try_tools_from_config to handle this as a Result",
)
}
fn try_tools_from_config(mut self, config: &ServerConfig) -> Result<Self> {
let synthesized = crate::tools::synthesize_from_config(config)?;
if synthesized.is_empty() {
tracing::warn!(
target: "pmcp_server_toolkit::builder_ext",
"try_tools_from_config: config declared zero [[tools]] entries — \
server will expose no tools (set RUST_LOG=warn to surface this)"
);
}
for (name, _info, handler) in synthesized {
self = self.tool_arc(name, handler);
}
Ok(self)
}
fn tools_from_config_with_connector(
self,
config: &ServerConfig,
connector: Arc<dyn SqlConnector>,
) -> Self {
self.try_tools_from_config_with_connector(config, connector)
.expect(
"tools_from_config_with_connector: synthesize_from_config_with_connector \
returned an error — prefer try_tools_from_config_with_connector to handle \
this as a Result",
)
}
fn try_tools_from_config_with_connector(
mut self,
config: &ServerConfig,
connector: Arc<dyn SqlConnector>,
) -> Result<Self> {
let synthesized = crate::tools::synthesize_from_config_with_connector(config, connector)?;
if synthesized.is_empty() {
tracing::warn!(
target: "pmcp_server_toolkit::builder_ext",
"try_tools_from_config_with_connector: config declared zero [[tools]] entries — \
server will expose no tools (set RUST_LOG=warn to surface this)"
);
}
for (name, _info, handler) in synthesized {
self = self.tool_arc(name, handler);
}
Ok(self)
}
fn code_mode_from_config(self, config: &ServerConfig) -> Self {
self.try_code_mode_from_config(config).expect(
"code_mode_from_config: register_code_mode_tools errored — \
prefer try_code_mode_from_config to handle (e.g. missing env var)",
)
}
fn try_code_mode_from_config(self, config: &ServerConfig) -> Result<Self> {
#[cfg(feature = "code-mode")]
{
crate::code_mode::register_code_mode_tools(self, config)
}
#[cfg(not(feature = "code-mode"))]
{
let _ = config;
tracing::warn!(
target: "pmcp_server_toolkit::builder_ext",
"try_code_mode_from_config called but `code-mode` feature is \
disabled at compile-time — skipping (T-83-08-02 visibility)"
);
Ok(self)
}
}
fn try_code_mode_from_config_with_connector(
self,
config: &ServerConfig,
connector: Arc<dyn SqlConnector>,
) -> Result<Self> {
#[cfg(feature = "code-mode")]
{
if config.code_mode.is_none() {
return Ok(self); }
let executor: Arc<dyn crate::code_mode::CodeExecutor> = Arc::new(
crate::code_mode::SqlCodeExecutor::new(connector, config.clone())?,
);
crate::code_mode::code_mode_tools_from_executor(
self,
config,
executor,
crate::code_mode::ValidationFlavor::Sql,
)
}
#[cfg(not(feature = "code-mode"))]
{
let _ = (config, connector);
tracing::warn!(
target: "pmcp_server_toolkit::builder_ext",
"try_code_mode_from_config_with_connector called but `code-mode` \
feature is disabled at compile-time — skipping (T-83-08-02 visibility)"
);
Ok(self)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{ServerConfig, ServerSection, ToolDecl};
use pmcp::Server;
fn min_cfg() -> ServerConfig {
ServerConfig {
server: ServerSection {
name: "test".to_string(),
version: "0.1.0".to_string(),
..Default::default()
},
tools: vec![ToolDecl {
name: "ping".to_string(),
description: Some("ping".to_string()),
..Default::default()
}],
..Default::default()
}
}
#[test]
fn tools_from_config_registers_synthesized_handlers() {
let cfg = min_cfg();
let server = Server::builder()
.name("test")
.version("0.1.0")
.tools_from_config(&cfg)
.build()
.expect("build");
assert!(
server.get_tool("ping").is_some(),
"tools_from_config must wire each [[tools]] entry via tool_arc (Phase 82)"
);
}
#[test]
fn try_tools_from_config_returns_ok_on_valid_config() {
let cfg = min_cfg();
let builder = Server::builder().name("t").version("0.1.0");
let result = builder.try_tools_from_config(&cfg);
assert!(result.is_ok(), "valid config must return Ok");
}
#[test]
fn code_mode_from_config_is_noop_when_block_absent() {
let cfg = min_cfg();
let _builder = Server::builder()
.name("t")
.version("0.1.0")
.code_mode_from_config(&cfg);
}
#[test]
fn try_code_mode_from_config_is_ok_when_block_absent() {
let cfg = min_cfg();
let builder = Server::builder().name("t").version("0.1.0");
let result = builder.try_code_mode_from_config(&cfg);
assert!(
result.is_ok(),
"code_mode = None must produce Ok (no-op) so callers can invoke unconditionally"
);
}
}