Skip to content

Add MCP (Model Context Protocol) client support - #200

Closed
C-K-Loan wants to merge 5 commits into
Blaizzy:mainfrom
C-K-Loan:pr9-clean
Closed

Add MCP (Model Context Protocol) client support#200
C-K-Loan wants to merge 5 commits into
Blaizzy:mainfrom
C-K-Loan:pr9-clean

Conversation

@C-K-Loan

@C-K-Loan C-K-Loan commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

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

  • A stdio JSON-RPC MCP client (NativMCPClient.swift), with NativProcessController extended to wire a child process's stdin (it previously only ever wired stdout/stderr for the app's own bundled server).
  • Tool names are namespaced mcp__<server>__<tool> so servers can't collide with each other or with native tools.
  • Servers are configured through a new MCP Servers settings panel — add/start/stop/remove, per-server tool-exemption list. Nothing is bundled or hardcoded — no server auto-starts or ships pre-configured; every server is explicitly added and explicitly started.
  • Consent gating is fail-closed: any MCP tool call needs an explicit in-chat approve/deny, same pattern as switch_model from 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.
  • 252/252 tests, zero regressions to existing native tools. Covered against a real fixture server, not a mock transport.

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

filesystem
filesystem demo
Full quality: https://github.com/C-K-Loan/nativ/blob/demo-assets/demos/mcp-filesystem.mp4

fetch
fetch demo
Full quality: https://github.com/C-K-Loan/nativ/blob/demo-assets/demos/mcp-fetch.mp4

memory
memory demo
Full quality: https://github.com/C-K-Loan/nativ/blob/demo-assets/demos/mcp-memory.mp4

Multiple servers together in one conversation:
mcp ecosystem demo
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.)

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.
@C-K-Loan

C-K-Loan commented Aug 4, 2026

Copy link
Copy Markdown
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:

  • Protocol layer: Mcp hub #193 wraps the official mcp-swift-sdk; this PR hand-rolls the stdio/JSON-RPC transport. Less protocol code to maintain long-term is a genuine advantage for Mcp hub #193's approach here — worth adopting regardless of which core ends up landing. This PR's upside is the flip side: zero external dependency for MCP support, no version pin or supply-chain surface to track — real tradeoff either way.
  • Catalog & discovery: Mcp hub #193's server catalog plus the verify-mcp-catalog.yml CI check is a real gap this PR doesn't address at all. Vetting community servers at scale is exactly the kind of infra this repo needs, independent of whichever client sits underneath it.
  • Consent gating: this PR routes every MCP tool call through the same approve/decline gate as switch_model (Generalize the chat tool-call loop into a pluggable tool registry #141), fail-closed even when a server can't be identified — covered by dedicated tests for that boundary. From reading Mcp hub #193's current state, MCP calls execute without that gate yet. Could well just be sequencing on a WIP, but flagging it since it's worth locking down early.
  • Test coverage: 266/266 here, against a real subprocess fixture rather than mocks.

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.

@Lazarus-931

Copy link
Copy Markdown
Collaborator

hi @C-K-Loan, yeah so I'd say closing this an leaving comments in #193 if what else to include on it, happy to merge with u as contributor!

@C-K-Loan

C-K-Loan commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Thanks sounds good sending a PR to your branch!

@C-K-Loan

C-K-Loan commented Aug 4, 2026

Copy link
Copy Markdown
Contributor Author

Closing in favor of #193 — the consent-gating + lifecycle pieces went over as #207/#208/#209.

@C-K-Loan C-K-Loan closed this Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants