//! Unified extension system for discovering, installing, authenticating, and activating //! MCP servers and WASM tools through conversational agent interactions. //! //! Extensions are the user-facing abstraction over MCP servers and WASM tools. The agent //! can search a built-in registry (or discover online), install, authenticate, and activate //! extensions at runtime without CLI commands. //! //! ```text //! User: "add notion" //! -> tool_search("notion") -> finds MCP server in registry //! -> tool_install("notion") -> saves config to mcp-servers.json //! -> tool_auth("notion") -> OAuth 2.1 flow, returns URL //! -> tool_activate("notion") -> connects, registers tools //! ``` pub mod discovery; pub mod manager; pub mod registry; pub use discovery::OnlineDiscovery; pub use manager::ExtensionManager; pub use registry::ExtensionRegistry; use serde::{Deserialize, Serialize}; /// The kind of extension, determining how it's installed, authenticated, and activated. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ExtensionKind { /// Hosted MCP server, HTTP transport, OAuth 2.1 auth. McpServer, /// Sandboxed WASM module, file-based, capabilities auth. WasmTool, /// WASM channel module (future: dynamic activation, currently needs restart). WasmChannel, } impl std::fmt::Display for ExtensionKind { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { ExtensionKind::McpServer => write!(f, "mcp_server"), ExtensionKind::WasmTool => write!(f, "wasm_tool"), ExtensionKind::WasmChannel => write!(f, "wasm_channel"), } } } /// A registry entry describing a known or discovered extension. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RegistryEntry { /// Unique identifier (e.g., "notion", "weather", "telegram"). pub name: String, /// Human-readable name (e.g., "Notion", "Weather Tool"). pub display_name: String, /// What kind of extension this is. pub kind: ExtensionKind, /// Short description of what this extension does. pub description: String, /// Search keywords beyond the name. #[serde(default)] pub keywords: Vec, /// Where to get this extension. pub source: ExtensionSource, /// How authentication works. pub auth_hint: AuthHint, } /// Where the extension binary or server lives. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum ExtensionSource { /// URL to a hosted MCP server. McpUrl { url: String }, /// Downloadable WASM binary. WasmDownload { wasm_url: String, #[serde(default)] capabilities_url: Option, }, /// Build from source repository. WasmBuildable { repo_url: String, #[serde(default)] build_dir: Option, }, /// Discovered online (not yet validated for a specific source type). Discovered { url: String }, } /// Hint about what authentication method is needed. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum AuthHint { /// MCP server supports Dynamic Client Registration (zero-config OAuth). Dcr, /// MCP server needs a pre-configured OAuth client_id. OAuthPreConfigured { /// URL where the user can create an OAuth app. setup_url: String, }, /// WASM tool has auth defined in its capabilities.json file. CapabilitiesAuth, /// No authentication needed. None, } /// Where a search result came from. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum ResultSource { /// From the built-in curated registry. Registry, /// From online discovery (validated). Discovered, } /// Result of searching for extensions. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SearchResult { /// The registry entry. #[serde(flatten)] pub entry: RegistryEntry, /// Where this result came from. pub source: ResultSource, /// Whether the endpoint was validated (for discovered entries). #[serde(default)] pub validated: bool, } /// Result of installing an extension. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InstallResult { pub name: String, pub kind: ExtensionKind, pub message: String, } /// Result of authenticating an extension. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuthResult { pub name: String, pub kind: ExtensionKind, /// OAuth URL to open (for OAuth flows). #[serde(skip_serializing_if = "Option::is_none")] pub auth_url: Option, /// Whether using local or remote callback. #[serde(skip_serializing_if = "Option::is_none")] pub callback_type: Option, /// Instructions for manual token entry (for WASM tools). #[serde(skip_serializing_if = "Option::is_none")] pub instructions: Option, /// URL for manual token setup. #[serde(skip_serializing_if = "Option::is_none")] pub setup_url: Option, /// Whether the tool is waiting for a token from the user. #[serde(default)] pub awaiting_token: bool, /// Current auth status. pub status: String, } /// Result of activating an extension. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ActivateResult { pub name: String, pub kind: ExtensionKind, /// Names of tools that were loaded/registered. pub tools_loaded: Vec, pub message: String, } /// An installed extension with its current status. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InstalledExtension { pub name: String, pub kind: ExtensionKind, #[serde(skip_serializing_if = "Option::is_none")] pub description: Option, /// Server or source URL (e.g. MCP server endpoint). #[serde(skip_serializing_if = "Option::is_none")] pub url: Option, pub authenticated: bool, pub active: bool, /// Tool names if active. #[serde(default)] pub tools: Vec, } /// Error type for extension operations. #[derive(Debug, thiserror::Error)] pub enum ExtensionError { #[error("Extension not found: {0}")] NotFound(String), #[error("Extension already installed: {0}")] AlreadyInstalled(String), #[error("Extension not installed: {0}")] NotInstalled(String), #[error("Authentication failed: {0}")] AuthFailed(String), #[error("Activation failed: {0}")] ActivationFailed(String), #[error("Installation failed: {0}")] InstallFailed(String), #[error("Discovery failed: {0}")] DiscoveryFailed(String), #[error("Invalid URL: {0}")] InvalidUrl(String), #[error("Download failed: {0}")] DownloadFailed(String), #[error("Config error: {0}")] Config(String), #[error("Channels require restart to activate")] ChannelNeedsRestart, #[error("{0}")] Other(String), }