├── requirements.txt ├── src ├── mcp_explorer │ ├── green.png │ ├── red.png │ └── mcp_explorer.py ├── fastmcp_http │ ├── fastmcp_http │ │ ├── __init__.py │ │ ├── __init__.pyi │ │ ├── client.pyi │ │ ├── server.pyi │ │ ├── client.py │ │ └── server.py │ ├── pyproject.toml │ └── readme.md └── mcp_registry │ ├── permission_management │ ├── qt_permission_dialog.py │ └── permission_server.py │ └── mcp_registry_server.py ├── start_registry_server.py ├── example ├── client_example.py └── server_example.py ├── LICENSE ├── readme.md └── .gitignore /requirements.txt: -------------------------------------------------------------------------------- 1 | flask[async] 2 | requests 3 | fastmcp-http 4 | pyside6 -------------------------------------------------------------------------------- /src/mcp_explorer/green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARadRareness/mcp-registry/HEAD/src/mcp_explorer/green.png -------------------------------------------------------------------------------- /src/mcp_explorer/red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ARadRareness/mcp-registry/HEAD/src/mcp_explorer/red.png -------------------------------------------------------------------------------- /src/fastmcp_http/fastmcp_http/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import FastMCPHttpClient 2 | from .server import FastMCPHttpServer 3 | -------------------------------------------------------------------------------- /start_registry_server.py: -------------------------------------------------------------------------------- 1 | from src.mcp_registry import mcp_registry_server 2 | 3 | if __name__ == "__main__": 4 | mcp_registry_server.run() 5 | -------------------------------------------------------------------------------- /src/fastmcp_http/fastmcp_http/__init__.pyi: -------------------------------------------------------------------------------- 1 | from .client import FastMCPHttpClient as FastMCPHttpClient 2 | from .server import FastMCPHttpServer as FastMCPHttpServer 3 | 4 | __all__ = ["FastMCPHttpClient", "FastMCPHttpServer"] 5 | -------------------------------------------------------------------------------- /example/client_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | FastMCP Echo Client Example 3 | """ 4 | 5 | from fastmcp_http.client import FastMCPHttpClient 6 | 7 | 8 | def main(): 9 | # Create client 10 | client = FastMCPHttpClient("http://127.0.0.1:31337") 11 | 12 | servers = client.list_servers() 13 | print(servers) 14 | 15 | tools = client.list_tools() 16 | print(tools) 17 | 18 | result = client.call_tool("EchoServer.echo_tool", {"text": "Hello, World!"}) 19 | print(result) 20 | 21 | 22 | if __name__ == "__main__": 23 | main() 24 | -------------------------------------------------------------------------------- /src/fastmcp_http/pyproject.toml: -------------------------------------------------------------------------------- 1 | # pyproject.toml 2 | 3 | [build-system] 4 | requires = ["setuptools>=61.0.0", "wheel"] 5 | build-backend = "setuptools.build_meta" 6 | 7 | [project] 8 | name = "fastmcp_http" 9 | version = "0.1.3" 10 | description = "FastMCP services via HTTP." 11 | readme = "readme.md" 12 | authors = [{ name = "Aradrareness", email = "38016746+ARadRareness@users.noreply.github.com" }] 13 | license = { text = "MIT" } 14 | classifiers = [ 15 | "Programming Language :: Python :: 3", 16 | "License :: OSI Approved :: MIT License", 17 | "Operating System :: OS Independent", 18 | ] 19 | dependencies = [ 20 | "requests", 21 | "mcp>=1.2.0rc1", 22 | "flask[async]", 23 | ] 24 | requires-python = ">=3.6" 25 | 26 | [project.urls] 27 | Homepage = "https://github.com/ARadRareness/mcp-registry" -------------------------------------------------------------------------------- /example/server_example.py: -------------------------------------------------------------------------------- 1 | """ 2 | FastMCP Echo Server 3 | """ 4 | 5 | import logging 6 | from fastmcp_http.server import FastMCPHttpServer 7 | 8 | logging.basicConfig( 9 | level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" 10 | ) 11 | logger = logging.getLogger(__name__) 12 | 13 | # Create server 14 | mcp = FastMCPHttpServer("EchoServer", description="A Server that echoes text") 15 | 16 | 17 | @mcp.tool() 18 | def echo_tool(text: str) -> str: 19 | """Echo the input text""" 20 | return f"Echo: {text}" 21 | 22 | 23 | @mcp.resource("echo://static") 24 | def echo_resource() -> str: 25 | return "Echo!" 26 | 27 | 28 | @mcp.resource("echo://{text}") 29 | def echo_template(text: str) -> str: 30 | """Echo the input text""" 31 | return f"Echo: {text}" 32 | 33 | 34 | @mcp.prompt("echo") 35 | def echo_prompt(text: str) -> str: 36 | return text 37 | 38 | 39 | if __name__ == "__main__": 40 | mcp.run_http() 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 ARadRareness 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 | -------------------------------------------------------------------------------- /src/mcp_registry/permission_management/qt_permission_dialog.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import os 3 | from PySide6.QtWidgets import QApplication, QMessageBox 4 | from PySide6.QtCore import Qt 5 | 6 | 7 | def read_file_content(file_path: str) -> str: 8 | if not os.path.exists(file_path): 9 | return "" 10 | with open(file_path, "r") as file: 11 | return file.read() 12 | 13 | 14 | def show_permission_dialog(file_path: str) -> bool: 15 | app = QApplication(sys.argv) 16 | 17 | description = read_file_content(file_path) 18 | 19 | msg_box = QMessageBox() 20 | msg_box.setWindowTitle("Permission Request") 21 | msg_box.setText(description) 22 | msg_box.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel) 23 | msg_box.setDefaultButton(QMessageBox.Cancel) 24 | msg_box.setWindowFlags(msg_box.windowFlags() | Qt.WindowStaysOnTopHint) 25 | 26 | result = msg_box.exec() 27 | return result == QMessageBox.Ok 28 | 29 | 30 | if __name__ == "__main__": 31 | if len(sys.argv) > 1: 32 | if show_permission_dialog(sys.argv[1]): 33 | sys.exit(0) 34 | else: 35 | sys.exit(1) 36 | -------------------------------------------------------------------------------- /src/fastmcp_http/fastmcp_http/client.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any, List 2 | from mcp import Tool, Resource 3 | from mcp.types import Prompt, TextContent, ImageContent, EmbeddedResource 4 | 5 | class FastMCPHttpClient: 6 | base_url: str 7 | 8 | def __init__(self, base_url: str) -> None: 9 | """Initialize the FastMCP HTTP client. 10 | 11 | Args: 12 | base_url: Base URL of the FastMCP HTTP server 13 | """ 14 | ... 15 | 16 | def list_tools(self) -> List[Tool]: 17 | """List available tools from the server.""" 18 | ... 19 | 20 | def call_tool( 21 | self, name: str, arguments: dict[str, Any] 22 | ) -> List[TextContent | ImageContent | EmbeddedResource]: 23 | """Call a tool with the given arguments.""" 24 | ... 25 | 26 | def list_resources(self) -> List[Resource]: 27 | """List available resources from the server.""" 28 | ... 29 | 30 | def read_resource(self, uri: str) -> bytes: 31 | """Read a resource from the server.""" 32 | ... 33 | 34 | def list_prompts(self) -> List[Prompt]: 35 | """List available prompts from the server.""" 36 | ... 37 | 38 | def get_prompt(self, name: str, arguments: dict[str, Any]) -> Prompt: 39 | """Get a prompt with the given arguments.""" 40 | ... 41 | -------------------------------------------------------------------------------- /src/fastmcp_http/readme.md: -------------------------------------------------------------------------------- 1 | # FastMCP-HTTP 2 | 3 | FastMCP-HTTP is a Python package that provides a HTTP REST client-server solution for MCP. It offers a unified interface for accessing tools, prompts and resources through HTTP endpoints. 4 | 5 | 6 | ## Installation 7 | 8 | ### From PyPI 9 | 10 | ```bash 11 | pip install fastmcp-http 12 | ``` 13 | 14 | ### From source 15 | 16 | To build and install the package from source: 17 | 18 | 1. Clone the repository 19 | 2. Navigate to the project directory 20 | 3. Install build and twine 21 | ```bash 22 | python -m pip install build 23 | ``` 24 | 4. Build the package: 25 | ```bash 26 | python -m build 27 | ``` 28 | 5. Install the package: 29 | ```bash 30 | pip install dist/fastmcp_http-X.Y.Z-py3-none-any.whl 31 | ``` 32 | 33 | # Examples 34 | 35 | ## FastMCPHttpServer 36 | 37 | ```python 38 | from fastmcp_http.server import FastMCPHttpServer 39 | 40 | mcp = FastMCPHttpServer("MyServer", description="My MCP Server") 41 | 42 | @mcp.tool() 43 | def my_tool(text: str) -> str: 44 | return f"Processed: {text}" 45 | 46 | if __name__ == "__main__": 47 | mcp.run_http(register_server=False, port=15151) 48 | ``` 49 | 50 | ## FastMCPHttpClient 51 | 52 | ```python 53 | from fastmcp_http.client import FastMCPHttpClient 54 | 55 | 56 | def main(): 57 | client = FastMCPHttpClient("http://127.0.0.1:15151") 58 | 59 | tools = client.list_tools() 60 | print(tools) 61 | 62 | result = client.call_tool("my_tool", {"text": "Hello, World!"}) 63 | print(result) 64 | 65 | 66 | if __name__ == "__main__": 67 | main() 68 | ``` -------------------------------------------------------------------------------- /src/mcp_registry/permission_management/permission_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | FastMCP Permission Server 3 | Handles permission requests through dialog boxes 4 | """ 5 | 6 | import logging 7 | import tempfile 8 | import os 9 | import sys 10 | import subprocess 11 | import time 12 | from fastmcp_http.server import FastMCPHttpServer 13 | 14 | logging.basicConfig( 15 | level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" 16 | ) 17 | logger = logging.getLogger(__name__) 18 | 19 | # Create server 20 | mcp = FastMCPHttpServer( 21 | "PermissionServer", description="A Server that handles permission requests" 22 | ) 23 | 24 | 25 | @mcp.tool() 26 | def ask_for_permission(description: str) -> bool: 27 | """ 28 | Display a permission request dialog and return the user's choice 29 | 30 | Args: 31 | description: The permission request description to show to the user 32 | 33 | Returns: 34 | bool: True if permission granted, False if denied 35 | """ 36 | with tempfile.NamedTemporaryFile(mode="w+t", delete=False) as temp_file: 37 | temp_file_name = temp_file.name 38 | temp_file.write(description) 39 | temp_file.flush() 40 | 41 | try: 42 | dialog_script = os.path.join( 43 | os.path.dirname(__file__), "qt_permission_dialog.py" 44 | ) 45 | result = subprocess.call([sys.executable, dialog_script, temp_file_name]) 46 | return result == 0 47 | finally: 48 | os.remove(temp_file_name) 49 | 50 | 51 | def start_server_delayed(): 52 | time.sleep(5) 53 | mcp.run_http() 54 | 55 | 56 | if __name__ == "__main__": 57 | print(ask_for_permission("Do you want to allow this?")) 58 | -------------------------------------------------------------------------------- /src/fastmcp_http/fastmcp_http/server.pyi: -------------------------------------------------------------------------------- 1 | from typing import Any, Optional 2 | from flask import Flask 3 | from mcp.server.fastmcp import FastMCP 4 | 5 | class FastMCPHttpServer(FastMCP): 6 | flask_app: Flask 7 | 8 | def __init__( 9 | self, 10 | name: Optional[str] = None, 11 | description: Optional[str] = None, 12 | **settings: Any, 13 | ) -> None: 14 | """Initialize the FastMCP HTTP server. 15 | 16 | Args: 17 | name: Optional name for the server 18 | **settings: Additional settings to pass to FastMCP 19 | """ 20 | ... 21 | 22 | def _setup_routes(self) -> None: 23 | """Set up the Flask routes for the HTTP server. 24 | 25 | Sets up the following endpoints: 26 | - GET /tools: List available tools 27 | - POST /tools/: Call a specific tool 28 | - GET /resources: List available resources 29 | - GET /resources/: Read a specific resource 30 | - GET /prompts: List available prompts 31 | - POST /prompts/: Get a specific prompt 32 | """ 33 | ... 34 | 35 | def register_server( 36 | self, 37 | server_url: str = "http://127.0.0.1", 38 | registry_url: str = "http://127.0.0.1:31337", 39 | ) -> int: 40 | """Register the server with the registry. Returns the port to use for the server. 41 | 42 | Args: 43 | server_url: URL of the server to register. 44 | registry_url: URL of the registry to register with. 45 | """ 46 | ... 47 | 48 | def run_http( 49 | self, host: str = "0.0.0.0", register_server: bool = True, port: int = 5000 50 | ) -> None: 51 | """Run the FastMCP HTTP server. 52 | 53 | Args: 54 | host: Host to bind to (default: "0.0.0.0") 55 | port: Port to listen on (default: 5000) 56 | register_server: Whether to register the server with the registry (default: True) 57 | """ 58 | ... 59 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # MCP Registry & FastMCP-HTTP 2 | This repository is a combination of two complementary components. 3 | 4 | MCP Registry is a server solution that manages and coordinates multiple MCP (Model Context Protocol) servers. It provides: 5 | - Central registration for MCP servers 6 | - Dynamic port allocation 7 | - Health monitoring of registered servers 8 | - Unified access to tools across all registered servers 9 | 10 | FastMCP-HTTP is a Python package that provides an HTTP REST client-server solution for MCP. It offers a unified interface for accessing tools, prompts and resources through HTTP endpoints. 11 | 12 | # Components 13 | 14 | ## HTTP Server 15 | The FastMCPHttpServer provides an HTTP server solution for MCP. 16 | 17 | ## HTTP Client 18 | The FastMCPHttpClient offers both synchronous and asynchronous interfaces to interact with FastMCP servers. 19 | It is extended to also function as a client to the MCP registry server. 20 | 21 | ## Registry Server 22 | The MCP Registry Server acts as a central coordinator for multiple MCP servers. It handles server registration, health monitoring, and provides a unified interface to access tools across all connected servers. 23 | 24 | ## MCP Explorer 25 | The MCP Explorer provides a graphical user interface for interacting with MCP servers and their tools. 26 | 27 | # Installation 28 | 29 | 1. Clone the repository 30 | 2. Install the dependencies: 31 | ```bash 32 | pip install -r requirements.txt 33 | ``` 34 | 35 | # Examples 36 | 37 | ## Using the registry server 38 | 39 | ### FastMCPHttpServer 40 | 41 | ```python 42 | from fastmcp_http.server import FastMCPHttpServer 43 | 44 | mcp = FastMCPHttpServer("MyServer", description="My MCP Server") 45 | 46 | @mcp.tool() 47 | def my_tool(text: str) -> str: 48 | return f"Processed: {text}" 49 | 50 | if __name__ == "__main__": 51 | mcp.run_http() 52 | ``` 53 | 54 | ### FastMCPHttpClient 55 | 56 | ```python 57 | from fastmcp_http.client import FastMCPHttpClient 58 | 59 | 60 | def main(): 61 | # Connect to the registry server 62 | client = FastMCPHttpClient("http://127.0.0.1:31337") 63 | 64 | servers = client.list_servers() 65 | print(servers) 66 | 67 | tools = client.list_tools() 68 | print(tools) 69 | 70 | result = client.call_tool("my_tool", {"text": "Hello, World!"}) 71 | print(result) 72 | 73 | 74 | if __name__ == "__main__": 75 | main() 76 | ``` 77 | 78 | ## Standalone 79 | 80 | ### FastMCPHttpServer 81 | 82 | ```python 83 | from fastmcp_http.server import FastMCPHttpServer 84 | 85 | mcp = FastMCPHttpServer("MyServer", description="My MCP Server") 86 | 87 | @mcp.tool() 88 | def my_tool(text: str) -> str: 89 | return f"Processed: {text}" 90 | 91 | if __name__ == "__main__": 92 | mcp.run_http(register_server=False, port=15151) 93 | ``` 94 | 95 | ### FastMCPHttpClient 96 | 97 | ```python 98 | from fastmcp_http.client import FastMCPHttpClient 99 | 100 | 101 | def main(): 102 | client = FastMCPHttpClient("http://127.0.0.1:15151") 103 | 104 | tools = client.list_tools() 105 | print(tools) 106 | 107 | result = client.call_tool("my_tool", {"text": "Hello, World!"}) 108 | print(result) 109 | 110 | 111 | if __name__ == "__main__": 112 | main() 113 | ``` 114 | 115 | ## Usage 116 | 117 | 1. Start the MCP Registry (start_registry_server.py) 118 | 2. Start an MCP server (and verify that it is properly registered in the registry) 119 | 3. Start a client and connect to the registry url 120 | 121 | 122 | # License 123 | MIT License -------------------------------------------------------------------------------- /src/fastmcp_http/fastmcp_http/client.py: -------------------------------------------------------------------------------- 1 | from typing import Any, List, Optional 2 | import requests 3 | from mcp import Tool, Resource 4 | from mcp.types import Prompt, TextContent, ImageContent, EmbeddedResource 5 | from pydantic import BaseModel 6 | 7 | 8 | class Server(BaseModel): 9 | name: str 10 | description: str 11 | url: str 12 | port: int 13 | 14 | 15 | class FastMCPHttpClient: 16 | def __init__(self, base_url: str): 17 | """Initialize the FastMCP HTTP client. 18 | 19 | Args: 20 | base_url: Base URL of the FastMCP HTTP server 21 | """ 22 | self.base_url = base_url.rstrip("/") 23 | 24 | def list_servers(self) -> List[Server]: 25 | """List available servers from the server, only works for registry servers.""" 26 | response = requests.get(f"{self.base_url}/servers") 27 | response.raise_for_status() 28 | return [Server.model_validate(server) for server in response.json()] 29 | 30 | def list_tools(self, server_name: Optional[str] = None) -> List[Tool]: 31 | """List available tools from the server.""" 32 | params = {} 33 | if server_name is not None: 34 | params["server_name"] = server_name 35 | 36 | response = requests.get(f"{self.base_url}/tools", params=params) 37 | response.raise_for_status() 38 | return [Tool.model_validate(tool) for tool in response.json()] 39 | 40 | def call_tool( 41 | self, name: str, arguments: dict[str, Any] 42 | ) -> List[TextContent | ImageContent | EmbeddedResource]: 43 | """Call a tool with the given arguments.""" 44 | payload = {"name": name, **arguments} 45 | response = requests.post(f"{self.base_url}/tools/call_tool", json=payload) 46 | response.raise_for_status() 47 | 48 | contents = [] 49 | for content_data in response.json(): 50 | if content_data.get("type") == "text": 51 | contents.append(TextContent.model_validate(content_data)) 52 | elif content_data.get("type") == "image": 53 | contents.append(ImageContent.model_validate(content_data)) 54 | elif content_data.get("type") == "embedded_resource": 55 | contents.append(EmbeddedResource.model_validate(content_data)) 56 | else: 57 | raise ValueError(f"Unknown content type: {content_data.get('type')}") 58 | return contents 59 | 60 | def list_resources(self) -> List[Resource]: 61 | """List available resources from the server.""" 62 | response = requests.get(f"{self.base_url}/resources") 63 | response.raise_for_status() 64 | return [Resource.model_validate(resource) for resource in response.json()] 65 | 66 | def read_resource(self, uri: str) -> bytes: 67 | """Read a resource from the server.""" 68 | response = requests.get(f"{self.base_url}/resources/{uri}") 69 | response.raise_for_status() 70 | return response.content 71 | 72 | def list_prompts(self) -> List[Prompt]: 73 | """List available prompts from the server.""" 74 | response = requests.get(f"{self.base_url}/prompts") 75 | response.raise_for_status() 76 | return [Prompt.model_validate(prompt) for prompt in response.json()] 77 | 78 | def get_prompt(self, name: str, arguments: dict[str, Any]) -> Prompt: 79 | """Get a prompt with the given arguments.""" 80 | response = requests.post(f"{self.base_url}/prompts/{name}", json=arguments) 81 | response.raise_for_status() 82 | return response.json() 83 | -------------------------------------------------------------------------------- /.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 | *.json -------------------------------------------------------------------------------- /src/fastmcp_http/fastmcp_http/server.py: -------------------------------------------------------------------------------- 1 | from mcp.server.fastmcp import FastMCP 2 | from flask import Flask, request, Response 3 | from typing import Any, Optional 4 | import json 5 | import requests 6 | 7 | 8 | class FastMCPHttpServer(FastMCP): 9 | def __init__( 10 | self, 11 | name: Optional[str] = None, 12 | description: Optional[str] = None, 13 | **settings: Any, 14 | ): 15 | super().__init__(name=name, **settings) 16 | self.description = description 17 | self.flask_app = Flask(__name__) 18 | self._setup_routes() 19 | 20 | def _setup_routes(self) -> None: 21 | @self.flask_app.route("/tools", methods=["GET"]) 22 | async def list_tools(): 23 | tools = await self.list_tools() 24 | return json.dumps([tool.model_dump() for tool in tools]) 25 | 26 | @self.flask_app.route("/tools/call_tool", methods=["POST"]) 27 | async def call_tool(): 28 | arguments = request.get_json() 29 | name = arguments.pop("name", None) 30 | if name is None: 31 | return json.dumps({"error": "Tool name not provided"}), 400 32 | result = await self.call_tool(name, arguments) 33 | return json.dumps([content.model_dump() for content in result]) 34 | 35 | @self.flask_app.route("/resources", methods=["GET"]) 36 | async def list_resources(): 37 | resources = await self.list_resources() 38 | # Convert resources to a list of dictionaries that can be JSON serialized 39 | resource_list = [] 40 | for resource in resources: 41 | try: 42 | resource_dict = resource.model_dump() 43 | # Ensure all values are JSON serializable 44 | for key, value in resource_dict.items(): 45 | if not isinstance( 46 | value, (str, int, float, bool, type(None), list, dict) 47 | ): 48 | resource_dict[key] = str(value) 49 | resource_list.append(resource_dict) 50 | except Exception as e: 51 | # Handle any serialization errors 52 | resource_list.append( 53 | {"error": f"Failed to serialize resource: {str(e)}"} 54 | ) 55 | return json.dumps(resource_list) 56 | 57 | @self.flask_app.route("/resources/", methods=["GET"]) 58 | async def read_resource(uri: str): 59 | content = await self.read_resource(uri) 60 | return Response(content) 61 | 62 | @self.flask_app.route("/prompts", methods=["GET"]) 63 | async def list_prompts(): 64 | prompts = await self.list_prompts() 65 | return json.dumps([prompt.model_dump() for prompt in prompts]) 66 | 67 | @self.flask_app.route("/prompts/", methods=["POST"]) 68 | async def get_prompt(name: str): 69 | arguments = request.get_json() 70 | result = await self.get_prompt(name, arguments) 71 | return json.dumps(result.model_dump()) 72 | 73 | @self.flask_app.route("/health", methods=["GET"]) 74 | async def health_check(): 75 | return json.dumps( 76 | { 77 | "status": "healthy", 78 | "server_name": self.name, 79 | "description": self.description, 80 | } 81 | ) 82 | 83 | def register_server( 84 | self, 85 | server_url: str = "http://127.0.0.1", 86 | registry_url: str = "http://127.0.0.1:31337", 87 | ) -> int: 88 | """Register the server with the registry. Returns the port to use for the server. 89 | 90 | Args: 91 | server_url: URL of the server to register. 92 | registry_url: URL of the registry to register with. 93 | """ 94 | server_data = { 95 | "server_url": server_url, 96 | "server_name": self.name or "unnamed_server", 97 | "server_description": self.description or "FastMCP HTTP Server", 98 | } 99 | 100 | try: 101 | response = requests.post( 102 | f"{registry_url}/register_server", 103 | json=server_data, 104 | headers={"Content-Type": "application/json"}, 105 | ) 106 | response.raise_for_status() 107 | print(f"Successfully registered server with registry at {registry_url}") 108 | return response.json()["server"]["port"] 109 | except requests.exceptions.RequestException as e: 110 | print(f"Failed to register server: {str(e)}") 111 | return 0 112 | 113 | def run_http( 114 | self, 115 | host: str = "0.0.0.0", 116 | register_server: bool = True, 117 | port: int = 5000, 118 | ) -> None: 119 | """Run the FastMCP HTTP server. 120 | 121 | Args: 122 | host: Host to bind to (default: "0.0.0.0") 123 | port: Port to listen on (default: 5000) 124 | register_server: Whether to register the server with the registry (default: True) 125 | """ 126 | if register_server: 127 | port = self.register_server() 128 | self.flask_app.run(host=host, port=port) 129 | -------------------------------------------------------------------------------- /src/mcp_explorer/mcp_explorer.py: -------------------------------------------------------------------------------- 1 | import json 2 | import sys 3 | from pathlib import Path 4 | 5 | from PySide6.QtCore import Qt, QSize 6 | from PySide6.QtGui import QIcon 7 | from PySide6.QtWidgets import ( 8 | QApplication, 9 | QMainWindow, 10 | QSplitter, 11 | QTreeWidget, 12 | QTreeWidgetItem, 13 | QVBoxLayout, 14 | QWidget, 15 | QPushButton, 16 | QScrollArea, 17 | QLabel, 18 | QToolBar, 19 | QLineEdit, 20 | QTextEdit, 21 | QCheckBox, 22 | ) 23 | 24 | from mcp.types import Tool 25 | from fastmcp_http.client import FastMCPHttpClient 26 | 27 | 28 | class ServerTreeItem(QTreeWidgetItem): 29 | def __init__(self, name: str, is_server: bool = False): 30 | super().__init__([name]) 31 | self.is_server = is_server 32 | self.server_name = name if is_server else None 33 | self.tool_name = None if is_server else name 34 | 35 | 36 | class MCPExplorer(QMainWindow): 37 | def __init__(self): 38 | super().__init__() 39 | self.setWindowTitle("MCP Server Explorer") 40 | self.resize(1000, 600) 41 | 42 | self.client = FastMCPHttpClient("http://127.0.0.1:31337") 43 | 44 | # Create toolbar 45 | toolbar = QToolBar() 46 | self.addToolBar(toolbar) 47 | 48 | # Add refresh button 49 | refresh_btn = QPushButton("Refresh Servers") 50 | refresh_btn.clicked.connect(self.refresh_servers) 51 | toolbar.addWidget(refresh_btn) 52 | 53 | # Create main splitter 54 | self.splitter = QSplitter(Qt.Horizontal) 55 | self.setCentralWidget(self.splitter) 56 | 57 | # Create server tree 58 | self.tree = QTreeWidget() 59 | self.tree.setHeaderLabel("Servers") 60 | self.tree.itemClicked.connect(self.on_item_selected) 61 | self.splitter.addWidget(self.tree) 62 | 63 | # Create info panel 64 | self.info_scroll = QScrollArea() 65 | self.info_scroll.setWidgetResizable(True) 66 | self.info_widget = QWidget() 67 | self.info_layout = QVBoxLayout(self.info_widget) 68 | self.info_scroll.setWidget(self.info_widget) 69 | self.splitter.addWidget(self.info_scroll) 70 | 71 | # Set initial splitter sizes 72 | self.splitter.setSizes([300, 700]) 73 | 74 | # Load servers 75 | self.refresh_servers() 76 | 77 | def refresh_servers(self): 78 | self.tree.clear() 79 | 80 | try: 81 | with open("servers.json") as f: 82 | servers = json.load(f) 83 | 84 | available_servers = set( 85 | server.name for server in self.client.list_servers() 86 | ) 87 | 88 | # Get the directory of the current script 89 | script_dir = Path(__file__).parent 90 | 91 | for server_name, server_info in servers.items(): 92 | server_item = ServerTreeItem(server_name, is_server=True) 93 | self.tree.addTopLevelItem(server_item) 94 | 95 | # Check if server is online and set icon 96 | is_online = server_name in available_servers 97 | icon_path = script_dir / ("green.png" if is_online else "red.png") 98 | icon = QIcon(str(icon_path)) 99 | # Set icon size to 12x12 pixels 100 | self.tree.setIconSize(QSize(10, 10)) 101 | server_item.setIcon(0, icon) 102 | 103 | if is_online: 104 | # Get tools for online server 105 | tools = self.client.list_tools(server_name) 106 | for tool in tools: 107 | # Strip server name from tool name 108 | display_name = tool.name.split(".")[-1] 109 | tool_item = ServerTreeItem(display_name) 110 | tool_item.full_tool_name = ( 111 | tool.name 112 | ) # Store full name for reference 113 | server_item.addChild(tool_item) 114 | 115 | # Expand the server item 116 | server_item.setExpanded(True) 117 | 118 | except Exception as e: 119 | print(f"Error loading servers: {e}") 120 | 121 | def get_server_tools(self, server_info) -> list[Tool]: 122 | return self.client.list_tools(server_info["name"]) 123 | 124 | def on_item_selected(self, item: ServerTreeItem): 125 | # Clear previous info 126 | for i in reversed(range(self.info_layout.count())): 127 | layout_item = self.info_layout.itemAt(i) 128 | if layout_item.widget(): # Check if the item has a widget 129 | layout_item.widget().setParent(None) 130 | else: 131 | self.info_layout.removeItem(layout_item) 132 | 133 | if item.is_server: 134 | self.show_server_info(item.server_name) 135 | else: 136 | self.show_tool_info(item.parent().server_name, item.tool_name) 137 | 138 | def show_server_info(self, server_name: str): 139 | with open("servers.json") as f: 140 | servers = json.load(f) 141 | 142 | server = servers[server_name] 143 | 144 | # Display server information 145 | for label in [ 146 | f"Server Name: {server['name']}", 147 | f"Description: {server['description']}", 148 | f"URL: {server['url']}:{server['port']}", 149 | "\nAvailable Tools:", 150 | ]: 151 | self.info_layout.addWidget(QLabel(label)) 152 | 153 | # Get and display tool information 154 | tools = self.get_server_tools(server) 155 | for tool in tools: 156 | display_name = tool.name.split(".")[-1] 157 | self.info_layout.addWidget(QLabel(f"\n• {display_name}")) 158 | if tool.description: 159 | self.info_layout.addWidget(QLabel(f" {tool.description}")) 160 | 161 | self.info_layout.addStretch() 162 | 163 | def show_tool_info(self, server_name: str, tool_name: str): 164 | # Get tool info 165 | tools = self.get_server_tools({"name": server_name}) 166 | full_tool_name = f"{server_name}.{tool_name}" 167 | tool = next((t for t in tools if t.name == full_tool_name), None) 168 | 169 | if tool: 170 | # Display stripped tool name 171 | display_name = tool.name.split(".")[-1] 172 | self.info_layout.addWidget(QLabel(f"Tool Name: {display_name}")) 173 | self.info_layout.addWidget(QLabel(f"Description: {tool.description}")) 174 | 175 | # Add input fields section 176 | self.info_layout.addWidget(QLabel("\nInputs:")) 177 | input_widgets = {} 178 | for prop_name, prop_info in tool.inputSchema.get("properties", {}).items(): 179 | # Create label and input field for each property 180 | field_label = QLabel(f" {prop_info.get('title', prop_name)}:") 181 | self.info_layout.addWidget(field_label) 182 | 183 | if prop_info.get("type") == "boolean": 184 | input_field = QCheckBox() 185 | input_widgets[prop_name] = input_field 186 | else: # default to text input 187 | input_field = QLineEdit() 188 | input_widgets[prop_name] = input_field 189 | self.info_layout.addWidget(input_field) 190 | 191 | # Add output section 192 | self.info_layout.addWidget(QLabel("\nOutput:")) 193 | output_field = QTextEdit() 194 | output_field.setReadOnly(True) 195 | output_field.setMinimumHeight(100) 196 | self.info_layout.addWidget(output_field) 197 | 198 | # Add invoke button 199 | invoke_btn = QPushButton("Invoke Tool") 200 | invoke_btn.clicked.connect( 201 | lambda: self.invoke_tool(server_name, tool, input_widgets, output_field) 202 | ) 203 | self.info_layout.addWidget(invoke_btn) 204 | self.info_layout.addStretch() 205 | 206 | def invoke_tool( 207 | self, server_name: str, tool: Tool, input_widgets: dict, output_field: QTextEdit 208 | ): 209 | try: 210 | servers = self.client.list_servers() 211 | server_info = next((s for s in servers if s.name == server_name), None) 212 | if server_info is None: 213 | output_field.setText(f"Server {server_name} not found") 214 | return 215 | 216 | # Collect input values 217 | inputs = {} 218 | for name, widget in input_widgets.items(): 219 | if isinstance(widget, QCheckBox): 220 | inputs[name] = widget.isChecked() 221 | else: 222 | inputs[name] = widget.text() 223 | 224 | # Call the tool 225 | output_field.setText("Invoking tool...") 226 | print(f"Invoking tool {tool.name} with inputs: {inputs}") 227 | result = self.client.call_tool(tool.name, inputs) 228 | 229 | # Display results 230 | output_text = "" 231 | for content in result: 232 | if hasattr(content, "text"): 233 | output_text += content.text + "\n" 234 | else: 235 | output_text += f"Received content of type: {type(content)}\n" 236 | 237 | output_field.setText(output_text) 238 | 239 | except Exception as e: 240 | output_field.setText(f"Error invoking tool: {str(e)}") 241 | 242 | 243 | if __name__ == "__main__": 244 | app = QApplication(sys.argv) 245 | window = MCPExplorer() 246 | window.show() 247 | sys.exit(app.exec()) 248 | -------------------------------------------------------------------------------- /src/mcp_registry/mcp_registry_server.py: -------------------------------------------------------------------------------- 1 | import time 2 | from flask import Flask, request, jsonify 3 | from dataclasses import dataclass 4 | from typing import Dict 5 | import socket 6 | import random 7 | import json 8 | import requests 9 | from pathlib import Path 10 | from datetime import datetime, timedelta 11 | from threading import Thread 12 | 13 | from fastmcp_http.client import FastMCPHttpClient 14 | from src.mcp_registry.permission_management import permission_server 15 | 16 | app = Flask(__name__) 17 | 18 | 19 | @dataclass 20 | class Server: 21 | name: str 22 | description: str 23 | url: str 24 | port: int 25 | 26 | 27 | # Global dictionaries to store servers and health status 28 | servers: Dict[str, Server] = {} 29 | health_cache: Dict[str, tuple[datetime, bool]] = {} 30 | 31 | # Add constants for storage 32 | STORAGE_FILE = Path("servers.json") 33 | 34 | # Add constant for permission server name 35 | PERMISSION_SERVER_NAME = "PermissionServer" 36 | 37 | 38 | def _generate_port( 39 | server_url: str, start_port: int = 5000, end_port: int = 65535 40 | ) -> int: 41 | """Generate an available port for the server. 42 | 43 | Args: 44 | server_url: The server URL to check ports against 45 | start_port: Minimum port number to consider (default: 5000) 46 | end_port: Maximum port number to consider (default: 65535) 47 | 48 | Returns: 49 | An available port number 50 | """ 51 | # Get host from server URL 52 | from urllib.parse import urlparse 53 | 54 | host = urlparse(server_url).hostname or "127.0.0.1" 55 | 56 | # Start with a random port in the range 57 | port = random.randint(start_port, end_port) 58 | 59 | while port <= end_port: 60 | sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 61 | try: 62 | sock.bind((host, port)) 63 | sock.close() 64 | return port 65 | except socket.error: 66 | port += 1 67 | finally: 68 | sock.close() 69 | 70 | raise RuntimeError("No available ports found in the specified range") 71 | 72 | 73 | def check_server_health(server: Server) -> bool: 74 | """Check if a server is healthy by pinging its health endpoint. 75 | Caches the result for 1 minutes. 76 | 77 | Args: 78 | server: Server instance to check 79 | 80 | Returns: 81 | bool: True if server is healthy, False otherwise 82 | """ 83 | # Check if we have a recent cached result 84 | if server.name in health_cache: 85 | last_check, is_healthy = health_cache[server.name] 86 | if datetime.now() - last_check < timedelta(seconds=30): 87 | return is_healthy 88 | 89 | try: 90 | response = requests.get(f"{server.url}:{server.port}/health", timeout=5) 91 | is_healthy = response.status_code == 200 92 | except requests.RequestException: 93 | is_healthy = False 94 | 95 | health_cache[server.name] = (datetime.now(), is_healthy) 96 | return is_healthy 97 | 98 | 99 | def load_servers() -> Dict[str, Server]: 100 | """Load servers from storage and verify they're running.""" 101 | if not STORAGE_FILE.exists(): 102 | return {} 103 | 104 | servers = {} 105 | try: 106 | with open(STORAGE_FILE, "r") as f: 107 | data = json.load(f) 108 | for server_data in data.values(): 109 | server = Server(**server_data) 110 | if check_server_health(server): 111 | servers[server.name] = server 112 | else: 113 | print(f"Server {server.name} appears to be down, skipping...") 114 | except Exception as e: 115 | print(f"Error loading servers: {e}") 116 | 117 | return servers 118 | 119 | 120 | def save_servers(): 121 | """Save current servers to storage.""" 122 | with open(STORAGE_FILE, "w") as f: 123 | json.dump({name: vars(server) for name, server in servers.items()}, f) 124 | 125 | 126 | @app.route("/register_server", methods=["POST"]) 127 | def register_server(): 128 | data = request.get_json() 129 | 130 | # Validate required fields 131 | required_fields = ["server_url", "server_name", "server_description"] 132 | if not all(field in data for field in required_fields): 133 | return jsonify({"error": "Missing required fields"}), 400 134 | 135 | # Block registration of permission server name by other servers 136 | if data["server_name"] == PERMISSION_SERVER_NAME: 137 | return jsonify({"error": "Reserved server name"}), 403 138 | 139 | port = _generate_port(data["server_url"]) 140 | 141 | # Create new server instance 142 | new_server = Server( 143 | url=data["server_url"], 144 | name=data["server_name"], 145 | description=data["server_description"], 146 | port=port, 147 | ) 148 | 149 | # Add to global dictionary 150 | servers[data["server_name"]] = new_server 151 | save_servers() # Save after registration 152 | print("Added server: ", data["server_name"]) 153 | 154 | return ( 155 | jsonify( 156 | { 157 | "message": "Server registered successfully", 158 | "server": { 159 | "name": new_server.name, 160 | "url": new_server.url, 161 | "description": new_server.description, 162 | "port": port, 163 | }, 164 | } 165 | ), 166 | 201, 167 | ) 168 | 169 | 170 | @app.route("/servers", methods=["GET"]) 171 | def get_servers(): 172 | """Return a list of all registered and healthy servers.""" 173 | return jsonify( 174 | [ 175 | { 176 | "name": server.name, 177 | "url": server.url, 178 | "description": server.description, 179 | "port": server.port, 180 | } 181 | for server in servers.values() 182 | if check_server_health(server) 183 | ] 184 | ) 185 | 186 | 187 | @app.route("/tools", methods=["GET"]) 188 | def get_tools(): 189 | """Return a list of tools from registered servers.""" 190 | server_name = request.args.get("server_name") 191 | 192 | all_tools = [] 193 | try: 194 | # Filter servers if server_name is provided 195 | target_servers = [servers[server_name]] if server_name else servers.values() 196 | 197 | for server in target_servers: 198 | try: 199 | if not check_server_health(server): 200 | continue 201 | client = FastMCPHttpClient(f"{server.url}:{server.port}") 202 | for tool in client.list_tools(): 203 | tool.name = f"{server.name}.{tool.name}" 204 | all_tools.append(tool) 205 | except requests.RequestException as e: 206 | print(f"Error fetching tools from {server.name}: {e}") 207 | health_cache[server.name] = (datetime.now(), False) 208 | continue 209 | 210 | return json.dumps([tool.model_dump() for tool in all_tools]) 211 | except KeyError: 212 | return jsonify({"error": f"Server '{server_name}' not found"}), 404 213 | except Exception as e: 214 | return jsonify({"error": str(e)}), 500 215 | 216 | 217 | @app.route("/tools/call_tool", methods=["POST"]) 218 | def call_tool(): 219 | """Call a tool on a specific server.""" 220 | data = request.get_json() 221 | name = data.pop("name", None) # Extract and remove name from arguments 222 | 223 | if name is None: 224 | return jsonify({"error": "Tool name not provided"}), 400 225 | 226 | server_name = None 227 | tool_name = name 228 | 229 | # Check if server name is specified (format: "server_name.tool_name") 230 | if "." in tool_name: 231 | server_name, tool_name = tool_name.split(".", 1) 232 | 233 | # Add permission server tool restriction 234 | if tool_name == "ask_for_permission" and server_name != PERMISSION_SERVER_NAME: 235 | return jsonify({"error": "Permission denied: unauthorized server"}), 403 236 | 237 | if server_name not in servers: 238 | return jsonify({"error": f"Server '{server_name}' not found"}), 404 239 | target_servers = [servers[server_name]] 240 | else: 241 | # If no server specified, search all servers for the tool 242 | if tool_name == "ask_for_permission": 243 | target_servers = [servers[PERMISSION_SERVER_NAME]] 244 | else: 245 | target_servers = [s for s in servers.values() if check_server_health(s)] 246 | 247 | # Try each potential server 248 | for server in target_servers: 249 | try: 250 | client = FastMCPHttpClient(f"{server.url}:{server.port}") 251 | # Check if the tool exists on this server 252 | available_tools = client.list_tools() 253 | print("AVAILABLE TOOLS", available_tools) 254 | if not any(t.name == tool_name for t in available_tools): 255 | continue 256 | 257 | # Found the tool, try to call it 258 | result = client.call_tool( 259 | tool_name, data 260 | ) # Use remaining data as arguments 261 | return jsonify([content.model_dump() for content in result]) 262 | 263 | except requests.RequestException: 264 | # If this server fails, try the next one 265 | continue 266 | 267 | # If we get here, we didn't find the tool on any server 268 | error_msg = f"Tool '{tool_name}' not found" 269 | if server_name: 270 | error_msg += f" on server '{server_name}'" 271 | print("ERROR", error_msg) 272 | return jsonify({"error": error_msg}), 404 273 | 274 | 275 | def load_permission_server(): 276 | permission_server_url = "http://127.0.0.1" 277 | permission_port = _generate_port(permission_server_url) 278 | 279 | permission_server_instance = Server( 280 | name=PERMISSION_SERVER_NAME, 281 | description=permission_server.mcp.description, # type: ignore 282 | url=permission_server_url, 283 | port=permission_port, 284 | ) 285 | 286 | # Add to global servers dict 287 | servers[PERMISSION_SERVER_NAME] = permission_server_instance 288 | 289 | # Start permission server in a new thread with the assigned port 290 | permission_thread = Thread( 291 | target=lambda: permission_server.mcp.run_http( 292 | register_server=False, port=permission_port 293 | ), 294 | daemon=True, 295 | ) 296 | permission_thread.start() 297 | time.sleep(1) 298 | 299 | 300 | def run(): 301 | # Register permission server first 302 | load_permission_server() 303 | 304 | # Load other servers on startup 305 | loaded_servers = load_servers() 306 | for server in loaded_servers.keys(): 307 | if server != PERMISSION_SERVER_NAME: # Don't override permission server 308 | servers[server] = loaded_servers[server] 309 | print("Loaded server:", server) 310 | 311 | save_servers() 312 | app.run(debug=False, port=31337) 313 | --------------------------------------------------------------------------------