MCP (Model Context Protocol)
Maki connects to external tool servers over MCP. Both stdio and HTTP transports are supported.
Configuration
Add servers under [mcp.*] in your MCP config:
- Global:
~/.config/maki/mcp.toml - Project:
.maki/mcp.toml(project config wins when both set a value)
Stdio
[mcp.filesystem]
command = ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp"]
[mcp.github]
command = ["gh", "mcp-server"]
environment = { GITHUB_TOKEN = "ghp_xxxx" }
timeout = 10000
enabled = falseHTTP
[mcp.analytics]
url = "https://mcp.example.com/mcp"
headers = { Authorization = "Bearer tok123" }
Some HTTP servers need OAuth but have no dynamic client registration. For those, give Maki a static client:
[mcp.acme]
url = "https://mcp.acme.example.com/mcp"
oauth = { client_id = "acme-client", client_secret = "s3cret", callback_port = 3118, callback_path = "/callback", callback_hostname = "localhost" }All options
| Field | Type | Default | Notes |
|---|---|---|---|
command | array | Stdio: program + args | |
url | string | HTTP: server URL | |
environment | map | Stdio only | |
headers | map | HTTP only | |
oauth | table | HTTP only: static client (client_id, optional client_secret, optional callback_port, optional callback_path, optional callback_hostname) | |
timeout | u64 | 30000 | Milliseconds (1-300000) |
enabled | bool | true | |
always_load | bool | false | Skip tool search, load all tools upfront |
Set command for stdio, url for HTTP. Pick one.
One option lives at the top level of mcp.toml, outside any server:
| Field | Type | Default | Notes |
|---|---|---|---|
defer_tools | usize | 10 | Defer tools only when more than this many exist |
Tool search
Every tool definition a server exposes costs context window space, on every request. Take Datadog's MCP server: with all toolsets on it ships over 100 tools, when a task often needs three.
So Maki, like Claude Code, defers MCP tools by default. The model sees one small tool_search tool that lists the deferred names, searches when it actually needs something, and the matches stay loaded for the rest of the session. Resume a session and the tools it was using come back. Subagents keep their own loads, so their searches don't bloat your main conversation.
server ships 117 tool definitions
│
more than defer_tools (10)?
│ no │ yes
▼ ▼
all load context gets one small tool: tool_search
upfront │
│ model: tool_search("logs")
▼
3 matches load, stay for the session
114 definitions never enter context
You don't configure anything for this. Add the server as usual:
[mcp.datadog]
url = "https://mcp.datadoghq.com/api/unstable/mcp-server/mcp?toolsets=all"
Ask about an incident, and the model searches for something like datadog logs, gets back the few matching tools, and the other hundred definitions never enter the conversation.
With 10 or fewer tools across all your servers there is no search step: at that size, searching costs more than it saves, so everything loads upfront. The top-level defer_tools key moves that line:
defer_tools = 30
[mcp.github]
url = "https://api.githubcopilot.com/mcp/"
Set it to 0 to always defer, or above your tool count to never defer.
If one server should skip the search step entirely, opt it out:
[mcp.linear]
command = ["linear-mcp-server"]
always_load = true
Good for small servers you rely on every turn. On a big server it defeats the point: every definition is back in your context on every request.
Naming and namespacing
Server names are ASCII alphanumeric, hyphens ok (no dots). Tools get prefixed with their server name: a read tool on the filesystem server becomes filesystem__read. Because of this, __ is reserved and names can't collide with built-in tools.
Permission rules for MCP tools use the same nested form under [mcp.<server>] in permissions.toml. See Permissions.
Runtime toggling
Open the MCP picker with /mcp. Turn servers on or off there; changes save back to your config (project or global, depending on which file defined the server).
Status
| Status | Meaning |
|---|---|
| Connecting | Waiting for the server to come up |
| Running | Tools available |
| Disabled | Off in config or toggled off in UI |
| Failed | Error shown in UI |
| NeedsAuth | Waiting for OAuth (see below) |
If one server fails, the rest still work.
OAuth
Some HTTP servers need auth. When that happens, Maki opens your browser to log in. Other servers keep working while you authenticate. Tokens refresh on their own. If you change the server URL, you log in again.
maki mcp auth <server-name> # manually trigger auth
maki mcp logout <server-name> # remove stored tokens
Servers without dynamic client registration need a client you registered yourself (e.g. your own app on their platform). Add it to the server config so the auth flow uses it instead of trying to register:
| Field | Type | Notes |
|---|---|---|
client_id | string | Client id of your registered app |
client_secret | string | Optional, for confidential clients |
callback_port | u16 | Optional, pins the loopback port so the redirect URI can be pre-registered |
callback_path | string | Optional, loopback path of the redirect URI (default /mcp/oauth/callback) |
callback_hostname | string | Optional, loopback hostname of the redirect URI (default 127.0.0.1) |
Set callback_port when the server only accepts exact redirect URIs. Otherwise Maki falls back to its default port, then to any free port, so the redirect URI changes between runs. Set callback_path when the server registered a different path (e.g. /callback). Set callback_hostname to localhost when the server registered the name form instead of the IP (the listener still binds to 127.0.0.1).
Headless machines
On a machine without a browser (say, a dev server over SSH), run maki mcp auth <server-name>. Maki prints the login URL. Open it on your laptop and log in. The browser lands on a http://127.0.0.1:19876/... page that fails to load. Copy that full URL from the address bar and paste it into the terminal to finish the login.
Prompts
MCP servers can expose prompts (reusable message templates). Maki shows them as slash commands in the command palette: /server:prompt-name. Type / to filter.
/github:create-pr # no arguments
/analytics:report monthly # one argument
/review:code src tests # multiple, positional
Skip a required argument and Maki shows a usage hint. Prompts are fetched at startup and on reconnect, so new ones need a restart. Only text content is supported.