sideload-by-url

This commit is contained in:
Matt Hill
2026-03-17 19:17:49 -06:00
parent d8f8759dea
commit f6db176f6d
4 changed files with 81 additions and 3 deletions

View File

@@ -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::<Never>,
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")]

View File

@@ -441,6 +441,12 @@ pub fn package<C: Context>() -> ParentHandler<C> {
.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)

View File

@@ -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<String, ToolEntry> mapping names → RPC methods + schemas
└── tools.rs — Tool registry (89 tools), HashMap<String, ToolEntry> 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

View File

@@ -299,6 +299,26 @@ pub fn tool_registry() -> HashMap<String, ToolEntry> {
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(),