feat(mcp): connector marketplace + split the Connectors surface (§7/§14/§15)
Fills a gap the blueprint names: the admin had to hand-author every
`mcp_catalog` entry. A remote feed of vetted connectors now proposes them
and the admin installs — the feed is *consultative*, so §14's risk axis is
untouched and the trust anchor stays on the box.
Marketplace client (`src/frontend/api/marketplace.rs`):
- Fetches the feed server-side (it sends no CORS headers) and caches it;
icons are proxied for the same reason.
- Verifies every declared SHA-256 before writing, fail-closed and
all-or-nothing. Feed-supplied paths are refused if they escape
`./scripts/<id>/`. Importing an `mcp_local` entry still demands the
admin-only `mcp.register_local_script`.
- Translates the feed's vocabulary into Skald's: `user`→`per_user`,
`mcp_local`→`local_script`. Scope is read, never inferred from transport
(a remote connector can be per-user — that is what `mcp.register_remote`
is for), and an unreadable `type` fails closed to the answer needing more
authority. The feed's `llm_short_description` maps to `description`, the
column `render_mcp_list` puts in front of the LLM for `activate_tools()`.
- Feed URL is config (`marketplace.url`), not a constant: an on-premise
product must not hard-require reaching one vendor's host.
Two silent failures found while wiring it:
- `transport_of` maps anything unknown to Stdio, so the feed's
`streamable-http` would have tried to spawn a command. Normalised on import.
- Some servers want their key as a query param, not a bearer header, and say
so with a `{key}` placeholder. Substituted at connect time in
`global_row_spec`/`user_row_spec` — never at rest, so the key stays in its
own column and the stored URL stays a template.
Pages, split by the question each answers:
- Connectors — what runs (`UserMcpView` = global ∪ per-user) and what I can
add. Same page for everyone; the admin just has more verbs. One Available
list with the verb per row: `per_user`→Activate, `global`→Enable globally.
Enabling a global is the admin's counterpart to activating a per-user one,
so the catalog picker dropdown is gone — the entry comes from the row.
- Connector Catalog (admin) — what this box offers. One `Add connector`
with two sources: marketplace first (vetted, hashed), manual second
(unvetted by nature) — the order mirrors the trust model.
- Marketplace (admin) — reached from the catalog, not the sidebar: it is a
destination of an action, not a place.
`available()` no longer returns `McpGlobalServerRow`: that row carries
`api_key` and this view now reaches every logged-in user. A slim `GlobalView`
crosses instead, and an admin sees every global (with `can_use` marking their
own) so one enabled for someone else stays manageable.
Also fixes `connectors-page` having no CSS rule at all — every sibling page
has one, so it never got `flex: 1` and left an empty column beside it.
This commit is contained in:
+26
-6
@@ -23,9 +23,11 @@ const DEFAULT_CONFIG_EMBEDDED: &str = include_str!("../default.config.yaml");
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Config {
|
||||
pub server: ServerConfig,
|
||||
pub web: WebConfig,
|
||||
pub llm: LlmConfig,
|
||||
pub server: ServerConfig,
|
||||
pub web: WebConfig,
|
||||
pub llm: LlmConfig,
|
||||
#[serde(default)]
|
||||
pub marketplace: MarketplaceConfig,
|
||||
#[serde(default)]
|
||||
pub tic: TicConfig,
|
||||
#[serde(default)]
|
||||
@@ -47,6 +49,23 @@ pub struct WebConfig {
|
||||
pub static_dir: String,
|
||||
}
|
||||
|
||||
/// The connector marketplace feed (blueprint §14/§15).
|
||||
///
|
||||
/// Configurable, not hardcoded: an on-premise product must not hard-require
|
||||
/// reaching one vendor's host. Point it at a self-hosted mirror, or an offline
|
||||
/// copy served locally, and nothing else changes.
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MarketplaceConfig {
|
||||
/// Base URL serving `connectors.json` and each `<folder>/connector.json`.
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl Default for MarketplaceConfig {
|
||||
fn default() -> Self {
|
||||
Self { url: "https://connectors.skaldagent.net".to_string() }
|
||||
}
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn into_split(self) -> (skald_core::config::CoreConfig, crate::frontend::config::FrontendConfig) {
|
||||
let tz = self.timezone.clone();
|
||||
@@ -58,9 +77,10 @@ impl Config {
|
||||
timezone: self.timezone,
|
||||
},
|
||||
crate::frontend::config::FrontendConfig {
|
||||
server: self.server,
|
||||
web: self.web,
|
||||
timezone: tz,
|
||||
server: self.server,
|
||||
web: self.web,
|
||||
marketplace: self.marketplace,
|
||||
timezone: tz,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user