# Directory Structure ``` ├── .env.example ├── .github │ └── workflows │ └── python-pytest.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── mcp_flowise │ ├── __init__.py │ ├── __main__.py │ ├── server_fastmcp.py │ ├── server_lowlevel.py │ └── utils.py ├── pyproject.toml ├── pytest.ini ├── README.md ├── smithery.yaml ├── test_mcp_call_tool_valid.py ├── test_mcp_handshake.py ├── tests │ ├── fixtures │ │ └── sample_chatflows.json │ ├── integration │ │ ├── test_flowise_integration.py │ │ ├── test_tool_prediction.py │ │ └── test_tool_registration_integration.py │ ├── README.md │ └── unit │ ├── test_chatflow_filters.py │ └── test_utils.py └── uv.lock ``` # Files -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | # Copy this file to .env and update the values as needed: 2 | 3 | # Flowise API key (Bearer token) 4 | FLOWISE_API_KEY=your_flowise_api_key_here 5 | 6 | # Flowise endpoint (default is http://localhost:3000) 7 | FLOWISE_API_ENDPOINT=http://localhost:3000 8 | 9 | # FastMCP Mode: Optionally set ONE or BOTH of these to lock in specific Chatflow or Assistant: 10 | FLOWISE_CHATFLOW_ID= 11 | FLOWISE_ASSISTANT_ID= 12 | 13 | # LowLevel Mode: Dynamically expose tools for each chatflow/assistant 14 | # Comma-separated list of chatflow IDs and their descriptions, e.g.: 15 | # "chatflow_id:My \\"First\\" Chatflow,another_id:My Second Chatflow" 16 | FLOWISE_CHATFLOW_DESCRIPTIONS= 17 | 18 | # Optional filters for FastMCP Mode (ignored in LowLevel Mode): 19 | # Whitelist: Comma-separated list of chatflow IDs to allow 20 | FLOWISE_CHATFLOW_WHITELIST= 21 | # Blacklist: Comma-separated list of chatflow IDs to deny 22 | FLOWISE_CHATFLOW_BLACKLIST= 23 | 24 | # Notes: 25 | # - If neither FLOWISE_CHATFLOW_ID nor FLOWISE_ASSISTANT_ID is set: 26 | # - Exposes 'list_chatflows' and 'create_prediction(chatflow_id, question)'. 27 | # - If exactly one is set: 28 | # - Exposes 'create_prediction(question)'. 29 | # - If both are set: 30 | # - The server will refuse to start. 31 | # - FLOWISE_CHATFLOW_DESCRIPTIONS is required for LowLevel Mode to dynamically expose tools. 32 | ``` -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- ``` 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | *.bak ``` -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- ```markdown 1 | # Testing `mcp-flowise` 2 | 3 | ## Structure 4 | - `unit/`: Contains unit tests for individual modules. 5 | - `integration/`: Contains integration tests for end-to-end scenarios. 6 | - `fixtures/`: Contains static example data and environment files for mocking. 7 | 8 | ## Running Tests 9 | 1. Install test dependencies: 10 | ```bash 11 | pip install pytest 12 | 13 | ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # mcp-flowise 2 | 3 | [](https://smithery.ai/server/@matthewhand/mcp-flowise) 4 | 5 | `mcp-flowise` is a Python package implementing a Model Context Protocol (MCP) server that integrates with the Flowise API. It provides a standardized and flexible way to list chatflows, create predictions, and dynamically register tools for Flowise chatflows or assistants. 6 | 7 | It supports two operation modes: 8 | 9 | - **LowLevel Mode (Default)**: Dynamically registers tools for all chatflows retrieved from the Flowise API. 10 | - **FastMCP Mode**: Provides static tools for listing chatflows and creating predictions, suitable for simpler configurations. 11 | 12 | <p align="center"> 13 | <img src="https://github.com/user-attachments/assets/d27afb05-c5d3-4cc9-9918-f7be8c715304" alt="Claude Desktop Screenshot"> 14 | </p> 15 | 16 | --- 17 | 18 | ## Features 19 | 20 | - **Dynamic Tool Exposure**: LowLevel mode dynamically creates tools for each chatflow or assistant. 21 | - **Simpler Configuration**: FastMCP mode exposes `list_chatflows` and `create_prediction` tools for minimal setup. 22 | - **Flexible Filtering**: Both modes support filtering chatflows via whitelists and blacklists by IDs or names (regex). 23 | - **MCP Integration**: Integrates seamlessly into MCP workflows. 24 | 25 | --- 26 | 27 | ## Installation 28 | 29 | ### Installing via Smithery 30 | 31 | To install mcp-flowise for Claude Desktop automatically via [Smithery](https://smithery.ai/server/@matthewhand/mcp-flowise): 32 | 33 | ```bash 34 | npx -y @smithery/cli install @matthewhand/mcp-flowise --client claude 35 | ``` 36 | 37 | ### Prerequisites 38 | 39 | - Python 3.12 or higher 40 | - `uvx` package manager 41 | 42 | ### Install and Run via `uvx` 43 | 44 | Confirm you can run the server directly from the GitHub repository using `uvx`: 45 | 46 | ```bash 47 | uvx --from git+https://github.com/matthewhand/mcp-flowise mcp-flowise 48 | ``` 49 | 50 | ### Adding to MCP Ecosystem (`mcpServers` Configuration) 51 | 52 | You can integrate `mcp-flowise` into your MCP ecosystem by adding it to the `mcpServers` configuration. Example: 53 | 54 | ```json 55 | { 56 | "mcpServers": { 57 | "mcp-flowise": { 58 | "command": "uvx", 59 | "args": [ 60 | "--from", 61 | "git+https://github.com/matthewhand/mcp-flowise", 62 | "mcp-flowise" 63 | ], 64 | "env": { 65 | "FLOWISE_API_KEY": "${FLOWISE_API_KEY}", 66 | "FLOWISE_API_ENDPOINT": "${FLOWISE_API_ENDPOINT}" 67 | } 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | --- 74 | 75 | ## Modes of Operation 76 | 77 | ### 1. FastMCP Mode (Simple Mode) 78 | 79 | Enabled by setting `FLOWISE_SIMPLE_MODE=true`. This mode: 80 | - Exposes two tools: `list_chatflows` and `create_prediction`. 81 | - Allows static configuration using `FLOWISE_CHATFLOW_ID` or `FLOWISE_ASSISTANT_ID`. 82 | - Lists all available chatflows via `list_chatflows`. 83 | 84 | <p align="center"> 85 | <img src="https://github.com/user-attachments/assets/0901ef9c-5d56-4f1e-a799-1e5d8e8343bd" alt="FastMCP Mode"> 86 | </p> 87 | 88 | ### 2. LowLevel Mode (FLOWISE_SIMPLE_MODE=False) 89 | 90 | **Features**: 91 | - Dynamically registers all chatflows as separate tools. 92 | - Tools are named after chatflow names (normalized). 93 | - Uses descriptions from the `FLOWISE_CHATFLOW_DESCRIPTIONS` variable, falling back to chatflow names if no description is provided. 94 | 95 | **Example**: 96 | - `my_tool(question: str) -> str` dynamically created for a chatflow. 97 | 98 | --- 99 | ## Running on Windows with `uvx` 100 | 101 | If you're using `uvx` on Windows and encounter issues with `--from git+https`, the recommended solution is to clone the repository locally and configure the `mcpServers` with the full path to `uvx.exe` and the cloned repository. Additionally, include `APPDATA`, `LOGLEVEL`, and other environment variables as required. 102 | 103 | ### Example Configuration for MCP Ecosystem (`mcpServers` on Windows) 104 | 105 | ```json 106 | { 107 | "mcpServers": { 108 | "flowise": { 109 | "command": "C:\\Users\\matth\\.local\\bin\\uvx.exe", 110 | "args": [ 111 | "--from", 112 | "C:\\Users\\matth\\downloads\\mcp-flowise", 113 | "mcp-flowise" 114 | ], 115 | "env": { 116 | "LOGLEVEL": "ERROR", 117 | "APPDATA": "C:\\Users\\matth\\AppData\\Roaming", 118 | "FLOWISE_API_KEY": "your-api-key-goes-here", 119 | "FLOWISE_API_ENDPOINT": "http://localhost:3000/" 120 | } 121 | } 122 | } 123 | } 124 | ``` 125 | 126 | ### Notes 127 | 128 | - **Full Paths**: Use full paths for both `uvx.exe` and the cloned repository. 129 | - **Environment Variables**: Point `APPDATA` to your Windows user profile (e.g., `C:\\Users\\<username>\\AppData\\Roaming`) if needed. 130 | - **Log Level**: Adjust `LOGLEVEL` as needed (`ERROR`, `INFO`, `DEBUG`, etc.). 131 | 132 | ## Environment Variables 133 | 134 | ### General 135 | 136 | - `FLOWISE_API_KEY`: Your Flowise API Bearer token (**required**). 137 | - `FLOWISE_API_ENDPOINT`: Base URL for Flowise (default: `http://localhost:3000`). 138 | 139 | ### LowLevel Mode (Default) 140 | 141 | - `FLOWISE_CHATFLOW_DESCRIPTIONS`: Comma-separated list of `chatflow_id:description` pairs. Example: 142 | ``` 143 | FLOWISE_CHATFLOW_DESCRIPTIONS="abc123:Chatflow One,xyz789:Chatflow Two" 144 | ``` 145 | 146 | ### FastMCP Mode (`FLOWISE_SIMPLE_MODE=true`) 147 | 148 | - `FLOWISE_CHATFLOW_ID`: Single Chatflow ID (optional). 149 | - `FLOWISE_ASSISTANT_ID`: Single Assistant ID (optional). 150 | - `FLOWISE_CHATFLOW_DESCRIPTION`: Optional description for the single tool exposed. 151 | 152 | --- 153 | 154 | ## Filtering Chatflows 155 | 156 | Filters can be applied in both modes using the following environment variables: 157 | 158 | - **Whitelist by ID**: 159 | `FLOWISE_WHITELIST_ID="id1,id2,id3"` 160 | - **Blacklist by ID**: 161 | `FLOWISE_BLACKLIST_ID="id4,id5"` 162 | - **Whitelist by Name (Regex)**: 163 | `FLOWISE_WHITELIST_NAME_REGEX=".*important.*"` 164 | - **Blacklist by Name (Regex)**: 165 | `FLOWISE_BLACKLIST_NAME_REGEX=".*deprecated.*"` 166 | 167 | > **Note**: Whitelists take precedence over blacklists. If both are set, the most restrictive rule is applied. 168 | 169 | - 170 | ## Security 171 | 172 | - **Protect Your API Key**: Ensure the `FLOWISE_API_KEY` is kept secure and not exposed in logs or repositories. 173 | - **Environment Configuration**: Use `.env` files or environment variables for sensitive configurations. 174 | 175 | Add `.env` to your `.gitignore`: 176 | 177 | ```bash 178 | # .gitignore 179 | .env 180 | ``` 181 | 182 | --- 183 | 184 | ## Troubleshooting 185 | 186 | - **Missing API Key**: Ensure `FLOWISE_API_KEY` is set correctly. 187 | - **Invalid Configuration**: If both `FLOWISE_CHATFLOW_ID` and `FLOWISE_ASSISTANT_ID` are set, the server will refuse to start. 188 | - **Connection Errors**: Verify `FLOWISE_API_ENDPOINT` is reachable. 189 | 190 | --- 191 | 192 | ## License 193 | 194 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 195 | 196 | ## TODO 197 | 198 | - [x] Fastmcp mode 199 | - [x] Lowlevel mode 200 | - [x] Filtering 201 | - [x] Claude desktop integration 202 | - [ ] Assistants 203 | ``` -------------------------------------------------------------------------------- /mcp_flowise/__init__.py: -------------------------------------------------------------------------------- ```python 1 | ``` -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- ``` 1 | [pytest] 2 | # asyncio_mode = strict 3 | # asyncio_default_fixture_loop_scope = function 4 | 5 | ``` -------------------------------------------------------------------------------- /tests/fixtures/sample_chatflows.json: -------------------------------------------------------------------------------- ```json 1 | [ 2 | {"id": "mock-id", "name": "Mock Chatflow 1"}, 3 | {"id": "mock-id-2", "name": "Mock Chatflow 2"} 4 | ] 5 | ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [build-system] 2 | requires = ["setuptools>=61.0", "wheel"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "mcp-flowise" 7 | version = "0.1.0" 8 | description = "MCP integration with the Flowise API for creating predictions and managing chatflows/assistants" 9 | readme = "README.md" 10 | authors = [ 11 | { name = "Matthew Hand", email = "[email protected]" } 12 | ] 13 | dependencies = [ 14 | "mcp[cli]>=1.2.0", 15 | "python-dotenv>=1.0.1", 16 | "requests>=2.25.0", 17 | ] 18 | 19 | [project.scripts] 20 | mcp-flowise = "mcp_flowise.__main__:main" 21 | 22 | [dependency-groups] 23 | dev = [ 24 | "pytest>=8.3.4", 25 | ] 26 | 27 | [tool.setuptools.packages] 28 | find = {include = ["mcp_flowise", "mcp_flowise.*"]} 29 | ``` -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- ```dockerfile 1 | # Use a Python base image that satisfies the project requirements 2 | FROM python:3.12-slim AS base 3 | 4 | # Install the uv package manager 5 | RUN pip install uvx 6 | 7 | # Set the working directory in the container 8 | WORKDIR /app 9 | 10 | # Copy the current directory contents into the container at /app 11 | COPY . . 12 | 13 | # Install dependencies 14 | RUN uvx sync --frozen --no-dev --no-editable 15 | 16 | # Expose the port the app runs on 17 | EXPOSE 8000 18 | 19 | # Set environment variables required for running the MCP server 20 | ENV FLOWISE_API_KEY=your_api_key 21 | ENV FLOWISE_API_ENDPOINT=http://localhost:3000 22 | 23 | # Define the command to run the app 24 | CMD ["uvx", "--from", "git+https://github.com/matthewhand/mcp-flowise", "mcp-flowise"] ``` -------------------------------------------------------------------------------- /.github/workflows/python-pytest.yml: -------------------------------------------------------------------------------- ```yaml 1 | name: Python Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | # Checkout the repository 15 | - uses: actions/checkout@v4 16 | 17 | # Set up Python environment 18 | - name: Set up Python 19 | uses: actions/setup-python@v4 20 | with: 21 | python-version: '3.12' 22 | 23 | # Install uv 24 | - name: Install uv 25 | uses: astral-sh/setup-uv@v4 26 | 27 | # Set up Python environment with uv 28 | - name: Set up Python 29 | run: uv python install 30 | 31 | # Sync dependencies with uv 32 | - name: Install dependencies 33 | run: uv sync --all-extras --dev 34 | 35 | # Run tests 36 | - name: Run tests 37 | run: uv run pytest tests/unit 38 | env: 39 | PYTHONPATH: ${{ github.workspace }} 40 | ``` -------------------------------------------------------------------------------- /tests/integration/test_tool_prediction.py: -------------------------------------------------------------------------------- ```python 1 | import unittest 2 | import os 3 | from mcp_flowise.utils import flowise_predict 4 | 5 | class TestToolPrediction(unittest.TestCase): 6 | """ 7 | Integration test for predicting results from a Flowise chatflow. 8 | """ 9 | 10 | def test_tool_prediction(self): 11 | """ 12 | Test the prediction function for a Flowise chatflow. 13 | """ 14 | # Check if FLOWISE_CHATFLOW_ID is set 15 | chatflow_id = os.getenv("FLOWISE_CHATFLOW_ID") 16 | if not chatflow_id: 17 | self.skipTest("FLOWISE_CHATFLOW_ID environment variable is not set.") 18 | 19 | question = "What is the weather like today?" 20 | print(f"Using chatflow_id: {chatflow_id}") 21 | 22 | # Make a prediction 23 | result = flowise_predict(chatflow_id, question) 24 | 25 | # Validate the response 26 | self.assertIsInstance(result, str) 27 | self.assertNotEqual(result.strip(), "", "Prediction result should not be empty.") 28 | print(f"Prediction result: {result}") 29 | 30 | if __name__ == "__main__": 31 | unittest.main() 32 | ``` -------------------------------------------------------------------------------- /smithery.yaml: -------------------------------------------------------------------------------- ```yaml 1 | # Smithery configuration file: https://smithery.ai/docs/deployments 2 | 3 | startCommand: 4 | type: stdio 5 | configSchema: 6 | # JSON Schema defining the configuration options for the MCP. 7 | type: object 8 | required: 9 | - flowiseApiKey 10 | properties: 11 | flowiseApiKey: 12 | type: string 13 | description: Your Flowise API Bearer token 14 | flowiseApiEndpoint: 15 | type: string 16 | default: http://localhost:3000 17 | description: Base URL for Flowise 18 | flowiseSimpleMode: 19 | type: boolean 20 | default: false 21 | description: Enable FastMCP mode for simpler configuration 22 | flowiseChatflowId: 23 | type: string 24 | description: Single Chatflow ID for FastMCP mode (optional) 25 | flowiseAssistantId: 26 | type: string 27 | description: Single Assistant ID for FastMCP mode (optional) 28 | commandFunction: 29 | # A function that produces the CLI command to start the MCP on stdio. 30 | |- 31 | (config) => ({command: 'uvx', args: ['--from', 'git+https://github.com/matthewhand/mcp-flowise', 'mcp-flowise'], env: {FLOWISE_API_KEY: config.flowiseApiKey, FLOWISE_API_ENDPOINT: config.flowiseApiEndpoint || 'http://localhost:3000', FLOWISE_SIMPLE_MODE: config.flowiseSimpleMode ? 'true' : 'false', FLOWISE_CHATFLOW_ID: config.flowiseChatflowId || '', FLOWISE_ASSISTANT_ID: config.flowiseAssistantId || ''}}) ``` -------------------------------------------------------------------------------- /test_mcp_handshake.py: -------------------------------------------------------------------------------- ```python 1 | import subprocess 2 | import time 3 | import json 4 | 5 | # Define JSON-RPC requests 6 | initialize_request = { 7 | "jsonrpc": "2.0", 8 | "id": 1, 9 | "method": "initialize", 10 | "params": { 11 | "protocolVersion": "1.0", 12 | "capabilities": {}, 13 | "clientInfo": {"name": "manual-client", "version": "0.1"} 14 | } 15 | } 16 | 17 | initialized_notification = { 18 | "jsonrpc": "2.0", 19 | "method": "notifications/initialized" 20 | } 21 | 22 | list_tools_request = { 23 | "jsonrpc": "2.0", 24 | "id": 2, 25 | "method": "tools/list" 26 | } 27 | 28 | # Start MCP server 29 | process = subprocess.Popen( 30 | ["uvx", "--from", ".", "mcp-flowise"], 31 | stdin=subprocess.PIPE, 32 | stdout=subprocess.PIPE, 33 | stderr=subprocess.PIPE, 34 | text=True 35 | ) 36 | 37 | try: 38 | # Send "initialize" request 39 | process.stdin.write(json.dumps(initialize_request) + "\n") 40 | process.stdin.flush() 41 | time.sleep(2.0) 42 | 43 | # Send "initialized" notification 44 | process.stdin.write(json.dumps(initialized_notification) + "\n") 45 | process.stdin.flush() 46 | time.sleep(2.0) 47 | 48 | # Send "tools/list" request 49 | process.stdin.write(json.dumps(list_tools_request) + "\n") 50 | process.stdin.flush() 51 | time.sleep(4) 52 | 53 | # Capture output 54 | stdout, stderr = process.communicate(timeout=5) 55 | 56 | # Print server responses 57 | print("STDOUT:") 58 | print(stdout) 59 | print("STDERR:") 60 | print(stderr) 61 | 62 | except subprocess.TimeoutExpired: 63 | print("MCP server process timed out.") 64 | process.kill() 65 | except Exception as e: 66 | print(f"An error occurred: {e}") 67 | finally: 68 | process.terminate() 69 | ``` -------------------------------------------------------------------------------- /mcp_flowise/__main__.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | Entry point for the mcp_flowise package. 3 | 4 | This script determines which server to run based on the presence of 5 | the FLOWISE_SIMPLE_MODE environment variable: 6 | - Low-Level Server: For dynamic tool creation based on chatflows. 7 | - FastMCP Server: For static tool configurations. 8 | """ 9 | 10 | import os 11 | import sys 12 | from dotenv import load_dotenv 13 | from mcp_flowise.utils import setup_logging 14 | from mcp_flowise.utils import fetch_chatflows 15 | 16 | # Load environment variables from .env if present 17 | load_dotenv() 18 | 19 | def main(): 20 | """ 21 | Main entry point for the mcp_flowise package. 22 | 23 | Depending on the FLOWISE_SIMPLE_MODE environment variable, this function 24 | launches either: 25 | - Low-Level Server (dynamic tools) 26 | - FastMCP Server (static tools) 27 | """ 28 | # Configure logging 29 | DEBUG = os.getenv("DEBUG", "").lower() in ("true", "1", "yes") 30 | logger = setup_logging(debug=DEBUG) 31 | 32 | logger.debug("Starting mcp_flowise package entry point.") 33 | 34 | chatflows = fetch_chatflows() 35 | logger.debug(f"Available chatflows: {chatflows}") 36 | 37 | # Default to Simple Mode unless explicitly disabled 38 | FLOWISE_SIMPLE_MODE = os.getenv("FLOWISE_SIMPLE_MODE", "true").lower() not in ("false", "0", "no") 39 | if FLOWISE_SIMPLE_MODE: 40 | logger.debug("FLOWISE_SIMPLE_MODE is enabled. Launching FastMCP Server.") 41 | from mcp_flowise.server_fastmcp import run_simple_server 42 | selected_server = run_simple_server 43 | else: 44 | logger.debug("FLOWISE_SIMPLE_MODE is disabled. Launching Low-Level Server.") 45 | from mcp_flowise.server_lowlevel import run_server 46 | selected_server = run_server 47 | 48 | # Run the selected server 49 | try: 50 | selected_server() 51 | except Exception as e: 52 | logger.critical("Unhandled exception occurred while running the server.", exc_info=True) 53 | sys.exit(1) 54 | 55 | 56 | if __name__ == "__main__": 57 | main() 58 | ``` -------------------------------------------------------------------------------- /test_mcp_call_tool_valid.py: -------------------------------------------------------------------------------- ```python 1 | import os 2 | import subprocess 3 | import time 4 | import json 5 | 6 | # Ensure the required environment variable is set 7 | FLOWISE_CHATFLOW_ID = os.getenv("FLOWISE_CHATFLOW_ID") 8 | if not FLOWISE_CHATFLOW_ID: 9 | print("Error: FLOWISE_CHATFLOW_ID environment variable is not set.") 10 | exit(1) 11 | 12 | # Define requests 13 | initialize_request = { 14 | "jsonrpc": "2.0", 15 | "id": 1, 16 | "method": "initialize", 17 | "params": { 18 | "protocolVersion": "1.0", 19 | "capabilities": {}, 20 | "clientInfo": {"name": "valid-client", "version": "0.1"} 21 | } 22 | } 23 | 24 | list_tools_request = { 25 | "jsonrpc": "2.0", 26 | "id": 2, 27 | "method": "tools/list" 28 | } 29 | 30 | call_tool_request = { 31 | "jsonrpc": "2.0", 32 | "id": 3, 33 | "method": "tools/call", 34 | "params": { 35 | "name": FLOWISE_CHATFLOW_ID, # Use the valid chatflow ID 36 | "arguments": {"question": "What is AI?"} 37 | } 38 | } 39 | 40 | # Start MCP server 41 | process = subprocess.Popen( 42 | ["uvx", "--from", ".", "mcp-flowise"], 43 | stdin=subprocess.PIPE, 44 | stdout=subprocess.PIPE, 45 | stderr=subprocess.PIPE, 46 | text=True 47 | ) 48 | 49 | try: 50 | # Initialize the server 51 | print("Sending initialize request...") 52 | process.stdin.write(json.dumps(initialize_request) + "\n") 53 | process.stdin.flush() 54 | 55 | # Wait until the server sends a response to the initialize request 56 | time.sleep(0.5) 57 | stdout_line = process.stdout.readline() 58 | while "id" not in stdout_line: # Look for a response containing "id" 59 | print(f"Server Response: {stdout_line.strip()}") 60 | stdout_line = process.stdout.readline() 61 | 62 | print("Initialization complete.") 63 | 64 | # List tools 65 | print("Sending tools/list request...") 66 | process.stdin.write(json.dumps(list_tools_request) + "\n") 67 | process.stdin.flush() 68 | time.sleep(0.5) 69 | 70 | # Call the valid tool 71 | print(f"Sending tools/call request for chatflow '{FLOWISE_CHATFLOW_ID}'...") 72 | process.stdin.write(json.dumps(call_tool_request) + "\n") 73 | process.stdin.flush() 74 | time.sleep(1) 75 | 76 | # Capture output 77 | stdout, stderr = process.communicate(timeout=5) 78 | 79 | # Print responses 80 | print("STDOUT:") 81 | print(stdout) 82 | 83 | print("STDERR:") 84 | print(stderr) 85 | 86 | except subprocess.TimeoutExpired: 87 | print("MCP server process timed out.") 88 | process.kill() 89 | except Exception as e: 90 | print(f"An error occurred: {e}") 91 | finally: 92 | process.terminate() 93 | ``` -------------------------------------------------------------------------------- /tests/integration/test_flowise_integration.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | Integration tests for Flowise MCP. 3 | These tests will run conditionally if the required environment variables are configured. 4 | """ 5 | 6 | import os 7 | import unittest 8 | from mcp_flowise.utils import fetch_chatflows, flowise_predict 9 | 10 | 11 | class IntegrationTests(unittest.TestCase): 12 | """ 13 | Integration tests for Flowise MCP. 14 | """ 15 | 16 | @unittest.skipUnless( 17 | os.getenv("FLOWISE_API_KEY") and os.getenv("FLOWISE_API_ENDPOINT"), 18 | "FLOWISE_API_KEY and FLOWISE_API_ENDPOINT must be set for integration tests.", 19 | ) 20 | def test_tool_discovery_in_lowlevel_mode(self): 21 | """ 22 | Test tool discovery in low-level mode by fetching tools from the Flowise server. 23 | """ 24 | chatflows = fetch_chatflows() 25 | self.assertGreater(len(chatflows), 0, "No chatflows discovered. Ensure the Flowise server is configured correctly.") 26 | print(f"Discovered chatflows: {[cf['name'] for cf in chatflows]}") 27 | 28 | @unittest.skipUnless( 29 | os.getenv("FLOWISE_API_KEY") and os.getenv("FLOWISE_API_ENDPOINT"), 30 | "FLOWISE_API_KEY and FLOWISE_API_ENDPOINT must be set for tool tests.", 31 | ) 32 | def test_call_specific_tool(self): 33 | """ 34 | Test calling a specific tool if available on the Flowise server. 35 | """ 36 | chatflows = fetch_chatflows() 37 | if not chatflows: 38 | self.skipTest("No chatflows discovered on the server. Skipping tool test.") 39 | 40 | # Handle cases with and without the FLOWISE_CHATFLOW_ID environment variable 41 | specific_chatflow_id = os.getenv("FLOWISE_CHATFLOW_ID") 42 | if specific_chatflow_id: 43 | # Look for the specified chatflow ID 44 | chatflow = next((cf for cf in chatflows if cf["id"] == specific_chatflow_id), None) 45 | if not chatflow: 46 | self.skipTest(f"Specified chatflow ID {specific_chatflow_id} not found. Skipping tool test.") 47 | else: 48 | # Fallback to the first chatflow if no ID is specified 49 | chatflow = chatflows[0] 50 | 51 | tool_name = chatflow.get("name") 52 | print(f"Testing tool: {tool_name} with ID: {chatflow['id']}") 53 | 54 | # Simulate tool call 55 | result = self.simulate_tool_call(tool_name, chatflow["id"], "Tell me a fun fact.") 56 | self.assertTrue( 57 | result.strip(), 58 | f"Unexpected empty response from tool {tool_name}: {result}" 59 | ) 60 | 61 | def simulate_tool_call(self, tool_name, chatflow_id, question): 62 | """ 63 | Simulates a tool call by directly using the flowise_predict function. 64 | 65 | Args: 66 | tool_name (str): The name of the tool. 67 | chatflow_id (str): The ID of the chatflow/tool. 68 | question (str): The question to ask. 69 | 70 | Returns: 71 | str: The response from the tool. 72 | """ 73 | return flowise_predict(chatflow_id, question) 74 | 75 | 76 | if __name__ == "__main__": 77 | unittest.main() 78 | ``` -------------------------------------------------------------------------------- /tests/integration/test_tool_registration_integration.py: -------------------------------------------------------------------------------- ```python 1 | import os 2 | import unittest 3 | import asyncio 4 | from mcp_flowise.server_lowlevel import run_server 5 | from mcp import types 6 | from multiprocessing import Process 7 | from time import sleep 8 | 9 | 10 | class TestToolRegistrationIntegration(unittest.TestCase): 11 | """ 12 | True integration test for tool registration and listing. 13 | """ 14 | 15 | @classmethod 16 | def setUpClass(cls): 17 | """ 18 | Set up the test environment and server. 19 | """ 20 | # Set the environment variable for chatflow descriptions 21 | os.environ["FLOWISE_CHATFLOW_DESCRIPTIONS"] = ( 22 | "chatflow1:Test Chatflow 1,chatflow2:Test Chatflow 2" 23 | ) 24 | 25 | # Start the server using asyncio.create_task 26 | # cls.loop = asyncio.get_event_loop() 27 | cls.loop = asyncio.new_event_loop() 28 | asyncio.set_event_loop(cls.loop) 29 | cls.server_task = cls.loop.create_task(cls.start_server()) 30 | 31 | @classmethod 32 | async def start_server(cls): 33 | """ 34 | Start the server as a coroutine. 35 | """ 36 | await run_server() 37 | 38 | @classmethod 39 | def tearDownClass(cls): 40 | """ 41 | Clean up the server task. 42 | """ 43 | cls.server_task.cancel() 44 | 45 | def test_tool_registration_and_listing(self): 46 | """ 47 | Test that tools are correctly registered and listed at runtime. 48 | """ 49 | async def run_client(): 50 | # Create a ListToolsRequest 51 | list_tools_request = types.ListToolsRequest(method="tools/list") 52 | 53 | # Simulate the request and get the response 54 | response = await self.mock_client_request(list_tools_request) 55 | 56 | # Validate the response 57 | tools = response.root.tools 58 | assert len(tools) == 2, "Expected 2 tools to be registered" 59 | assert tools[0].name == "test_chatflow_1" 60 | assert tools[0].description == "Test Chatflow 1" 61 | assert tools[1].name == "test_chatflow_2" 62 | assert tools[1].description == "Test Chatflow 2" 63 | 64 | asyncio.run(run_client()) 65 | 66 | async def mock_client_request(self, request): 67 | """ 68 | Mock client request for testing purposes. Replace with actual client logic. 69 | """ 70 | return types.ServerResult( 71 | root=types.ListToolsResult( 72 | tools=[ 73 | types.Tool( 74 | name="test_chatflow_1", 75 | description="Test Chatflow 1", 76 | inputSchema={ 77 | "type": "object", 78 | "properties": { 79 | "question": {"type": "string"} 80 | }, 81 | "required": ["question"] 82 | } 83 | ), 84 | types.Tool( 85 | name="test_chatflow_2", 86 | description="Test Chatflow 2", 87 | inputSchema={ 88 | "type": "object", 89 | "properties": { 90 | "question": {"type": "string"} 91 | }, 92 | "required": ["question"] 93 | } 94 | ), 95 | ] 96 | ) 97 | ) 98 | 99 | 100 | if __name__ == "__main__": 101 | unittest.main() 102 | ``` -------------------------------------------------------------------------------- /tests/unit/test_chatflow_filters.py: -------------------------------------------------------------------------------- ```python 1 | import os 2 | import unittest 3 | from unittest.mock import patch 4 | from mcp_flowise.utils import filter_chatflows 5 | 6 | 7 | class TestChatflowFilters(unittest.TestCase): 8 | """ 9 | Unit tests for chatflow filtering logic in mcp_flowise.utils. 10 | """ 11 | 12 | def setUp(self): 13 | """ 14 | Reset the environment variables for filtering logic. 15 | """ 16 | os.environ.pop("FLOWISE_WHITELIST_ID", None) 17 | os.environ.pop("FLOWISE_BLACKLIST_ID", None) 18 | os.environ.pop("FLOWISE_WHITELIST_NAME_REGEX", None) 19 | os.environ.pop("FLOWISE_BLACKLIST_NAME_REGEX", None) 20 | 21 | def test_no_filters(self): 22 | """ 23 | Test that all chatflows are returned when no filters are set. 24 | """ 25 | chatflows = [ 26 | {"id": "chatflow1", "name": "First Chatflow"}, 27 | {"id": "chatflow2", "name": "Second Chatflow"}, 28 | ] 29 | filtered = filter_chatflows(chatflows) 30 | self.assertEqual(len(filtered), len(chatflows)) 31 | self.assertListEqual(filtered, chatflows) 32 | 33 | @patch.dict(os.environ, {"FLOWISE_WHITELIST_ID": "chatflow1,chatflow3"}) 34 | def test_whitelist_id_filter(self): 35 | """ 36 | Test that only whitelisted chatflows by ID are returned. 37 | """ 38 | chatflows = [ 39 | {"id": "chatflow1", "name": "First Chatflow"}, 40 | {"id": "chatflow2", "name": "Second Chatflow"}, 41 | {"id": "chatflow3", "name": "Third Chatflow"}, 42 | ] 43 | filtered = filter_chatflows(chatflows) 44 | self.assertEqual(len(filtered), 2) 45 | self.assertTrue(all(cf["id"] in {"chatflow1", "chatflow3"} for cf in filtered)) 46 | 47 | @patch.dict(os.environ, {"FLOWISE_BLACKLIST_ID": "chatflow2"}) 48 | def test_blacklist_id_filter(self): 49 | """ 50 | Test that blacklisted chatflows by ID are excluded. 51 | """ 52 | chatflows = [ 53 | {"id": "chatflow1", "name": "First Chatflow"}, 54 | {"id": "chatflow2", "name": "Second Chatflow"}, 55 | ] 56 | filtered = filter_chatflows(chatflows) 57 | self.assertEqual(len(filtered), 1) 58 | self.assertEqual(filtered[0]["id"], "chatflow1") 59 | 60 | @patch.dict(os.environ, {"FLOWISE_WHITELIST_NAME_REGEX": ".*First.*"}) 61 | def test_whitelist_name_regex_filter(self): 62 | """ 63 | Test that only chatflows matching the whitelist name regex are returned. 64 | """ 65 | chatflows = [ 66 | {"id": "chatflow1", "name": "First Chatflow"}, 67 | {"id": "chatflow2", "name": "Second Chatflow"}, 68 | ] 69 | filtered = filter_chatflows(chatflows) 70 | self.assertEqual(len(filtered), 1) 71 | self.assertEqual(filtered[0]["name"], "First Chatflow") 72 | 73 | @patch.dict(os.environ, {"FLOWISE_BLACKLIST_NAME_REGEX": ".*Second.*"}) 74 | def test_blacklist_name_regex_filter(self): 75 | """ 76 | Test that chatflows matching the blacklist name regex are excluded. 77 | """ 78 | chatflows = [ 79 | {"id": "chatflow1", "name": "First Chatflow"}, 80 | {"id": "chatflow2", "name": "Second Chatflow"}, 81 | ] 82 | filtered = filter_chatflows(chatflows) 83 | self.assertEqual(len(filtered), 1) 84 | self.assertEqual(filtered[0]["name"], "First Chatflow") 85 | 86 | @patch.dict( 87 | os.environ, 88 | { 89 | "FLOWISE_WHITELIST_ID": "chatflow1", 90 | "FLOWISE_BLACKLIST_NAME_REGEX": ".*Second.*", 91 | }, 92 | ) 93 | def test_whitelist_and_blacklist_combined(self): 94 | """ 95 | Test that whitelist takes precedence over blacklist. 96 | """ 97 | chatflows = [ 98 | {"id": "chatflow1", "name": "Second Chatflow"}, 99 | {"id": "chatflow2", "name": "Another Chatflow"}, 100 | ] 101 | filtered = filter_chatflows(chatflows) 102 | self.assertEqual(len(filtered), 1) 103 | self.assertEqual(filtered[0]["id"], "chatflow1") 104 | 105 | 106 | if __name__ == "__main__": 107 | unittest.main() 108 | ``` -------------------------------------------------------------------------------- /mcp_flowise/server_fastmcp.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | Provides the FastMCP server logic for mcp_flowise. 3 | 4 | This server exposes a limited set of tools (list_chatflows, create_prediction) 5 | and uses environment variables to determine the chatflow or assistant configuration. 6 | """ 7 | 8 | import os 9 | import sys 10 | import json 11 | from mcp.server.fastmcp import FastMCP 12 | from mcp_flowise.utils import flowise_predict, fetch_chatflows, redact_api_key, setup_logging 13 | 14 | # Environment variables 15 | FLOWISE_API_KEY = os.getenv("FLOWISE_API_KEY", "") 16 | FLOWISE_API_ENDPOINT = os.getenv("FLOWISE_API_ENDPOINT", "http://localhost:3000") 17 | FLOWISE_CHATFLOW_ID = os.getenv("FLOWISE_CHATFLOW_ID") 18 | FLOWISE_ASSISTANT_ID = os.getenv("FLOWISE_ASSISTANT_ID") 19 | FLOWISE_CHATFLOW_DESCRIPTION = os.getenv("FLOWISE_CHATFLOW_DESCRIPTION") 20 | FLOWISE_CHATFLOW_WHITELIST = os.getenv("FLOWISE_CHATFLOW_WHITELIST") 21 | FLOWISE_CHATFLOW_BLACKLIST = os.getenv("FLOWISE_CHATFLOW_BLACKLIST") 22 | 23 | DEBUG = os.getenv("DEBUG", "").lower() in ("true", "1", "yes") 24 | 25 | # Configure logging 26 | logger = setup_logging(debug=DEBUG) 27 | 28 | # Log key environment variable values 29 | logger.debug(f"Flowise API Key (redacted): {redact_api_key(FLOWISE_API_KEY)}") 30 | logger.debug(f"Flowise API Endpoint: {FLOWISE_API_ENDPOINT}") 31 | logger.debug(f"Flowise Chatflow ID: {FLOWISE_CHATFLOW_ID}") 32 | logger.debug(f"Flowise Assistant ID: {FLOWISE_ASSISTANT_ID}") 33 | logger.debug(f"Flowise Chatflow Description: {FLOWISE_CHATFLOW_DESCRIPTION}") 34 | 35 | # Initialize MCP Server 36 | mcp = FastMCP("FlowiseMCP-with-EnvAuth") 37 | 38 | 39 | @mcp.tool() 40 | def list_chatflows() -> str: 41 | """ 42 | List all available chatflows from the Flowise API. 43 | 44 | This function respects optional whitelisting or blacklisting if configured 45 | via FLOWISE_CHATFLOW_WHITELIST or FLOWISE_CHATFLOW_BLACKLIST. 46 | 47 | Returns: 48 | str: A JSON-encoded string of filtered chatflows. 49 | """ 50 | logger.debug("Handling list_chatflows tool.") 51 | chatflows = fetch_chatflows() 52 | 53 | # Apply whitelisting 54 | if FLOWISE_CHATFLOW_WHITELIST: 55 | whitelist = set(FLOWISE_CHATFLOW_WHITELIST.split(",")) 56 | chatflows = [cf for cf in chatflows if cf["id"] in whitelist] 57 | logger.debug(f"Applied whitelist filter: {whitelist}") 58 | 59 | # Apply blacklisting 60 | if FLOWISE_CHATFLOW_BLACKLIST: 61 | blacklist = set(FLOWISE_CHATFLOW_BLACKLIST.split(",")) 62 | chatflows = [cf for cf in chatflows if cf["id"] not in blacklist] 63 | logger.debug(f"Applied blacklist filter: {blacklist}") 64 | 65 | logger.debug(f"Filtered chatflows: {chatflows}") 66 | return json.dumps(chatflows) 67 | 68 | 69 | @mcp.tool() 70 | def create_prediction(*, chatflow_id: str = None, question: str) -> str: 71 | """ 72 | Create a prediction by sending a question to a specific chatflow or assistant. 73 | 74 | Args: 75 | chatflow_id (str, optional): The ID of the chatflow to use. Defaults to FLOWISE_CHATFLOW_ID. 76 | question (str): The question or prompt to send to the chatflow. 77 | 78 | Returns: 79 | str: The raw JSON response from Flowise API or an error message if something goes wrong. 80 | """ 81 | logger.debug(f"create_prediction called with chatflow_id={chatflow_id}, question={question}") 82 | chatflow_id = chatflow_id or FLOWISE_CHATFLOW_ID 83 | 84 | if not chatflow_id and not FLOWISE_ASSISTANT_ID: 85 | logger.error("No chatflow_id or assistant_id provided or pre-configured.") 86 | return json.dumps({"error": "chatflow_id or assistant_id is required"}) 87 | 88 | try: 89 | # Determine which chatflow ID to use 90 | target_chatflow_id = chatflow_id or FLOWISE_ASSISTANT_ID 91 | 92 | # Call the prediction function and return the raw JSON result 93 | result = flowise_predict(target_chatflow_id, question) 94 | logger.debug(f"Prediction result: {result}") 95 | return result # Returning raw JSON as a string 96 | except Exception as e: 97 | logger.error(f"Unhandled exception in create_prediction: {e}", exc_info=True) 98 | return json.dumps({"error": str(e)}) 99 | 100 | def run_simple_server(): 101 | """ 102 | Run the FastMCP version of the Flowise server. 103 | 104 | This function ensures proper configuration and handles server initialization. 105 | 106 | Raises: 107 | SystemExit: If both FLOWISE_CHATFLOW_ID and FLOWISE_ASSISTANT_ID are set simultaneously. 108 | """ 109 | if FLOWISE_CHATFLOW_ID and FLOWISE_ASSISTANT_ID: 110 | logger.error("Both FLOWISE_CHATFLOW_ID and FLOWISE_ASSISTANT_ID are set. Set only one.") 111 | sys.exit(1) 112 | 113 | try: 114 | logger.debug("Starting MCP server (FastMCP version)...") 115 | mcp.run(transport="stdio") 116 | except Exception as e: 117 | logger.error("Unhandled exception in MCP server.", exc_info=True) 118 | sys.exit(1) 119 | ``` -------------------------------------------------------------------------------- /tests/unit/test_utils.py: -------------------------------------------------------------------------------- ```python 1 | import unittest 2 | from unittest.mock import patch, Mock 3 | import requests 4 | from mcp_flowise.utils import flowise_predict, fetch_chatflows, filter_chatflows, normalize_tool_name 5 | 6 | class TestUtils(unittest.TestCase): 7 | 8 | @patch("requests.post") 9 | def test_flowise_predict_success(self, mock_post: Mock) -> None: 10 | """ 11 | Test successful prediction response. 12 | """ 13 | mock_post.return_value = Mock( 14 | status_code=200, 15 | text='{"text": "Mock Prediction"}', 16 | ) 17 | response = flowise_predict("valid_chatflow_id", "What's AI?") 18 | self.assertEqual(response, '{"text": "Mock Prediction"}') # Success case 19 | mock_post.assert_called_once() 20 | 21 | @patch("requests.post", side_effect=requests.Timeout) 22 | def test_flowise_predict_timeout(self, mock_post: Mock) -> None: 23 | """ 24 | Test prediction handling of timeout. 25 | """ 26 | response = flowise_predict("valid_chatflow_id", "What's AI?") 27 | self.assertIn("error", response) # Assert the response contains the error key 28 | # self.assertIn("Timeout", response) # Timeout-specific assertion 29 | 30 | @patch("requests.post") 31 | def test_flowise_predict_http_error(self, mock_post: Mock) -> None: 32 | """ 33 | Test prediction handling of HTTP errors. 34 | """ 35 | mock_post.return_value = Mock( 36 | status_code=500, 37 | raise_for_status=Mock(side_effect=requests.HTTPError("500 Error")), 38 | text='{"error": "500 Error"}', 39 | ) 40 | response = flowise_predict("valid_chatflow_id", "What's AI?") 41 | self.assertIn("error", response) 42 | self.assertIn("500 Error", response) 43 | 44 | @patch("requests.get") 45 | def test_fetch_chatflows_success(self, mock_get: Mock) -> None: 46 | """ 47 | Test successful fetching of chatflows. 48 | """ 49 | mock_get.return_value = Mock( 50 | status_code=200, 51 | json=Mock(return_value=[{"id": "1", "name": "Chatflow 1"}, {"id": "2", "name": "Chatflow 2"}]), 52 | ) 53 | chatflows = fetch_chatflows() 54 | self.assertEqual(len(chatflows), 2) 55 | self.assertEqual(chatflows[0]["id"], "1") 56 | self.assertEqual(chatflows[0]["name"], "Chatflow 1") 57 | mock_get.assert_called_once() 58 | 59 | @patch("requests.get", side_effect=requests.Timeout) 60 | def test_fetch_chatflows_timeout(self, mock_get: Mock) -> None: 61 | """ 62 | Test handling of timeout when fetching chatflows. 63 | """ 64 | chatflows = fetch_chatflows() 65 | self.assertEqual(chatflows, []) # Should return an empty list on timeout 66 | 67 | @patch("requests.get") 68 | def test_fetch_chatflows_http_error(self, mock_get: Mock) -> None: 69 | """ 70 | Test handling of HTTP errors when fetching chatflows. 71 | """ 72 | mock_get.return_value = Mock( 73 | status_code=500, 74 | raise_for_status=Mock(side_effect=requests.HTTPError("500 Error")), 75 | ) 76 | chatflows = fetch_chatflows() 77 | self.assertEqual(chatflows, []) # Should return an empty list on HTTP error 78 | 79 | def test_filter_chatflows(self) -> None: 80 | """ 81 | Test filtering of chatflows based on whitelist and blacklist criteria. 82 | """ 83 | chatflows = [ 84 | {"id": "1", "name": "Chatflow 1"}, 85 | {"id": "2", "name": "Chatflow 2"}, 86 | {"id": "3", "name": "Chatflow 3"}, 87 | ] 88 | 89 | # Mock environment variables 90 | with patch.dict("os.environ", { 91 | "FLOWISE_WHITELIST_ID": "1,2", 92 | "FLOWISE_BLACKLIST_ID": "3", 93 | "FLOWISE_WHITELIST_NAME_REGEX": "", 94 | "FLOWISE_BLACKLIST_NAME_REGEX": "", 95 | }): 96 | filtered = filter_chatflows(chatflows) 97 | self.assertEqual(len(filtered), 2) 98 | self.assertEqual(filtered[0]["id"], "1") 99 | self.assertEqual(filtered[1]["id"], "2") 100 | 101 | # Mock environment variables for blacklist only 102 | with patch.dict("os.environ", { 103 | "FLOWISE_WHITELIST_ID": "", 104 | "FLOWISE_BLACKLIST_ID": "2", 105 | "FLOWISE_WHITELIST_NAME_REGEX": "", 106 | "FLOWISE_BLACKLIST_NAME_REGEX": "", 107 | }): 108 | filtered = filter_chatflows(chatflows) 109 | self.assertEqual(len(filtered), 2) 110 | self.assertEqual(filtered[0]["id"], "1") 111 | self.assertEqual(filtered[1]["id"], "3") 112 | 113 | def test_normalize_tool_name(self) -> None: 114 | """ 115 | Test normalization of tool names. 116 | """ 117 | self.assertEqual(normalize_tool_name("Tool Name"), "tool_name") 118 | self.assertEqual(normalize_tool_name("Tool-Name"), "tool_name") 119 | self.assertEqual(normalize_tool_name("Tool_Name"), "tool_name") 120 | self.assertEqual(normalize_tool_name("ToolName"), "toolname") 121 | self.assertEqual(normalize_tool_name(""), "unknown_tool") 122 | self.assertEqual(normalize_tool_name(None), "unknown_tool") 123 | 124 | if __name__ == "__main__": 125 | unittest.main() ``` -------------------------------------------------------------------------------- /mcp_flowise/server_lowlevel.py: -------------------------------------------------------------------------------- ```python 1 | ''' 2 | Low-Level Server for the Flowise MCP. 3 | 4 | This server dynamically registers tools based on the provided chatflows 5 | retrieved from the Flowise API. Tool names are normalized for safety 6 | and consistency, and potential conflicts are logged. 7 | 8 | Descriptions for tools are prioritized from FLOWISE_CHATFLOW_DESCRIPTIONS, 9 | falling back to the chatflow names when not provided. 10 | 11 | Conflicts in tool names after normalization are handled gracefully by 12 | skipping those chatflows. 13 | ''' 14 | 15 | import os 16 | import sys 17 | import asyncio 18 | import json 19 | from typing import List, Dict, Any 20 | from mcp import types 21 | from mcp.server.lowlevel import Server 22 | from mcp.server.models import InitializationOptions 23 | from mcp.server.stdio import stdio_server 24 | from mcp_flowise.utils import ( 25 | flowise_predict, 26 | fetch_chatflows, 27 | normalize_tool_name, 28 | setup_logging, 29 | ) 30 | 31 | # Configure logging 32 | DEBUG = os.getenv("DEBUG", "").lower() in ("true", "1", "yes") 33 | logger = setup_logging(debug=DEBUG) 34 | 35 | # Global tool mapping: tool name to chatflow ID 36 | NAME_TO_ID_MAPPING: Dict[str, str] = {} 37 | tools: List[types.Tool] = [] 38 | 39 | # Initialize the Low-Level MCP Server 40 | mcp = Server("FlowiseMCP-with-EnvAuth") 41 | 42 | 43 | def get_chatflow_descriptions() -> Dict[str, str]: 44 | """ 45 | Parse the FLOWISE_CHATFLOW_DESCRIPTIONS environment variable for descriptions. 46 | 47 | Returns: 48 | dict: A dictionary mapping chatflow IDs to descriptions. 49 | """ 50 | descriptions_env = os.getenv("FLOWISE_CHATFLOW_DESCRIPTIONS", "") 51 | if not descriptions_env: 52 | logger.debug("No FLOWISE_CHATFLOW_DESCRIPTIONS provided.") 53 | return {} 54 | 55 | logger.debug("Retrieved FLOWISE_CHATFLOW_DESCRIPTIONS: %s", descriptions_env) 56 | descriptions = {} 57 | for pair in descriptions_env.split(","): 58 | if ":" not in pair: 59 | logger.warning("Invalid format in FLOWISE_CHATFLOW_DESCRIPTIONS: %s", pair) 60 | continue 61 | chatflow_id, description = map(str.strip, pair.split(":", 1)) 62 | if chatflow_id and description: 63 | descriptions[chatflow_id] = description 64 | logger.debug("Parsed FLOWISE_CHATFLOW_DESCRIPTIONS: %s", descriptions) 65 | return descriptions 66 | 67 | 68 | async def dispatcher_handler(request: types.CallToolRequest) -> types.ServerResult: 69 | """ 70 | Dispatcher handler that routes CallToolRequest to the appropriate tool handler based on the tool name. 71 | """ 72 | try: 73 | tool_name = request.params.name 74 | logger.debug("Dispatcher received CallToolRequest for tool: %s", tool_name) 75 | 76 | if tool_name not in NAME_TO_ID_MAPPING: 77 | logger.error("Unknown tool requested: %s", tool_name) 78 | return types.ServerResult( 79 | root=types.CallToolResult( 80 | content=[types.TextContent(type="text", text="Unknown tool requested")] 81 | ) 82 | ) 83 | 84 | chatflow_id = NAME_TO_ID_MAPPING[tool_name] 85 | question = request.params.arguments.get("question", "") 86 | if not question: 87 | logger.error("Missing 'question' argument for tool: %s", tool_name) 88 | return types.ServerResult( 89 | root=types.CallToolResult( 90 | content=[types.TextContent(type="text", text="Missing 'question' argument.")] 91 | ) 92 | ) 93 | 94 | logger.debug("Dispatching prediction for chatflow_id: %s with question: %s", chatflow_id, question) 95 | 96 | # Call the prediction function 97 | try: 98 | result = flowise_predict(chatflow_id, question) 99 | logger.debug("Prediction result: %s", result) 100 | except Exception as pred_err: 101 | logger.error("Error during prediction: %s", pred_err, exc_info=True) 102 | result = json.dumps({"error": "Error occurred during prediction."}) 103 | 104 | # Pass the raw JSON response or error JSON back to the client 105 | return types.ServerResult( 106 | root=types.CallToolResult( 107 | content=[types.TextContent(type="text", text=result)] 108 | ) 109 | ) 110 | except Exception as e: 111 | logger.error("Unhandled exception in dispatcher_handler: %s", e, exc_info=True) 112 | return types.ServerResult( 113 | root=types.CallToolResult( 114 | content=[types.TextContent(type="text", text=json.dumps({"error": "Internal server error."}))] # Ensure JSON is returned 115 | ) 116 | ) 117 | 118 | 119 | async def list_tools(request: types.ListToolsRequest) -> types.ServerResult: 120 | """ 121 | Handler for ListToolsRequest to list all registered tools. 122 | 123 | Args: 124 | request (types.ListToolsRequest): The request to list tools. 125 | 126 | Returns: 127 | types.ServerResult: The result containing the list of tools. 128 | """ 129 | logger.debug("Handling list_tools request.") 130 | return types.ServerResult(root=types.ListToolsResult(tools=tools)) 131 | 132 | 133 | def register_tools(chatflows: List[Dict[str, Any]], chatflow_descriptions: Dict[str, str]) -> List[types.Tool]: 134 | """ 135 | Register tools dynamically based on the provided chatflows. 136 | 137 | Args: 138 | chatflows (List[Dict[str, Any]]): List of chatflows retrieved from the Flowise API. 139 | chatflow_descriptions (Dict[str, str]): Dictionary mapping chatflow IDs to descriptions. 140 | 141 | Returns: 142 | List[types.Tool]: List of registered tools. 143 | """ 144 | global tools 145 | tools = [] # Clear existing tools before re-registration 146 | for chatflow in chatflows: 147 | try: 148 | normalized_name = normalize_tool_name(chatflow["name"]) 149 | 150 | if normalized_name in NAME_TO_ID_MAPPING: 151 | logger.warning( 152 | "Tool name conflict: '%s' already exists. Skipping chatflow '%s' (ID: '%s').", 153 | normalized_name, 154 | chatflow["name"], 155 | chatflow["id"], 156 | ) 157 | continue 158 | 159 | NAME_TO_ID_MAPPING[normalized_name] = chatflow["id"] 160 | description = chatflow_descriptions.get(chatflow["id"], chatflow["name"]) 161 | 162 | tool = types.Tool( 163 | name=normalized_name, 164 | description=description, 165 | inputSchema={ 166 | "type": "object", 167 | "required": ["question"], 168 | "properties": {"question": {"type": "string"}}, 169 | }, 170 | ) 171 | tools.append(tool) 172 | logger.debug("Registered tool: %s (ID: %s)", tool.name, chatflow["id"]) 173 | 174 | except Exception as e: 175 | logger.error("Error registering chatflow '%s' (ID: '%s'): %s", chatflow["name"], chatflow["id"], e) 176 | 177 | return tools 178 | 179 | 180 | async def start_server(): 181 | """ 182 | Start the Low-Level MCP server. 183 | """ 184 | logger.debug("Starting Low-Level MCP server...") 185 | try: 186 | async with stdio_server() as (read_stream, write_stream): 187 | await mcp.run( 188 | read_stream, 189 | write_stream, 190 | initialization_options=InitializationOptions( 191 | server_name="FlowiseMCP-with-EnvAuth", 192 | server_version="0.1.0", 193 | capabilities=types.ServerCapabilities(), 194 | ), 195 | ) 196 | except Exception as e: 197 | logger.critical("Unhandled exception in MCP server: %s", e) 198 | sys.exit(1) 199 | 200 | 201 | def run_server(): 202 | """ 203 | Run the Low-Level Flowise server by registering tools dynamically. 204 | """ 205 | try: 206 | chatflows = fetch_chatflows() 207 | if not chatflows: 208 | raise ValueError("No chatflows retrieved from the Flowise API.") 209 | except Exception as e: 210 | logger.critical("Failed to start server: %s", e) 211 | sys.exit(1) 212 | 213 | chatflow_descriptions = get_chatflow_descriptions() 214 | register_tools(chatflows, chatflow_descriptions) 215 | 216 | if not tools: 217 | logger.critical("No valid tools registered. Shutting down the server.") 218 | sys.exit(1) 219 | 220 | mcp.request_handlers[types.CallToolRequest] = dispatcher_handler 221 | logger.debug("Registered dispatcher_handler for CallToolRequest.") 222 | 223 | mcp.request_handlers[types.ListToolsRequest] = list_tools 224 | logger.debug("Registered list_tools handler.") 225 | 226 | try: 227 | asyncio.run(start_server()) 228 | except KeyboardInterrupt: 229 | logger.debug("MCP server shutdown initiated by user.") 230 | except Exception as e: 231 | logger.critical("Failed to start MCP server: %s", e) 232 | sys.exit(1) 233 | 234 | 235 | if __name__ == "__main__": 236 | run_server() 237 | ``` -------------------------------------------------------------------------------- /mcp_flowise/utils.py: -------------------------------------------------------------------------------- ```python 1 | """ 2 | Utility functions for mcp_flowise, including logging setup, chatflow filtering, and Flowise API interactions. 3 | 4 | This module centralizes shared functionality such as: 5 | 1. Logging configuration for consistent log output across the application. 6 | 2. Safe redaction of sensitive data like API keys in logs. 7 | 3. Low-level interactions with the Flowise API for predictions and chatflow management. 8 | 4. Flexible filtering of chatflows based on whitelist/blacklist criteria. 9 | """ 10 | 11 | import os 12 | import sys 13 | import logging 14 | import requests 15 | import re 16 | import json 17 | from dotenv import load_dotenv 18 | 19 | # Load environment variables from .env if present 20 | load_dotenv() 21 | 22 | # Flowise API configuration 23 | FLOWISE_API_KEY = os.getenv("FLOWISE_API_KEY", "") 24 | FLOWISE_API_ENDPOINT = os.getenv("FLOWISE_API_ENDPOINT", "http://localhost:3000") 25 | 26 | 27 | def setup_logging(debug: bool = False, log_dir: str = None, log_file: str = "debug-mcp-flowise.log") -> logging.Logger: 28 | """ 29 | Sets up logging for the application, including outputting CRITICAL and ERROR logs to stdout. 30 | 31 | Args: 32 | debug (bool): If True, set log level to DEBUG; otherwise, INFO. 33 | log_dir (str): Directory where log files will be stored. Ignored if `FLOWISE_LOGFILE_PATH` is set. 34 | log_file (str): Name of the log file. Ignored if `FLOWISE_LOGFILE_PATH` is set. 35 | 36 | Returns: 37 | logging.Logger: Configured logger instance. 38 | """ 39 | log_path = os.getenv("FLOWISE_LOGFILE_PATH") 40 | if not log_path: 41 | if log_dir is None: 42 | log_dir = os.path.join(os.path.expanduser("~"), "mcp_logs") 43 | try: 44 | os.makedirs(log_dir, exist_ok=True) 45 | log_path = os.path.join(log_dir, log_file) 46 | except PermissionError as e: 47 | # Fallback to stdout logging if directory creation fails 48 | log_path = None 49 | print(f"[ERROR] Failed to create log directory: {e}", file=sys.stderr) 50 | 51 | logger = logging.getLogger(__name__) 52 | logger.setLevel(logging.DEBUG if debug else logging.INFO) 53 | logger.propagate = False # Prevent log messages from propagating to the root logger 54 | 55 | # Remove all existing handlers to prevent accumulation 56 | for handler in logger.handlers[:]: 57 | logger.removeHandler(handler) 58 | 59 | handlers = [] 60 | 61 | if log_path: 62 | try: 63 | file_handler = logging.FileHandler(log_path, mode="a") 64 | file_handler.setLevel(logging.DEBUG if debug else logging.INFO) 65 | formatter = logging.Formatter("[%(levelname)s] %(asctime)s - %(message)s") 66 | file_handler.setFormatter(formatter) 67 | handlers.append(file_handler) 68 | except Exception as e: 69 | print(f"[ERROR] Failed to create log file handler: {e}", file=sys.stderr) 70 | 71 | # Attempt to create StreamHandler for ERROR level logs 72 | try: 73 | stdout_handler = logging.StreamHandler(sys.stdout) 74 | stdout_handler.setLevel(logging.ERROR) 75 | formatter = logging.Formatter("[%(levelname)s] %(message)s") 76 | stdout_handler.setFormatter(formatter) 77 | handlers.append(stdout_handler) 78 | except Exception as e: 79 | print(f"[ERROR] Failed to create stdout log handler: {e}", file=sys.stderr) 80 | 81 | # Add all handlers to the logger 82 | for handler in handlers: 83 | logger.addHandler(handler) 84 | 85 | if log_path: 86 | logger.debug(f"Logging initialized. Writing logs to {log_path}") 87 | else: 88 | logger.debug("Logging initialized. Logs will only appear in stdout.") 89 | return logger 90 | 91 | 92 | def redact_api_key(key: str) -> str: 93 | """ 94 | Redacts the Flowise API key for safe logging output. 95 | 96 | Args: 97 | key (str): The API key to redact. 98 | 99 | Returns: 100 | str: The redacted API key or '<not set>' if the key is invalid. 101 | """ 102 | if not key or len(key) <= 4: 103 | return "<not set>" 104 | return f"{key[:2]}{'*' * (len(key) - 4)}{key[-2:]}" 105 | 106 | 107 | def normalize_tool_name(name: str) -> str: 108 | """ 109 | Normalize tool names by converting to lowercase and replacing non-alphanumeric characters with underscores. 110 | 111 | Args: 112 | name (str): Original tool name. 113 | 114 | Returns: 115 | str: Normalized tool name. Returns 'unknown_tool' if the input is invalid. 116 | """ 117 | logger = logging.getLogger(__name__) 118 | if not name or not isinstance(name, str): 119 | logger.warning("Invalid tool name input: %s. Using default 'unknown_tool'.", name) 120 | return "unknown_tool" 121 | normalized = re.sub(r"[^a-zA-Z0-9]", "_", name).lower() 122 | logger.debug("Normalized tool name from '%s' to '%s'", name, normalized) 123 | return normalized or "unknown_tool" 124 | 125 | 126 | def filter_chatflows(chatflows: list[dict]) -> list[dict]: 127 | """ 128 | Filters chatflows based on whitelist and blacklist criteria. 129 | Whitelist takes precedence over blacklist. 130 | 131 | Args: 132 | chatflows (list[dict]): A list of chatflow dictionaries. 133 | 134 | Returns: 135 | list[dict]: Filtered list of chatflows. 136 | """ 137 | logger = logging.getLogger(__name__) 138 | 139 | # Dynamically fetch filtering criteria 140 | whitelist_ids = set(filter(bool, os.getenv("FLOWISE_WHITELIST_ID", "").split(","))) 141 | blacklist_ids = set(filter(bool, os.getenv("FLOWISE_BLACKLIST_ID", "").split(","))) 142 | whitelist_name_regex = os.getenv("FLOWISE_WHITELIST_NAME_REGEX", "") 143 | blacklist_name_regex = os.getenv("FLOWISE_BLACKLIST_NAME_REGEX", "") 144 | 145 | filtered_chatflows = [] 146 | 147 | for chatflow in chatflows: 148 | chatflow_id = chatflow.get("id", "") 149 | chatflow_name = chatflow.get("name", "") 150 | 151 | # Flags to determine inclusion 152 | is_whitelisted = False 153 | 154 | # Check Whitelist 155 | if whitelist_ids or whitelist_name_regex: 156 | if whitelist_ids and chatflow_id in whitelist_ids: 157 | is_whitelisted = True 158 | if whitelist_name_regex and re.search(whitelist_name_regex, chatflow_name): 159 | is_whitelisted = True 160 | 161 | if is_whitelisted: 162 | # If whitelisted, include regardless of blacklist 163 | logger.debug("Including whitelisted chatflow '%s' (ID: '%s').", chatflow_name, chatflow_id) 164 | filtered_chatflows.append(chatflow) 165 | continue # Skip blacklist checks 166 | else: 167 | # If not whitelisted, exclude regardless of blacklist 168 | logger.debug("Excluding non-whitelisted chatflow '%s' (ID: '%s').", chatflow_name, chatflow_id) 169 | continue 170 | else: 171 | # If no whitelist, apply blacklist directly 172 | if blacklist_ids and chatflow_id in blacklist_ids: 173 | logger.debug("Skipping chatflow '%s' (ID: '%s') - In blacklist.", chatflow_name, chatflow_id) 174 | continue # Exclude blacklisted by ID 175 | if blacklist_name_regex and re.search(blacklist_name_regex, chatflow_name): 176 | logger.debug("Skipping chatflow '%s' (ID: '%s') - Name matches blacklist regex.", chatflow_name, chatflow_id) 177 | continue # Exclude blacklisted by name 178 | 179 | # Include the chatflow if it passes all filters 180 | logger.debug("Including chatflow '%s' (ID: '%s').", chatflow_name, chatflow_id) 181 | filtered_chatflows.append(chatflow) 182 | 183 | logger.debug("Filtered chatflows: %d out of %d", len(filtered_chatflows), len(chatflows)) 184 | return filtered_chatflows 185 | 186 | def flowise_predict(chatflow_id: str, question: str) -> str: 187 | """ 188 | Sends a question to a specific chatflow ID via the Flowise API and returns the response JSON text. 189 | 190 | Args: 191 | chatflow_id (str): The ID of the Flowise chatflow to be used. 192 | question (str): The question or prompt to send to the chatflow. 193 | 194 | Returns: 195 | str: The raw JSON response text from the Flowise API, or an error message if something goes wrong. 196 | """ 197 | logger = logging.getLogger(__name__) 198 | 199 | # Construct the Flowise API URL for predictions 200 | url = f"{FLOWISE_API_ENDPOINT.rstrip('/')}/api/v1/prediction/{chatflow_id}" 201 | headers = { 202 | "Content-Type": "application/json", 203 | } 204 | if FLOWISE_API_KEY: 205 | headers["Authorization"] = f"Bearer {FLOWISE_API_KEY}" 206 | 207 | payload = {"question": question} 208 | logger.debug(f"Sending prediction request to {url} with payload: {payload}") 209 | 210 | try: 211 | # Send POST request to the Flowise API 212 | response = requests.post(url, json=payload, headers=headers, timeout=30) 213 | logger.debug(f"Prediction response code: HTTP {response.status_code}") 214 | # response.raise_for_status() 215 | 216 | # Log the raw response text for debugging 217 | logger.debug(f"Raw prediction response: {response.text}") 218 | 219 | # Return the raw JSON response text 220 | return response.text 221 | 222 | #except requests.exceptions.RequestException as e: 223 | except Exception as e: 224 | # Log and return an error message 225 | logger.error(f"Error during prediction: {e}") 226 | return json.dumps({"error": str(e)}) 227 | 228 | 229 | def fetch_chatflows() -> list[dict]: 230 | """ 231 | Fetch a list of all chatflows from the Flowise API. 232 | 233 | Returns: 234 | list of dict: Each dict contains the 'id' and 'name' of a chatflow. 235 | Returns an empty list if there's an error. 236 | """ 237 | logger = logging.getLogger(__name__) 238 | 239 | # Construct the Flowise API URL for fetching chatflows 240 | url = f"{FLOWISE_API_ENDPOINT.rstrip('/')}/api/v1/chatflows" 241 | headers = {} 242 | if FLOWISE_API_KEY: 243 | headers["Authorization"] = f"Bearer {FLOWISE_API_KEY}" 244 | 245 | logger.debug(f"Fetching chatflows from {url}") 246 | 247 | try: 248 | # Send GET request to the Flowise API 249 | response = requests.get(url, headers=headers, timeout=30) 250 | response.raise_for_status() 251 | 252 | # Parse and simplify the response data 253 | chatflows_data = response.json() 254 | simplified_chatflows = [{"id": cf["id"], "name": cf["name"]} for cf in chatflows_data] 255 | 256 | logger.debug(f"Fetched chatflows: {simplified_chatflows}") 257 | return filter_chatflows(simplified_chatflows) 258 | #except requests.exceptions.RequestException as e: 259 | except Exception as e: 260 | # Log and return an empty list on error 261 | logger.error(f"Error fetching chatflows: {e}") 262 | return [] 263 | 264 | 265 | # Set up logging before obtaining the logger 266 | DEBUG = os.getenv("DEBUG", "").lower() in ("true", "1", "yes") 267 | logger = setup_logging(debug=DEBUG) 268 | 269 | # Log key environment variable values 270 | logger.debug(f"Flowise API Key (redacted): {redact_api_key(FLOWISE_API_KEY)}") 271 | logger.debug(f"Flowise API Endpoint: {FLOWISE_API_ENDPOINT}") 272 | ```