Add MCP (Model Context Protocol) client support - #200
Draft
C-K-Loan wants to merge 5 commits into
Draft
Conversation
MCP servers speak JSON-RPC over a child process's stdin/stdout, which NativProcessController could not do: it wired stdout and stderr but never stdin, and only ever launched the bundled server executable. It now optionally wires an input pipe and can launch an arbitrary executable, so the same process supervision covers MCP servers too. On top of that: the protocol types, a codec for the initialize/tools.list/ tools.call exchange, and a connection that owns one server's lifecycle and tool inventory. Nothing starts a server on its own — a connection stays stopped until something explicitly starts it. Tool names are namespaced as mcp__<server>__<tool> so two servers offering a read_file cannot collide with each other or with a native tool. Covered against a real fixture server (Tests/NativTests/Fixtures/ fake_mcp_server.py) rather than a mock transport, so the framing and the process handling are exercised for real.
Adds the configured-server list to NativSettings so it round-trips with the rest of the app's settings, dropping entries whose name or command would be unusable on the way through normalization. NativModel owns the manager and exposes add/remove/start/stop. Starting is always explicit — nothing here launches a server as a side effect of loading settings. Servers are launched with a PATH resolved from a login shell, reusing the same probe the Hugging Face environment already uses: launchd's PATH omits /opt/homebrew/bin, so npx and uvx resolve when the app is run from Xcode or a terminal but not when it is launched from Finder, which is how anyone outside this repo will run it.
A bridge turns a running server's tool list into the same tool definitions the native tools already produce, and routes calls back to the server that owns them. The registry gains an optional bridge on its execution context, so a chat with no MCP servers configured builds exactly the tool list it did before. Dispatch stays name-based: a call resolves to a native handler first, then to the bridge, then fails as unsupported. Failure payloads route the same way, so an MCP tool that errors reads like any other failed tool call rather than surfacing raw transport detail. Consent moves behind ChatConsentGatedAction, which currently recognizes switch_model and MCP calls. It is deliberately fail-closed: a tool whose server cannot be identified still requires approval instead of quietly running. The registry's definitions and execute become main-actor isolated, since resolving the bridge reads a connection's published tool inventory; the registry tests move with them in the same commit for that reason.
The tool-call loop's consent branch was written for switch_model specifically. It now asks ChatConsentGatedAction what a call needs, so MCP calls take the same approve/deny path — including the cancel and decline outcomes — instead of running unprompted. The prompt text follows: switch_model keeps its wording, an MCP call names the tool and the server it belongs to and says it will change data outside Nativ, and anything else falls back to a generic approval request rather than claiming something specific about a tool it does not recognize. Only switch_model still requires the app model, so its absence fails that one tool rather than every gated call.
A panel to add, start, stop, and remove servers, alongside the existing Integrations and Extensions tabs. Each row shows its server's state and tool count, and a failed server shows why it failed rather than just reading as off. Adding a server takes a command, arguments, an optional environment, and an optional list of tools that may run without asking. Everything not on that list needs approval, so the exemption is opt-in per server rather than a global switch. Nothing starts automatically: a newly added server sits stopped until it is started here.
Contributor
Author
|
@Lazarus-931 — noticed #193 after opening this one. Looks like we ended up solving the same problem independently, quick comparison of our branches:
Given the split — #193 ahead on protocol choice and discovery UX, this one ahead on the safety/execution path — there's probably a stronger combined version than either alone. Happy to help wire the consent-gating and process-lifecycle handling into #193's catalog/SDK approach if that's useful, or split it however @Blaizzy prefers. |
Collaborator
Contributor
Author
|
Thanks sounds good sending a PR to your branch! |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This adds MCP client support to Nativ's chat — any MCP server can plug into the same tool registry PR #141 introduced, alongside the native tools.
What changed
NativMCPClient.swift), withNativProcessControllerextended to wire a child process's stdin (it previously only ever wired stdout/stderr for the app's own bundled server).mcp__<server>__<tool>so servers can't collide with each other or with native tools.switch_modelfrom Generalize the chat tool-call loop into a pluggable tool registry #141 — extended so a tool whose server can't be identified still requires approval rather than running quietly.On #154 — two layers of the same stack, not competing. This answers "can Nativ talk to MCP servers at all, safely." #154's catalog/CI-verification idea answers "how do we vet many community servers at scale" — additive, not conflicting.
Demo
filesystemFull quality: https://github.com/C-K-Loan/nativ/blob/demo-assets/demos/mcp-filesystem.mp4
fetchFull quality: https://github.com/C-K-Loan/nativ/blob/demo-assets/demos/mcp-fetch.mp4
memoryFull quality: https://github.com/C-K-Loan/nativ/blob/demo-assets/demos/mcp-memory.mp4
Multiple servers together in one conversation:

Full quality: https://github.com/C-K-Loan/nativ/blob/demo-assets/demos/mcp-ecosystem.mp4
(Browser/Playwright demo to follow shortly — server works, clip's still in review.)