#
tokens: 19403/50000 22/22 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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 | [![smithery badge](https://smithery.ai/badge/@matthewhand/mcp-flowise)](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 | 
```