# Directory Structure ``` ├── .env.example ├── clean.sh ├── iot_mcp_server.py ├── light_bulb_simulator.py ├── mem0_mcp.egg-info │ ├── dependency_links.txt │ ├── PKG-INFO │ ├── requires.txt │ ├── SOURCES.txt │ └── top_level.txt ├── memory_mcp_server.py ├── pyproject.toml ├── README.md ├── requirements.txt ├── templates │ └── index.html └── utils.py ``` # Files -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- ``` 1 | # IoT MCP Server configuration 2 | MQTT_BROKER=localhost 3 | MQTT_PORT=1883 4 | HOST=0.0.0.0 5 | PORT=8090 6 | TRANSPORT=sse 7 | 8 | # Memory MCP Server configuration 9 | HOST=0.0.0.0 10 | PORT=8050 11 | TRANSPORT=sse 12 | 13 | # LLM Configuration for Memory Server 14 | LLM_PROVIDER=openai # Options: openai, openrouter, ollama 15 | LLM_API_KEY=your_api_key_here 16 | LLM_CHOICE=gpt-4 # Model name (gpt-4, llama3, etc.) 17 | LLM_BASE_URL=http://localhost:11434 # For Ollama 18 | 19 | # Embedding Model Configuration 20 | EMBEDDING_MODEL_CHOICE=text-embedding-3-small # For OpenAI 21 | # EMBEDDING_MODEL_CHOICE=nomic-embed-text # For Ollama 22 | 23 | # Vector Database Configuration 24 | DATABASE_URL=postgresql://user:password@localhost:5432/vector_db ``` -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- ```markdown 1 | # MCP Servers for IoT and Memory Management 2 | 3 | This repository contains two Model Context Protocol (MCP) servers: 4 | 1. IoT Device Control MCP Server 5 | 2. Memory Management MCP Server 6 | 7 | ## IoT Device Control MCP Server 8 | 9 | A Model Context Protocol (MCP) server for controlling and monitoring IoT devices such as smart lights, sensors, and other connected devices. 10 | 11 | ### Purpose 12 | 13 | This server provides a standardized interface for IoT device control, monitoring, and state management through the Model Context Protocol. 14 | 15 | ### Use Cases 16 | 17 | - Home automation 18 | - Industrial IoT monitoring 19 | - Remote device management 20 | - Smart building control systems 21 | 22 | ### Features 23 | 24 | - Send commands to IoT devices 25 | - Query device state and status 26 | - Subscribe to real-time device updates 27 | - Support for MQTT protocol 28 | 29 | ### API Tools 30 | 31 | - `send_command`: Send a command to an IoT device 32 | - `get_device_state`: Get the current state of an IoT device 33 | - `subscribe_to_updates`: Subscribe to real-time updates from a device 34 | 35 | ## Memory Management MCP Server 36 | 37 | A Model Context Protocol (MCP) server for persistent memory storage and retrieval using the Mem0 framework. 38 | 39 | ### Purpose 40 | 41 | This server enables long-term memory storage and semantic search capabilities through the Model Context Protocol. 42 | 43 | ### Use Cases 44 | 45 | - Conversation history storage 46 | - Knowledge management 47 | - Contextual awareness in AI applications 48 | - Persistent information storage 49 | 50 | ### Features 51 | 52 | - Save information to long-term memory 53 | - Retrieve all stored memories 54 | - Search memories using semantic search 55 | 56 | ### API Tools 57 | 58 | - `save_memory`: Save information to long-term memory 59 | - `get_all_memories`: Get all stored memories for the user 60 | - `search_memories`: Search memories using semantic search 61 | 62 | ## Getting Started 63 | 64 | 1. Clone this repository 65 | 2. Install dependencies: `pip install -r requirements.txt` 66 | 3. Create a `.env` file based on the `.env.example` template 67 | 4. Run the IoT server: `python iot_mcp_server.py` 68 | 5. Run the Memory server: `python memory_mcp_server.py` 69 | 70 | ## Environment Variables 71 | 72 | ### IoT MCP Server 73 | - `MQTT_BROKER`: MQTT broker address (default: "localhost") 74 | - `MQTT_PORT`: MQTT broker port (default: 1883) 75 | - `HOST`: Server host address (default: "0.0.0.0") 76 | - `PORT`: Server port (default: "8090") 77 | - `TRANSPORT`: Transport type, "sse" or "stdio" (default: "sse") 78 | 79 | ### Memory MCP Server 80 | - `MEM0_API_KEY`: API key for Mem0 service (optional) 81 | - `MEM0_ENDPOINT`: Endpoint URL for Mem0 service (default: "https://api.mem0.ai") 82 | - `HOST`: Server host address (default: "0.0.0.0") 83 | - `PORT`: Server port (default: "8050") 84 | - `TRANSPORT`: Transport type, "sse" or "stdio" (default: "sse") 85 | 86 | ## Repository Structure 87 | 88 | - `iot_mcp_server.py` - IoT device control MCP server implementation 89 | - `memory_mcp_server.py` - Memory management MCP server implementation 90 | - `utils.py` - Utility functions used by the servers 91 | - `requirements.txt` - Package dependencies 92 | - `.env.example` - Template for environment variables configuration 93 | - `README.md` - Documentation ``` -------------------------------------------------------------------------------- /mem0_mcp.egg-info/dependency_links.txt: -------------------------------------------------------------------------------- ``` 1 | 2 | ``` -------------------------------------------------------------------------------- /mem0_mcp.egg-info/top_level.txt: -------------------------------------------------------------------------------- ``` 1 | templates 2 | ``` -------------------------------------------------------------------------------- /mem0_mcp.egg-info/requires.txt: -------------------------------------------------------------------------------- ``` 1 | httpx>=0.28.1 2 | mcp[cli]>=1.3.0 3 | mem0ai>=0.1.88 4 | vecs>=0.4.5 5 | ``` -------------------------------------------------------------------------------- /clean.sh: -------------------------------------------------------------------------------- ```bash 1 | find . -type d -name "__pycache__" -exec rm -rf {} \; 2 | rm -fr venv 3 | ``` -------------------------------------------------------------------------------- /mem0_mcp.egg-info/SOURCES.txt: -------------------------------------------------------------------------------- ``` 1 | README.md 2 | pyproject.toml 3 | mem0_mcp.egg-info/PKG-INFO 4 | mem0_mcp.egg-info/SOURCES.txt 5 | mem0_mcp.egg-info/dependency_links.txt 6 | mem0_mcp.egg-info/requires.txt 7 | mem0_mcp.egg-info/top_level.txt ``` -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- ```toml 1 | [project] 2 | name = "mem0-mcp" 3 | version = "0.1.0" 4 | description = "MCP server for integrating long term memory into AI agents with Mem0" 5 | readme = "README.md" 6 | requires-python = ">=3.12" 7 | dependencies = [ 8 | "httpx>=0.28.1", 9 | "mcp[cli]>=1.3.0", 10 | "mem0ai>=0.1.88", 11 | "vecs>=0.4.5" 12 | ] ``` -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- ``` 1 | fastmcp>=0.1.0 2 | paho-mqtt>=2.0.0 3 | python-dotenv>=1.0.0 4 | # mem0>=0.2.0 # Commented out as it's not available on PyPI 5 | # psycopg2-binary>=2.9.6 # Only needed for memory server 6 | # openai>=1.0.0 # Only needed for memory server 7 | setuptools>=65.5.1 # Required by some dependencies 8 | flask>=2.0.0 # For the light bulb simulator web server ``` -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- ```python 1 | from mem0 import Memory 2 | import os 3 | from dotenv import load_dotenv 4 | 5 | # Custom instructions for memory processing 6 | # These aren't being used right now but Mem0 does support adding custom prompting 7 | # for handling memory retrieval and processing. 8 | CUSTOM_INSTRUCTIONS = """ 9 | Extract the Following Information: 10 | 11 | - Key Information: Identify and save the most important details. 12 | - Context: Capture the surrounding context to understand the memory's relevance. 13 | - Connections: Note any relationships to other topics or memories. 14 | - Importance: Highlight why this information might be valuable in the future. 15 | - Source: Record where this information came from when applicable. 16 | """ 17 | 18 | def get_mem0_client(): 19 | load_dotenv() 20 | 21 | # Get LLM provider and configuration 22 | llm_provider = os.getenv('LLM_PROVIDER') 23 | llm_api_key = os.getenv('LLM_API_KEY') 24 | llm_model = os.getenv('LLM_CHOICE') 25 | embedding_model = os.getenv('EMBEDDING_MODEL_CHOICE') 26 | 27 | # Initialize config dictionary 28 | config = {} 29 | 30 | # Configure LLM based on provider 31 | if llm_provider == 'openai' or llm_provider == 'openrouter': 32 | config["llm"] = { 33 | "provider": "openai", 34 | "config": { 35 | "model": llm_model, 36 | "temperature": 0.2, 37 | "max_tokens": 2000, 38 | } 39 | } 40 | 41 | # Set API key in environment if not already set 42 | if llm_api_key and not os.environ.get("OPENAI_API_KEY"): 43 | os.environ["OPENAI_API_KEY"] = llm_api_key 44 | 45 | # For OpenRouter, set the specific API key 46 | if llm_provider == 'openrouter' and llm_api_key: 47 | os.environ["OPENROUTER_API_KEY"] = llm_api_key 48 | 49 | elif llm_provider == 'ollama': 50 | config["llm"] = { 51 | "provider": "ollama", 52 | "config": { 53 | "model": llm_model, 54 | "temperature": 0.2, 55 | "max_tokens": 2000, 56 | } 57 | } 58 | 59 | # Set base URL for Ollama if provided 60 | llm_base_url = os.getenv('LLM_BASE_URL') 61 | if llm_base_url: 62 | config["llm"]["config"]["ollama_base_url"] = llm_base_url 63 | 64 | # Configure embedder based on provider 65 | if llm_provider == 'openai': 66 | config["embedder"] = { 67 | "provider": "openai", 68 | "config": { 69 | "model": embedding_model or "text-embedding-3-small", 70 | "embedding_dims": 1536 # Default for text-embedding-3-small 71 | } 72 | } 73 | 74 | # Set API key in environment if not already set 75 | if llm_api_key and not os.environ.get("OPENAI_API_KEY"): 76 | os.environ["OPENAI_API_KEY"] = llm_api_key 77 | 78 | elif llm_provider == 'ollama': 79 | config["embedder"] = { 80 | "provider": "ollama", 81 | "config": { 82 | "model": embedding_model or "nomic-embed-text", 83 | "embedding_dims": 768 # Default for nomic-embed-text 84 | } 85 | } 86 | 87 | # Set base URL for Ollama if provided 88 | embedding_base_url = os.getenv('LLM_BASE_URL') 89 | if embedding_base_url: 90 | config["embedder"]["config"]["ollama_base_url"] = embedding_base_url 91 | 92 | # Configure Supabase vector store 93 | config["vector_store"] = { 94 | "provider": "supabase", 95 | "config": { 96 | "connection_string": os.environ.get('DATABASE_URL', ''), 97 | "collection_name": "mem0_memories", 98 | "embedding_model_dims": 1536 if llm_provider == "openai" else 768 99 | } 100 | } 101 | 102 | # config["custom_fact_extraction_prompt"] = CUSTOM_INSTRUCTIONS 103 | 104 | # Create and return the Memory client 105 | return Memory.from_config(config) ``` -------------------------------------------------------------------------------- /memory_mcp_server.py: -------------------------------------------------------------------------------- ```python 1 | from mcp.server.fastmcp import FastMCP, Context 2 | from contextlib import asynccontextmanager 3 | from collections.abc import AsyncIterator 4 | from dataclasses import dataclass 5 | from dotenv import load_dotenv 6 | from mem0 import Memory 7 | import asyncio 8 | import json 9 | import os 10 | 11 | from utils import get_mem0_client 12 | 13 | load_dotenv() 14 | 15 | # Default user ID for memory operations 16 | DEFAULT_USER_ID = "user" 17 | 18 | # Create a dataclass for our application context 19 | @dataclass 20 | class Mem0Context: 21 | """Context for the Mem0 MCP server.""" 22 | mem0_client: Memory 23 | 24 | @asynccontextmanager 25 | async def mem0_lifespan(server: FastMCP) -> AsyncIterator[Mem0Context]: 26 | """ 27 | Manages the Mem0 client lifecycle. 28 | 29 | Args: 30 | server: The FastMCP server instance 31 | 32 | Yields: 33 | Mem0Context: The context containing the Mem0 client 34 | """ 35 | # Create and return the Memory client with the helper function in utils.py 36 | mem0_client = get_mem0_client() 37 | 38 | try: 39 | yield Mem0Context(mem0_client=mem0_client) 40 | finally: 41 | # No explicit cleanup needed for the Mem0 client 42 | pass 43 | 44 | # Initialize FastMCP server with the Mem0 client as context 45 | mcp = FastMCP( 46 | "mcp-mem0", 47 | description="MCP server for long term memory storage and retrieval with Mem0", 48 | lifespan=mem0_lifespan, 49 | host=os.getenv("HOST", "0.0.0.0"), 50 | port=os.getenv("PORT", "8050") 51 | ) 52 | 53 | @mcp.tool() 54 | async def save_memory(ctx: Context, text: str) -> str: 55 | """Save information to your long-term memory. 56 | 57 | This tool is designed to store any type of information that might be useful in the future. 58 | The content will be processed and indexed for later retrieval through semantic search. 59 | 60 | Args: 61 | ctx: The MCP server provided context which includes the Mem0 client 62 | text: The content to store in memory, including any relevant details and context 63 | """ 64 | try: 65 | mem0_client = ctx.request_context.lifespan_context.mem0_client 66 | messages = [{"role": "user", "content": text}] 67 | mem0_client.add(messages, user_id=DEFAULT_USER_ID) 68 | return f"Successfully saved memory: {text[:100]}..." if len(text) > 100 else f"Successfully saved memory: {text}" 69 | except Exception as e: 70 | return f"Error saving memory: {str(e)}" 71 | 72 | @mcp.tool() 73 | async def get_all_memories(ctx: Context) -> str: 74 | """Get all stored memories for the user. 75 | 76 | Call this tool when you need complete context of all previously memories. 77 | 78 | Args: 79 | ctx: The MCP server provided context which includes the Mem0 client 80 | 81 | Returns a JSON formatted list of all stored memories, including when they were created 82 | and their content. Results are paginated with a default of 50 items per page. 83 | """ 84 | try: 85 | mem0_client = ctx.request_context.lifespan_context.mem0_client 86 | memories = mem0_client.get_all(user_id=DEFAULT_USER_ID) 87 | if isinstance(memories, dict) and "results" in memories: 88 | flattened_memories = [memory["memory"] for memory in memories["results"]] 89 | else: 90 | flattened_memories = memories 91 | return json.dumps(flattened_memories, indent=2) 92 | except Exception as e: 93 | return f"Error retrieving memories: {str(e)}" 94 | 95 | @mcp.tool() 96 | async def search_memories(ctx: Context, query: str, limit: int = 3) -> str: 97 | """Search memories using semantic search. 98 | 99 | This tool should be called to find relevant information from your memory. Results are ranked by relevance. 100 | Always search your memories before making decisions to ensure you leverage your existing knowledge. 101 | 102 | Args: 103 | ctx: The MCP server provided context which includes the Mem0 client 104 | query: Search query string describing what you're looking for. Can be natural language. 105 | limit: Maximum number of results to return (default: 3) 106 | """ 107 | try: 108 | mem0_client = ctx.request_context.lifespan_context.mem0_client 109 | memories = mem0_client.search(query, user_id=DEFAULT_USER_ID, limit=limit) 110 | if isinstance(memories, dict) and "results" in memories: 111 | flattened_memories = [memory["memory"] for memory in memories["results"]] 112 | else: 113 | flattened_memories = memories 114 | return json.dumps(flattened_memories, indent=2) 115 | except Exception as e: 116 | return f"Error searching memories: {str(e)}" 117 | 118 | async def main(): 119 | transport = os.getenv("TRANSPORT", "sse") 120 | if transport == 'sse': 121 | # Run the MCP server with sse transport 122 | await mcp.run_sse_async() 123 | else: 124 | # Run the MCP server with stdio transport 125 | await mcp.run_stdio_async() 126 | 127 | if __name__ == "__main__": 128 | asyncio.run(main()) ``` -------------------------------------------------------------------------------- /light_bulb_simulator.py: -------------------------------------------------------------------------------- ```python 1 | from flask import Flask, render_template, request, jsonify 2 | import paho.mqtt.client as mqtt 3 | import json 4 | import threading 5 | import logging 6 | import os 7 | import time 8 | from dotenv import load_dotenv 9 | 10 | # Load environment variables 11 | load_dotenv() 12 | 13 | # Configure logging 14 | logging.basicConfig(level=logging.INFO, 15 | format='%(asctime)s - %(name)s - %(levelname)s - %(message)s') 16 | logger = logging.getLogger(__name__) 17 | 18 | app = Flask(__name__) 19 | 20 | # MQTT Settings 21 | MQTT_BROKER = os.getenv("MQTT_BROKER", "localhost") 22 | MQTT_PORT = int(os.getenv("MQTT_PORT", "1883")) 23 | DEVICE_ID = "light_bulb_001" 24 | COMMAND_TOPIC = f"devices/{DEVICE_ID}/command" 25 | STATE_TOPIC = f"devices/{DEVICE_ID}/state" 26 | STATE_REQUEST_TOPIC = f"devices/{DEVICE_ID}/state/request" 27 | 28 | # Global device state 29 | device_state = { 30 | "device_id": DEVICE_ID, 31 | "type": "light", # Add device type 32 | "online": True, 33 | "last_seen": None, 34 | "properties": { 35 | "power": False, # False is off, True is on 36 | "brightness": 100, 37 | "color": "#FFFF00" # Yellow light 38 | } 39 | } 40 | 41 | # Initialize MQTT client with protocol version parameter to address deprecation warning 42 | mqtt_client = mqtt.Client(protocol=mqtt.MQTTv311) 43 | 44 | # Set up MQTT callbacks 45 | def on_connect(client, userdata, flags, rc): 46 | if rc == 0: 47 | logger.info(f"Connected to MQTT broker at {MQTT_BROKER}:{MQTT_PORT}") 48 | # Subscribe to command topic 49 | client.subscribe(COMMAND_TOPIC) 50 | client.subscribe(STATE_REQUEST_TOPIC) 51 | # Subscribe to broadcast requests 52 | client.subscribe("devices/broadcast/request") 53 | # Publish initial state immediately after connection 54 | publish_state() 55 | else: 56 | logger.error(f"Failed to connect to MQTT broker with result code {rc}") 57 | 58 | def on_message(client, userdata, message): 59 | topic = message.topic 60 | payload = message.payload.decode("utf-8") 61 | logger.info(f"Received message on {topic}: {payload}") 62 | 63 | try: 64 | # Handle command messages 65 | if topic == COMMAND_TOPIC: 66 | handle_command(payload) 67 | # Handle state request messages 68 | elif topic == STATE_REQUEST_TOPIC: 69 | publish_state() 70 | # Handle broadcast requests 71 | elif topic == "devices/broadcast/request": 72 | # Parse the message 73 | msg_data = json.loads(payload) 74 | action = msg_data.get("action") 75 | 76 | # If action is to report state, publish our state 77 | if action == "report_state": 78 | logger.info(f"Responding to broadcast request: {msg_data.get('request_id')}") 79 | publish_state() 80 | 81 | # Also publish to the broadcast response topic 82 | response = { 83 | "device_id": DEVICE_ID, 84 | "response_to": msg_data.get("request_id"), 85 | "state": device_state 86 | } 87 | mqtt_client.publish("devices/broadcast/response", json.dumps(response)) 88 | except Exception as e: 89 | logger.error(f"Error processing message: {str(e)}") 90 | 91 | def handle_command(payload_str): 92 | try: 93 | payload = json.loads(payload_str) 94 | command = payload.get("command") 95 | 96 | logger.info(f"Processing command: {command} with payload: {payload_str}") 97 | 98 | if command == "toggle": 99 | # Toggle power state 100 | device_state["properties"]["power"] = not device_state["properties"]["power"] 101 | logger.info(f"Toggled light bulb power to: {device_state['properties']['power']}") 102 | 103 | elif command == "set_power" and "payload" in payload: 104 | # Set power state directly 105 | power_state = payload["payload"].get("power", False) 106 | old_state = device_state["properties"]["power"] 107 | device_state["properties"]["power"] = bool(power_state) 108 | logger.info(f"Set light bulb power from {old_state} to {device_state['properties']['power']}") 109 | 110 | elif command == "set_brightness" and "payload" in payload: 111 | # Set brightness 112 | brightness = payload["payload"].get("brightness", 100) 113 | device_state["properties"]["brightness"] = max(0, min(100, int(brightness))) 114 | logger.info(f"Set light bulb brightness to: {device_state['properties']['brightness']}") 115 | 116 | elif command == "set_color" and "payload" in payload: 117 | # Set color 118 | color = payload["payload"].get("color") 119 | if color: 120 | device_state["properties"]["color"] = color 121 | logger.info(f"Set light bulb color to: {device_state['properties']['color']}") 122 | else: 123 | logger.warning(f"Unknown command or missing payload: {command}") 124 | 125 | # Publish updated state 126 | publish_state() 127 | except json.JSONDecodeError: 128 | logger.error(f"Invalid JSON payload: {payload_str}") 129 | except Exception as e: 130 | logger.error(f"Error handling command: {str(e)}") 131 | import traceback 132 | logger.error(traceback.format_exc()) 133 | 134 | def publish_state(): 135 | """Publish current state to MQTT""" 136 | try: 137 | # Update last_seen timestamp 138 | device_state["last_seen"] = time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()) 139 | mqtt_client.publish(STATE_TOPIC, json.dumps(device_state)) 140 | logger.info(f"Published state: {json.dumps(device_state)}") 141 | except Exception as e: 142 | logger.error(f"Error publishing state: {str(e)}") 143 | 144 | # Heartbeat function to periodically publish state 145 | def heartbeat(): 146 | while True: 147 | try: 148 | publish_state() 149 | except Exception as e: 150 | logger.error(f"Error in heartbeat: {str(e)}") 151 | time.sleep(60) # Publish state every 60 seconds 152 | 153 | # Routes 154 | @app.route('/') 155 | def index(): 156 | return render_template('index.html', device_state=device_state) 157 | 158 | @app.route('/api/state', methods=['GET']) 159 | def get_state(): 160 | return jsonify(device_state) 161 | 162 | @app.route('/api/toggle', methods=['POST']) 163 | def toggle_light(): 164 | device_state["properties"]["power"] = not device_state["properties"]["power"] 165 | publish_state() 166 | return jsonify({"success": True, "power": device_state["properties"]["power"]}) 167 | 168 | @app.route('/api/brightness', methods=['POST']) 169 | def set_brightness(): 170 | data = request.json 171 | brightness = data.get('brightness', 100) 172 | device_state["properties"]["brightness"] = max(0, min(100, int(brightness))) 173 | publish_state() 174 | return jsonify({"success": True, "brightness": device_state["properties"]["brightness"]}) 175 | 176 | @app.route('/api/color', methods=['POST']) 177 | def set_color(): 178 | data = request.json 179 | color = data.get('color', "#FFFF00") 180 | device_state["properties"]["color"] = color 181 | publish_state() 182 | return jsonify({"success": True, "color": device_state["properties"]["color"]}) 183 | 184 | def start_mqtt(): 185 | mqtt_client.on_connect = on_connect 186 | mqtt_client.on_message = on_message 187 | 188 | try: 189 | mqtt_client.connect(MQTT_BROKER, MQTT_PORT, 60) 190 | mqtt_client.loop_start() 191 | except Exception as e: 192 | logger.error(f"Failed to connect to MQTT broker: {str(e)}") 193 | 194 | if __name__ == '__main__': 195 | # Start MQTT client in a separate thread 196 | mqtt_thread = threading.Thread(target=start_mqtt) 197 | mqtt_thread.daemon = True 198 | mqtt_thread.start() 199 | 200 | # Start heartbeat in a separate thread 201 | heartbeat_thread = threading.Thread(target=heartbeat) 202 | heartbeat_thread.daemon = True 203 | heartbeat_thread.start() 204 | 205 | # Give MQTT client time to connect and publish initial state 206 | time.sleep(2) 207 | 208 | # Start Flask web server 209 | app.run(host='0.0.0.0', port=7003, debug=True, use_reloader=False) ``` -------------------------------------------------------------------------------- /templates/index.html: -------------------------------------------------------------------------------- ```html 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | <head> 4 | <meta charset="UTF-8"> 5 | <meta name="viewport" content="width=device-width, initial-scale=1.0"> 6 | <title>IoT Light Bulb Simulator</title> 7 | <style> 8 | body { 9 | font-family: Arial, sans-serif; 10 | max-width: 800px; 11 | margin: 0 auto; 12 | padding: 20px; 13 | text-align: center; 14 | background-color: #f5f5f5; 15 | } 16 | .container { 17 | background-color: white; 18 | border-radius: 8px; 19 | padding: 20px; 20 | box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); 21 | } 22 | h1 { 23 | color: #333; 24 | } 25 | .light-bulb { 26 | width: 100px; 27 | height: 150px; 28 | margin: 30px auto; 29 | position: relative; 30 | } 31 | .bulb { 32 | width: 80px; 33 | height: 80px; 34 | background-color: #eee; 35 | border-radius: 50%; 36 | margin: 0 auto; 37 | position: relative; 38 | transition: all 0.3s ease; 39 | box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); 40 | } 41 | .bulb.on { 42 | box-shadow: 0 0 30px; 43 | } 44 | .base { 45 | width: 30px; 46 | height: 20px; 47 | background-color: #555; 48 | margin: 0 auto; 49 | border-radius: 3px; 50 | } 51 | .controls { 52 | margin-top: 40px; 53 | display: flex; 54 | flex-direction: column; 55 | align-items: center; 56 | gap: 20px; 57 | } 58 | .toggle-btn { 59 | background-color: #4CAF50; 60 | color: white; 61 | border: none; 62 | padding: 10px 20px; 63 | border-radius: 4px; 64 | cursor: pointer; 65 | font-size: 16px; 66 | transition: background-color 0.3s; 67 | } 68 | .toggle-btn:hover { 69 | background-color: #45a049; 70 | } 71 | .slider-container { 72 | width: 100%; 73 | max-width: 300px; 74 | } 75 | .slider { 76 | width: 100%; 77 | height: 25px; 78 | background: #d3d3d3; 79 | outline: none; 80 | -webkit-appearance: none; 81 | border-radius: 10px; 82 | } 83 | .slider::-webkit-slider-thumb { 84 | -webkit-appearance: none; 85 | appearance: none; 86 | width: 25px; 87 | height: 25px; 88 | background: #4CAF50; 89 | cursor: pointer; 90 | border-radius: 50%; 91 | } 92 | .slider::-moz-range-thumb { 93 | width: 25px; 94 | height: 25px; 95 | background: #4CAF50; 96 | cursor: pointer; 97 | border-radius: 50%; 98 | } 99 | .color-picker { 100 | margin-top: 10px; 101 | } 102 | .status { 103 | margin-top: 20px; 104 | padding: 10px; 105 | background-color: #f0f0f0; 106 | border-radius: 4px; 107 | font-family: monospace; 108 | text-align: left; 109 | max-height: 200px; 110 | overflow-y: auto; 111 | } 112 | .mqtt-info { 113 | margin-top: 20px; 114 | background-color: #e8f4f8; 115 | padding: 10px; 116 | border-radius: 4px; 117 | font-size: 0.9em; 118 | text-align: left; 119 | } 120 | </style> 121 | </head> 122 | <body> 123 | <div class="container"> 124 | <h1>IoT Light Bulb Simulator</h1> 125 | <p>Device ID: <strong id="device-id">{{ device_state.device_id }}</strong></p> 126 | 127 | <div class="light-bulb"> 128 | <div id="bulb" class="bulb {% if device_state.properties.power %}on{% endif %}" 129 | style="background-color: {% if device_state.properties.power %}{{ device_state.properties.color }}{% else %}#eee{% endif %}; 130 | opacity: {% if device_state.properties.power %}{{ device_state.properties.brightness/100 }}{% else %}1{% endif %};"> 131 | </div> 132 | <div class="base"></div> 133 | </div> 134 | 135 | <div class="controls"> 136 | <button id="toggle-btn" class="toggle-btn"> 137 | {% if device_state.properties.power %}Turn Off{% else %}Turn On{% endif %} 138 | </button> 139 | 140 | <div class="slider-container"> 141 | <label for="brightness">Brightness: <span id="brightness-value">{{ device_state.properties.brightness }}</span>%</label> 142 | <input type="range" min="1" max="100" value="{{ device_state.properties.brightness }}" class="slider" id="brightness"> 143 | </div> 144 | 145 | <div class="color-picker"> 146 | <label for="color">Color:</label> 147 | <input type="color" id="color" value="{{ device_state.properties.color }}"> 148 | </div> 149 | </div> 150 | 151 | <div class="status"> 152 | <h3>Device State:</h3> 153 | <pre id="state-display">{{ device_state | tojson(indent=2) }}</pre> 154 | </div> 155 | 156 | <div class="mqtt-info"> 157 | <h3>MQTT Information:</h3> 158 | <p><strong>Broker:</strong> {{ device_state.mqtt_broker if device_state.mqtt_broker else 'localhost' }}:{{ device_state.mqtt_port if device_state.mqtt_port else '1883' }}</p> 159 | <p><strong>Command Topic:</strong> devices/{{ device_state.device_id }}/command</p> 160 | <p><strong>State Topic:</strong> devices/{{ device_state.device_id }}/state</p> 161 | <p><strong>State Request Topic:</strong> devices/{{ device_state.device_id }}/state/request</p> 162 | </div> 163 | </div> 164 | 165 | <script> 166 | // Elements 167 | const bulb = document.getElementById('bulb'); 168 | const toggleBtn = document.getElementById('toggle-btn'); 169 | const brightnessSlider = document.getElementById('brightness'); 170 | const brightnessValue = document.getElementById('brightness-value'); 171 | const colorPicker = document.getElementById('color'); 172 | const stateDisplay = document.getElementById('state-display'); 173 | 174 | // Current state 175 | let deviceState = {{ device_state | tojson }}; 176 | 177 | // Update UI based on state 178 | function updateUI() { 179 | // Update bulb appearance 180 | if (deviceState.properties.power) { 181 | bulb.classList.add('on'); 182 | bulb.style.backgroundColor = deviceState.properties.color; 183 | bulb.style.opacity = deviceState.properties.brightness / 100; 184 | toggleBtn.textContent = 'Turn Off'; 185 | } else { 186 | bulb.classList.remove('on'); 187 | bulb.style.backgroundColor = '#eee'; 188 | bulb.style.opacity = 1; 189 | toggleBtn.textContent = 'Turn On'; 190 | } 191 | 192 | // Update controls 193 | brightnessSlider.value = deviceState.properties.brightness; 194 | brightnessValue.textContent = deviceState.properties.brightness; 195 | colorPicker.value = deviceState.properties.color; 196 | 197 | // Update state display 198 | stateDisplay.textContent = JSON.stringify(deviceState, null, 2); 199 | } 200 | 201 | // Toggle light 202 | toggleBtn.addEventListener('click', async () => { 203 | try { 204 | const response = await fetch('/api/toggle', { 205 | method: 'POST', 206 | headers: { 207 | 'Content-Type': 'application/json' 208 | } 209 | }); 210 | const data = await response.json(); 211 | if (data.success) { 212 | deviceState.properties.power = data.power; 213 | updateUI(); 214 | } 215 | } catch (error) { 216 | console.error('Error toggling light:', error); 217 | } 218 | }); 219 | 220 | // Set brightness 221 | brightnessSlider.addEventListener('input', () => { 222 | brightnessValue.textContent = brightnessSlider.value; 223 | if (deviceState.properties.power) { 224 | bulb.style.opacity = brightnessSlider.value / 100; 225 | } 226 | }); 227 | 228 | brightnessSlider.addEventListener('change', async () => { 229 | try { 230 | const response = await fetch('/api/brightness', { 231 | method: 'POST', 232 | headers: { 233 | 'Content-Type': 'application/json' 234 | }, 235 | body: JSON.stringify({ 236 | brightness: parseInt(brightnessSlider.value) 237 | }) 238 | }); 239 | const data = await response.json(); 240 | if (data.success) { 241 | deviceState.properties.brightness = data.brightness; 242 | updateUI(); 243 | } 244 | } catch (error) { 245 | console.error('Error setting brightness:', error); 246 | } 247 | }); 248 | 249 | // Set color 250 | colorPicker.addEventListener('change', async () => { 251 | try { 252 | const response = await fetch('/api/color', { 253 | method: 'POST', 254 | headers: { 255 | 'Content-Type': 'application/json' 256 | }, 257 | body: JSON.stringify({ 258 | color: colorPicker.value 259 | }) 260 | }); 261 | const data = await response.json(); 262 | if (data.success) { 263 | deviceState.properties.color = data.color; 264 | updateUI(); 265 | } 266 | } catch (error) { 267 | console.error('Error setting color:', error); 268 | } 269 | }); 270 | 271 | // Periodically refresh state from server 272 | setInterval(async () => { 273 | try { 274 | const response = await fetch('/api/state'); 275 | const data = await response.json(); 276 | deviceState = data; 277 | updateUI(); 278 | } catch (error) { 279 | console.error('Error fetching state:', error); 280 | } 281 | }, 2000); 282 | </script> 283 | </body> 284 | </html> ``` -------------------------------------------------------------------------------- /iot_mcp_server.py: -------------------------------------------------------------------------------- ```python 1 | from mcp.server.fastmcp import FastMCP, Context 2 | from contextlib import asynccontextmanager 3 | from collections.abc import AsyncIterator 4 | from dataclasses import dataclass, field 5 | from paho.mqtt.client import Client as MQTTClient 6 | import asyncio 7 | import json 8 | import os 9 | import time 10 | from dotenv import load_dotenv 11 | 12 | # Load environment variables from .env file 13 | load_dotenv() 14 | 15 | @dataclass 16 | class IoTContext: 17 | """Context for the IoT MCP server.""" 18 | mqtt_client: MQTTClient 19 | connected_devices: dict = field(default_factory=dict) 20 | 21 | # Create a global MQTT client that can be reused across sessions 22 | global_mqtt_client = None 23 | global_devices = {} 24 | 25 | @asynccontextmanager 26 | async def iot_lifespan(server: FastMCP) -> AsyncIterator[IoTContext]: 27 | """ 28 | Manages the MQTT client lifecycle. 29 | 30 | Args: 31 | server: The FastMCP server instance 32 | 33 | Yields: 34 | IoTContext: The context containing the MQTT client 35 | """ 36 | global global_mqtt_client, global_devices 37 | 38 | # Initialize MQTT client only if it doesn't exist 39 | if global_mqtt_client is None: 40 | print("Creating new MQTT client") 41 | # Use MQTTv311 protocol to address deprecation warning 42 | import paho.mqtt.client as mqtt # Import here to ensure we have the module 43 | mqtt_client = MQTTClient(client_id="iot_mcp_server", protocol=mqtt.MQTTv311) 44 | broker_address = os.getenv("MQTT_BROKER", "localhost") 45 | broker_port = int(os.getenv("MQTT_PORT", "1883")) 46 | 47 | # Set up MQTT callbacks for device discovery 48 | def on_connect(client, userdata, flags, rc): 49 | if rc == 0: 50 | print(f"Connected to MQTT broker at {broker_address}:{broker_port}") 51 | # Subscribe to all device state topics for discovery 52 | client.subscribe("devices/+/state") 53 | # Subscribe to broadcast responses 54 | client.subscribe("devices/broadcast/response") 55 | print("Subscribed to device state topics") 56 | else: 57 | print(f"Failed to connect to MQTT broker with result code {rc}") 58 | 59 | def on_message(client, userdata, message): 60 | topic = message.topic 61 | payload = message.payload.decode("utf-8") 62 | print(f"MQTT: Received message on {topic}: {payload[:100]}...") 63 | 64 | # Check if this is a device state message 65 | if topic.startswith("devices/") and topic.endswith("/state"): 66 | try: 67 | # Parse device data 68 | device_data = json.loads(payload) 69 | device_id = device_data.get("device_id") 70 | 71 | if device_id: 72 | # Update our device registry 73 | global_devices[device_id] = { 74 | "id": device_id, 75 | "type": device_data.get("type", "unknown"), 76 | "online": device_data.get("online", True), 77 | "last_seen": time.time(), 78 | "properties": device_data.get("properties", {}), 79 | "topic": topic 80 | } 81 | print(f"Discovered/updated device: {device_id} with properties: {device_data.get('properties', {})}") 82 | except json.JSONDecodeError: 83 | print(f"Received invalid JSON in device state: {payload}") 84 | except Exception as e: 85 | print(f"Error processing device state: {str(e)}") 86 | 87 | # Set callbacks 88 | mqtt_client.on_connect = on_connect 89 | mqtt_client.on_message = on_message 90 | 91 | try: 92 | # Connect to the MQTT broker 93 | print(f"Connecting to MQTT broker at {broker_address}:{broker_port}") 94 | mqtt_client.connect(broker_address, broker_port) 95 | mqtt_client.loop_start() # Start the MQTT client loop in a separate thread 96 | 97 | # Wait a short time to ensure connection and subscription 98 | await asyncio.sleep(1) 99 | 100 | # Force a request for device states (using a broadcast topic instead of wildcards) 101 | print("Requesting states from all devices via broadcast") 102 | mqtt_client.publish("devices/broadcast/request", json.dumps({"request_id": "startup", "action": "report_state"})) 103 | 104 | # Add the light bulb simulator as a known device if we're running locally 105 | if broker_address in ("localhost", "127.0.0.1"): 106 | print("Running locally - adding light bulb as a default device") 107 | global_devices["light_bulb_001"] = { 108 | "id": "light_bulb_001", 109 | "type": "light", 110 | "online": True, 111 | "last_seen": time.time(), 112 | "properties": { 113 | "power": False, 114 | "brightness": 100, 115 | "color": "#FFFF00" 116 | }, 117 | "topic": "devices/light_bulb_001/state" 118 | } 119 | 120 | global_mqtt_client = mqtt_client 121 | except Exception as e: 122 | print(f"Failed to connect MQTT client: {str(e)}") 123 | # Clean up if connection failed 124 | mqtt_client.loop_stop() 125 | mqtt_client = None 126 | else: 127 | print("Reusing existing MQTT client") 128 | 129 | # Create context with global client and devices 130 | context = IoTContext(mqtt_client=global_mqtt_client, connected_devices=global_devices) 131 | 132 | try: 133 | yield context 134 | finally: 135 | # Don't disconnect the client, it will be reused 136 | pass 137 | 138 | # Initialize FastMCP server with the IoT context 139 | mcp = FastMCP( 140 | "mcp-iot", 141 | description="MCP server for IoT device control", 142 | lifespan=iot_lifespan, 143 | host=os.getenv("HOST", "0.0.0.0"), 144 | port=os.getenv("PORT", "8090") 145 | ) 146 | 147 | @mcp.tool() 148 | async def list_devices(ctx: Context, **kwargs) -> str: 149 | """List all connected IoT devices. 150 | 151 | This tool returns information about all IoT devices that have been discovered 152 | on the MQTT network. For each device, it provides the ID, type, online status, 153 | and available commands. 154 | 155 | Note: Do NOT try to call a device ID directly. Instead, use the command tools like: 156 | - light_bulb_on - To turn the light bulb ON 157 | - light_bulb_off - To turn the light bulb OFF 158 | - light_bulb_toggle - To toggle the light bulb 159 | - check_light_status - To check light status 160 | """ 161 | try: 162 | print(f"List devices tool called with kwargs: {kwargs}") 163 | return """Available device: light_bulb_001 (light) 164 | 165 | To control the light bulb, DO NOT call the device ID directly. Instead use: 166 | - light_bulb_on: Turn the light ON 167 | - light_bulb_off: Turn the light OFF 168 | - light_bulb_toggle: Toggle the light ON/OFF 169 | - check_light_status: Check the current status 170 | """ 171 | except Exception as e: 172 | print(f"Error in list_devices: {str(e)}") 173 | return "Found light_bulb_001 device. Use light_bulb_on or light_bulb_off to control it." 174 | 175 | @mcp.tool() 176 | async def turn_on_light(ctx: Context, **kwargs) -> str: 177 | """Turn ON the light bulb. 178 | 179 | This tool will explicitly turn on the light bulb, setting its power state to true. 180 | 181 | Args: 182 | ctx: The MCP server provided context 183 | dummy: Optional parameter that does nothing (required for schema compatibility) 184 | """ 185 | try: 186 | print(f"Turn on light called with kwargs: {kwargs}") 187 | mqtt_client = ctx.request_context.lifespan_context.mqtt_client 188 | topic = "devices/light_bulb_001/command" 189 | 190 | # Prepare the command to set power on 191 | message = { 192 | "command": "set_power", 193 | "timestamp": time.time(), 194 | "payload": { 195 | "power": True 196 | } 197 | } 198 | 199 | # Publish the message 200 | print(f"Sending turn ON command: {json.dumps(message)}") 201 | result = mqtt_client.publish(topic, json.dumps(message)) 202 | print(f"MQTT publish result: {result.rc}") 203 | 204 | # Request an immediate state update to verify the change 205 | await asyncio.sleep(0.5) # Give the bulb time to process the command 206 | state_topic = "devices/light_bulb_001/state/request" 207 | mqtt_client.publish(state_topic, json.dumps({"request_id": "verify_power_on"})) 208 | 209 | return "Light bulb has been turned ON" 210 | except Exception as e: 211 | print(f"Error in turn_on_light: {str(e)}") 212 | import traceback 213 | print(traceback.format_exc()) 214 | return f"Error turning on light: {str(e)}" 215 | 216 | @mcp.tool() 217 | async def turn_off_light(ctx: Context, **kwargs) -> str: 218 | """Turn OFF the light bulb. 219 | 220 | This tool will explicitly turn off the light bulb, setting its power state to false. 221 | 222 | Args: 223 | ctx: The MCP server provided context 224 | dummy: Optional parameter that does nothing (required for schema compatibility) 225 | """ 226 | try: 227 | print(f"Turn off light called with kwargs: {kwargs}") 228 | mqtt_client = ctx.request_context.lifespan_context.mqtt_client 229 | topic = "devices/light_bulb_001/command" 230 | 231 | # Prepare the command to set power off 232 | message = { 233 | "command": "set_power", 234 | "timestamp": time.time(), 235 | "payload": { 236 | "power": False 237 | } 238 | } 239 | 240 | # Publish the message 241 | mqtt_client.publish(topic, json.dumps(message)) 242 | return "Light bulb has been turned OFF" 243 | except Exception as e: 244 | return f"Error turning off light: {str(e)}" 245 | 246 | @mcp.tool() 247 | async def toggle_light(ctx: Context, **kwargs) -> str: 248 | """Toggle the light bulb on/off. 249 | 250 | This is a simplified tool to toggle the light bulb power state. 251 | 252 | Args: 253 | ctx: The MCP server provided context 254 | dummy: Optional parameter that does nothing (required for schema compatibility) 255 | """ 256 | try: 257 | print(f"Toggle light called with kwargs: {kwargs}") 258 | mqtt_client = ctx.request_context.lifespan_context.mqtt_client 259 | topic = "devices/light_bulb_001/command" 260 | 261 | # Prepare the toggle command 262 | message = { 263 | "command": "toggle", 264 | "timestamp": time.time() 265 | } 266 | 267 | # Publish the message 268 | mqtt_client.publish(topic, json.dumps(message)) 269 | return "Light bulb toggle command sent successfully" 270 | except Exception as e: 271 | return f"Error toggling light: {str(e)}" 272 | 273 | @mcp.tool() 274 | async def check_light_status(ctx: Context, **kwargs) -> str: 275 | """Check the current status of the light bulb. 276 | 277 | Returns the current power state, brightness and color of the light bulb. 278 | """ 279 | try: 280 | connected_devices = ctx.request_context.lifespan_context.connected_devices 281 | if "light_bulb_001" in connected_devices: 282 | properties = connected_devices["light_bulb_001"].get("properties", {}) 283 | power = "ON" if properties.get("power", False) else "OFF" 284 | brightness = properties.get("brightness", 100) 285 | color = properties.get("color", "#FFFF00") 286 | 287 | # Request a fresh state update 288 | mqtt_client = ctx.request_context.lifespan_context.mqtt_client 289 | state_topic = "devices/light_bulb_001/state/request" 290 | mqtt_client.publish(state_topic, json.dumps({"request_id": "check_status"})) 291 | 292 | return f"Light bulb status: Power is {power}, Brightness is {brightness}%, Color is {color}" 293 | else: 294 | return "Light bulb status: Device not found in registry" 295 | except Exception as e: 296 | return f"Error checking light status: {str(e)}" 297 | 298 | @mcp.tool() 299 | async def light_bulb_on(ctx: Context, **kwargs) -> str: 300 | """Turn the light bulb ON. 301 | 302 | This command turns on the light with ID 'light_bulb_001'. 303 | """ 304 | return await turn_on_light(ctx, **kwargs) 305 | 306 | @mcp.tool() 307 | async def light_bulb_off(ctx: Context, **kwargs) -> str: 308 | """Turn the light bulb OFF. 309 | 310 | This command turns off the light with ID 'light_bulb_001'. 311 | """ 312 | return await turn_off_light(ctx, **kwargs) 313 | 314 | @mcp.tool() 315 | async def light_bulb_toggle(ctx: Context, **kwargs) -> str: 316 | """Toggle the light bulb ON/OFF. 317 | 318 | This command toggles the light with ID 'light_bulb_001'. 319 | """ 320 | return await toggle_light(ctx, **kwargs) 321 | 322 | @mcp.tool() 323 | async def get_help(ctx: Context, **kwargs) -> str: 324 | """Get help on how to control IoT devices. 325 | 326 | This tool provides information about how to properly control the connected IoT devices. 327 | """ 328 | return """ 329 | ## IoT Device Control Help 330 | 331 | The following commands are available to control the light bulb: 332 | 333 | 1. `light_bulb_on` - Turn the light bulb ON 334 | 2. `light_bulb_off` - Turn the light bulb OFF 335 | 3. `light_bulb_toggle` - Toggle the light bulb ON/OFF 336 | 4. `check_light_status` - Check the current status of the light bulb 337 | 338 | IMPORTANT: You cannot control devices by using their device ID directly (e.g., "light_bulb_001"). 339 | Always use the specific command functions listed above. 340 | """ 341 | 342 | @mcp.tool() 343 | async def check_command(ctx: Context, command: str) -> str: 344 | """Check if a command is valid and provide guidance. 345 | 346 | This is a helper tool to check if a command exists and provide guidance on how to use it. 347 | 348 | Args: 349 | ctx: The MCP server provided context 350 | command: The command or device ID to check 351 | """ 352 | if command == "light_bulb_001": 353 | return """ 354 | ERROR: "light_bulb_001" is a device ID, not a command. 355 | 356 | To control this light bulb, use one of these commands: 357 | - light_bulb_on - Turn the light ON 358 | - light_bulb_off - Turn the light OFF 359 | - light_bulb_toggle - Toggle the light ON/OFF 360 | - check_light_status - Check the status 361 | """ 362 | 363 | valid_commands = [ 364 | "list_devices", "turn_on_light", "turn_off_light", "toggle_light", 365 | "check_light_status", "light_bulb_on", "light_bulb_off", "light_bulb_toggle" 366 | ] 367 | 368 | if command in valid_commands: 369 | return f"The command '{command}' is valid. You can use it to control the light bulb." 370 | else: 371 | return f""" 372 | The command '{command}' is not recognized. 373 | 374 | Available commands: 375 | - light_bulb_on - Turn the light ON 376 | - light_bulb_off - Turn the light OFF 377 | - light_bulb_toggle - Toggle the light ON/OFF 378 | - check_light_status - Check the status 379 | """ 380 | 381 | async def main(): 382 | """Run the MCP server with the configured transport.""" 383 | transport = os.getenv("TRANSPORT", "sse") 384 | 385 | if transport == 'sse': 386 | # Run the MCP server with SSE transport 387 | print("Starting MCP server with SSE transport...") 388 | await mcp.run_sse_async() 389 | else: 390 | # Run the MCP server with stdio transport 391 | print("Starting MCP server with stdio transport...") 392 | await mcp.run_stdio_async() 393 | 394 | if __name__ == "__main__": 395 | asyncio.run(main()) ```