pi-mcp-bridge
A Pi Agent extension that bridges any Model Context Protocol (MCP) server into Pi using three LLM-callable tools —
CallMcpTool,FetchMcpResource, andListMcpResources— plus a filesystem-first registry and Cursor-style system-prompt injection that keeps the agent's context window cheap and cache-friendly.
English · 简体中文
Why
Cursor's Dynamic Context Discovery essay makes a sharp observation: exposing every MCP tool directly to the LLM bloats the system prompt and burns context. The fix is to expose only a few generic tools and let the model fetch specific tool schemas on demand from a compact, discoverable registry.
pi-mcp-bridge brings that pattern to the Pi Agent — and aligns with how Cursor actually does it:
CallMcpTool— call any MCP tool byserver+toolName+arguments.FetchMcpResource— read any MCP resource byserver+uri, optionally saving to disk.ListMcpResources— list the resources exposed by a server (discover before you fetch).- Filesystem is everything — each MCP server is described by
registry/<server>/meta.json+registry/<server>/tools/<tool>.json. The agent reads these files to learn how to call a tool, then invokesCallMcpToolwith the right arguments. - Cursor-style system-prompt injection — on each turn, a compact Markdown index of the registry is appended to the system prompt via the
before_agent_startevent (not prepended as a user message). The system prompt is the most stable cache prefix, so the block is cached across turns as long as the registry doesn't change. For small registries (≤ 10 tools by default), fullinputSchemas are inlined so the model can call correctly on the first try; for larger registries, the block falls back to names + descriptions and the model reads schema files on demand. - Server
instructionscaptured — the MCP protocol'sInitializeResult.instructions(the server's own description of its purpose and usage) is captured at sync time, persisted tometa.json, and rendered as a blockquote under each server header — so the model sees the server's intended usage pattern, not just our tool descriptions. - Lazy by default — MCP servers connect only when their tools are called, and disconnect after a configurable idle timeout.
- No vendor lock-in — the registry is plain JSON. You can
git diffit, hand-edit it, or generate it from a live MCP server with/mcp-bridge sync.
Architecture (60-second tour)
┌──────────────────────────────────────────────────────────────────┐
│ Pi Agent (LLM) │
│ system prompt ◀── MCP registry block appended via │
│ before_agent_start (Cursor-style) │
│ tools: [CallMcpTool, FetchMcpResource, ListMcpResources] │
└───────────────┬──────────────────────────────────────────────────┘
│ CallMcpTool({server, toolName, arguments})
▼
┌──────────────────────────────────────────────────────────────────┐
│ pi-mcp-bridge │
│ 1. resolve (server, toolName) → registry/<server>/tools/*.json │
│ 2. lazy connect to that MCP server (idle timeout) │
│ 3. forward arguments, await result │
│ 4. output-guard: truncate + spill to temp file │
│ 5. return ContentBlocks to Pi │
└───────────────┬──────────────────────────────────────────────────┘
│ MCP protocol (stdio / HTTP / SSE)
▼
┌──────────────────────────────────────────────────────────────────┐
│ MCP servers (filesystem, github, slack, …) │
└──────────────────────────────────────────────────────────────────┘
See docs/architecture.md for the full module map, design decisions, and behavior contracts.
Quick start
1. Install
pi install npm:@qianhuan-lxs/pi-mcp-bridge
This installs the package under ~/.pi/agent/npm/ and auto-registers the extension via its pi.extensions manifest — no manual config editing required.
Note:
pi installrequires Pi v0.74+. If you're on an older Pi or want to manage it manually, add"@qianhuan-lxs/pi-mcp-bridge"to thepackagesarray in your~/.pi/agent/settings.json.
2. Register the extension
Add to your Pi agent config (e.g. ~/.pi/agent.json):
{
"extensions": [
"pi-mcp-bridge"
]
}
3. Add an MCP server to the registry
stdio — context7 (library docs lookup)
# Inside Pi — sync a live MCP server's tools into the registry (primary path)
/mcp-bridge sync context7 -- npx -y @upstash/context7-mcp
# Or add a server stub first (with an env var), then sync
/mcp-bridge add github --env GITHUB_PERSONAL_ACCESS_TOKEN -- npx -y @modelcontextprotocol/server-github
/mcp-bridge sync github
# Validate / list / check status
/mcp-bridge validate
/mcp-bridge list
/mcp-bridge status
Streamable HTTP (modern MCP HTTP transport)
Start the server in a separate terminal:
npx -y @modelcontextprotocol/server-everything streamableHttp
# serves at http://localhost:3000/mcp
Then in Pi:
/mcp-bridge add everything-http --url http://localhost:3000/mcp --description "Everything MCP (Streamable HTTP)"
/mcp-bridge sync everything-http
SSE (legacy HTTP transport)
Start the server in a separate terminal:
npx -y @modelcontextprotocol/server-everything sse
# serves at http://localhost:3001/sse
Then in Pi:
/mcp-bridge add everything-sse --url http://localhost:3001/sse --description "Everything MCP (SSE)"
/mcp-bridge sync everything-sse
Transport auto-detection: for
kind: "http"servers,/mcp-bridge syncand lazy-connect both probe StreamableHTTP first and fall back to SSE automatically — you don't pick the transport explicitly; the URL is enough.
Why slash commands? Registry management happens inside Pi via
/mcp-bridge ...so there's no PATH setup and no separate CLI binary to install. An optionalcli.tsis still included for scripting — run it vianpx tsx ./node_modules/@qianhuan-lxs/pi-mcp-bridge/cli.ts <cmd>.
This produces:
~/.pi/agent/mcp-registry/
context7/
meta.json
tools/
resolve-library-id.json
query-docs.json
...
everything-http/
meta.json
tools/...
index.json
4. Restart Pi and ask
> use context7 to look up the latest AgentScope documentation
The agent will:
- Read the MCP registry block from its system prompt (injected via
before_agent_start). For small registries the fullinputSchemais already inline; for large ones it sees the server'sfolder:path and reads<folder>/tools/<tool>.jsonon demand. - Call
CallMcpTool({server:"context7", toolName:"resolve-library-id", arguments:{...}}), thenCallMcpTool({server:"context7", toolName:"query-docs", arguments:{...}}). - Receive the result (truncated if large, with a temp-file spill for the full content).
To discover resources first, use ListMcpResources({server:"..."}), then FetchMcpResource({server, uri}).
Registry layout
~/.pi/agent/mcp-registry/
<server>/
meta.json # server config: command, env, transport, timeouts, instructions
tools/
<tool>.json # one file per tool: name, description, inputSchema
index.json # aggregate index (rebuilt by `sync` / `validate`)
meta.json example (stdio — context7):
{
"name": "context7",
"description": "Context7 documentation MCP server",
"instructions": "Use this server to fetch up-to-date documentation for libraries. Always call resolve-library-id first, then query-docs.",
"transport": {
"kind": "stdio",
"command": "npx",
"args": ["-y", "@upstash/context7-mcp"],
"env": {}
},
"auth": { "kind": "none" },
"lifecycle": { "mode": "lazy", "idleTimeoutMinutes": 10 },
"syncedFrom": "live-server",
"syncedAt": "2026-07-19T06:00:00.000Z"
}
meta.json example (HTTP — Streamable HTTP or SSE, same shape):
{
"name": "everything-http",
"description": "Everything MCP (Streamable HTTP)",
"transport": {
"kind": "http",
"url": "http://localhost:3000/mcp",
"headers": {}
},
"auth": { "kind": "none" },
"lifecycle": { "mode": "lazy", "idleTimeoutMinutes": 10 },
"syncedFrom": "live-server",
"syncedAt": "2026-07-19T06:00:00.000Z"
}
instructionsis captured automatically from the MCP server'sinitializeresponse during/mcp-bridge sync. You can also hand-edit it. For HTTP servers, the transport kind is just"http"— sync and lazy-connect auto-probe StreamableHTTP then fall back to SSE.
tools/resolve-library-id.json example:
{
"name": "resolve-library-id",
"description": "Resolve a Context7-compatible library ID from a library name.",
"inputSchema": {
"type": "object",
"properties": {
"query": { "type": "string" },
"libraryName": { "type": "string" }
},
"required": ["query", "libraryName"]
}
}
See docs/config-format.md for the full schema reference.
Context injection (how the model learns about MCP)
The injected block is appended to the system prompt on every turn via the before_agent_start event. It walks a truncation ladder (most detail first; first level that fits the token budget wins):
| Level | Content | When used |
|---|---|---|
1. renderWithSchemas |
tool names + descriptions + full inputSchema JSON inline + server instructions |
registry ≤ schemaInjectionToolLimit tools (default 10) AND fits budget |
2. renderFull(80) |
tool names + 80-char descriptions + server instructions |
level 1 skipped/overflowed |
3. renderFull(40) |
tool names + 40-char descriptions + instructions |
level 2 overflowed |
4. renderKeysOnly |
tool keys only + instructions |
level 3 overflowed |
5. renderCountsOnly |
server names + tool counts | level 4 overflowed |
Each server header includes folder: <absolute descriptor path> so the model knows where to ls/read for schemas. The block also includes a MANDATORY: read the tool's descriptor file before calling CallMcpTool instruction (with a caveat that inline schemas let the model skip the read).
Why system-prompt injection? It's the most cache-friendly injection point — the system prompt is the stable cache prefix, so the block is cached across turns as long as the registry doesn't change. (Earlier versions prepended a user message via the context event, which worked but shifted the message array and was less cache-friendly. v0.3.0 switched to before_agent_start to match Cursor's approach.)
Slash commands
The /mcp-bridge command is the primary interface for registry management (no separate CLI binary, no PATH setup):
/mcp-bridge sync <server> [--env K=V]... [--force] -- <command> [args...]
Connect to a live MCP server, capture its instructions + tools/resources,
and write meta.json + tools/*.json into the registry. Auto-reloads the
agent context for the next turn.
/mcp-bridge add <server> [--env K=V]... -- <command> [args...]
Add a server and auto-sync its tools/resources in one step.
/mcp-bridge add <server> --url <url> [--description <text>]
Add an HTTP-transport server and auto-sync against the URL.
/mcp-bridge remove <server> [--keep-config]
Delete the server's registry directory, close any live connection,
and remove it from mcp-servers.json (skip JSON edit with --keep-config).
Aliases: rm, delete.
/mcp-bridge validate
Validate the registry against the JSON schemas and rebuild index.json.
/mcp-bridge list
List all servers in the registry and their tools.
/mcp-bridge status
Show servers/tools, MCP context occupancy (~tokens vs budget),
and tokens saved vs always-inlining full schemas.
/mcp-bridge reload
Reconcile optional mcp-servers.json (added/updated/0-tool auto-sync),
re-read the registry, refresh system-prompt context on the next turn.
/mcp-bridge approve <server>
(Only when `requireConsent` is on.) Approve a server so CallMcpTool calls go through.
/mcp-bridge revoke <server>
Revoke consent for a server; future CallMcpTool calls will be blocked until re-approved.
An optional cli.ts wraps the same logic for scripting/CI:
npx tsx ./node_modules/@qianhuan-lxs/pi-mcp-bridge/cli.ts <sync|add|validate|list> ...
Configuration
MCP servers file (OpenCode-aligned, optional)
Hand-edit transport config in a single JSON file — same shape as OpenCode's mcp block. The bridge reconciles it into the filesystem registry (meta.json) on session_start and /mcp-bridge reload, then auto-syncs servers that were added, had transport updated, or are configured but still have 0 tools.
After pi update --extensions, restart Pi so the new extension code loads — /mcp-bridge reload only reloads the registry + mcp-servers.json, not the extension itself.
Paths (project overrides global by server name):
- Global:
~/.pi/agent/mcp-servers.json - Project:
.pi/mcp-servers.json
{
"mcp": {
"context7": {
"type": "local",
"command": ["npx", "-y", "@upstash/context7-mcp"],
"enabled": true
},
"filesystem": {
"type": "local",
"command": ["npx", "-y", "@modelcontextprotocol/server-filesystem", "/path/to/root"],
"environment": { "FOO": "bar" }
},
"docs": {
"type": "remote",
"url": "https://mcp.example.com/mcp",
"headers": { "Authorization": "Bearer ..." },
"enabled": true
}
}
}
| Field | Meaning |
|---|---|
type |
"local" (stdio) or "remote" (HTTP / SSE) |
command |
Local only: command + args as a string array (OpenCode style) |
environment / cwd |
Local only: env vars / working directory |
url / headers |
Remote only |
oauth |
Remote only: OAuth object (stored in meta.auth for Phase 2) or false |
enabled |
false skips the server (OpenCode semantics); default enabled |
timeout |
Request timeout ms → meta.lifecycle.requestTimeoutMs |
Policies: registry servers missing from the file are warned, never deleted. /mcp-bridge add still works as a shortcut; if mcp-servers.json already exists, add also upserts an OpenCode-shaped entry into it.
Tool schemas stay in registry/<server>/tools/*.json (sync product) — edit the JSON for transport, not for schemas.
Bridge settings
~/.pi/agent/mcp-bridge.json (all fields optional; defaults shown):
{
"idleTimeout": 10, // minutes, default 10, 0 to disable
"requestTimeoutMs": 0, // ms, 0 = use SDK default
"outputGuard": true, // truncate oversized tool outputs
"contextBudgetTokens": 4000, // max tokens for the injected system-prompt block
"schemaInjectionToolLimit": 10, // registries with > N tools skip inline schemas
// 0 = disable inline schemas entirely
"uiViewer": "auto", // "auto" | "browser" | "glimpse"
"requireConsent": false // gate CallMcpTool behind /mcp-bridge approve (default false)
}
Environment overrides:
PI_CODING_AGENT_DIR— override the Pi agent directory (default~/.pi/agent).PI_MCP_BRIDGE_REGISTRY— override the registry root (default<agent dir>/mcp-registry).MCP_OUTPUT_GUARD=0— disable the output guard.
License
MIT 2026 qianhuan-lxs