serve_once() runs one MCP session over the current process's stdin/
stdout to completion -- the entry point main.rs's --mcp-stdio flag calls
into (next commit). No bearer-token check here: spawning this process
at all requires the same local-user privilege as any other command, so
process-spawn capability is the trust boundary for stdio, not a header.
Mirrors sync::credentials: mint_and_store() generates a fresh 32-byte
random token per set_mcp_enabled call and writes it via `keyring`
(Windows Credential Manager); the token is never persisted to
settings.json/wa.db/logs. verify() is a constant-time compare so token
checking doesn't leak timing information about a partial match
(NFR-SEC-5).
bind_loopback() refuses to bind anything that doesn't resolve to a
loopback address -- unit tested directly (non-loopback IP, unparseable
host, and a real 127.0.0.1:0 bind). serve() runs a hyper HTTP/1 accept
loop in front of rmcp's StreamableHttpService (a bare tower_service, not
an axum app); every request needs `Authorization: Bearer <token>`
(constant-time compared via mcp::token::verify) before it ever reaches
the MCP service -- an unauthenticated request never reaches rmcp at all.
rmcp gains transport-streamable-http-server + transport-io (the actual
serving code -- the base "server" feature only got tool routing) plus
client + transport-streamable-http-client-reqwest, used solely by this
crate's own in-process MCP-client tests (WA never opens an outbound MCP
connection at runtime, so this is not new egress, FR-MCP-7). hyper/
hyper-util/http-body-util/http/bytes/tower-service are the low-level glue
to run HTTP/1 over a loopback TcpListener in front of rmcp's
StreamableHttpService (a bare tower_service::Service, not an axum app).
All are already in Cargo.lock transitively via reqwest/tauri -- no new
crates, just promoted to direct deps under the existing `mcp` feature.
WaMcpHandler wires list_recent_meetings/get_transcript/get_action_items/
get_feature_brief to Store, re-checking ExposeScope + the recordings
gate independently in every handler (FR-MCP-3) and logging every read
via record_mcp_access + an "mcp://access" event (FR-MCP-5) before scope
is even evaluated, so the audit trail is "what was asked for", not just
"what was returned". get_feature_brief calls through the existing
commands::get_feature_brief stub per the M1/M2 integration contract --
it always errors right now since M1's brief storage isn't implemented
in this worktree yet; the `selected`-scope exposed-flag check is left
as a marked TODO for when M1 lands.
Pure functions (no DB, no cargo feature gate) so scope control is
testable without a running server: meetings_visible/brief_visible/
recording_gate_ok/meeting_allowed. Documents the conservative choice for
`selected` scope on meetings/transcript/action-items -- there's no
per-meeting selection flag in the schema yet (only feature_briefs.exposed
does), so `selected` behaves like `none` there until a real "pick which
meetings" mechanism exists, rather than silently behaving like `all`.
ExposeScope/McpTransport gain as_str/parse (privacy-safe fallback: an
unparsed scope becomes None, not All/Selected) so commands.rs can read
them out of Settings' string fields. Declares the mcp submodules
(scope always compiled; handler/http_transport/server/stdio_transport/
token behind the `mcp` cargo feature) and removes the todo!() RmcpServer
stub -- its real implementation moves to mcp::server in the next commits.
Adds list_action_items (backs the get_action_items MCP tool -- distinct
from list_pending_reminders, which is reminder-scoped across all
meetings) and record_mcp_access/list_mcp_access_log (FR-MCP-5 audit
trail) to the Store trait + SqliteStore, reusing the mcp_access_log
table from migrations/0003_ai_mcp.sql.
Adds mcp_enabled/mcp_transport/mcp_port/mcp_expose/mcp_expose_recordings
to Settings, all defaulting to off/none so an upgrading settings.json
gets a fully-local default (FR-MCP-1). The auth token itself never lives
here -- OS credential store only (see mcp::token, next commit).