Skip to content
Draft

MCP #3137

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ This repo is the **monorepo for all Start9 products**. Each product is a thin to
- Frontend: Angular 22 + TypeScript + Taiga UI 5 — one workspace rooted at the repo root (`angular.json`, `package.json` at root) with shared source libraries in `shared-libs/ts-modules`
- Container runtime: Node.js/TypeScript with LXC
- Database/State: Patch-DB (in-repo at `shared-libs/crates/patch-db`) — diff-based store with reactive frontend sync
- API: JSON-RPC via rpc-toolkit (see `shared-libs/crates/start-core/rpc-toolkit.md`)
- API: JSON-RPC via rpc-toolkit (see `shared-libs/crates/start-core/rpc-toolkit.md`), MCP for LLM agents (see `shared-libs/crates/start-core/src/mcp/ARCHITECTURE.md`)
- Auth: password + session cookie, public/private key signatures, local authcookie (see `shared-libs/crates/start-core/src/middleware/auth/`)

## Repository layout
Expand Down Expand Up @@ -45,7 +45,7 @@ start-technologies/ # repo root (monorepo)

## Components

- **`shared-libs/crates/start-core/`** — the entire Rust backend, one library crate. Modules: `bins`, `registry`, `tunnel`, `service`, `s9pk`, `net`, `db`, `install`, `update`, `lxc`, `os_install`, `backup`, `sign`, `version`, … Handles RPC API, service lifecycle, networking (DNS, ACME, WiFi, Tor, WireGuard), backups, and database state. All product bins depend on it. See [shared-libs/crates/start-core/ARCHITECTURE.md](shared-libs/crates/start-core/ARCHITECTURE.md).
- **`shared-libs/crates/start-core/`** — the entire Rust backend, one library crate. Modules: `bins`, `registry`, `tunnel`, `service`, `s9pk`, `net`, `db`, `install`, `update`, `lxc`, `os_install`, `backup`, `sign`, `version`, `mcp`, … Handles RPC API, MCP server for LLM agents, service lifecycle, networking (DNS, ACME, WiFi, Tor, WireGuard), backups, and database state. All product bins depend on it. See [shared-libs/crates/start-core/ARCHITECTURE.md](shared-libs/crates/start-core/ARCHITECTURE.md).

- **Product bins** — thin wrappers (feature toggles + `include_dir!` UI embed) over `start-core`:
- `start-os` package → `startbox` (main daemon `startd`) and `start-container` (runs inside LXC containers)
Expand Down Expand Up @@ -102,12 +102,24 @@ StartOS uses Patch-DB for reactive state sync:

The UI is therefore eventually consistent with the backend — after a mutating API call, the frontend waits for the corresponding PatchDB diff before resolving, so the UI reflects the result immediately.

## MCP server (LLM agent interface)

