# 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())
```