Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions apps/sim/lib/mcp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,14 @@ export class McpClient {

/**
* Creates a new MCP client
*
* No session ID parameter (we disconnect after each operation).
* The SDK handles session management automatically via Mcp-Session-Id header.
*
* @param config - Server configuration
* @param securityPolicy - Optional security policy
* @param sessionId - Optional session ID for session restoration (from previous connection)
*/
constructor(config: McpServerConfig, securityPolicy?: McpSecurityPolicy, sessionId?: string) {
constructor(config: McpServerConfig, securityPolicy?: McpSecurityPolicy) {
this.config = config
this.connectionStatus = { connected: false }
this.securityPolicy = securityPolicy ?? {
Expand All @@ -65,7 +68,6 @@ export class McpClient {
requestInit: {
headers: this.config.headers,
},
sessionId,
})

this.client = new Client(
Expand Down
87 changes: 15 additions & 72 deletions apps/sim/lib/mcp/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,42 +42,17 @@ interface CacheStats {

class McpService {
private toolCache = new Map<string, ToolCache>()
private readonly cacheTimeout = MCP_CONSTANTS.CACHE_TIMEOUT
private readonly maxCacheSize = 1000
private readonly cacheTimeout = MCP_CONSTANTS.CACHE_TIMEOUT // 30 seconds
private readonly maxCacheSize = MCP_CONSTANTS.MAX_CACHE_SIZE // 1000
private cleanupInterval: NodeJS.Timeout | null = null
private cacheHits = 0
private cacheMisses = 0
private entriesEvicted = 0

private sessionCache = new Map<string, string>()

constructor() {
this.startPeriodicCleanup()
}

/**
* Get cached session ID for a server
*/
private getCachedSessionId(serverId: string): string | undefined {
return this.sessionCache.get(serverId)
}

/**
* Cache session ID for a server
*/
private cacheSessionId(serverId: string, sessionId: string): void {
this.sessionCache.set(serverId, sessionId)
logger.debug(`Cached session ID for server ${serverId}`)
}

/**
* Clear cached session ID for a server
*/
private clearCachedSessionId(serverId: string): void {
this.sessionCache.delete(serverId)
logger.debug(`Cleared cached session ID for server ${serverId}`)
}

/**
* Start periodic cleanup of expired cache entries
*/
Expand Down Expand Up @@ -341,49 +316,9 @@ class McpService {
allowedOrigins: config.url ? [new URL(config.url).origin] : undefined,
}

const cachedSessionId = this.getCachedSessionId(config.id)

const client = new McpClient(config, securityPolicy, cachedSessionId)

try {
await client.connect()

const newSessionId = client.getSessionId()
if (newSessionId) {
this.cacheSessionId(config.id, newSessionId)
}

return client
} catch (error) {
if (cachedSessionId && this.isSessionError(error)) {
logger.debug(`Session restoration failed for server ${config.id}, retrying fresh`)
this.clearCachedSessionId(config.id)

const freshClient = new McpClient(config, securityPolicy)
await freshClient.connect()

const freshSessionId = freshClient.getSessionId()
if (freshSessionId) {
this.cacheSessionId(config.id, freshSessionId)
}

return freshClient
}

throw error
}
}

private isSessionError(error: unknown): boolean {
if (error instanceof Error) {
const message = error.message.toLowerCase()
return (
message.includes('no valid session') ||
message.includes('invalid session') ||
message.includes('session expired')
)
}
return false
const client = new McpClient(config, securityPolicy)
await client.connect()
return client
}

/**
Expand Down Expand Up @@ -466,21 +401,29 @@ class McpService {
})
)

let failedCount = 0
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
allTools.push(...result.value)
} else {
failedCount++
logger.warn(
`[${requestId}] Failed to discover tools from server ${servers[index].name}:`,
result.reason
)
}
})

this.setCacheEntry(cacheKey, allTools)
if (failedCount === 0) {
this.setCacheEntry(cacheKey, allTools)
} else {
logger.warn(
`[${requestId}] Skipping cache due to ${failedCount} failed server(s) - will retry on next request`
)
}

logger.info(
`[${requestId}] Discovered ${allTools.length} tools from ${servers.length} servers`
`[${requestId}] Discovered ${allTools.length} tools from ${servers.length - failedCount}/${servers.length} servers`
)
return allTools
} catch (error) {
Expand Down
3 changes: 2 additions & 1 deletion apps/sim/lib/mcp/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@ import type { McpApiResponse } from '@/lib/mcp/types'
*/
export const MCP_CONSTANTS = {
EXECUTION_TIMEOUT: 60000,
CACHE_TIMEOUT: 5 * 60 * 1000,
CACHE_TIMEOUT: 30 * 1000,
DEFAULT_RETRIES: 3,
DEFAULT_CONNECTION_TIMEOUT: 30000,
MAX_CACHE_SIZE: 1000,
} as const

/**
Expand Down