#
tokens: 13720/50000 14/14 files
lines: on (toggle) GitHub
raw markdown copy reset
# 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())
```