Add a server
Click Add server in the sidebar. Two transports:
Remote (streamable HTTP) — paste the endpoint URL. Before you connect, the built-in endpoint check can diagnose it: whether it speaks MCP at all, which auth model it advertises, and — if you pasted a documentation page instead of the endpoint — which URLs in that page look like the real one.
Local (stdio) — a command plus its arguments, one argument per line (so a path with a space stays one argument). Environment variables go in as KEY=value lines. Paste straight out of a JSON config file; the textareas take it intact.
If a stdio server works in your terminal but not here, it is almost always PATH. mcpi resolves your login shell's PATH and injects it into every spawned server, so npx, uvx, and bun commands resolve the way you expect.
Connect
Connecting does three things in one action: dials the server, snapshots its contract, and records the snapshot in the local store. The record is what turns "connected" into "connected — and here is what changed since last time."
If the server wants OAuth, mcpi sends the first request unauthenticated; a server that needs sign-in answers with a challenge, and you get a Sign in button instead of an error. Your browser opens, you authorize, and the token lands in the macOS Keychain — reconnecting never means re-authenticating.
If the server needs an API key, add it as a header when saving (Authorization: Bearer …).
If the connection fails, the error names the actual cause. One worth knowing: servers still speaking the deprecated 2024-11-05 HTTP+SSE transport cannot connect over streamable HTTP — mcpi says so explicitly rather than showing a generic failure.
Browse and call
The middle pane lists what the server advertises — tools, resources, prompts — with a filter box for servers that advertise ninety tools. Select one and the detail pane shows a form generated from its input schema.
The form handles what real schema generators emit: pydantic's nullable idioms, schemars $refs, zod literal unions, documented enums. Anything it genuinely cannot represent falls back to a raw JSON editor for that field alone, with a visible raw badge — a form that silently mangles a payload is worse than one that admits the limit.
Run the call; the response lands below the form, and the call lands in history. Open any history entry to load its exact arguments back into the form and fire it again.