StartOS includes an [MCP](https://modelcontextprotocol.io/) (Model Context Protocol) server at `/mcp`, enabling LLM agents to discover and invoke the same operations available through the UI and CLI. The MCP server runs inside the StartOS server process alongside the RPC API.

- **Tools**: The operationally relevant RPC methods are exposed as MCP tools with LLM-optimized descriptions and JSON Schema inputs (see the exclusion list in the MCP architecture doc). Agents call `tools/list` to discover what's available and `tools/call` to invoke operations.
- **Resources**: System state is exposed via MCP resources backed by Patch-DB. Agents subscribe to `startos:///public` and receive debounced revision diffs over SSE, maintaining a local state cache without polling.
- **Auth**: Same session cookie auth as the UI — no separate credentials.
- **Transport**: MCP Streamable HTTP — POST for requests, GET for SSE notification stream, DELETE for session teardown.

See [shared-libs/crates/start-core/src/mcp/ARCHITECTURE.md](shared-libs/crates/start-core/src/mcp/ARCHITECTURE.md) for implementation details.

## Further reading

- [shared-libs/crates/start-core/ARCHITECTURE.md](shared-libs/crates/start-core/ARCHITECTURE.md) — Rust backend
- [shared-libs/ts-modules/ARCHITECTURE.md](shared-libs/ts-modules/ARCHITECTURE.md) — Angular frontend
- [projects/start-os/container-runtime/AGENTS.md](projects/start-os/container-runtime/AGENTS.md) — container runtime
- [shared-libs/crates/start-core/rpc-toolkit.md](shared-libs/crates/start-core/rpc-toolkit.md) — JSON-RPC handler patterns
- [shared-libs/crates/start-core/src/mcp/ARCHITECTURE.md](shared-libs/crates/start-core/src/mcp/ARCHITECTURE.md) — MCP server
- [shared-libs/crates/start-core/s9pk-structure.md](shared-libs/crates/start-core/s9pk-structure.md) — S9PK package format
- [shared-libs/crates/start-core/exver.md](shared-libs/crates/start-core/exver.md) — extended versioning format
- [shared-libs/crates/start-core/VERSION_BUMP.md](shared-libs/crates/start-core/VERSION_BUMP.md) — version bumping guide
8 changes: 8 additions & 0 deletions shared-libs/crates/start-core/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ how `startbox` dispatches to `startd`, `start-cli`, etc. The per-entrypoint logi
- `src/context/` — Context types (RpcContext, CliContext, InitContext, DiagnosticContext)
- `src/service/` — Service lifecycle management with actor pattern (`service_actor.rs`)
- `src/db/model/` — Patch-DB models (`public.rs` synced to frontend, `private.rs` backend-only)
- `src/mcp/` — MCP server for LLM agents (see [MCP Server](#mcp-server) below)
- `src/net/` — Networking (DNS, ACME, WiFi, Tor, WireGuard, gateway/NAT)
- `src/s9pk/` — S9PK package format (merkle archive)
- `src/lxc/` — LXC container management for packages
Expand Down Expand Up @@ -81,6 +82,12 @@ See [i18n-patterns.md](i18n-patterns.md) for internationalization key convention

See [core-rust-patterns.md](core-rust-patterns.md) for common utilities (Invoke trait, Guard pattern, mount guards, Apply trait, etc.).

## MCP Server

The MCP (Model Context Protocol) server at `src/mcp/` exposes the StartOS RPC API to LLM agents via the Streamable HTTP transport at `/mcp`. Tools wrap the existing RPC handlers; resources expose Patch-DB state with debounced SSE subscriptions; auth reuses the UI session cookie.

See [src/mcp/ARCHITECTURE.md](src/mcp/ARCHITECTURE.md) for transport details, session lifecycle, tool dispatch, resource subscriptions, CORS, and body size limits.

## Cross-layer verification

Rust types marked `#[ts(export)]` are the source of truth for TypeScript consumers (the web UI
Expand All @@ -104,4 +111,5 @@ Until both steps run, a changed `#[ts(export)]` type is out of sync with everyth
- [patchdb.md](patchdb.md) — Patch-DB watch patterns and TypedDbWatch
- [i18n-patterns.md](i18n-patterns.md) — Internationalization conventions
- [core-rust-patterns.md](core-rust-patterns.md) — Common Rust utilities
- [src/mcp/ARCHITECTURE.md](src/mcp/ARCHITECTURE.md) — MCP server (LLM agent interface)
- [s9pk-structure.md](s9pk-structure.md) — S9PK package format
10 changes: 5 additions & 5 deletions shared-libs/crates/start-core/src/db/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,13 @@ pub struct SubscribeRes {
pub guid: Guid,
}

struct DbSubscriber {
rev: u64,
sub: UnboundedReceiver<Revision>,
sync_db: watch::Receiver<u64>,
pub(crate) struct DbSubscriber {
pub(crate) rev: u64,
pub(crate) sub: UnboundedReceiver<Revision>,
pub(crate) sync_db: watch::Receiver<u64>,
}
impl DbSubscriber {
async fn recv(&mut self) -> Option<Revision> {
pub(crate) async fn recv(&mut self) -> Option<Revision> {
loop {
tokio::select! {
rev = self.sub.recv() => {
Expand Down
52 changes: 52 additions & 0 deletions shared-libs/crates/start-core/src/install/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use crate::context::{CliContext, RpcContext};
use crate::db::model::package::{ManifestPreference, PackageStateMatchModelRef};
use crate::prelude::*;
use crate::progress::{FullProgress, FullProgressTracker, PhasedProgressBar};
use crate::registry::asset::BufferedHttpSource;
use crate::registry::context::{RegistryContext, RegistryUrlParams};
use crate::registry::package::get::GetPackageResponse;
use crate::rpc_continuations::{Guid, RpcContinuation};
Expand Down Expand Up @@ -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)]
#[group(skip)]
#[ts(export)]
Expand Down
7 changes: 7 additions & 0 deletions shared-libs/crates/start-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ pub mod init;
pub mod install;
pub mod logs;
pub mod lxc;
pub mod mcp;
pub mod middleware;
pub mod net;
pub mod notifications;
Expand Down Expand Up @@ -441,6 +442,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)
Expand Down
Loading