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
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ function McpInputWithTags({
const [cursorPosition, setCursorPosition] = useState(0)
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
const inputRef = useRef<HTMLInputElement>(null)
const inputNameRef = useRef(`mcp_input_${Math.random()}`)

const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value
Expand Down Expand Up @@ -104,11 +105,19 @@ function McpInputWithTags({
onDragOver={handleDragOver}
placeholder={placeholder}
disabled={disabled}
name={inputNameRef.current}
autoComplete='off'
autoCapitalize='off'
spellCheck='false'
data-form-type='other'
data-lpignore='true'
data-1p-ignore
readOnly
onFocus={(e) => e.currentTarget.removeAttribute('readOnly')}
className={cn(!isPassword && 'text-transparent caret-foreground')}
/>
{!isPassword && (
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-3 text-sm'>
<div className='pointer-events-none absolute inset-0 flex items-center overflow-hidden bg-transparent px-[8px] py-[6px] font-medium font-sans text-sm'>
<div className='whitespace-pre'>
{formatDisplayText(value?.toString() || '', {
accessiblePrefixes,
Expand Down Expand Up @@ -157,6 +166,7 @@ function McpTextareaWithTags({
const [cursorPosition, setCursorPosition] = useState(0)
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
const textareaRef = useRef<HTMLTextAreaElement>(null)
const textareaNameRef = useRef(`mcp_textarea_${Math.random()}`)

const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const newValue = e.target.value
Expand Down Expand Up @@ -220,9 +230,16 @@ function McpTextareaWithTags({
placeholder={placeholder}
disabled={disabled}
rows={rows}
name={textareaNameRef.current}
autoComplete='off'
autoCapitalize='off'
spellCheck='false'
data-form-type='other'
data-lpignore='true'
data-1p-ignore
className={cn('min-h-[80px] resize-none text-transparent caret-foreground')}
/>
<div className='pointer-events-none absolute inset-0 overflow-auto whitespace-pre-wrap break-words p-3 text-sm'>
<div className='pointer-events-none absolute inset-0 overflow-auto whitespace-pre-wrap break-words px-[8px] py-[8px] font-medium font-sans text-sm'>
{formatDisplayText(value || '', {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
Expand Down Expand Up @@ -298,6 +315,17 @@ export function McpDynamicArgs({
if (disabled) return

const current = currentArgs()

if (value === '' && (current[paramName] === undefined || current[paramName] === null)) {
return
}

if (value === '') {
const { [paramName]: _, ...rest } = current
setToolArgs(Object.keys(rest).length > 0 ? rest : {})
return
}

const updated = { ...current, [paramName]: value }
setToolArgs(updated)
},
Expand Down Expand Up @@ -509,7 +537,32 @@ export function McpDynamicArgs({
}

return (
<div className='space-y-4'>
<div className='relative space-y-4'>
{/* Hidden dummy inputs to prevent browser password manager autofill */}
<input
type='text'
name='fakeusernameremembered'
autoComplete='username'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
<input
type='password'
name='fakepasswordremembered'
autoComplete='current-password'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
<input
type='email'
name='fakeemailremembered'
autoComplete='email'
style={{ position: 'absolute', left: '-9999px', opacity: 0, pointerEvents: 'none' }}
tabIndex={-1}
readOnly
/>
{toolSchema.properties &&
Object.entries(toolSchema.properties).map(([paramName, paramSchema]) => {
const inputType = getInputType(paramSchema as any)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2107,7 +2107,10 @@ export function ToolInput({
<ShortInput
blockId={blockId}
subBlockId={`${subBlockId}-tool-${toolIndex}-${param.id}`}
placeholder={param.description}
placeholder={
param.description ||
`Enter ${formatParameterLabel(param.id).toLowerCase()}`
}
password={isPasswordParameter(param.id)}
config={{
id: `${subBlockId}-tool-${toolIndex}-${param.id}`,
Expand Down
8 changes: 5 additions & 3 deletions apps/sim/hooks/queries/mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,14 @@ export function useCreateMcpServer() {
}

logger.info(`Created MCP server: ${config.name} in workspace: ${workspaceId}`)
return { ...serverData, connectionStatus: 'disconnected' as const }
return {
...serverData,
connectionStatus: 'disconnected' as const,
serverId: data.data?.serverId,
}
},
onSuccess: (_data, variables) => {
// Invalidate servers list to refetch
queryClient.invalidateQueries({ queryKey: mcpKeys.servers(variables.workspaceId) })
// Invalidate tools as new server may provide new tools
queryClient.invalidateQueries({ queryKey: mcpKeys.tools(variables.workspaceId) })
},
})
Expand Down
13 changes: 12 additions & 1 deletion apps/sim/lib/mcp/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ export class McpClient {
'2024-11-05', // Initial stable release
]

constructor(config: McpServerConfig, securityPolicy?: McpSecurityPolicy) {
/**
* Creates a new MCP client
* @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) {
this.config = config
this.connectionStatus = { connected: false }
this.securityPolicy = securityPolicy ?? {
Expand All @@ -59,6 +65,7 @@ export class McpClient {
requestInit: {
headers: this.config.headers,
},
sessionId,
})

this.client = new Client(
Expand Down Expand Up @@ -255,6 +262,10 @@ export class McpClient {
return typeof serverVersion === 'string' ? serverVersion : undefined
}

getSessionId(): string | undefined {
return this.transport.sessionId
}

/**
* Request user consent for tool execution
*/
Expand Down
142 changes: 97 additions & 45 deletions apps/sim/lib/mcp/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,35 @@ class McpService {
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 @@ -306,7 +331,7 @@ class McpService {
}

/**
* Create and connect to an MCP client with security policy
* Create and connect to an MCP client
*/
private async createClient(config: McpServerConfig): Promise<McpClient> {
const securityPolicy = {
Expand All @@ -316,9 +341,49 @@ class McpService {
allowedOrigins: config.url ? [new URL(config.url).origin] : undefined,
}

const client = new McpClient(config, securityPolicy)
await client.connect()
return client
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
}

/**
Expand All @@ -332,33 +397,25 @@ class McpService {
): Promise<McpToolResult> {
const requestId = generateRequestId()

try {
logger.info(
`[${requestId}] Executing MCP tool ${toolCall.name} on server ${serverId} for user ${userId}`
)
logger.info(
`[${requestId}] Executing MCP tool ${toolCall.name} on server ${serverId} for user ${userId}`
)

const config = await this.getServerConfig(serverId, workspaceId)
if (!config) {
throw new Error(`Server ${serverId} not found or not accessible`)
}
const config = await this.getServerConfig(serverId, workspaceId)
if (!config) {
throw new Error(`Server ${serverId} not found or not accessible`)
}

const resolvedConfig = await this.resolveConfigEnvVars(config, userId, workspaceId)
const resolvedConfig = await this.resolveConfigEnvVars(config, userId, workspaceId)

const client = await this.createClient(resolvedConfig)
const client = await this.createClient(resolvedConfig)

try {
const result = await client.callTool(toolCall)
logger.info(`[${requestId}] Successfully executed tool ${toolCall.name}`)
return result
} finally {
await client.disconnect()
}
} catch (error) {
logger.error(
`[${requestId}] Failed to execute tool ${toolCall.name} on server ${serverId}:`,
error
)
throw error
try {
const result = await client.callTool(toolCall)
logger.info(`[${requestId}] Successfully executed tool ${toolCall.name}`)
return result
} finally {
await client.disconnect()
}
}

Expand Down Expand Up @@ -442,28 +499,23 @@ class McpService {
): Promise<McpTool[]> {
const requestId = generateRequestId()

try {
logger.info(`[${requestId}] Discovering tools from server ${serverId} for user ${userId}`)
logger.info(`[${requestId}] Discovering tools from server ${serverId} for user ${userId}`)

const config = await this.getServerConfig(serverId, workspaceId)
if (!config) {
throw new Error(`Server ${serverId} not found or not accessible`)
}
const config = await this.getServerConfig(serverId, workspaceId)
if (!config) {
throw new Error(`Server ${serverId} not found or not accessible`)
}

const resolvedConfig = await this.resolveConfigEnvVars(config, userId, workspaceId)
const resolvedConfig = await this.resolveConfigEnvVars(config, userId, workspaceId)

const client = await this.createClient(resolvedConfig)
const client = await this.createClient(resolvedConfig)

try {
const tools = await client.listTools()
logger.info(`[${requestId}] Discovered ${tools.length} tools from server ${config.name}`)
return tools
} finally {
await client.disconnect()
}
} catch (error) {
logger.error(`[${requestId}] Failed to discover tools from server ${serverId}:`, error)
throw error
try {
const tools = await client.listTools()
logger.info(`[${requestId}] Discovered ${tools.length} tools from server ${config.name}`)
return tools
} finally {
await client.disconnect()
}
}

Expand Down