Needed so RmcpServer::tools() can hand back TOOL_DESCRIPTORS without a manual field-by-field clone (next commit).
164 lines
4.7 KiB
Rust
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>;
|
|
}
|