use std::sync::Arc;
use newt_core::Router;
use newt_inference::BackendRegistry;
pub mod caveats;
pub mod handlers;
pub mod server;
#[cfg(feature = "pyo3")]
pub mod pyo3_module;
const DEFAULT_OLLAMA_MODEL: &str = "llama3.1:8b";
pub async fn run_stdio(persona: Option<&str>) -> anyhow::Result<()> {
run_with_io(tokio::io::stdin(), tokio::io::stdout(), persona).await
}
pub async fn run_with_io<R, W>(
reader: R,
mut writer: W,
persona: Option<&str>,
) -> anyhow::Result<()>
where
R: tokio::io::AsyncRead + Unpin,
W: tokio::io::AsyncWrite + Unpin,
{
let registry = build_default_registry().await?;
let router = Arc::new(Router::new());
let (toolset, persona_tools) = connect_persona(persona).await?;
let mut server = server::McpServer::new();
handlers::register_handlers(
&mut server,
registry,
router,
Arc::new(tokio::sync::Mutex::new(toolset)),
Arc::new(persona_tools),
);
server.run(reader, &mut writer).await
}
async fn connect_persona(
persona: Option<&str>,
) -> anyhow::Result<(newt_mcp_client::McpToolset, Option<Vec<String>>)> {
let Some(name) = persona else {
return Ok((newt_mcp_client::McpToolset::empty(), None));
};
let profile = read_with_notices(|report| {
newt_core::RoleProfile::load_from_dir(name, &newt_core::Config::personas_dir(), report)
})?;
let cfg =
crate::read_with_notices(|report| newt_core::Config::resolve(report)).unwrap_or_default();
let workspace = std::env::current_dir().unwrap_or_default();
let ws = workspace.to_string_lossy();
let caveats = profile
.caveats
.as_ref()
.map(newt_core::CaveatProfile::to_caveats)
.unwrap_or_else(|| {
newt_core::ToolPermissions {
preset: newt_core::PermissionPreset::ReadOnly,
extra_exec: Vec::new(),
net: Vec::new(),
prompt: false,
..Default::default()
}
.to_caveats(&ws)
});
let explicit_hosts = cfg
.tui
.as_ref()
.map(|t| t.permissions.net.as_slice())
.unwrap_or_default();
let toolset =
newt_mcp_client::McpToolset::connect(&ws, &cfg.mcp_servers, true, &caveats, explicit_hosts)
.await;
tracing::info!(
persona = name,
connected_servers = toolset.summary().len(),
"MCP server: persona activated"
);
Ok((toolset, profile.tools))
}
async fn build_default_registry() -> anyhow::Result<Arc<BackendRegistry>> {
let cfg =
crate::read_with_notices(|report| newt_core::Config::resolve(report)).unwrap_or_default();
let registry = registry_from_config(&cfg)?;
if !registry.is_empty() {
tracing::info!(
providers = registry.len(),
"MCP server: using configured provider plugin registry"
);
return Ok(Arc::new(registry));
}
let mut registry = BackendRegistry::new();
match newt_inference::local::LocalOllamaBackend::discover(DEFAULT_OLLAMA_MODEL).await {
Ok(backend) => {
tracing::info!(
model = DEFAULT_OLLAMA_MODEL,
endpoint = backend.endpoint(),
"MCP server: discovered local Ollama backend"
);
registry.register(Arc::new(backend));
}
Err(e) => {
tracing::warn!(
error = %e,
"MCP server: no local Ollama discovered; goal_run will fail with NoBackendForTier"
);
}
}
Ok(Arc::new(registry))
}
fn registry_from_config(cfg: &newt_core::Config) -> anyhow::Result<BackendRegistry> {
BackendRegistry::load_from_config(cfg)
}
fn read_with_notices<T>(
operation: impl FnOnce(&mut dyn FnMut(newt_core::tty::Notice<'static>)) -> T,
) -> T {
let mut notices = Vec::new();
let result = operation(&mut |notice| notices.push(notice));
for notice in notices {
let _ = notice.diagnostic(newt_core::tty::LineCaps::None, false, std::io::stderr());
}
result
}
#[cfg(test)]
mod tests {
use newt_core::config::ProviderConfig;
use newt_core::{Config, Tier};
fn provider(name: &str) -> ProviderConfig {
ProviderConfig {
name: name.into(),
command: "newt-provider-openai".into(),
model: Some("gpt-test".into()),
env_pass: vec!["OPENAI_API_KEY".into()],
tiers: vec![Tier::Complex],
}
}
#[test]
fn registry_from_config_registers_provider_plugins() {
let cfg = Config {
providers: vec![provider("openai-provider")],
..Config::default()
};
let registry = super::registry_from_config(&cfg).unwrap();
assert_eq!(registry.names(), vec!["openai-provider"]);
assert!(registry.pick(Tier::Complex).is_ok());
}
}