Files
WhispAssist/src-tauri/src/mcp/mod.rs
T
iamdoubz 58d565907b feat(mcp): McpToolDescriptor is Copy (T10.4)
Needed so RmcpServer::tools() can hand back TOOL_DESCRIPTORS without a
manual field-by-field clone (next commit).
2026-07-07 00:21:58 -05:00

164 lines
4.7 KiB
Rust

//! MCP server — WhispAssist as a tool source for coding agents (Phase 10b, ADR-0011).
//!
//! WA hosts a LOCAL Model Context Protocol server so the user's own agents (Claude Code,
//! Codex, Copilot, OpenCode, …) can pull meeting context and "feature briefs" and start
//! coding. This is the primary "get started right away" handoff (PULL model).
//!
//! Security invariants (enforced here; see CLAUDE.md / NFR-SEC-5):
//! - OFF by default; binds to LOOPBACK only; token required.
//! - Tools-first surface (Copilot cloud supports tools, not resources/prompts).
//! - Scope-limited: never serves recordings unless explicitly allowed.
//! - INBOUND only — opens no outbound socket, adds nothing to the egress allowlist (FR-MCP-7).
//! - Every agent read is logged (FR-MCP-5).
use crate::models::{FeatureBrief, MeetingId};
use async_trait::async_trait;
pub mod scope;
#[cfg(feature = "mcp")]
pub mod handler;
#[cfg(feature = "mcp")]
pub mod http_transport;
#[cfg(feature = "mcp")]
pub mod server;
#[cfg(feature = "mcp")]
pub mod stdio_transport;
#[cfg(feature = "mcp")]
pub mod token;
#[cfg(feature = "mcp")]
pub use server::RmcpServer;
#[derive(Debug, thiserror::Error)]
pub enum McpError {
#[error("refusing to bind non-loopback address")]
NonLoopback,
#[error("unauthorized: missing or invalid token")]
Unauthorized,
#[error("server error: {0}")]
Server(String),
}
#[derive(Debug, thiserror::Error)]
pub enum BriefError {
#[error("llm error: {0}")]
Llm(String),
#[error("meeting not found")]
NotFound,
}
#[derive(Debug, Clone)]
pub struct McpConfig {
pub transport: McpTransport,
pub port: u16,
pub expose: ExposeScope,
pub expose_recordings: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum McpTransport {
/// Streamable HTTP on http://127.0.0.1:<port>/mcp (loopback only).
Http,
/// JSON-RPC over stdio (a thin adapter the agent spawns).
Stdio,
}
impl McpTransport {
pub fn as_str(self) -> &'static str {
match self {
McpTransport::Http => "http",
McpTransport::Stdio => "stdio",
}
}
/// Unknown/missing values fall back to `Http` — the safer default to
/// document to the user (stdio requires a client that spawns a process).
pub fn parse(s: &str) -> Self {
match s {
"stdio" => McpTransport::Stdio,
_ => McpTransport::Http,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExposeScope {
None,
Selected,
All,
}
impl ExposeScope {
pub fn as_str(self) -> &'static str {
match self {
ExposeScope::None => "none",
ExposeScope::Selected => "selected",
ExposeScope::All => "all",
}
}
/// Unknown values fall back to `None` — scope-control is a privacy
/// control, so an unparsed value must never silently become permissive.
pub fn parse(s: &str) -> Self {
match s {
"selected" => ExposeScope::Selected,
"all" => ExposeScope::All,
_ => ExposeScope::None,
}
}
}
/// Returned on start: where to point the agent + the token it must present.
pub struct McpHandle {
pub endpoint: String,
pub token: String,
}
#[derive(Debug, Clone, Copy)]
pub struct McpToolDescriptor {
pub name: &'static str,
pub description: &'static str,
}
/// The four tools-first-surface descriptors (FR-MCP-2), shared by the trait's
/// default listing and anything else that needs to enumerate them without a
/// running server (e.g. the settings/privacy UI).
pub const TOOL_DESCRIPTORS: [McpToolDescriptor; 4] = [
McpToolDescriptor {
name: "list_recent_meetings",
description: "Recent meetings (scoped).",
},
McpToolDescriptor {
name: "get_transcript",
description: "Transcript for a meeting (scoped).",
},
McpToolDescriptor {
name: "get_action_items",
description: "Action items for a meeting.",
},
McpToolDescriptor {
name: "get_feature_brief",
description: "Agent-ready spec distilled from a meeting.",
},
];
/// The MCP server. Built on the official Rust SDK (`rmcp`, feature `mcp`).
#[async_trait]
pub trait McpServer: Send + Sync {
async fn start(&self, cfg: McpConfig) -> Result<McpHandle, McpError>;
async fn stop(&self, handle: McpHandle) -> Result<(), McpError>;
/// Tools-first surface (FR-MCP-2).
fn tools(&self) -> Vec<McpToolDescriptor>;
}
/// Distills a transcript into an agent-ready spec (FR-MCP-4) using the configured LlmProvider.
#[async_trait]
pub trait FeatureBriefBuilder: Send + Sync {
async fn build(
&self,
meeting_id: &MeetingId,
target_repo: Option<&str>,
) -> Result<FeatureBrief, BriefError>;
}