├── mcp_flowise ├── __init__.py ├── __main__.py ├── server_fastmcp.py ├── server_lowlevel.py └── utils.py ├── pytest.ini ├── tests ├── fixtures │ └── sample_chatflows.json ├── README.md ├── integration │ ├── test_tool_prediction.py │ ├── test_flowise_integration.py │ └── test_tool_registration_integration.py └── unit │ ├── test_chatflow_filters.py │ └── test_utils.py ├── pyproject.toml ├── Dockerfile ├── .github └── workflows │ └── python-pytest.yml ├── LICENSE ├── .env.example ├── smithery.yaml ├── test_mcp_handshake.py ├── test_mcp_call_tool_valid.py ├── .gitignore ├── README.md └── uv.lock /mcp_flowise/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | # asyncio_mode = strict 3 | # asyncio_default_fixture_loop_scope = function 4 | 5 | -------------------------------------------------------------------------------- /tests/fixtures/sample_chatflows.json: -------------------------------------------------------------------------------- 1 | [ 2 | {"id": "mock-id", "name": "Mock Chatflow 1"}, 3 | {"id": "mock-id-2", "name": "Mock Chatflow 2"} 4 | ] 5 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /pyproject.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 = "matthewhandau@gmail.com" } 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: -------------------------------------------------------------------------------- 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: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 mhand 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/integration/test_tool_prediction.py: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /smithery.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: -------------------------------------------------------------------------------- 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: -------------------------------------------------------------------------------- 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: -------------------------------------------------------------------------------- 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: -------------------------------------------------------------------------------- 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: -------------------------------------------------------------------------------- 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 | -------------------------------------------------------------------------------- /.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/unit/test_chatflow_filters.py: -------------------------------------------------------------------------------- 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: -------------------------------------------------------------------------------- 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: -------------------------------------------------------------------------------- 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() -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 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 |

13 | Claude Desktop Screenshot 14 |

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 |

