mod acquire;
mod bundle;
mod cache;
mod download;
mod error;
mod launch;
mod pins;
mod platform;
use std::ops::Deref;
use std::path::PathBuf;
use std::process::Stdio;
use rmcp::service::RunningService;
use rmcp::{RoleClient, ServiceExt};
pub use cache::{ENV_BIN, ENV_BUNDLE};
pub use error::{Error, Result};
pub use pins::{Pin, PINS, STACKQL_VERSION};
pub use platform::Platform;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Mode {
#[default]
ReadOnly,
Safe,
DeleteSafe,
FullAccess,
}
impl Mode {
pub fn as_str(self) -> &'static str {
match self {
Mode::ReadOnly => "read_only",
Mode::Safe => "safe",
Mode::DeleteSafe => "delete_safe",
Mode::FullAccess => "full_access",
}
}
}
#[cfg(feature = "sidecar")]
pub fn fetch_bundle() -> Result<PathBuf> {
let platform = Platform::detect()?;
let pin = pins::pin_for(platform)?;
let dest = cache::bin_cache_root()?
.join(pins::STACKQL_VERSION)
.join(pin.bundle_name);
if dest.is_file() && download::sha256_file(&dest)? == pin.sha256 {
return Ok(dest);
}
download::download_verified(&pins::bundle_url(pin), pin.sha256, &dest)?;
Ok(dest)
}
#[macro_export]
macro_rules! include_bundle {
() => {
include_bytes!(env!(
"STACKQL_MCP_BUNDLE_FILE",
"set STACKQL_MCP_BUNDLE_FILE to the absolute path of the platform .mcpb bundle \
(see stackql_mcp::fetch_bundle)"
))
};
}
pub struct StackqlMcp;
impl StackqlMcp {
pub fn builder() -> Builder {
Builder::default()
}
}
#[derive(Default)]
pub struct Builder {
mode: Mode,
auth: Option<serde_json::Value>,
approot: Option<PathBuf>,
acquisition: acquire::Acquisition,
}
impl Builder {
pub fn mode(mut self, mode: Mode) -> Self {
self.mode = mode;
self
}
pub fn auth(mut self, auth: serde_json::Value) -> Self {
self.auth = Some(auth);
self
}
pub fn approot(mut self, approot: impl Into<PathBuf>) -> Self {
self.approot = Some(approot.into());
self
}
pub fn binary(mut self, path: impl Into<PathBuf>) -> Self {
self.acquisition.binary = Some(path.into());
self
}
pub fn bundle_path(mut self, path: impl Into<PathBuf>) -> Self {
self.acquisition.bundle_path = Some(path.into());
self
}
#[cfg(feature = "vendored")]
pub fn bundle_bytes(mut self, bytes: &'static [u8]) -> Self {
self.acquisition.bundle_bytes = Some(bytes);
self
}
pub fn command(&self) -> Result<std::process::Command> {
let binary = acquire::resolve_binary(&self.acquisition)?;
let approot = self.resolved_approot()?;
let mut cmd = std::process::Command::new(binary);
cmd.args(launch::launch_args(self.mode, &approot, self.auth.as_ref()));
Ok(cmd)
}
pub async fn start(self) -> Result<RunningServer> {
let approot = self.resolved_approot()?;
let acquisition = self.acquisition;
let binary = tokio::task::spawn_blocking(move || acquire::resolve_binary(&acquisition))
.await
.map_err(|e| Error::Mcp(format!("acquisition task failed: {e}")))??;
let mut child = tokio::process::Command::new(&binary)
.args(launch::launch_args(self.mode, &approot, self.auth.as_ref()))
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.kill_on_drop(true)
.spawn()
.map_err(Error::Spawn)?;
let stdout = child
.stdout
.take()
.ok_or_else(|| Error::Mcp("child stdout not captured".into()))?;
let stdin = child
.stdin
.take()
.ok_or_else(|| Error::Mcp("child stdin not captured".into()))?;
let client = ()
.serve((stdout, stdin))
.await
.map_err(|e| Error::Mcp(format!("initialize failed: {e}")))?;
Ok(RunningServer {
child,
client,
binary,
})
}
fn resolved_approot(&self) -> Result<PathBuf> {
match &self.approot {
Some(p) => Ok(p.clone()),
None => cache::default_approot(),
}
}
}
pub struct RunningServer {
child: tokio::process::Child,
client: RunningService<RoleClient, ()>,
binary: PathBuf,
}
impl RunningServer {
pub fn client(&self) -> &RunningService<RoleClient, ()> {
&self.client
}
pub fn pid(&self) -> Option<u32> {
self.child.id()
}
pub fn binary_path(&self) -> &std::path::Path {
&self.binary
}
pub async fn shutdown(self) -> Result<()> {
let RunningServer {
mut child, client, ..
} = self;
let _ = client.cancel().await;
let _ = child.kill().await;
Ok(())
}
}
impl Deref for RunningServer {
type Target = RunningService<RoleClient, ()>;
fn deref(&self) -> &Self::Target {
&self.client
}
}