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 provides three lifecycle modes that control when servers connect, how long they stay connected, and whether they auto-reconnect. Each mode is designed for different use cases.
Lifecycle Modes
Lazy Mode (Default)
Behavior:
Don’t connect at startup
Connect on first tool call
Disconnect after idle timeout (default: 10 minutes)
Reconnect automatically on next tool call
Configuration:
{
"mcpServers" : {
"my-server" : {
"command" : "npx" ,
"args" : [ "-y" , "some-mcp-server" ],
"lifecycle" : "lazy" // This is the default
}
}
}
When to use:
Servers you use occasionally
Servers with high startup cost that you want to keep idle
Servers you don’t need immediately at session start
Implementation:
When a tool is called on a lazy server:
const connected = await lazyConnect ( state , serverName )
if ( ! connected ) {
const failedAgo = getFailureAgeSeconds ( state , serverName )
return {
content: [{
type: "text" ,
text: `Server " ${ serverName } " not available ${ failedAgo !== null ? ` (failed ${ failedAgo } s ago)` : "" } `
}],
details: { error: "server_unavailable" , server: serverName },
}
}
The lazyConnect function checks if the server is in failure backoff (default: 60 seconds) before attempting connection.
Lazy servers rely on the metadata cache for search/list/describe operations. The cache enables these features to work even when the server isn’t connected.
Eager Mode
Behavior:
Connect at startup (during session initialization)
Don’t auto-reconnect if connection drops
No idle timeout by default (set idleTimeout explicitly to enable)
Useful for getting metadata into cache quickly
Configuration:
{
"mcpServers" : {
"my-server" : {
"command" : "npx" ,
"args" : [ "-y" , "some-mcp-server" ],
"lifecycle" : "eager" ,
"idleTimeout" : 5 // Optional: disconnect after 5 minutes of inactivity
}
}
}
When to use:
You want tools available immediately at session start
You don’t need the server to stay connected permanently
You want to populate the metadata cache on first run
You’re okay with manual reconnection if the server crashes
Implementation:
During initialization, eager servers are included in the startup connection pool:
const startupServers = bootstrapAll
? serverEntries
: serverEntries . filter (([, definition ]) => {
const mode = definition . lifecycle ?? "lazy"
return mode === "keep-alive" || mode === "eager"
})
// Connect selected servers in parallel (max 10 concurrent)
const results = await parallelLimit ( startupServers , 10 , async ([ name , definition ]) => {
try {
const connection = await manager . connect ( name , definition )
return { name , definition , connection , error: null }
} catch ( error ) {
const message = error instanceof Error ? error . message : String ( error )
return { name , definition , connection: null , error: message }
}
})
Eager mode sets idleTimeout to 0 by default (no auto-disconnect). This differs from lazy mode, which uses the global 10-minute timeout. Set idleTimeout explicitly if you want eager servers to disconnect when idle.
Keep-Alive Mode
Behavior:
Connect at startup
Auto-reconnect via health checks (every 30 seconds)
Never disconnect due to idle timeout
Best for servers you always need available
Configuration:
{
"mcpServers" : {
"my-server" : {
"command" : "npx" ,
"args" : [ "-y" , "critical-mcp-server" ],
"lifecycle" : "keep-alive"
}
}
}
When to use:
Critical servers that should always be available
Servers where connection time is expensive
Servers you use frequently throughout the session
Databases, browsers, or stateful services
Implementation:
Keep-alive servers are registered in a separate tracking map:
markKeepAlive ( name : string , definition : ServerDefinition ): void {
this . keepAliveServers . set ( name , definition )
}
The health check runs every 30 seconds and attempts reconnection:
private async checkConnections (): Promise < void > {
// Reconnect keep-alive servers
for ( const [ name , definition ] of this.keepAliveServers) {
const connection = this . manager . getConnection ( name )
if ( ! connection || connection . status !== "connected" ) {
try {
await this . manager . connect ( name , definition )
console . log ( `MCP: Reconnected to ${ name } ` )
this . onReconnect ?.( name ) // Update metadata cache
} catch ( error ) {
console . error ( `MCP: Failed to reconnect to ${ name } :` , error )
}
}
}
// Shutdown idle non-keep-alive servers
for ( const [ name ] of this.allServers) {
if ( this . keepAliveServers . has ( name )) continue // Skip keep-alive
const timeout = this . getIdleTimeout ( name )
if ( timeout > 0 && this . manager . isIdle ( name , timeout )) {
await this . manager . close ( name )
this . onIdleShutdown ?.( name )
}
}
}
Keep-alive servers are never subject to idle timeout, even if you set idleTimeout in the config. The setting is ignored for keep-alive mode.
Idle Timeout Configuration
You can configure idle timeouts globally or per-server:
Global Timeout
{
"settings" : {
"idleTimeout" : 10 // Minutes (default)
},
"mcpServers" : { }
}
Per-Server Override
{
"mcpServers" : {
"fast-server" : {
"command" : "node" ,
"args" : [ "./server.js" ],
"lifecycle" : "lazy" ,
"idleTimeout" : 2 // Disconnect after 2 minutes
},
"slow-server" : {
"command" : "npx" ,
"args" : [ "-y" , "heavy-mcp-server" ],
"lifecycle" : "lazy" ,
"idleTimeout" : 30 // Keep alive for 30 minutes
}
}
}
Per-server idleTimeout always overrides the global setting.
How Idle Detection Works
A server is considered idle when:
No active requests - inFlight === 0
Enough time has passed - Date.now() - lastUsedAt > timeoutMs
isIdle ( name : string , timeoutMs : number ): boolean {
const connection = this . connections . get ( name )
if ( ! connection || connection . status !== "connected" ) return false
if ( connection . inFlight && connection . inFlight > 0 ) return false
return ( Date . now () - connection . lastUsedAt ) > timeoutMs
}
Every tool call updates the lastUsedAt timestamp:
try {
state . manager . touch ( serverName ) // Update lastUsedAt
state . manager . incrementInFlight ( serverName )
// Call the tool...
} finally {
state . manager . decrementInFlight ( serverName )
state . manager . touch ( serverName ) // Update again after completion
}
Setting idleTimeout: 0 disables idle disconnect for that server (but it’s not the same as keep-alive mode, which also adds auto-reconnect).
Lifecycle Mode Comparison
Feature Lazy Eager Keep-Alive Connect at startup ✗ ✓ ✓ Auto-reconnect ✗ ✗ ✓ Default idle timeout 10 min None (0) None (ignored) Cache-only operation ✓ ✓ ✗ Health checks ✗ ✗ ✓ Use case Occasional use Quick start Always available
Health Check Internals
The lifecycle manager starts health checks in the background:
startHealthChecks ( intervalMs = 30000 ): void {
this . healthCheckInterval = setInterval (() => {
this . checkConnections ()
}, intervalMs )
this . healthCheckInterval . unref () // Don't block process exit
}
The unref() call is important - it allows Pi to shut down gracefully even if the health check timer is still running.
Reconnect Callback
When a keep-alive server reconnects, the adapter needs to update its tool metadata:
lifecycle . setReconnectCallback (( serverName ) => {
updateServerMetadata ( state , serverName )
updateMetadataCache ( state , serverName )
state . failureTracker . delete ( serverName )
updateStatusBar ( state )
})
This ensures the cache stays in sync with the actual server state.
Choosing the Right Mode
Lazy Use for most servers. Connects on-demand, disconnects when idle. Minimal resource usage.
Eager Use when you want tools available immediately but don’t need persistent connection.
Keep-Alive Use for critical servers (database, browser, etc.) that should always be connected.
Best Practices
Start with lazy mode for all servers, then promote specific servers to eager/keep-alive based on usage patterns
Use keep-alive sparingly - each keep-alive server runs continuously, consuming memory and file descriptors
Set appropriate idle timeouts - short timeouts (2-5 min) for fast-starting servers, longer timeouts (30+ min) for slow-starting servers
Monitor health check logs - if you see frequent reconnection attempts, your server might be unstable
Use eager mode for first run - populate the metadata cache, then switch to lazy for subsequent sessions