mcp server
pi-mcp
Terminal-first MCP client for Pi: user-clickable OAuth, automatic callbacks, and no browser launches.
Set up
Runs on your computer. Pick your agent and operating system.
Before you start
- Node.js 20 or newer
{
"mcpServers": {
"prjct-app-pi-mcp": {
"command": "npx",
"args": [
"-y",
"@prjct.app/pi-mcp"
]
}
}
}same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12
Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs
README
synced Oct 17, 2026pi-mcp
A terminal-first MCP client for Pi. No automatic browsers, no MCP App rendering.
Status: development preview. Development targets Pi 0.99.1, MCP SDK 2.0.0, Node 22.22.2 (macOS). Node ≥ 22.19.
Install
pi install npm:@prjct.app/pi-mcp
Local integration
git switch develop
npm ci --ignore-scripts
npm run check
npm test
npm run build
pi --no-extensions -e ./build
--no-extensions prevents loading the existing MCP adapter alongside this one. Do not register both: they expose the same mcp tool and command. This does not modify installed packages or Pi configuration.
Configuration
Reads mcp.json files in this order (later entries replace servers entirely, no merge):
$XDG_CONFIG_HOME/mcp/mcp.json(default~/.config/mcp/mcp.json)<Pi agent directory>/mcp.json(~/.pi/agent; honorsPI_CODING_AGENT_DIR)<project>/.mcp.json(only when Pi trusts the project)<project>/<Pi config directory>/mcp.json(only when trusted)
{
"mcpServers": {
"linear": { "url": "https://mcp.linear.app/mcp", "auth": "oauth" },
"local": { "command": "node", "args": ["/absolute/path/to/server.js"] }
}
}
Server fields
| Field | Meaning |
|---|---|
url | Streamable HTTP endpoint; HTTPS required except loopback HTTP |
command, args, cwd, env | Local stdio process; mutually exclusive with HTTP |
headers | Explicit HTTP headers; supports ${ENV_VAR} |
auth | oauth or bearer; omit for unauthenticated servers |
bearerTokenEnv | Environment variable with the bearer token |
oauth.grantType | authorization_code (default) or client_credentials |
oauth.clientId | Pre-registered client ID; requires exact oauth.issuer |
oauth.clientSecretEnv | Env var with OAuth client secret (only client_credentials) |
oauth.issuer | Expected authorization-server issuer (exact-match binding) |
oauth.redirectUri | HTTP loopback callback; defaults to http://127.0.0.1:32187/callback |
oauth.scope | Requested scopes |
protocolVersion | auto (default), legacy, or 2026-07-28 |
requestTimeoutMs | Request deadline; defaults to 15s |
disabled | Keep entry visible but prevent connection/auth |
${ENV_VAR} works in URLs, commands, arguments, and env values. Missing variables fail closed. Relative cwd resolves against the config file's directory.
Machine OAuth (client_credentials)
{
"mcpServers": {
"internal-api": {
"url": "https://api.example.com/mcp",
"auth": "oauth",
"oauth": {
"grantType": "client_credentials",
"clientId": "pi-service",
"clientSecretEnv": "INTERNAL_API_CLIENT_SECRET",
"issuer": "https://auth.example.com",
"scope": "mcp:read"
}
}
}
}
Requires an exact HTTPS issuer. No redirect URIs or client metadata docs. The secret is read from the environment variable only when connecting; acquired tokens live in session memory only.
Examples: Notion, Stripe, GitHub, Supabase, Mobbin
{
"mcpServers": {
"notion": { "url": "https://mcp.notion.com/mcp", "auth": "oauth" },
"stripe": { "url": "https://mcp.stripe.com", "auth": "oauth" },
"github": { "url": "https://api.githubcopilot.com/mcp/", "auth": "bearer", "bearerTokenEnv": "GITHUB_PERSONAL_ACCESS_TOKEN" },
"supabase": { "url": "https://mcp.supabase.com/mcp", "auth": "oauth" },
"mobbin": { "url": "https://api.mobbin.com/mcp", "auth": "oauth" }
}
}
Official references: Notion MCP, Stripe MCP, GitHub remote MCP, Supabase MCP, Mobbin MCP.
Stripe uses OAuth for interactive sign-in: run /mcp auth stripe and follow the authorization link. If you previously configured bearerTokenEnv, remove it when switching to "auth": "oauth", then reload Pi before authenticating. For unattended clients, Stripe also supports an agent API key through "auth": "bearer" and "bearerTokenEnv": "STRIPE_AGENT_API_KEY"; that environment variable must be set before starting Pi.
Supabase accepts ?project_ref=<ref>&read_only=true on the URL to scope the server to one project and a read-only Postgres role; features=database,docs limits the tool groups. For CI, use "auth": "bearer", "bearerTokenEnv": "SUPABASE_ACCESS_TOKEN" with a personal access token instead of OAuth.
Commands
| Command | Result |
|---|---|
/mcp | Interactive manager: pick a server, see its connection and credential state, and act on it; n adds a server from its URL |
/mcp status | Connection and credential state per server (does not connect) |
/mcp add <url> | Set up a server from its MCP endpoint or its documentation page (see below) |
/mcp tools <server> | Compact list of up to 20 tools (connects on demand) |
/mcp connect <server> | Connect now and report the advertised tool count; undoes disconnect |
/mcp disconnect <server> | Close the connection and refuse new ones for this session |
/mcp reconnect <server> | Reset the connection and connect again |
/mcp auth <server> | Request a fresh authorization link (also re-authenticates a signed-in server) |
/mcp logout <server> | Forget the server's saved OAuth credentials (asks for confirmation in the manager) |
The manager shows only actions that fit the server's state: connect, list tools, reconnect, disconnect, authenticate or re-authenticate, show or cancel a pending authorization link, and sign out. Credential state is read locally (OS keyring or environment) and never triggers network calls. Signing out removes local credentials only; tokens are not revoked at the provider. It uses Pi's standard dialogs, so it works in the TUI and over RPC.
Add a server from a URL
/mcp add <url> takes either kind of URL and walks you through the rest. /mcp add alone, or n in the /mcp panel, asks for the URL.
- MCP endpoint, for example
https://mcp.svelte.dev/mcp. pi-mcp sends one MCPinitializerequest, with no credentials and no redirects followed, and reads how the server signs in. A JSON-RPC reply means no sign-in. A 401 with protected-resource metadata means OAuth, and you get the usual authorization link after saving. A 401 without it means a token: you are asked for the name of the environment variable that holds it, never the token itself. Then you pick a name (suggested from the host,sveltehere). No model is involved. - Documentation page, for example
https://svelte.dev/docs/ai/mcp. The page goes to the agent as a visible message. The agent reads the page, follows its links to the setup details, asks you whatever only you can decide (which setup, which account, where a token lives), and writes the entry withmcp({ action: "add", ... }).
The mcp tool description only lists add among its actions; how to use it travels in the /mcp add message and in the reply to an incomplete add call, so requests that never add a server carry no extra instructions.
You can also just ask in the chat ("connect https://mcp.svelte.dev/mcp"). An entry with only a url is checked the same way, so the agent does not need to know how the server signs in.
Before anything is saved, a dialog shows you the exact entry, the program it would start (for stdio servers) and the page it came from. Endpoints and documentation are untrusted, so there is no way to skip this step. Without an interactive session (print or JSON mode), nothing can be added.
- The entry goes to
~/.pi/agent/mcp.json. A name defined in another configuration file is never touched. An existing name in that file is replaced only after a "Replace" confirmation. - The entry is checked exactly as loading checks it, before any network request. The error names unsupported fields (documentation snippets often include
"type": "http") and missing environment variables, so the agent can correct the entry before you are asked. A saved entry that referenced a missing variable would stop every server from loading. - Secrets are never written to the file. Use
bearerTokenEnvor${VAR}references. - After saving, the session reloads (other servers reconnect on their next use), the server connects, and its tools are listed. If the connection fails, the entry stays saved and the reason is shown, so it can be replaced.
OAuth flow
When the agent needs OAuth credentials, it shows one link. The user clicks and approves. Pi receives the callback automatically, stores the tokens, and notifies the agent. No pasted URLs, codes, or confirmations required. Pi never opens the browser itself.
The loopback receiver exists only during authorization (max 5 minutes), bound to loopback, validating state and callback target. No widgets or scripts.
Agent tool
A single proxy tool keeps the surface small. It is reached only from codemode scripts: mcp has codemode exposure, so the model sees codemode, never a direct mcp tool. Naming mcp in --tools or defaultTools declares it directly again.
// inside a codemode script: tools.mcp(...)
await tools.mcp({ action: "status" })
await tools.mcp({ action: "tools", server: "jira", query: "search", limit: 5 })
await tools.mcp({ action: "call", server: "jira", tool: "name", args: {} })
await tools.mcp({ action: "resources", server: "local" })
await tools.mcp({ action: "templates", server: "local" })
await tools.mcp({ action: "read", server: "local", uri: "example://resource" })
await tools.mcp({ action: "prompts", server: "local" })
await tools.mcp({ action: "prompt", server: "local", prompt: "name", args: {} })
await tools.mcp({ action: "complete", server: "local", uri: "example://docs/{topic}", argument: "topic", value: "auth" })
await tools.mcp({ action: "add", server: "svelte", config: { url: "https://mcp.svelte.dev/mcp" }, uri: "https://svelte.dev/docs/ai/remote-setup" }) // sign-in is detected
When credentials are missing, the tool returns authorization_required and places one clickable authorization link in the conversation. Do not open it, poll, or ask for callbacks. Wait for the mcp-auth message. MCP tool errors become Pi errors.
Limits
| Concern | Limit |
|---|---|
| Configuration | 64 servers, 1 MiB per file |
| Tool discovery | SDK pagination capped at 32 pages; display defaults to 20 tools |
| Response cache | Session memory; 512 entries per client |
| Inline text | 50 KiB or 2,000 lines |
| Overflow files | Private; 50 MiB per session |
| Images | 4 inline, max 2 MiB base64 each (PNG/JPEG/GIF/WebP) |
| OAuth | 20s per network phase, 5min callback window |
| Credential locks | 30s wait; never steal locks automatically |
Protocol and safety
- MCP
2026-07-28+ legacy negotiation via SDK 2.0.0 over JSON-RPC 2.0. - No MCP App capabilities advertised. HTML never executed. App-only tools filtered.
- Connections are lazy and resident until reset or session teardown.
- No automatic replay of failures, polling loop, or idle reconnect.
- Server descriptions/results are untrusted data (not user consent).
- A configured stdio executable can spawn programs itself.
Development
npm run check # typecheck + immutability
npm test # offline tests (fixtures, no prod)
npm run check:package # dry-run tarball
npm run build # generates build/ (unpacked tarball)
python3 scripts/smoke-tui.py # isolated real TUI smoke test
build/ is an unpacked tarball, not transpiled code. Automated tests never read real credentials or contact production MCP services. Live checks require explicit authorization.
For an authorized Linear check (read-only):
node --import tsx scripts/smoke-linear.ts --live
Architecture · Compatibility · Releases · Contributing · Changelog · MIT
