use std::sync::Arc;
use pmcp::ServerBuilder;
use crate::config::ServerConfig;
use crate::error::Result;
use crate::policy::ToolkitHooks;
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>;
fn try_tools_from_config_with(
self,
config: &ServerConfig,
hooks: &ToolkitHooks,
) -> Result<Self>;
fn try_tools_from_config_with_connector_and_hooks(
self,
config: &ServerConfig,
connector: Arc<dyn SqlConnector>,
hooks: &ToolkitHooks,
) -> Result<Self>;
}
fn warn_if_policy_has_no_surface(hooks: &ToolkitHooks, entry_point: &str) {
if hooks.request_policy().is_some() {
tracing::warn!(
target: "pmcp_server_toolkit::builder_ext",
entry_point = %entry_point,
"a RequestPolicy is registered but THIS assembly path has no HTTP egress \
surface to apply it to, so it will never run. E1 governs the curated HTTP \
connector and the Code Mode executor; SQL connector traffic is not \
intercepted. Register the policy on the path that builds those (the \
OpenAPI binary's build_server)."
);
}
}
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(self, config: &ServerConfig) -> Result<Self> {
self.try_tools_from_config_with(config, &ToolkitHooks::default())
}
fn try_tools_from_config_with(
mut self,
config: &ServerConfig,
hooks: &ToolkitHooks,
) -> Result<Self> {
crate::policy::emit_validation_report(config, hooks);
warn_if_policy_has_no_surface(hooks, "try_tools_from_config_with");
let synthesized = crate::tools::synthesize_from_config_and_hooks(config, hooks)?;
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(
self,
config: &ServerConfig,
connector: Arc<dyn SqlConnector>,
) -> Result<Self> {
self.try_tools_from_config_with_connector_and_hooks(
config,
connector,
&ToolkitHooks::default(),
)
}
fn try_tools_from_config_with_connector_and_hooks(
mut self,
config: &ServerConfig,
connector: Arc<dyn SqlConnector>,
hooks: &ToolkitHooks,
) -> Result<Self> {
crate::policy::emit_validation_report(config, hooks);
warn_if_policy_has_no_surface(hooks, "try_tools_from_config_with_connector_and_hooks");
let synthesized = crate::tools::synthesize_from_config_with_connector_and_hooks(
config, connector, hooks,
)?;
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_with_registers_handlers_and_reaches_the_validator() {
use crate::policy::{ArgumentRefusal, ArgumentValidator, ToolkitHooks};
use serde_json::Value;
use std::sync::Arc;
struct RefuseAll;
impl ArgumentValidator for RefuseAll {
fn validate(&self, _args: &Value) -> std::result::Result<(), ArgumentRefusal> {
Err(ArgumentRefusal::new(
"this tool is administratively disabled",
))
}
}
let cfg = min_cfg();
let hooks = ToolkitHooks::default().with_argument_validator("ping", Arc::new(RefuseAll));
let server = Server::builder()
.name("test")
.version("0.1.0")
.try_tools_from_config_with(&cfg, &hooks)
.expect("ok")
.build()
.expect("build");
assert!(
server.get_tool("ping").is_some(),
"the hooks-taking entry point must register handlers exactly as the wrapper does"
);
}
#[test]
fn try_tools_from_config_is_a_thin_wrapper_over_the_hooks_variant() {
use crate::policy::ToolkitHooks;
let cfg = min_cfg();
let via_wrapper = Server::builder()
.name("t")
.version("0.1.0")
.try_tools_from_config(&cfg)
.expect("ok")
.build()
.expect("build");
let via_hooks = Server::builder()
.name("t")
.version("0.1.0")
.try_tools_from_config_with(&cfg, &ToolkitHooks::default())
.expect("ok")
.build()
.expect("build");
assert!(via_wrapper.get_tool("ping").is_some());
assert!(via_hooks.get_tool("ping").is_some());
}
#[test]
fn a_policy_on_the_sql_path_is_reported_and_not_an_error() {
use crate::policy::{OutboundRequest, PolicyRefusal, RequestPolicy, ToolkitHooks};
use std::sync::Arc;
struct RefuseAll;
#[async_trait::async_trait]
impl RequestPolicy for RefuseAll {
async fn check(
&self,
_req: &OutboundRequest<'_>,
) -> std::result::Result<(), PolicyRefusal> {
Err(PolicyRefusal::new("refused"))
}
}
let hooks = ToolkitHooks::default().with_request_policy(Arc::new(RefuseAll));
super::warn_if_policy_has_no_surface(&hooks, "unit-test");
let cfg = min_cfg();
let builder = Server::builder()
.name("t")
.version("0.1.0")
.try_tools_from_config_with(&cfg, &hooks);
assert!(
builder.is_ok(),
"an unreachable policy warns; it must never fail the build"
);
}
#[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"
);
}
}