use newt_core::mcp::{McpServerEntry, TransportKind};
use newt_mcp_client::{connect_http, connect_stdio, namespaced, split_namespaced, ConnectedServer};
use serde_json::{json, Value};
pub(crate) struct Mcp {
servers: Vec<ConnectedServer>,
sanitize_server_names: bool,
}
fn server_prefix(name: &str, sanitize: bool) -> String {
if sanitize {
name.replace('-', "_")
} else {
name.to_owned()
}
}
fn parse_scheme_host(url: Option<&str>) -> (String, String) {
let Some(url) = url else {
return (String::new(), String::new());
};
let (scheme, rest) = url.split_once("://").unwrap_or(("", url));
let authority = rest.split(['/', '?', '#']).next().unwrap_or(rest);
let authority = authority.rsplit_once('@').map_or(authority, |(_, h)| h); let host = if let Some(v6) = authority.strip_prefix('[') {
v6.split(']').next().unwrap_or(v6) } else {
authority.split(':').next().unwrap_or(authority) };
(scheme.to_ascii_lowercase(), host.to_ascii_lowercase())
}
fn host_is_loopback(host: &str) -> bool {
host == "localhost" || host == "::1" || host.starts_with("127.")
}
fn bearer_allowed_for_url(url: Option<&str>, allow_insecure_hosts: &[String]) -> bool {
let (scheme, host) = parse_scheme_host(url);
if scheme == "https" || host_is_loopback(&host) {
return true;
}
!host.is_empty()
&& allow_insecure_hosts
.iter()
.any(|h| h.eq_ignore_ascii_case(&host))
}
fn apply_transport_security(
entry: &mut McpServerEntry,
token: Option<String>,
allow_insecure_hosts: &[String],
) {
let (scheme, host) = parse_scheme_host(entry.url.as_deref());
let secure = scheme == "https" || host_is_loopback(&host);
let allowed = bearer_allowed_for_url(entry.url.as_deref(), allow_insecure_hosts);
if !secure {
match &token {
Some(_) if allowed => tracing::warn!(
"MCP server `{}`: UNENCRYPTED connection to `{}` (no TLS) — sending the \
OAuth Bearer anyway ([tui].mcp_allow_insecure_hosts opt-in)",
entry.name,
host
),
Some(_) => tracing::warn!(
"MCP server `{}`: UNENCRYPTED connection to `{}` (no TLS) — WITHHOLDING the \
OAuth Bearer token. Use https, or add `{}` to [tui].mcp_allow_insecure_hosts \
to override.",
entry.name,
host,
host
),
None => tracing::warn!(
"MCP server `{}`: UNENCRYPTED connection to `{}` (no TLS).",
entry.name,
host
),
}
}
if let (Some(token), true) = (token, allowed) {
entry
.headers
.insert("Authorization".into(), format!("Bearer {token}"));
}
}
impl Mcp {
#[cfg(test)]
pub(crate) fn empty() -> Self {
Self {
servers: Vec::new(),
sanitize_server_names: true,
}
}
pub(crate) async fn connect(
workspace: &str,
cfg_servers: &[McpServerEntry],
sanitize_server_names: bool,
allow_insecure_hosts: &[String],
) -> Self {
let home = std::env::var_os("HOME").map(std::path::PathBuf::from);
let entries = newt_core::mcp::discover(
cfg_servers,
home.as_deref(),
std::path::Path::new(workspace),
);
let mut servers = Vec::new();
for entry in &entries {
let result = match entry.transport {
TransportKind::Stdio => connect_stdio(entry).await,
TransportKind::Http => {
let mut enriched = entry.clone();
let already_authed = enriched.headers.contains_key("Authorization")
|| enriched.headers.contains_key("authorization");
let token = if already_authed {
None
} else {
crate::mcp_token::load_bearer_token(&entry.name).await
};
apply_transport_security(&mut enriched, token, allow_insecure_hosts);
connect_http(&enriched).await
}
TransportKind::Sse => {
tracing::warn!(
"MCP server `{}`: legacy SSE transport is not supported \
(use streamable-HTTP, `type = \"http\"`) — skipped",
entry.name
);
continue;
}
};
match result {
Ok(connected) => servers.push(connected),
Err(e) => tracing::warn!("MCP server `{}` skipped: {e:#}", entry.name),
}
}
Self {
servers,
sanitize_server_names,
}
}
pub(crate) fn is_empty(&self) -> bool {
self.servers.is_empty()
}
pub(crate) fn summary(&self) -> Vec<(String, usize)> {
self.servers
.iter()
.map(|s| (s.name.clone(), s.tools.len()))
.collect()
}
pub(crate) fn tool_defs(&self) -> Vec<Value> {
let mut out = Vec::new();
for server in &self.servers {
for tool in &server.tools {
out.push(json!({
"type": "function",
"function": {
"name": namespaced(&server_prefix(&server.name, self.sanitize_server_names), &tool.name),
"description": tool.description,
"parameters": tool.input_schema,
}
}));
}
}
out
}
pub(crate) fn handles(&self, name: &str) -> bool {
match split_namespaced(name) {
Some((server, _)) => self
.servers
.iter()
.any(|s| server_prefix(&s.name, self.sanitize_server_names) == server),
None => false,
}
}
pub(crate) async fn call(&mut self, name: &str, args: &Value) -> String {
let Some((server_name, tool)) = split_namespaced(name) else {
return format!("error: `{name}` is not a namespaced MCP tool");
};
let Some(server) = self
.servers
.iter_mut()
.find(|s| server_prefix(&s.name, self.sanitize_server_names) == server_name)
else {
return format!("error: no connected MCP server `{server_name}`");
};
match server.conn.call_tool(tool, args.clone()).await {
Ok(result) => format_result(&result),
Err(e) => format!("error: {e}"),
}
}
}
#[async_trait::async_trait]
impl newt_core::agentic::McpTools for Mcp {
fn handles(&self, name: &str) -> bool {
Self::handles(self, name)
}
fn tool_defs(&self) -> Vec<Value> {
Self::tool_defs(self)
}
async fn call(&mut self, name: &str, args: &Value) -> String {
Self::call(self, name, args).await
}
}
fn format_result(result: &Value) -> String {
let mut text = String::new();
if let Some(items) = result.get("content").and_then(Value::as_array) {
for item in items {
if let Some(t) = item.get("text").and_then(Value::as_str) {
if !text.is_empty() {
text.push('\n');
}
text.push_str(t);
}
}
}
if text.is_empty() {
text = result.to_string();
}
if result
.get("isError")
.and_then(Value::as_bool)
.unwrap_or(false)
{
format!("tool error: {text}")
} else {
text
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_handles_nothing_and_has_no_defs() {
let mcp = Mcp::empty();
assert!(mcp.is_empty());
assert!(!mcp.handles("git__status"));
assert!(mcp.tool_defs().is_empty());
}
fn http_entry(url: &str) -> McpServerEntry {
McpServerEntry {
name: "MaaS".into(),
transport: TransportKind::Http,
command: None,
args: Vec::new(),
env: std::collections::BTreeMap::new(),
url: Some(url.into()),
headers: std::collections::BTreeMap::new(),
}
}
#[test]
fn parse_scheme_host_handles_common_shapes() {
assert_eq!(
parse_scheme_host(Some("https://a.b/c")),
("https".into(), "a.b".into())
);
assert_eq!(
parse_scheme_host(Some("http://127.0.0.1:8080/x")),
("http".into(), "127.0.0.1".into())
);
assert_eq!(
parse_scheme_host(Some("http://u@Host:9/x")),
("http".into(), "host".into())
);
assert_eq!(
parse_scheme_host(Some("http://[::1]:7/x")),
("http".into(), "::1".into())
);
assert_eq!(parse_scheme_host(None), (String::new(), String::new()));
}
#[test]
fn bearer_allowed_only_over_https_loopback_or_allowlist() {
let none: &[String] = &[];
assert!(bearer_allowed_for_url(
Some("https://api.maas.com/mcp"),
none
));
assert!(bearer_allowed_for_url(Some("http://localhost:9/mcp"), none));
assert!(bearer_allowed_for_url(Some("http://127.0.0.1:9/mcp"), none));
assert!(bearer_allowed_for_url(Some("http://[::1]:9/mcp"), none));
assert!(!bearer_allowed_for_url(
Some("http://api.maas.com/mcp"),
none
));
assert!(!bearer_allowed_for_url(None, none));
let allow = vec!["api.maas.com".to_string()];
assert!(bearer_allowed_for_url(
Some("http://API.MaaS.com/mcp"),
&allow
));
}
#[test]
fn apply_transport_security_withholds_token_over_plain_http() {
let mut e = http_entry("http://api.maas.com/mcp");
apply_transport_security(&mut e, Some("SECRET".into()), &[]);
assert!(
!e.headers.contains_key("Authorization"),
"Bearer leaked over plaintext http: {:?}",
e.headers
);
}
#[test]
fn apply_transport_security_injects_over_https_and_allowlisted() {
let mut https = http_entry("https://api.maas.com/mcp");
apply_transport_security(&mut https, Some("SECRET".into()), &[]);
assert_eq!(
https.headers.get("Authorization").map(String::as_str),
Some("Bearer SECRET")
);
let mut allowed = http_entry("http://api.maas.com/mcp");
apply_transport_security(
&mut allowed,
Some("SECRET".into()),
&["api.maas.com".to_string()],
);
assert_eq!(
allowed.headers.get("Authorization").map(String::as_str),
Some("Bearer SECRET")
);
let mut loopback = http_entry("http://127.0.0.1:9/mcp");
apply_transport_security(&mut loopback, Some("SECRET".into()), &[]);
assert!(loopback.headers.contains_key("Authorization"));
}
#[test]
fn server_prefix_toggle() {
assert_eq!(server_prefix("acme-server", true), "acme_server");
assert_eq!(server_prefix("multi-part-name", true), "multi_part_name");
assert_eq!(server_prefix("plainserver", true), "plainserver");
assert_eq!(server_prefix("acme-server", false), "acme-server");
assert_eq!(server_prefix("multi-part-name", false), "multi-part-name");
assert_eq!(server_prefix("plainserver", false), "plainserver");
let tool = format!("{}__probe_tool", server_prefix("acme-server", true));
assert_eq!(tool, "acme_server__probe_tool");
}
#[test]
fn format_result_joins_text_content() {
let r =
json!({ "content": [{"type":"text","text":"hello"},{"type":"text","text":"world"}] });
assert_eq!(format_result(&r), "hello\nworld");
}
#[test]
fn format_result_flags_errors_and_falls_back_to_json() {
let err = json!({ "content": [{"type":"text","text":"boom"}], "isError": true });
assert_eq!(format_result(&err), "tool error: boom");
let weird = json!({ "structured": 1 });
assert!(format_result(&weird).contains("structured"));
}
}