# 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:3010)
7 | FLOWISE_API_ENDPOINT=http://localhost:3010
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 | .env
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/@andydukes/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/@andydukes/mcp-flowise):
32 |
33 | ```bash
34 | npx -y @smithery/cli install @andydukes/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/andydukes/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/andydukes/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:3010/"
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:3010`).
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 | "pytest>=8.3.4",
16 | "python-dotenv>=1.0.1",
17 | "requests>=2.25.0",
18 | ]
19 |
20 | [project.scripts]
21 | mcp-flowise = "mcp_flowise.__main__:main"
22 |
23 | [dependency-groups]
24 | dev = [
25 | "pytest>=8.3.4",
26 | ]
27 |
28 | [tool.setuptools.packages]
29 | find = {include = ["mcp_flowise", "mcp_flowise.*"]}
30 |
```
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
```dockerfile
1 | # Use a Python base image that satisfies the project requirements
2 | FROM python:3.12-slim
3 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
4 |
5 | # Set the working directory in the container
6 |
7 |
8 | # Copy the project into the image
9 | ADD . /app
10 |
11 | # Sync the project into a new environment, asserting the lockfile is up to date
12 | WORKDIR /app
13 | RUN uv sync --locked
14 |
15 | # Expose the port the app runs on
16 | EXPOSE 8000
17 |
18 | # Set environment variables required for running the MCP server
19 | ENV FLOWISE_API_KEY=Eq8Nu-hPsTqNaYudT71HLllG8B0sx_oM6h64Q9wOC8Q
20 | ENV FLOWISE_API_ENDPOINT=http://localhost:3010
21 |
22 | # Define the command to run the app
23 | CMD ["uvx", "--from", "git+https://github.com/andydukes/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:3010
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/andydukes/mcp-flowise', 'mcp-flowise'], env: {FLOWISE_API_KEY: config.flowiseApiKey, FLOWISE_API_ENDPOINT: config.flowiseApiEndpoint || 'http://localhost:3010', FLOWISE_SIMPLE_MODE: config.flowiseSimpleMode ? 'true' : 'false', FLOWISE_CHATFLOW_ID: config.flowiseChatflowId || '', FLOWISE_ASSISTANT_ID: config.flowiseAssistantId || ''}})
32 |
```
--------------------------------------------------------------------------------
/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:3010")
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:3010")
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 |
```