MCP — Model Context Protocol servers

Overview

mouaif includes an MCP client that connects to any Model Context Protocol server. MCP servers provide third-party integrations (such as databases, custom APIs, browser automation, or search) over stdio or HTTP transports. Once a server is configured, its tools are automatically made available to the AI assistant in your chat sessions.

Usage

Scope: global or per-project

Every MCP server can be configured in one of two scopes:

Adding a server

  1. Open the MCP servers page for the scope you want (the + button creates in that scope; the editor also shows an App / This project segmented control).

  2. Tap + to add a server. Fill in:

  1. Tap Save, then Start on the row to spawn the child and discover tools.

    Configured servers are enabled by default and start on demand — there is no separate Enabled flag; the per-server authorization mode is the gate (off = disabled, anything else = enabled). Opening a chat no longer auto-starts a server. A server starts the first time the model calls one of its tools (transparent on-demand start), or when you tap Start/reload. Once running it advertises its tools until you Stop it or restart mouaif.

    A project-scoped entry is committed to <projectDir>/.mcp.json under servers. Legacy <projectDir>/.mouaif.json mcp.servers entries are still read as a fallback until the editor saves MCP config. An app-scoped entry never touches a project file. The child process itself is in-memory only — it restarts on mouaif restart — and the discovered tool list is cached in the app SQLite store (~/.mouaif/store.sqlite, keyed by project directory + server id) so a stopped server still shows what it advertised the last time it ran, and the model keeps its tool surface in a new chat. Keeping the cache out of .mcp.json leaves the project file small and hand-editable; older builds that wrote an inline toolCache onto the entry are migrated into the store on first read and stripped from the file on the next write. Servers are stopped on SIGINT, SIGTERM, and process.exit.

In a chat

When the model decides to call an MCP tool, the server intercepts the tool_call event, dispatches it to the matching server, and forwards the result back as a tool_result SSE event. The chat UI renders the call and the result as inline cards in the transcript:

Per-chat tool picker

On a brand-new chat, the transcript carries a Tools available to the model card. Native tools (shell, subagent, the file tools) render as toggle chips; MCP tools render as a nested checkbox list — one parent row per configured MCP server, with an indented child row per discovered tool:

[ ] filesystem           (server)   ready · 4 tools   ▾
    [ ] mcp__filesystem__read_file
    [x] mcp__filesystem__list_files
    [ ] mcp__filesystem__search_files
    [ ] mcp__filesystem__write_file
[ ] playwright           (server)   stopped · 0 tools
    (no tools — start the server in Settings → MCP)

Authorization

Server startup is not gated — adding a server is the user's explicit "I trust this binary" decision. Every tool call is routed through the MCP authorization gate (default ask), which is layered — most specific first:

  1. Per tool — an entry under the project's authorization.tools.<composedName> (e.g. mcp__filesystem__write_file) gates that one tool.

  2. Per server — an entry under the project's authorization.servers.<serverSlug> gates every tool on that server.

  3. Project default — the project's top-level authorization mode (in .mcp.json) gates every MCP call with no more specific override.

  4. App default — the app store's mcp.authorization mode is the fallback for every project that has not set its own project default. This is what Settings → App defaults → MCP servers edits.

    The app store has no server registry, so the app layer carries only the single shared gate (mode + allowlist); per-server and per-tool rules are project-scoped. The first layer with a mode wins; ask counts as a decision, so a per-server ask can tighten a project allow, and a project default can tighten the app default. Every layer accepts the same modes:

    ModeBehavior
    offThe covered specs are hidden from the model (no prompt tokens) — one tool, one server, or every mcp__* spec. Calls that still arrive return ETOOL_DISABLED.
    askEvery call must be approved by the user in the UI before the runner executes.
    allowlistCalls whose summary matches an allowlist regex run without prompting. The rest fall through to ask. Allowlist patterns are edited from the raw project file / app store — there is no dedicated textarea in the UI.
    allowEvery covered call is auto-approved until the user flips the mode back.

    The Settings UI mirrors the layering everywhere the tool tree appears:

Lifecycle

ActionTriggerEffect
StartTap Start/↻ on a row, or the first model tool-call against a stopped-but-enabled serverSpawn the child, run the MCP initialize handshake, run tools/list, cache the result in the app store. Status moves to ready.
StopTap Stop, edit the server, or mouaif shuts downClose the child. The cached tool list is kept so the surface stays visible. Status moves to stopped.
DeleteTap Delete on a rowClose the child and clear the cached tool list with the entry.
Refresh toolsTap Refresh on a ready serverRe-run tools/list without restarting the child; the cache is overwritten. Useful when the server's tool set changes at runtime.
RestartTap Start on a ready serverStop, then re-start; a fresh tools/list runs and overwrites the cache.
ErroredChild crashes, JSON-RPC fails, or initialize times outThe session marks itself errored; the next call returns EMCP_NOSESSION. The Settings UI shows the typed error inline.

A server that crashes mid-chat is treated as ETOOL_DISABLED for the rest of the chat and re-arms on next chat open (the user can tap Start to re-spawn).

Behavior