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
The metadata cache enables Pi MCP Adapter to provide search, list, and describe functionality without active server connections . It also powers direct tool registration at startup, eliminating the need to connect all servers just to discover their tools.
Cache Location
Metadata is stored at:
~/.pi/agent/mcp-cache.json
The cache persists across Pi sessions and survives system reboots.
Cache Structure
interface MetadataCache {
version : number // Cache format version (currently 1)
servers : Record < string , ServerCacheEntry >
}
interface ServerCacheEntry {
configHash : string // SHA-256 hash of server config
tools : CachedTool [] // Tool definitions
resources : CachedResource [] // Resource definitions
cachedAt : number // Unix timestamp in milliseconds
}
interface CachedTool {
name : string
description ?: string
inputSchema ?: unknown // JSON Schema object
}
interface CachedResource {
uri : string // MCP resource URI
name : string
description ?: string
}
Example Cache Entry
{
"version" : 1 ,
"servers" : {
"chrome-devtools" : {
"configHash" : "a7f3e9c8b2d1f4e5c6a8b9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0" ,
"tools" : [
{
"name" : "navigate" ,
"description" : "Navigate to a URL" ,
"inputSchema" : {
"type" : "object" ,
"properties" : {
"url" : { "type" : "string" , "description" : "URL to navigate to" }
},
"required" : [ "url" ]
}
},
{
"name" : "take_screenshot" ,
"description" : "Take a screenshot of the page or element" ,
"inputSchema" : {
"type" : "object" ,
"properties" : {
"format" : { "type" : "string" , "enum" : [ "png" , "jpeg" , "webp" ], "default" : "png" },
"fullPage" : { "type" : "boolean" , "description" : "Full page instead of viewport" }
}
}
}
],
"resources" : [],
"cachedAt" : 1709500800000
}
}
}
Cache Validation
The cache is only used if it’s valid for a given server. Validation checks three criteria:
1. Config Hash Match
The adapter computes a SHA-256 hash of server configuration fields that affect tool output:
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.
If the config hash doesn’t match, the cache entry is considered stale.
2. Timestamp Check
Cache entries expire after 7 days by default:
const CACHE_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000 // 7 days
export function isServerCacheValid (
entry : ServerCacheEntry ,
definition : ServerEntry ,
maxAgeMs : number = CACHE_MAX_AGE_MS
) : boolean {
if ( ! entry || entry . configHash !== computeServerHash ( definition )) return false
if ( ! entry . cachedAt || typeof entry . cachedAt !== "number" ) return false
if ( maxAgeMs > 0 && Date . now () - entry . cachedAt > maxAgeMs ) return false
return true
}
Old cache entries are ignored, forcing a fresh connection to get updated metadata.
3. Structure Check
The cache loader validates the JSON structure:
export function loadMetadataCache () : MetadataCache | null {
if ( ! existsSync ( CACHE_PATH )) return null
try {
const raw = JSON . parse ( readFileSync ( CACHE_PATH , "utf-8" ))
if ( ! raw || typeof raw !== "object" ) return null
if ( raw . version !== CACHE_VERSION ) return null
if ( ! raw . servers || typeof raw . servers !== "object" ) return null
return raw as MetadataCache
} catch {
return null
}
}
Corrupted or incompatible cache files are rejected.
Cache Population
The cache is updated whenever a server connects successfully:
function updateMetadataCache ( state : McpExtensionState , serverName : string ) : void {
const connection = state . manager . getConnection ( serverName )
if ( ! connection || connection . status !== "connected" ) return
const definition = state . config . mcpServers [ serverName ]
if ( ! definition ) return
const entry : ServerCacheEntry = {
configHash: computeServerHash ( definition ),
tools: serializeTools ( connection . tools ),
resources: serializeResources ( connection . resources ),
cachedAt: Date . now (),
}
saveMetadataCache ({ version: 1 , servers: { [serverName]: entry } })
}
Serialization
The adapter serializes MCP tools and resources into a minimal format:
export function serializeTools ( tools : McpTool []) : CachedTool [] {
return tools
. filter ( t => t ?. name ) // Skip invalid tools
. map ( t => ({
name: t . name ,
description: t . description ,
inputSchema: t . inputSchema ,
}))
}
export function serializeResources ( resources : McpResource []) : CachedResource [] {
return resources
. filter ( r => r ?. name && r ?. uri ) // Skip invalid resources
. map ( r => ({
uri: r . uri ,
name: r . name ,
description: r . description ,
}))
}
Only essential fields are stored to minimize cache size.
Cache Reconstruction
When loading from cache, the adapter reconstructs ToolMetadata objects:
export function reconstructToolMetadata (
serverName : string ,
entry : ServerCacheEntry ,
prefix : "server" | "none" | "short" ,
exposeResources ?: boolean
) : ToolMetadata [] {
const metadata : ToolMetadata [] = []
// Reconstruct tools
for ( const tool of entry . tools ?? []) {
if ( ! tool ?. name ) continue
metadata . push ({
name: formatToolName ( tool . name , serverName , prefix ),
originalName: tool . name ,
description: tool . description ?? "" ,
inputSchema: tool . inputSchema ,
})
}
// Reconstruct resources as tools (if enabled)
if ( exposeResources !== false ) {
for ( const resource of entry . resources ?? []) {
if ( ! resource ?. name || ! resource ?. uri ) continue
const baseName = `get_ ${ resourceNameToToolName ( resource . name ) } `
metadata . push ({
name: formatToolName ( baseName , serverName , prefix ),
originalName: baseName ,
description: resource . description ?? `Read resource: ${ resource . uri } ` ,
resourceUri: resource . uri ,
})
}
}
return metadata
}
Tool names are formatted according to the configured prefix mode (server, short, or none).
Cache-Powered Features
1. Offline Search
Search works without active connections:
mcp ({ search: "screenshot" })
The adapter searches through cached metadata:
for ( const [ serverName , metadata ] of state . toolMetadata . entries ()) {
if ( server && serverName !== server ) continue
for ( const tool of metadata ) {
if ( pattern . test ( tool . name ) || pattern . test ( tool . description )) {
matches . push ({ server: serverName , tool })
}
}
}
2. Offline Describe
Tool descriptions and schemas come from cache:
mcp ({ describe: "chrome_devtools_take_screenshot" })
No server connection required - the adapter reads from state.toolMetadata.
Direct tools register from cache at startup:
const directSpecs = resolveDirectTools (
earlyConfig ,
earlyCache , // Cache loaded before server connections
prefix ,
envRaw ?. split ( "," ). map ( s => s . trim ()). filter ( Boolean ),
)
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 when actually called
},
})
}
On the first session after configuring a new server, the cache won’t exist yet. Direct tools fall back to proxy-only mode. After the first connection (automatic for eager/keep-alive, manual for lazy), the cache is populated and direct tools become available on restart.
Cache Write Strategy
The adapter uses atomic writes to prevent corruption:
export function saveMetadataCache ( cache : MetadataCache ) : void {
const dir = dirname ( CACHE_PATH )
mkdirSync ( dir , { recursive: true })
// Merge with existing cache (other servers)
let merged : MetadataCache = { version: CACHE_VERSION , servers: {} }
try {
if ( existsSync ( CACHE_PATH )) {
const existing = JSON . parse ( readFileSync ( CACHE_PATH , "utf-8" )) as MetadataCache
if ( existing && existing . version === CACHE_VERSION && existing . servers ) {
merged . servers = { ... existing . servers }
}
}
} catch {
// Ignore parse errors and proceed with empty cache
}
merged . version = CACHE_VERSION
merged . servers = { ... merged . servers , ... cache . servers }
// Atomic write: tmp file + rename
const tmpPath = ` ${ CACHE_PATH } . ${ process . pid } .tmp`
writeFileSync ( tmpPath , JSON . stringify ( merged , null , 2 ), "utf-8" )
renameSync ( tmpPath , CACHE_PATH )
}
The two-step write (temp file + rename) ensures the cache is never left in a partially-written state.
Bootstrap Behavior
On the very first run (no cache file exists), the adapter connects to all servers to populate the cache:
const cachePath = getMetadataCachePath ()
const cacheFileExists = existsSync ( cachePath )
let cache = loadMetadataCache ()
let bootstrapAll = false
if ( ! cacheFileExists ) {
bootstrapAll = true
saveMetadataCache ({ version: 1 , servers: {} }) // Create empty cache
} else if ( ! cache ) {
cache = { version: 1 , servers: {} }
saveMetadataCache ( cache )
}
const startupServers = bootstrapAll
? serverEntries // Connect ALL servers on first run
: serverEntries . filter (([, definition ]) => {
const mode = definition . lifecycle ?? "lazy"
return mode === "keep-alive" || mode === "eager"
})
After the first session, lazy servers no longer connect at startup - they rely on cache.
Cache Invalidation
The cache is automatically invalidated when:
Config changes - Any change to command, args, env, url, etc.
Cache expires - Entries older than 7 days
Manual reconnect - Using /mcp reconnect or mcp({ connect: "..." })
You can force a refresh by reconnecting:
mcp ({ connect: "server-name" }) // Refreshes cache
Or via command:
/mcp reconnect server-name
Fast Startup No need to connect all servers at launch. Cached metadata is available instantly.
Offline Operation Search, list, and describe work without active connections.
Low Memory Only connect servers when actually needed. Lazy servers stay idle.
Direct Tools Register individual tools from cache without spawning server processes.
Best Practices
Let the cache warm up naturally - Don’t force-connect all servers just to populate cache. Eager/keep-alive servers will handle it.
Use eager mode for cache population - If you want to pre-populate cache for a lazy server, temporarily set it to eager, restart Pi, then switch back to lazy.
Monitor cache size - Large servers (100+ tools) create large cache entries. The cache file is typically under 100 KB for most setups.
Don’t edit the cache manually - The adapter validates cache structure and hashes. Manual edits will likely be rejected.
Understand bootstrap behavior - The first run connects all servers. Subsequent runs respect lifecycle modes.