85 | FastMCP Mode 86 |

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\\\\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/server_lowlevel.py: -------------------------------------------------------------------------------- 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: -------------------------------------------------------------------------------- 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 '' if the key is invalid. 101 | """ 102 | if not key or len(key) <= 4: 103 | return "" 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 | -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | requires-python = ">=3.13" 3 | 4 | [[package]] 5 | name = "annotated-types" 6 | version = "0.7.0" 7 | source = { registry = "https://pypi.org/simple" } 8 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 9 | wheels = [ 10 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 11 | ] 12 | 13 | [[package]] 14 | name = "anyio" 15 | version = "4.8.0" 16 | source = { registry = "https://pypi.org/simple" } 17 | dependencies = [ 18 | { name = "idna" }, 19 | { name = "sniffio" }, 20 | ] 21 | sdist = { url = "https://files.pythonhosted.org/packages/a3/73/199a98fc2dae33535d6b8e8e6ec01f8c1d76c9adb096c6b7d64823038cde/anyio-4.8.0.tar.gz", hash = "sha256:1d9fe889df5212298c0c0723fa20479d1b94883a2df44bd3897aa91083316f7a", size = 181126 } 22 | wheels = [ 23 | { url = "https://files.pythonhosted.org/packages/46/eb/e7f063ad1fec6b3178a3cd82d1a3c4de82cccf283fc42746168188e1cdd5/anyio-4.8.0-py3-none-any.whl", hash = "sha256:b5011f270ab5eb0abf13385f851315585cc37ef330dd88e27ec3d34d651fd47a", size = 96041 }, 24 | ] 25 | 26 | [[package]] 27 | name = "certifi" 28 | version = "2024.12.14" 29 | source = { registry = "https://pypi.org/simple" } 30 | sdist = { url = "https://files.pythonhosted.org/packages/0f/bd/1d41ee578ce09523c81a15426705dd20969f5abf006d1afe8aeff0dd776a/certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db", size = 166010 } 31 | wheels = [ 32 | { url = "https://files.pythonhosted.org/packages/a5/32/8f6669fc4798494966bf446c8c4a162e0b5d893dff088afddf76414f70e1/certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56", size = 164927 }, 33 | ] 34 | 35 | [[package]] 36 | name = "charset-normalizer" 37 | version = "3.4.1" 38 | source = { registry = "https://pypi.org/simple" } 39 | sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } 40 | wheels = [ 41 | { url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 }, 42 | { url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 }, 43 | { url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 }, 44 | { url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 }, 45 | { url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 }, 46 | { url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 }, 47 | { url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 }, 48 | { url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 }, 49 | { url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 }, 50 | { url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 }, 51 | { url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 }, 52 | { url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 }, 53 | { url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 }, 54 | { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, 55 | ] 56 | 57 | [[package]] 58 | name = "click" 59 | version = "8.1.8" 60 | source = { registry = "https://pypi.org/simple" } 61 | dependencies = [ 62 | { name = "colorama", marker = "sys_platform == 'win32'" }, 63 | ] 64 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } 65 | wheels = [ 66 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, 67 | ] 68 | 69 | [[package]] 70 | name = "colorama" 71 | version = "0.4.6" 72 | source = { registry = "https://pypi.org/simple" } 73 | sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697 } 74 | wheels = [ 75 | { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 }, 76 | ] 77 | 78 | [[package]] 79 | name = "h11" 80 | version = "0.14.0" 81 | source = { registry = "https://pypi.org/simple" } 82 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 83 | wheels = [ 84 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 85 | ] 86 | 87 | [[package]] 88 | name = "httpcore" 89 | version = "1.0.7" 90 | source = { registry = "https://pypi.org/simple" } 91 | dependencies = [ 92 | { name = "certifi" }, 93 | { name = "h11" }, 94 | ] 95 | sdist = { url = "https://files.pythonhosted.org/packages/6a/41/d7d0a89eb493922c37d343b607bc1b5da7f5be7e383740b4753ad8943e90/httpcore-1.0.7.tar.gz", hash = "sha256:8551cb62a169ec7162ac7be8d4817d561f60e08eaa485234898414bb5a8a0b4c", size = 85196 } 96 | wheels = [ 97 | { url = "https://files.pythonhosted.org/packages/87/f5/72347bc88306acb359581ac4d52f23c0ef445b57157adedb9aee0cd689d2/httpcore-1.0.7-py3-none-any.whl", hash = "sha256:a3fff8f43dc260d5bd363d9f9cf1830fa3a458b332856f34282de498ed420edd", size = 78551 }, 98 | ] 99 | 100 | [[package]] 101 | name = "httpx" 102 | version = "0.28.1" 103 | source = { registry = "https://pypi.org/simple" } 104 | dependencies = [ 105 | { name = "anyio" }, 106 | { name = "certifi" }, 107 | { name = "httpcore" }, 108 | { name = "idna" }, 109 | ] 110 | sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 } 111 | wheels = [ 112 | { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 }, 113 | ] 114 | 115 | [[package]] 116 | name = "httpx-sse" 117 | version = "0.4.0" 118 | source = { registry = "https://pypi.org/simple" } 119 | sdist = { url = "https://files.pythonhosted.org/packages/4c/60/8f4281fa9bbf3c8034fd54c0e7412e66edbab6bc74c4996bd616f8d0406e/httpx-sse-0.4.0.tar.gz", hash = "sha256:1e81a3a3070ce322add1d3529ed42eb5f70817f45ed6ec915ab753f961139721", size = 12624 } 120 | wheels = [ 121 | { url = "https://files.pythonhosted.org/packages/e1/9b/a181f281f65d776426002f330c31849b86b31fc9d848db62e16f03ff739f/httpx_sse-0.4.0-py3-none-any.whl", hash = "sha256:f329af6eae57eaa2bdfd962b42524764af68075ea87370a2de920af5341e318f", size = 7819 }, 122 | ] 123 | 124 | [[package]] 125 | name = "idna" 126 | version = "3.10" 127 | source = { registry = "https://pypi.org/simple" } 128 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 129 | wheels = [ 130 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 131 | ] 132 | 133 | [[package]] 134 | name = "iniconfig" 135 | version = "2.0.0" 136 | source = { registry = "https://pypi.org/simple" } 137 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } 138 | wheels = [ 139 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, 140 | ] 141 | 142 | [[package]] 143 | name = "markdown-it-py" 144 | version = "3.0.0" 145 | source = { registry = "https://pypi.org/simple" } 146 | dependencies = [ 147 | { name = "mdurl" }, 148 | ] 149 | sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596 } 150 | wheels = [ 151 | { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528 }, 152 | ] 153 | 154 | [[package]] 155 | name = "mcp" 156 | version = "1.2.0" 157 | source = { registry = "https://pypi.org/simple" } 158 | dependencies = [ 159 | { name = "anyio" }, 160 | { name = "httpx" }, 161 | { name = "httpx-sse" }, 162 | { name = "pydantic" }, 163 | { name = "pydantic-settings" }, 164 | { name = "sse-starlette" }, 165 | { name = "starlette" }, 166 | { name = "uvicorn" }, 167 | ] 168 | sdist = { url = "https://files.pythonhosted.org/packages/ab/a5/b08dc846ebedae9f17ced878e6975826e90e448cd4592f532f6a88a925a7/mcp-1.2.0.tar.gz", hash = "sha256:2b06c7ece98d6ea9e6379caa38d74b432385c338fb530cb82e2c70ea7add94f5", size = 102973 } 169 | wheels = [ 170 | { url = "https://files.pythonhosted.org/packages/af/84/fca78f19ac8ce6c53ba416247c71baa53a9e791e98d3c81edbc20a77d6d1/mcp-1.2.0-py3-none-any.whl", hash = "sha256:1d0e77d8c14955a5aea1f5aa1f444c8e531c09355c829b20e42f7a142bc0755f", size = 66468 }, 171 | ] 172 | 173 | [package.optional-dependencies] 174 | cli = [ 175 | { name = "python-dotenv" }, 176 | { name = "typer" }, 177 | ] 178 | 179 | [[package]] 180 | name = "mcp-flowise" 181 | version = "0.1.0" 182 | source = { editable = "." } 183 | dependencies = [ 184 | { name = "mcp", extra = ["cli"] }, 185 | { name = "python-dotenv" }, 186 | { name = "requests" }, 187 | ] 188 | 189 | [package.dev-dependencies] 190 | dev = [ 191 | { name = "pytest" }, 192 | ] 193 | 194 | [package.metadata] 195 | requires-dist = [ 196 | { name = "mcp", extras = ["cli"], specifier = ">=1.2.0" }, 197 | { name = "python-dotenv", specifier = ">=1.0.1" }, 198 | { name = "requests", specifier = ">=2.25.0" }, 199 | ] 200 | 201 | [package.metadata.requires-dev] 202 | dev = [{ name = "pytest", specifier = ">=8.3.4" }] 203 | 204 | [[package]] 205 | name = "mdurl" 206 | version = "0.1.2" 207 | source = { registry = "https://pypi.org/simple" } 208 | sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729 } 209 | wheels = [ 210 | { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, 211 | ] 212 | 213 | [[package]] 214 | name = "packaging" 215 | version = "24.2" 216 | source = { registry = "https://pypi.org/simple" } 217 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 218 | wheels = [ 219 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 220 | ] 221 | 222 | [[package]] 223 | name = "pluggy" 224 | version = "1.5.0" 225 | source = { registry = "https://pypi.org/simple" } 226 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } 227 | wheels = [ 228 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, 229 | ] 230 | 231 | [[package]] 232 | name = "pydantic" 233 | version = "2.10.5" 234 | source = { registry = "https://pypi.org/simple" } 235 | dependencies = [ 236 | { name = "annotated-types" }, 237 | { name = "pydantic-core" }, 238 | { name = "typing-extensions" }, 239 | ] 240 | sdist = { url = "https://files.pythonhosted.org/packages/6a/c7/ca334c2ef6f2e046b1144fe4bb2a5da8a4c574e7f2ebf7e16b34a6a2fa92/pydantic-2.10.5.tar.gz", hash = "sha256:278b38dbbaec562011d659ee05f63346951b3a248a6f3642e1bc68894ea2b4ff", size = 761287 } 241 | wheels = [ 242 | { url = "https://files.pythonhosted.org/packages/58/26/82663c79010b28eddf29dcdd0ea723439535fa917fce5905885c0e9ba562/pydantic-2.10.5-py3-none-any.whl", hash = "sha256:4dd4e322dbe55472cb7ca7e73f4b63574eecccf2835ffa2af9021ce113c83c53", size = 431426 }, 243 | ] 244 | 245 | [[package]] 246 | name = "pydantic-core" 247 | version = "2.27.2" 248 | source = { registry = "https://pypi.org/simple" } 249 | dependencies = [ 250 | { name = "typing-extensions" }, 251 | ] 252 | sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } 253 | wheels = [ 254 | { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, 255 | { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, 256 | { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, 257 | { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, 258 | { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, 259 | { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, 260 | { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, 261 | { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, 262 | { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, 263 | { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, 264 | { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, 265 | { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, 266 | { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, 267 | { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, 268 | ] 269 | 270 | [[package]] 271 | name = "pydantic-settings" 272 | version = "2.7.1" 273 | source = { registry = "https://pypi.org/simple" } 274 | dependencies = [ 275 | { name = "pydantic" }, 276 | { name = "python-dotenv" }, 277 | ] 278 | sdist = { url = "https://files.pythonhosted.org/packages/73/7b/c58a586cd7d9ac66d2ee4ba60ca2d241fa837c02bca9bea80a9a8c3d22a9/pydantic_settings-2.7.1.tar.gz", hash = "sha256:10c9caad35e64bfb3c2fbf70a078c0e25cc92499782e5200747f942a065dec93", size = 79920 } 279 | wheels = [ 280 | { url = "https://files.pythonhosted.org/packages/b4/46/93416fdae86d40879714f72956ac14df9c7b76f7d41a4d68aa9f71a0028b/pydantic_settings-2.7.1-py3-none-any.whl", hash = "sha256:590be9e6e24d06db33a4262829edef682500ef008565a969c73d39d5f8bfb3fd", size = 29718 }, 281 | ] 282 | 283 | [[package]] 284 | name = "pygments" 285 | version = "2.19.1" 286 | source = { registry = "https://pypi.org/simple" } 287 | sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581 } 288 | wheels = [ 289 | { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, 290 | ] 291 | 292 | [[package]] 293 | name = "pytest" 294 | version = "8.3.4" 295 | source = { registry = "https://pypi.org/simple" } 296 | dependencies = [ 297 | { name = "colorama", marker = "sys_platform == 'win32'" }, 298 | { name = "iniconfig" }, 299 | { name = "packaging" }, 300 | { name = "pluggy" }, 301 | ] 302 | sdist = { url = "https://files.pythonhosted.org/packages/05/35/30e0d83068951d90a01852cb1cef56e5d8a09d20c7f511634cc2f7e0372a/pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761", size = 1445919 } 303 | wheels = [ 304 | { url = "https://files.pythonhosted.org/packages/11/92/76a1c94d3afee238333bc0a42b82935dd8f9cf8ce9e336ff87ee14d9e1cf/pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6", size = 343083 }, 305 | ] 306 | 307 | [[package]] 308 | name = "python-dotenv" 309 | version = "1.0.1" 310 | source = { registry = "https://pypi.org/simple" } 311 | sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } 312 | wheels = [ 313 | { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, 314 | ] 315 | 316 | [[package]] 317 | name = "requests" 318 | version = "2.32.3" 319 | source = { registry = "https://pypi.org/simple" } 320 | dependencies = [ 321 | { name = "certifi" }, 322 | { name = "charset-normalizer" }, 323 | { name = "idna" }, 324 | { name = "urllib3" }, 325 | ] 326 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } 327 | wheels = [ 328 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, 329 | ] 330 | 331 | [[package]] 332 | name = "rich" 333 | version = "13.9.4" 334 | source = { registry = "https://pypi.org/simple" } 335 | dependencies = [ 336 | { name = "markdown-it-py" }, 337 | { name = "pygments" }, 338 | ] 339 | sdist = { url = "https://files.pythonhosted.org/packages/ab/3a/0316b28d0761c6734d6bc14e770d85506c986c85ffb239e688eeaab2c2bc/rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098", size = 223149 } 340 | wheels = [ 341 | { url = "https://files.pythonhosted.org/packages/19/71/39c7c0d87f8d4e6c020a393182060eaefeeae6c01dab6a84ec346f2567df/rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90", size = 242424 }, 342 | ] 343 | 344 | [[package]] 345 | name = "shellingham" 346 | version = "1.5.4" 347 | source = { registry = "https://pypi.org/simple" } 348 | sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310 } 349 | wheels = [ 350 | { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755 }, 351 | ] 352 | 353 | [[package]] 354 | name = "sniffio" 355 | version = "1.3.1" 356 | source = { registry = "https://pypi.org/simple" } 357 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 358 | wheels = [ 359 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 360 | ] 361 | 362 | [[package]] 363 | name = "sse-starlette" 364 | version = "2.2.1" 365 | source = { registry = "https://pypi.org/simple" } 366 | dependencies = [ 367 | { name = "anyio" }, 368 | { name = "starlette" }, 369 | ] 370 | sdist = { url = "https://files.pythonhosted.org/packages/71/a4/80d2a11af59fe75b48230846989e93979c892d3a20016b42bb44edb9e398/sse_starlette-2.2.1.tar.gz", hash = "sha256:54470d5f19274aeed6b2d473430b08b4b379ea851d953b11d7f1c4a2c118b419", size = 17376 } 371 | wheels = [ 372 | { url = "https://files.pythonhosted.org/packages/d9/e0/5b8bd393f27f4a62461c5cf2479c75a2cc2ffa330976f9f00f5f6e4f50eb/sse_starlette-2.2.1-py3-none-any.whl", hash = "sha256:6410a3d3ba0c89e7675d4c273a301d64649c03a5ef1ca101f10b47f895fd0e99", size = 10120 }, 373 | ] 374 | 375 | [[package]] 376 | name = "starlette" 377 | version = "0.45.2" 378 | source = { registry = "https://pypi.org/simple" } 379 | dependencies = [ 380 | { name = "anyio" }, 381 | ] 382 | sdist = { url = "https://files.pythonhosted.org/packages/90/4f/e1c9f4ec3dae67a94c9285ed275355d5f7cf0f3a5c34538c8ae5412af550/starlette-0.45.2.tar.gz", hash = "sha256:bba1831d15ae5212b22feab2f218bab6ed3cd0fc2dc1d4442443bb1ee52260e0", size = 2574026 } 383 | wheels = [ 384 | { url = "https://files.pythonhosted.org/packages/aa/ab/fe4f57c83620b39dfc9e7687ebad59129ff05170b99422105019d9a65eec/starlette-0.45.2-py3-none-any.whl", hash = "sha256:4daec3356fb0cb1e723a5235e5beaf375d2259af27532958e2d79df549dad9da", size = 71505 }, 385 | ] 386 | 387 | [[package]] 388 | name = "typer" 389 | version = "0.15.1" 390 | source = { registry = "https://pypi.org/simple" } 391 | dependencies = [ 392 | { name = "click" }, 393 | { name = "rich" }, 394 | { name = "shellingham" }, 395 | { name = "typing-extensions" }, 396 | ] 397 | sdist = { url = "https://files.pythonhosted.org/packages/cb/ce/dca7b219718afd37a0068f4f2530a727c2b74a8b6e8e0c0080a4c0de4fcd/typer-0.15.1.tar.gz", hash = "sha256:a0588c0a7fa68a1978a069818657778f86abe6ff5ea6abf472f940a08bfe4f0a", size = 99789 } 398 | wheels = [ 399 | { url = "https://files.pythonhosted.org/packages/d0/cc/0a838ba5ca64dc832aa43f727bd586309846b0ffb2ce52422543e6075e8a/typer-0.15.1-py3-none-any.whl", hash = "sha256:7994fb7b8155b64d3402518560648446072864beefd44aa2dc36972a5972e847", size = 44908 }, 400 | ] 401 | 402 | [[package]] 403 | name = "typing-extensions" 404 | version = "4.12.2" 405 | source = { registry = "https://pypi.org/simple" } 406 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 407 | wheels = [ 408 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 409 | ] 410 | 411 | [[package]] 412 | name = "urllib3" 413 | version = "2.3.0" 414 | source = { registry = "https://pypi.org/simple" } 415 | sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 } 416 | wheels = [ 417 | { url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 }, 418 | ] 419 | 420 | [[package]] 421 | name = "uvicorn" 422 | version = "0.34.0" 423 | source = { registry = "https://pypi.org/simple" } 424 | dependencies = [ 425 | { name = "click" }, 426 | { name = "h11" }, 427 | ] 428 | sdist = { url = "https://files.pythonhosted.org/packages/4b/4d/938bd85e5bf2edeec766267a5015ad969730bb91e31b44021dfe8b22df6c/uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9", size = 76568 } 429 | wheels = [ 430 | { url = "https://files.pythonhosted.org/packages/61/14/33a3a1352cfa71812a3a21e8c9bfb83f60b0011f5e36f2b1399d51928209/uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4", size = 62315 }, 431 | ] 432 | --------------------------------------------------------------------------------