├── 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 | [](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 |
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 |
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 |
--------------------------------------------------------------------------------