diff --git a/core/src/install/mod.rs b/core/src/install/mod.rs index 80687c0c2..28ac15825 100644 --- a/core/src/install/mod.rs +++ b/core/src/install/mod.rs @@ -21,6 +21,7 @@ use tracing::instrument; use ts_rs::TS; use crate::context::{CliContext, RpcContext}; +use crate::registry::asset::BufferedHttpSource; use crate::db::model::package::{ManifestPreference, PackageStateMatchModelRef}; use crate::prelude::*; use crate::progress::{FullProgress, FullProgressTracker, PhasedProgressBar}; @@ -285,6 +286,57 @@ pub async fn sideload( Ok(SideloadResponse { upload, progress }) } +#[derive(Deserialize, Serialize, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export)] +pub struct SideloadUrlParams { + #[ts(type = "string")] + url: Url, +} + +#[instrument(skip_all)] +pub async fn sideload_url( + ctx: RpcContext, + SideloadUrlParams { url }: SideloadUrlParams, +) -> Result<(), Error> { + if !matches!(url.scheme(), "http" | "https") { + return Err(Error::new( + eyre!("URL scheme must be http or https, got: {}", url.scheme()), + ErrorKind::InvalidRequest, + )); + } + + let progress_tracker = FullProgressTracker::new(); + let download_progress = progress_tracker.add_phase("Downloading".into(), Some(100)); + let client = ctx.client.clone(); + let db = ctx.db.clone(); + let pt_ref = progress_tracker.clone(); + + let download = ctx + .services + .install( + ctx.clone(), + || async move { + let source = BufferedHttpSource::new(client, url, download_progress).await?; + let key = db.peek().await.into_private().into_developer_key(); + crate::s9pk::load(source, || Ok(key.de()?.0), Some(&pt_ref)).await + }, + None, + None::, + Some(progress_tracker), + ) + .await?; + + tokio::spawn(async move { + if let Err(e) = async { download.await?.await }.await { + tracing::error!("Error sideloading package from URL: {e}"); + tracing::debug!("{e:?}"); + } + }); + + Ok(()) +} + #[derive(Debug, Clone, Deserialize, Serialize, Parser, TS)] #[ts(export)] #[serde(rename_all = "camelCase")] diff --git a/core/src/lib.rs b/core/src/lib.rs index 904999d59..15c23ea40 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -441,6 +441,12 @@ pub fn package() -> ParentHandler { .with_metadata("get_session", Value::Bool(true)) .no_cli(), ) + .subcommand( + "sideload-url", + from_fn_async(install::sideload_url) + .with_metadata("sync_db", Value::Bool(true)) + .no_cli(), + ) .subcommand( "install", from_fn_async_local(install::cli_install) diff --git a/core/src/mcp/ARCHITECTURE.md b/core/src/mcp/ARCHITECTURE.md index 9dbf67b47..0b8f5bc83 100644 --- a/core/src/mcp/ARCHITECTURE.md +++ b/core/src/mcp/ARCHITECTURE.md @@ -39,7 +39,7 @@ core/src/mcp/ ├── mod.rs — HTTP handlers, routing, MCP method dispatch, shell execution, CORS ├── protocol.rs — JSON-RPC 2.0 types, MCP request/response structs, error codes ├── session.rs — Session map, create/remove/sweep, resource subscriptions with debounce -└── tools.rs — Tool registry (88 tools), HashMap mapping names → RPC methods + schemas +└── tools.rs — Tool registry (89 tools), HashMap mapping names → RPC methods + schemas ``` ## Tool Dispatch @@ -89,7 +89,7 @@ Resource URIs are validated to only allow `/public/**` subtrees and the special ## Excluded RPC Methods -Of the ~194 RPC methods registered in the StartOS backend, 87 are exposed as MCP tools (plus 1 MCP-only tool: `package.shell`). The remaining 105 are excluded for the following reasons. +Of the ~195 RPC methods registered in the StartOS backend, 88 are exposed as MCP tools (plus 1 MCP-only tool: `package.shell`). The remaining 105 are excluded for the following reasons. ### Wrong context — Setup / Init / Diagnostic modes @@ -139,7 +139,7 @@ These configure the Start9 tunnel service, which has its own management interfac | Method | Reason | |--------|--------| -| `package.sideload` | Requires multipart file upload via middleware, not JSON-RPC params | +| `package.sideload` | Requires multipart file upload via REST continuation, not JSON-RPC params. Use `package.sideload-by-url` MCP tool (backed by `package.sideload-url` RPC) which accepts a URL instead | ### Security — host-level shell access excluded diff --git a/core/src/mcp/tools.rs b/core/src/mcp/tools.rs index 54ce0e71a..9d5a044a4 100644 --- a/core/src/mcp/tools.rs +++ b/core/src/mcp/tools.rs @@ -299,6 +299,26 @@ pub fn tool_registry() -> HashMap { sync_db: true, needs_session: false, }, + ToolEntry { + definition: ToolDefinition { + name: "package.sideload-by-url".into(), + description: "Install a package (service) from a direct URL to an .s9pk file. \ + Use this to install packages from download links rather than from a registry. \ + The download and installation is asynchronous — subscribe to \ + startos:///public/packageData to monitor installation progress in real time.".into(), + input_schema: json!({ + "type": "object", + "properties": { + "url": { "type": "string", "description": "Direct URL to an .s9pk package file (http or https)" } + }, + "required": ["url"], + "additionalProperties": false + }), + }, + rpc_method: "package.sideload-url", + sync_db: true, + needs_session: false, + }, ToolEntry { definition: ToolDefinition { name: "package.uninstall".into(),