MCP Support via MCPorter
June 30, 2026 · View on GitHub
Flemma supports the Model Context Protocol (MCP) through MCPorter, a standalone CLI toolkit that handles server discovery, connection management, and OAuth. Flemma discovers MCPorter's servers at startup and registers each tool as a native Flemma tool definition -- the model sees them alongside built-in tools like bash and read.
Why MCPorter?
MCP is a large, evolving protocol with OAuth flows, multiple transports (HTTP, stdio, SSE), connection pooling, and credential management. Rather than reimplement all of that inside a Neovim plugin, Flemma delegates to MCPorter:
- OAuth and credentials -- MCPorter handles browser-based OAuth flows, token caching, and auto-refresh. Flemma never touches credentials for MCP servers.
- Server management -- MCPorter auto-discovers servers from your config and from editor configs (Claude Code, Cursor, VS Code, etc.). Add a server once and it's available everywhere.
- Stable CLI interface -- Flemma talks to MCPorter through
mcporter list --jsonandmcporter call, both machine-readable. The integration is a thin shell around well-defined commands. - Maintained separately -- MCPorter tracks MCP protocol changes, transport updates, and server quirks independently. Flemma gets those fixes for free.
Setup
1. Install MCPorter
# Homebrew
brew tap steipete/tap && brew install steipete/tap/mcporter
# npm (global)
npm install -g mcporter
2. Configure your MCP servers
MCPorter reads from ~/.mcporter/mcporter.json (global) and config/mcporter.json (per-project). It also auto-imports servers from Claude Code, Cursor, Windsurf, and VS Code.
// ~/.mcporter/mcporter.json
{
"mcpServers": {
"linear": {
"baseUrl": "https://mcp.linear.app/mcp"
},
"slack": {
"command": "npx",
"args": ["slack-mcp-server"],
"env": {
"SLACK_MCP_XOXC_TOKEN": "${SLACK_MCP_XOXC_TOKEN}",
"SLACK_MCP_XOXD_TOKEN": "${SLACK_MCP_XOXD_TOKEN}"
}
}
},
"imports": ["claude-code", "cursor"]
}
Verify your servers are reachable:
mcporter list
3. Enable in Flemma
require("flemma").setup({
tools = {
mcporter = {
enabled = true,
include = { "slack.*", "linear.*" },
},
},
})
That's it. On the next Neovim startup, Flemma discovers the servers, fetches their tool schemas, and registers them. You'll see them in :Flemma status verbose under the tools section.
Configuration
All MCPorter settings live under tools.mcporter:
tools = {
mcporter = {
enabled = false, -- master switch (default: off)
path = "mcporter", -- binary path or command
timeout = 60, -- per-operation timeout in seconds
startup = {
concurrency = 4, -- max parallel schema fetches
},
include = {}, -- glob patterns: matching tools are enabled
exclude = {}, -- glob patterns: matching tools are skipped entirely
},
}
Include / exclude
Glob patterns use * as a wildcard. Both match against the full tool name (server.tool_name).
- Exclude runs first -- matching tools are not registered at all.
- Include runs second -- matching tools are marked
enabled = true. - Remainder -- tools that survive exclude but don't match include are registered with
enabled = false(available for per-file opt-in).
| Goal | Config |
|---|---|
| Enable all Slack tools | include = { "slack.*" } |
| Enable Slack + Linear search | include = { "slack.*", "linear.search_*" } |
| Enable everything | include = { "*" } |
| Discover everything, enable nothing | include = {} (default) |
| Skip GitHub entirely | exclude = { "github.*" } |
Per-file opt-in
Tools registered with enabled = false (discovered but not included) can be enabled in individual .chat files via frontmatter:
```lua
flemma.opt.tools:append({"slack.channels_list", "slack.conversations_unreads"})
```
@System:
You are a Slack assistant.
@You:
List the public channels and my unread messages.
This lets you discover all available tools at startup but only pay the token cost for tools relevant to each conversation.
How it works
Discovery (startup)
When tools.mcporter.enabled is true, Flemma runs a three-phase discovery at startup:
- Gate check -- verify the
mcporterbinary is on$PATH(or at the configured path). - Server manifest -- run
mcporter list --jsonto get all configured servers and their health status. Unhealthy servers are skipped. - Schema fanout -- for each healthy server, run
mcporter list <server> --json --schemato fetch tool names, descriptions, and input schemas. Up tostartup.concurrency(default 4) fetches run in parallel.
Tools from fast servers become available immediately -- you don't have to wait for every server to respond. If a server times out or fails, its tools are skipped and the rest proceed normally.
As a performance shortcut, servers whose entire toolset is excluded by an exclude pattern (e.g. exclude = { "slack.*" } matches every slack.* tool) skip the schema fetch entirely — Flemma never spawns mcporter list <server> --schema for them.
Tool naming
Each discovered tool is named server.tool_name using a dot separator. The server name is used verbatim — dots inside it are preserved, so a server named my.custom.server simply becomes part of the joined name.
| MCPorter server + tool | Flemma tool name |
|---|---|
slack + channels_list | slack.channels_list |
github + search_code | github.search_code |
my.custom.server + do_thing | my.custom.server.do_thing |
On the wire (in API requests to LLM providers), every dot is encoded to __ to satisfy provider name constraints ([a-zA-Z0-9_-]+) — so my.custom.server.do_thing is sent as my__custom__server__do_thing. This encoding is transparent — you always use dots in config and frontmatter, and the progress bar and other UI surfaces decode __ back to . for display.
Because the full dotted name is what include/exclude match against, write your globs with dots too: my.custom.server.* matches every tool from my.custom.server, and my.custom.server.do_thing matches that one tool.
Note
The tool name separator changed from : to . in v0.12. Older configs using "server:tool" patterns will no longer match — update them to "server.tool".
Execution
When the model invokes an MCP tool, Flemma runs:
mcporter call <server>.<tool> --args '<json>' --output json
The response is parsed as an MCP CallToolResult. Only text content blocks are extracted -- image, audio, and resource blocks are not representable in the .chat buffer format and are dropped with a log warning. If the response is not shaped like a CallToolResult (no content array), Flemma falls back to inserting the raw output text verbatim — so non-conforming MCP servers still return something usable. If the MCP server returns a tool-level error (isError: true), it surfaces as a tool error in the conversation.
Outputs run through ctx.truncate.truncate_with_overflow the same way every other tool's output does, so large responses get truncated and the full text saved to the tool result store.
Full tool citizens
Discovered MCP tools are ordinary Flemma tool definitions, so they get the same treatment as built-ins:
- Harness parameters. Every MCP tool's schema gains the auto-injected
flemma.save_toparameter (offered to all tools, for redirecting large output to a file). Because MCP calls run asynchronously, they also gain theflemma.backgroundparameter, letting the model background a slow call and pick the result up later. See Harness parameters for details. - Approval flow. Every MCP call passes through the standard tool approval cycle — it lands as a
**Tool Result:**placeholder with a status (pending/approved/denied/rejected) and is only dispatched once approved, exactly like abashorwritecall.
Security
Unlike built-in tools, MCP execution is not sandboxed. Flemma runs each call by spawning the mcporter CLI directly with vim.fn.jobstart — there is no argv-level sandbox wrapper around the command, so the MCP server (and whatever it does) runs with your full user privileges. This is an asymmetry with built-in tools, which can be wrapped by a sandbox backend; see What the sandbox does and does not do. Only enable MCP servers you trust, and rely on the approval flow above to review what each call does before it runs.
Timeouts
| Scope | Default | Config |
|---|---|---|
| Per-operation (list, schema fetch, call) | 60s | tools.mcporter.timeout |
| Global discovery | 120s | Framework limit -- partial results kept |
Troubleshooting
Run :Flemma status verbose to see all registered tools, their source, and enabled state.
| Problem | Fix |
|---|---|
| No MCP tools appear | Check tools.mcporter.enabled = true. Run mcporter list in your terminal to verify servers are healthy. |
| Tool shows as disabled | It wasn't matched by your include patterns. Add the pattern or enable it per-file via frontmatter. |
| "Binary not found" in logs | mcporter isn't on $PATH. Set tools.mcporter.path to the full path, or install it globally. |
| Tool call fails | Run mcporter call <server>.<tool> --args '{}' --output json manually to debug. Check mcporter auth <server> if OAuth is required. |
| Discovery is slow | Reduce the number of servers, or increase startup.concurrency. Servers that time out are skipped after timeout seconds. |
| "Waiting for tool definitions" | Discovery is still running. This clears automatically once all servers respond or time out. |
Further reading
- MCPorter documentation -- server configuration, OAuth, ad-hoc servers, daemon mode
- tools.md -- Flemma's tool system, approval policies, custom tools
- configuration.md -- full config reference
- templates.md -- per-file settings and frontmatter