mcp: install dep reconciler, connector login/status endpoints, agent prompt updates
Nightly Build / build (push) Successful in 6m26s
Nightly Build / build (push) Successful in 6m26s
This commit is contained in:
@@ -314,6 +314,72 @@ async fn run_in_container(container: &str, workdir: &Path, script: &str, label:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Installs a **global** connector's dependencies on the HOST, into `.pydeps`
|
||||
/// (python) / `node_modules` (node) beside its files in `connectors/<folder>/`.
|
||||
///
|
||||
/// The host counterpart of [`ensure_installed`]: a `global` connector runs in the
|
||||
/// Skald process, not a container (§7), so its declared deps must resolve on the
|
||||
/// host — `global_row_spec` puts `<dir>/.pydeps` on the server's `PYTHONPATH`. Unlike
|
||||
/// the per-user reconciler this is not hash-guarded: the deps land in the same
|
||||
/// `connectors/<folder>/` tree the hash would cover, so it simply relies on `pip`/
|
||||
/// `npm` being idempotent (a satisfied requirement is a fast no-op). Called at
|
||||
/// enable time; the installed `.pydeps` is durable and survives a restart, so the
|
||||
/// boot relaunch needs no reinstall.
|
||||
pub async fn ensure_installed_host(folder: &str) -> Result<()> {
|
||||
let dir = connector_dir(folder)?;
|
||||
if !dir.is_dir() {
|
||||
// Nothing shipped for this connector on this box; a caller that truly needs
|
||||
// the files fails later with its own message.
|
||||
return Ok(());
|
||||
}
|
||||
if dir.join("package.json").is_file() {
|
||||
run_on_host(
|
||||
&dir,
|
||||
"npm ci --omit=dev --no-audit --no-fund 2>&1 || npm install --omit=dev --no-audit --no-fund 2>&1",
|
||||
"npm",
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dir.join("requirements.txt").is_file() {
|
||||
run_on_host(
|
||||
&dir,
|
||||
&format!(
|
||||
"python3 -m pip install --break-system-packages --target {PYDEPS_DIR} \
|
||||
-r requirements.txt 2>&1"
|
||||
),
|
||||
"pip",
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Runs a shell `script` on the HOST at `workdir`, under the same install timeout,
|
||||
/// failing with the tail of the output on a non-zero exit. The host counterpart of
|
||||
/// [`run_in_container`], for a `global` connector whose deps live beside its files in
|
||||
/// `connectors/<id>/` rather than inside a container.
|
||||
async fn run_on_host(workdir: &Path, script: &str, label: &str) -> Result<()> {
|
||||
let output = tokio::time::timeout(
|
||||
Duration::from_secs(DEPS_INSTALL_TIMEOUT_SECS),
|
||||
tokio::process::Command::new("sh")
|
||||
.arg("-c").arg(script)
|
||||
.current_dir(workdir)
|
||||
.output(),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| anyhow::anyhow!("{label} install timed out after {DEPS_INSTALL_TIMEOUT_SECS}s"))?
|
||||
.with_context(|| format!("failed to run {label} install on host"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let mut combined = String::from_utf8_lossy(&output.stdout).to_string();
|
||||
combined.push_str(&String::from_utf8_lossy(&output.stderr));
|
||||
let tail: String = combined.lines().rev().take(12).collect::<Vec<_>>()
|
||||
.into_iter().rev().collect::<Vec<_>>().join("\n");
|
||||
bail!("{label} install failed:\n{tail}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -30,7 +30,7 @@ pub mod oauth;
|
||||
mod provider;
|
||||
pub mod verify;
|
||||
|
||||
pub use install::{CONNECTORS_DIR, MANIFEST_FILE, connector_dir, install_into_home, split_script_path};
|
||||
pub use install::{CONNECTORS_DIR, MANIFEST_FILE, connector_dir, ensure_installed_host, install_into_home, split_script_path};
|
||||
pub use oauth::DeliverSpec;
|
||||
pub use provider::{McpProvider, UserMcpView};
|
||||
pub use verify::{VerifyReport, VerifyTarget, apply_placeholders, run_verify};
|
||||
@@ -508,7 +508,17 @@ fn substitute_named_tokens(
|
||||
/// Builds a spec for a globally-active connector — host transport (`launch_in`
|
||||
/// = None), so it runs in the Skald process, not in any container (§7).
|
||||
pub fn global_row_spec(row: &crate::db::mcp_global_servers::McpGlobalServerRow) -> McpServerSpec {
|
||||
let env = row.env();
|
||||
let mut env = row.env();
|
||||
// A `global` python `local_script` connector's deps are installed on the host
|
||||
// under `<dir>/.pydeps` (see `install::ensure_installed_host`); point the
|
||||
// interpreter at them, mirroring `user_row_spec`. `args()[0]` is the host-absolute
|
||||
// script path (set by `global_enable`), so the derived `.pydeps` path is absolute
|
||||
// too and resolves regardless of the process cwd. A no-op for a remote connector
|
||||
// (no python command → None) or before the first install (python ignores a
|
||||
// missing `PYTHONPATH` entry).
|
||||
if let Some(pp) = python_pydeps_path(row.command.as_deref(), &row.args()) {
|
||||
env.entry("PYTHONPATH".to_string()).or_insert(pp);
|
||||
}
|
||||
let (url, api_key) = apply_key_placeholder(row.url.clone(), row.api_key.clone(), &env);
|
||||
McpServerSpec {
|
||||
config: McpServerConfig {
|
||||
|
||||
Reference in New Issue
Block a user