diff --git a/packages/components/credentials/AnswerAgentApi.credential.ts b/packages/components/credentials/AnswerAgentApi.credential.ts new file mode 100644 index 00000000000..497dac5811d --- /dev/null +++ b/packages/components/credentials/AnswerAgentApi.credential.ts @@ -0,0 +1,37 @@ +import { INodeParams, INodeCredential } from '../src/Interface' + +class AnswerAgentApi implements INodeCredential { + label: string + name: string + version: number + description: string + inputs: INodeParams[] + + constructor() { + this.label = 'AnswerAgent API' + this.name = 'answerAgentApi' + this.version = 1.0 + this.description = + 'You can find your API key in API Keys settings' + this.inputs = [ + { + label: 'API Key', + name: 'apiKey', + type: 'password', + placeholder: 'your-api-key' + }, + { + label: 'Instance Domain', + name: 'instanceDomain', + type: 'string', + optional: true, + default: process.env.API_HOST || process.env.FLOWISE_DOMAIN || '', + placeholder: 'https://your-instance.answerai.com', + description: + 'The domain of the AnswerAgent instance to connect to. Change this to connect to a different AnswerAgent instance.' + } + ] + } +} + +module.exports = { credClass: AnswerAgentApi } diff --git a/packages/components/nodes/tools/MCP/AnswerAgent/AnswerAgentMCP.ts b/packages/components/nodes/tools/MCP/AnswerAgent/AnswerAgentMCP.ts index e81e602a0ec..8a709360ed6 100644 --- a/packages/components/nodes/tools/MCP/AnswerAgent/AnswerAgentMCP.ts +++ b/packages/components/nodes/tools/MCP/AnswerAgent/AnswerAgentMCP.ts @@ -7,8 +7,8 @@ * - Implements INode interface * - Sets tags = ['AAI'] for UI Answer tab integration * - Sets category = 'Tools (MCP)' - * - Automatically uses user's API key from database (no manual credential setup required) - * - Uses API_HOST environment variable for base URL + * - Uses standard Flowise credential pattern (answerAgentApi) + * - Supports connecting to any AnswerAgent instance via credential domain config * - Exposes available actions via mcpActions input * - Registers the node as module.exports = { nodeClass: AnswerAgent_MCP } * @@ -17,7 +17,7 @@ */ import { Tool } from '@langchain/core/tools' import { ICommonObject, INode, INodeData, INodeOptionsValue, INodeParams } from '../../../../src/Interface' -import { getNodeModulesPackagePath } from '../../../../src/utils' +import { getCredentialData, getCredentialParam, getNodeModulesPackagePath } from '../../../../src/utils' import { MCPToolkit } from '../core' class AnswerAgent_MCP implements INode { @@ -30,19 +30,26 @@ class AnswerAgent_MCP implements INode { category: string baseClasses: string[] documentation: string + credential: INodeParams inputs: INodeParams[] tags: string[] constructor() { this.label = 'AnswerAgent MCP' this.name = 'answerAgentMCP' - this.version = 1.0 + this.version = 2.0 this.type = 'AnswerAgent MCP Tool' this.icon = 'answerai-square-black.png' this.category = 'Tools (MCP)' this.tags = ['AAI'] - this.description = 'MCP server that integrates with AnswerAgent API • Zero configuration required' + this.description = 'MCP server that integrates with AnswerAgent API' this.documentation = 'https://www.npmjs.com/package/@answerai/answeragent-mcp' + this.credential = { + label: 'Connect Credential', + name: 'credential', + type: 'credential', + credentialNames: ['answerAgentApi'] + } this.inputs = [ { label: 'Available Actions', @@ -100,17 +107,20 @@ class AnswerAgent_MCP implements INode { } async getTools(nodeData: INodeData, options: ICommonObject): Promise { - // Get API host from environment variable - const apiHost = options.user?.chatflowDomain - if (!apiHost) { - console.error('AnswerAgent MCP: API_HOST environment variable is not set') + // Get API key and optional instance domain from credential + const credentialData = await getCredentialData(nodeData.credential ?? '', options) + const apiKey = getCredentialParam('apiKey', credentialData, nodeData) + const instanceDomain = getCredentialParam('instanceDomain', credentialData, nodeData) + + if (!apiKey) { + console.error('AnswerAgent MCP: Missing AnswerAgent API Key') return [] } - // Get user's API key from database - const apiKey = await this.getUserApiKey(nodeData, options) - if (!apiKey) { - console.error('AnswerAgent MCP: Unable to retrieve user API key from database') + // Use instance domain from credential, fall back to current instance domain or env var + const apiHost = instanceDomain || options.user?.chatflowDomain || process.env.API_HOST || process.env.FLOWISE_DOMAIN + if (!apiHost) { + console.error('AnswerAgent MCP: No instance domain configured and API_HOST is not set') return [] } @@ -121,7 +131,7 @@ class AnswerAgent_MCP implements INode { command: process.execPath, args: [packagePath], env: { - ANSWERAGENT_AI_API_HOST: apiHost, + ANSWERAGENT_AI_API_BASE_URL: apiHost, ANSWERAGENT_AI_API_TOKEN: apiKey } } @@ -134,72 +144,6 @@ class AnswerAgent_MCP implements INode { return tools } - - /** - * Retrieve the user's API key from the database - * This automatically uses the user's default API key, eliminating the need for manual credential setup - */ - private async getUserApiKey(nodeData: INodeData, options: ICommonObject): Promise { - try { - // Get database connection from options - const appDataSource = options.appDataSource - if (!appDataSource) { - console.error('No database connection available') - return null - } - - // Get user ID - handle both contexts: - // 1. Load method context: options.userId - // 2. Workflow execution context: options.user.id - const userId = options.userId || options.user?.id - if (!userId) { - console.error('No user ID available in options') - return null - } - - // Get organization ID - handle both contexts: - // 1. Load method context: options.organizationId - // 2. Workflow execution context: options.user.organizationId - const organizationId = options.organizationId || options.user?.organizationId - if (!organizationId) { - console.error('No organization ID available') - return null - } - - // Fallback to ApiKey table - const databaseEntities = options.databaseEntities - if (!databaseEntities || !databaseEntities['ApiKey']) { - console.error('ApiKey entity not available in databaseEntities') - return null - } - - // Query the database for the user's API keys using the entity from databaseEntities - - const apiKeys = await appDataSource.getRepository(databaseEntities['ApiKey']).find({ - where: { - userId: userId, - organizationId: organizationId, - isActive: true - }, - order: { - updatedDate: 'DESC' // Get the most recently updated key - } - }) - - if (apiKeys.length === 0) { - console.error('No active API keys found for user') - return null - } - - // Return the first (most recent) API key - const selectedApiKey = apiKeys[0].apiKey - - return selectedApiKey - } catch (error) { - console.error('Error retrieving user API key:', error) - return null - } - } } module.exports = { nodeClass: AnswerAgent_MCP } diff --git a/packages/ui/src/components/SidekickSetupModal.jsx b/packages/ui/src/components/SidekickSetupModal.jsx index d5422efe464..be54519e874 100644 --- a/packages/ui/src/components/SidekickSetupModal.jsx +++ b/packages/ui/src/components/SidekickSetupModal.jsx @@ -1,5 +1,5 @@ 'use client' -import { useCallback, useState, useEffect } from 'react' +import { useCallback, useState, useEffect, useContext } from 'react' import { useSearchParams, useRouter } from 'next/navigation' import { useDispatch } from 'react-redux' import dynamic from 'next/dynamic' @@ -14,8 +14,10 @@ import useNotifier from '@/utils/useNotifier' // UI components and utilities import { IconX } from '@tabler/icons-react' +import { flowContext } from '@/store/context/ReactFlowContext' +import { FLOWISE_CREDENTIAL_ID } from '@/store/constant' import { useSidekickWithCredentials } from '@/hooks/useSidekickWithCredentials' -import { updateFlowDataWithCredentials } from '@/utils/flowCredentialsHelper' +import { extractAllCredentials, updateFlowDataWithCredentials } from '@/utils/flowCredentialsHelper' import { getCredentialModalDismissed, setCredentialModalDismissed } from '@/utils/credentialModalPreference' import useConfirm from '@/hooks/useConfirm' @@ -23,9 +25,25 @@ import useConfirm from '@/hooks/useConfirm' const UnifiedCredentialsModal = dynamic(() => import('@/ui-component/dialog/UnifiedCredentialsModal'), { ssr: false }) const ConfirmDialog = dynamic(() => import('@/ui-component/dialog/ConfirmDialog'), { ssr: false }) +const applyCredentialToNode = (node, credentialAssignments) => { + if (!credentialAssignments[node.id] || !node.data) return node + return { + ...node, + data: { + ...node.data, + credential: credentialAssignments[node.id], + inputs: { + ...node.data.inputs, + [FLOWISE_CREDENTIAL_ID]: credentialAssignments[node.id] + } + } + } +} + const SidekickSetupModal = ({ sidekickId, onComplete }) => { const { confirm } = useConfirm() const preferenceScope = sidekickId ? `sidekick:${sidekickId}` : null + const { reactFlowInstance } = useContext(flowContext) // Local state to track if user has skipped setup for this instance const [hasSkipped, setHasSkipped] = useState(false) @@ -106,12 +124,27 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { async (credentialAssignments, options) => { try { if (credentialAssignments && Object.keys(credentialAssignments).length > 0) { - const updatedFlowData = updateFlowDataWithCredentials(sidekick.flowData, credentialAssignments) - await updateSidekick({ - flowData: JSON.stringify(updatedFlowData) - }) - // Notify canvas and other listeners to refresh with updated credentials - window.dispatchEvent(new CustomEvent('credentials-updated', { detail: { chatflowId: sidekickId } })) + let flowDataForSave + if (reactFlowInstance) { + const rfObject = reactFlowInstance.toObject() + rfObject.nodes = rfObject.nodes.map((node) => applyCredentialToNode(node, credentialAssignments)) + flowDataForSave = JSON.stringify(rfObject) + } else { + const updatedFlowData = updateFlowDataWithCredentials(sidekick.flowData, credentialAssignments) + flowDataForSave = JSON.stringify(updatedFlowData) + } + await updateSidekick({ flowData: flowDataForSave }) + + if (reactFlowInstance) { + reactFlowInstance.setNodes((nodes) => nodes.map((node) => applyCredentialToNode(node, credentialAssignments))) + } + + // Skip refetch on canvas to preserve unsaved changes + window.dispatchEvent( + new CustomEvent('credentials-updated', { + detail: { chatflowId: sidekickId, skipRefetch: !!reactFlowInstance } + }) + ) enqueueSnackbar({ message: 'Credentials saved successfully!', options: { variant: 'success' } @@ -124,7 +157,16 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { handleModalError('Error assigning credentials. Please try again.') } }, - [sidekick, updateSidekick, enqueueSnackbar, handleURLCleanup, handleModalError, persistDismissPreference] + [ + sidekick, + reactFlowInstance, + updateSidekick, + enqueueSnackbar, + sidekickId, + handleURLCleanup, + handleModalError, + persistDismissPreference + ] ) const handleModalSkip = useCallback( @@ -168,12 +210,23 @@ const SidekickSetupModal = ({ sidekickId, onComplete }) => { return null } + // When canvas is open, use live nodes for credentials. Otherwise fall back to SWR data. + let effectiveCredentials = credentialsToShow + try { + if (reactFlowInstance) { + const { allCredentials } = extractAllCredentials(reactFlowInstance.toObject()) + if (allCredentials?.length) effectiveCredentials = allCredentials + } + } catch (error) { + console.error('[SidekickSetupModal] Failed to extract credentials from canvas:', error) + } + return ( <> (multiple ? [] : '') const addNewOption = [{ label: '- Create New -', name: '-create-' }] - let [internalValue, setInternalValue] = useState(value ?? 'choose an option') + const [internalValue, setInternalValue] = useState(value ?? 'choose an option') + + useEffect(() => { + setInternalValue(value ?? 'choose an option') + }, [value]) + const { reactFlowInstance } = useContext(flowContext) const fetchCredentialList = async () => { diff --git a/packages/ui/src/views/agentflowsv2/Canvas.jsx b/packages/ui/src/views/agentflowsv2/Canvas.jsx index 350b58c6fd7..11ef11b1ecc 100644 --- a/packages/ui/src/views/agentflowsv2/Canvas.jsx +++ b/packages/ui/src/views/agentflowsv2/Canvas.jsx @@ -679,7 +679,7 @@ const AgentflowCanvas = ({ chatflowid: chatflowId }) => { // Re-fetch canvas when credentials are updated via the setup modal useEffect(() => { const handleCredentialsUpdated = (e) => { - if (e.detail?.chatflowId === chatflowId) { + if (e.detail?.chatflowId === chatflowId && !e.detail?.skipRefetch) { getSpecificChatflowApi.request(chatflowId) } } diff --git a/packages/ui/src/views/canvas/CredentialInputHandler.jsx b/packages/ui/src/views/canvas/CredentialInputHandler.jsx index e713a22d1fa..700f1db46d8 100644 --- a/packages/ui/src/views/canvas/CredentialInputHandler.jsx +++ b/packages/ui/src/views/canvas/CredentialInputHandler.jsx @@ -95,6 +95,14 @@ const CredentialInputHandler = ({ inputParam, data, onSelect, disabled = false } setCredentialId(data?.credential || (data?.inputs && data.inputs[FLOWISE_CREDENTIAL_ID]) || '') }, [data]) + useEffect(() => { + const handleCredentialsUpdated = () => { + setReloadTimestamp(Date.now().toString()) + } + window.addEventListener('credentials-updated', handleCredentialsUpdated) + return () => window.removeEventListener('credentials-updated', handleCredentialsUpdated) + }, []) + return (
{inputParam && ( diff --git a/packages/ui/src/views/canvas/index.jsx b/packages/ui/src/views/canvas/index.jsx index eb648a65a35..c81bb2896a5 100644 --- a/packages/ui/src/views/canvas/index.jsx +++ b/packages/ui/src/views/canvas/index.jsx @@ -604,7 +604,7 @@ const Canvas = ({ chatflowid: chatflowId }) => { // Re-fetch canvas when credentials are updated via the setup modal useEffect(() => { const handleCredentialsUpdated = (e) => { - if (e.detail?.chatflowId === chatflowId) { + if (e.detail?.chatflowId === chatflowId && !e.detail?.skipRefetch) { getSpecificChatflowApi.request(chatflowId) } } diff --git a/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx b/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx index b1b9c2b1121..50b285142c0 100644 --- a/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx +++ b/packages/ui/src/views/marketplaces/MarketplaceCanvas.jsx @@ -65,7 +65,7 @@ const MarketplaceCanvas = () => {