Documentation Index Fetch the complete documentation index at: https://mintlify.com/nicobailon/pi-mcp-adapter/llms.txt
Use this file to discover all available pages before exploring further.
Overview
Pi MCP Adapter uses a proxy pattern to give you access to hundreds of MCP tools through a single, compact interface. Instead of registering every tool in your context window, the adapter provides one gateway tool that dynamically routes calls to the appropriate MCP server.
Core Components
The architecture consists of four main components working together:
The mcp tool is the only tool that appears in your context by default (unless you configure direct tools). It provides multiple modes of operation:
mcp ({ }) // Show server status
mcp ({ server: "name" }) // List tools from server
mcp ({ search: "query" }) // Search for tools
mcp ({ describe: "tool_name" }) // Show tool details
mcp ({ connect: "server-name" }) // Connect and refresh metadata
mcp ({ tool: "name" , args: '{...}' }) // Call a tool
The proxy tool consumes approximately 200 tokens compared to 10k+ tokens for registering all tools individually.
The tool’s mode is determined by which parameters you provide, with priority: tool > connect > describe > search > server > status.
2. Server Manager
The McpServerManager class (server-manager.ts) handles all MCP server connections:
Connection pooling - Reuses healthy connections instead of creating duplicates
Connection deduplication - Prevents concurrent connection attempts to the same server
Transport abstraction - Supports stdio (local commands) and HTTP (remote servers)
Transport fallback - Tries StreamableHTTP first, falls back to SSE for legacy servers
OAuth integration - Injects bearer tokens for authenticated servers
Idle tracking - Tracks lastUsedAt timestamp and inFlight request count
How Connection Deduplication Works
When multiple tool calls request the same server simultaneously, the manager stores a single Promise<ServerConnection> in a connectPromises Map. All concurrent requests await the same promise instead of spawning multiple processes. if ( this . connectPromises . has ( name )) {
return this . connectPromises . get ( name ) !
}
const promise = this . createConnection ( name , definition )
this . connectPromises . set ( name , promise )
try {
const connection = await promise
this . connections . set ( name , connection )
return connection
} finally {
this . connectPromises . delete ( name )
}
3. Lifecycle Manager
The McpLifecycleManager class (lifecycle.ts) handles three distinct lifecycle modes:
Lazy mode (default) - Don’t connect at startup, connect on first use, disconnect when idle
Eager mode - Connect at startup but don’t auto-reconnect
Keep-alive mode - Connect at startup, auto-reconnect via health checks, never disconnect
The lifecycle manager runs health checks every 30 seconds:
startHealthChecks ( intervalMs = 30000 ): void {
this . healthCheckInterval = setInterval (() => {
this . checkConnections ()
}, intervalMs )
this . healthCheckInterval . unref () // Don't block process exit
}
The health check interval uses unref() so it won’t prevent Pi from shutting down gracefully.
For keep-alive servers, the manager attempts automatic reconnection if the connection drops:
if ( ! connection || connection . status !== "connected" ) {
try {
await this . manager . connect ( name , definition )
console . log ( `MCP: Reconnected to ${ name } ` )
this . onReconnect ?.( name ) // Update tool metadata
} catch ( error ) {
console . error ( `MCP: Failed to reconnect to ${ name } :` , error )
}
}
For lazy/eager servers, the manager checks if they’re idle and closes them:
for ( const [ name ] of this . allServers ) {
if ( this . keepAliveServers . has ( name )) continue
const timeout = this . getIdleTimeout ( name )
if ( timeout > 0 && this . manager . isIdle ( name , timeout )) {
await this . manager . close ( name )
this . onIdleShutdown ?.( name )
}
}
The metadata-cache.ts module persists tool and resource metadata to disk at ~/.pi/agent/mcp-cache.json:
interface ServerCacheEntry {
configHash : string // SHA-256 of server config
tools : CachedTool [] // Tool names, descriptions, schemas
resources : CachedResource [] // Resource URIs and descriptions
cachedAt : number // Timestamp for cache expiration
}
Cache validation ensures metadata matches the current config:
export function computeServerHash ( definition : ServerEntry ) : string {
const identity : Record < string , unknown > = {
command: definition . command ,
args: definition . args ,
env: definition . env ,
cwd: definition . cwd ,
url: definition . url ,
headers: definition . headers ,
auth: definition . auth ,
bearerToken: definition . bearerToken ,
bearerTokenEnv: definition . bearerTokenEnv ,
exposeResources: definition . exposeResources ,
}
const normalized = stableStringify ( identity )
return createHash ( "sha256" ). update ( normalized ). digest ( "hex" )
}
Lifecycle settings (lifecycle, idleTimeout, debug) are intentionally excluded from the hash because they don’t affect which tools a server exposes.
The cache enables:
Fast startup - No need to connect all servers at launch
Offline search - Search and describe tools without active connections
Direct tool registration - Register individual tools from cache at startup
Cache entries expire after 7 days by default:
const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000
Component Interaction Flow
Initialization
Load config from ~/.pi/agent/mcp.json
Load metadata cache from ~/.pi/agent/mcp-cache.json
Register servers with lifecycle manager
Hydrate tool metadata from cache (if valid)
Connect eager/keep-alive servers in parallel (max 10 concurrent)
Start lifecycle health checks
Tool Call
LLM calls mcp({ tool: "name", args: '{...}' })
Adapter finds tool in cached metadata
Check if server is connected via server manager
If not connected, lazy connect (if not in backoff)
Server manager increments inFlight counter
Call MCP server via client.callTool()
Transform MCP content to Pi format
Server manager decrements inFlight, updates lastUsedAt
Idle Shutdown
Health check runs every 30 seconds
For non-keep-alive servers, check if idle
Compare Date.now() - lastUsedAt against timeout
Verify inFlight === 0 (no active requests)
Close connection via server manager
Invoke onIdleShutdown callback
Reconnection
Health check detects keep-alive server disconnected
Server manager attempts connection
Fetch fresh tools and resources from server
Update in-memory tool metadata
Write updated cache entry to disk
Invoke onReconnect callback
npx Binary Resolution
For servers using "command": "npx", the adapter resolves the actual binary path to avoid spawning the ~143 MB npm parent process:
if ( command === "npx" || command === "npm" ) {
const resolved = await resolveNpxBinary ( command , args )
if ( resolved ) {
command = resolved . isJs ? "node" : resolved . binPath
args = resolved . isJs ? [ resolved . binPath , ... resolved . extraArgs ] : resolved . extraArgs
console . log ( `MCP: ${ name } resolved to ${ resolved . binPath } (skipping npm parent)` )
}
}
This optimization significantly reduces memory usage when running multiple MCP servers.
While the proxy pattern is the default, you can configure specific tools to register as first-class Pi tools:
{
"mcpServers" : {
"github" : {
"command" : "npx" ,
"args" : [ "-y" , "@modelcontextprotocol/server-github" ],
"directTools" : [ "search_repositories" , "get_file_contents" ]
}
}
}
Direct tools are registered from the cache at startup, so no server connection is required:
for ( const spec of directSpecs ) {
pi . registerTool ({
name: spec . prefixedName ,
label: `MCP: ${ spec . originalName } ` ,
description: spec . description || "(no description)" ,
parameters: Type . Unsafe < Record < string , unknown >>(
spec . inputSchema || { type: "object" , properties: {} }
),
async execute ( _toolCallId , params ) {
// Lazy connect if needed, then call tool
},
})
}
Each direct tool costs ~150-300 tokens. Good for targeted sets of 5-20 tools. For servers with 75+ tools, stick with the proxy.