#
tokens: 18060/50000 7/7 files
lines: on (toggle) GitHub
raw markdown copy reset
# Directory Structure

```
├── .env-example
├── .gitignore
├── .python-version
├── LICENSE
├── meraki-mcp.py
├── pyproject.toml
├── README.md
└── requirements.txt
```

# Files

--------------------------------------------------------------------------------
/.python-version:
--------------------------------------------------------------------------------

```
1 | 3.13
2 | 
```

--------------------------------------------------------------------------------
/.env-example:
--------------------------------------------------------------------------------

```
1 | MERAKI_API_KEY="Meraki API Key here"
2 | MERAKI_ORG_ID="Meraki Org ID here"
```

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # Python
 2 | __pycache__/
 3 | *.py[cod]
 4 | *$py.class
 5 | *.so
 6 | .Python
 7 | build/
 8 | develop-eggs/
 9 | dist/
10 | downloads/
11 | eggs/
12 | .eggs/
13 | lib/
14 | lib64/
15 | parts/
16 | sdist/
17 | var/
18 | wheels/
19 | *.egg-info/
20 | .installed.cfg
21 | *.egg
22 | uv.lock
23 | 
24 | # Virtual Environment
25 | venv/
26 | ENV/
27 | env/
28 | .env
29 | .venv/
30 | 
31 | # IDE
32 | .idea/
33 | .vscode/
34 | *.swp
35 | *.swo
36 | 
37 | # OS
38 | .DS_Store
39 | Thumbs.db
40 | 
41 | # Documentation
42 | ADDITIONAL_TOOLS_ROADMAP.md
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
  1 | # Meraki Magic MCP
  2 | 
  3 | Meraki Magic is a Python-based MCP (Model Context Protocol) server for Cisco's Meraki Dashboard. Meraki Magic provides tools for querying the Meraki Dashboard API to discover, monitor, and manage your Meraki environment.
  4 | 
  5 | ## Features
  6 | 
  7 | - **Comprehensive Network Management**: Full network discovery, monitoring, and management
  8 | - **Advanced Device Management**: Device provisioning, monitoring, and live tools
  9 | - **Wireless Management**: Complete wireless SSID and RF profile management
 10 | - **Switch Management**: Port management, VLAN configuration, and QoS rules
 11 | - **Appliance Management**: VPN, firewall, content filtering, and security management
 12 | - **Camera Management**: Analytics, snapshots, and sense configuration
 13 | - **Network Automation**: Action batches and bulk operations
 14 | - **Live Device Tools**: Ping, cable testing, LED control, and wake-on-LAN
 15 | - **Advanced Monitoring**: Events, alerts, and performance analytics
 16 | 
 17 | ## Installation
 18 | 
 19 | 1. Clone the repository:
 20 | ```bash
 21 | git clone https://github.com/mkutka/meraki-magic.git
 22 | cd meraki-magic-mcp
 23 | ```
 24 | 
 25 | 2. Create a virtual environment and activate it:
 26 | ```bash
 27 | python -m venv .venv
 28 | source .venv/bin/activate  # On Windows: .venv\Scripts\activate
 29 | ```
 30 | 
 31 | 3. Install dependencies:
 32 | ```bash
 33 | pip install -r requirements.txt
 34 | ```
 35 | 
 36 | ## Configuration
 37 | 
 38 | 1. Copy the example environment file:
 39 | ```bash
 40 | cp .env-example .env
 41 | ```
 42 | 
 43 | 2. Update the `.env` file with your Meraki API Key and Organization ID:
 44 | ```env
 45 | MERAKI_API_KEY="Meraki API Key here"
 46 | MERAKI_ORG_ID="Meraki Org ID here"
 47 | ```
 48 | 
 49 | ## Usage With Claude Desktop Client
 50 | 
 51 | 1. Configure Claude Desktop to use this MCP server:
 52 | 
 53 | - Open Claude Desktop
 54 | - Go to Settings > Developer > Edit Config
 55 | - Add the following configuration file `claude_desktop_config.json`
 56 | 
 57 | ```json
 58 | {
 59 |   "mcpServers": {
 60 |       "Meraki_Magic_MCP": {
 61 |         "command": "/Users/mkutka/meraki-magic-mcp/.venv/bin/fastmcp",
 62 |         "args": [
 63 |           "run",
 64 |           "/Users/mkutka/meraki-magic-mcp/meraki-mcp.py"
 65 |         ]
 66 |       }
 67 |   }
 68 | }
 69 | ```
 70 | 
 71 | - Replace the paths above to reflect your local environment.
 72 | 
 73 | 2. Restart Claude Desktop
 74 | 
 75 | 3. Interact with Claude Desktop
 76 | 
 77 | ## Network Tools Guide
 78 | 
 79 | This guide provides a comprehensive overview of all the network tools available in the Meraki Magic MCP, organized by category and functionality.
 80 | 
 81 | ### Table of Contents
 82 | 
 83 | 1. [Organization Management Tools](#organization-management-tools)
 84 | 2. [Network Management Tools](#network-management-tools)
 85 | 3. [Device Management Tools](#device-management-tools)
 86 | 4. [Wireless Management Tools](#wireless-management-tools)
 87 | 5. [Switch Management Tools](#switch-management-tools)
 88 | 6. [Appliance Management Tools](#appliance-management-tools)
 89 | 7. [Camera Management Tools](#camera-management-tools)
 90 | 8. [Network Automation Tools](#network-automation-tools)
 91 | 9. [Advanced Monitoring Tools](#advanced-monitoring-tools)
 92 | 10. [Live Device Tools](#live-device-tools)
 93 | 
 94 | ---
 95 | 
 96 | ## Organization Management Tools
 97 | 
 98 | ### Basic Organization Operations
 99 | - **`get_organizations()`** - Get a list of organizations the user has access to
100 | - **`get_organization_details(org_id)`** - Get details for a specific organization
101 | - **`get_organization_status(org_id)`** - Get the status and health of an organization
102 | - **`get_organization_inventory(org_id)`** - Get the inventory for an organization
103 | - **`get_organization_license(org_id)`** - Get the license state for an organization
104 | - **`get_organization_conf_change(org_id)`** - Get the org change state for an organization
105 | 
106 | ### Advanced Organization Management
107 | - **`get_organization_admins(org_id)`** - Get a list of organization admins
108 | - **`create_organization_admin(org_id, email, name, org_access, tags, networks)`** - Create a new organization admin
109 | - **`get_organization_api_requests(org_id, timespan)`** - Get organization API request history
110 | - **`get_organization_webhook_logs(org_id, timespan)`** - Get organization webhook logs
111 | 
112 | ### Network Management
113 | - **`get_networks(org_id)`** - Get a list of networks from Meraki
114 | - **`create_network(name, tags, productTypes, org_id, copyFromNetworkId)`** - Create a new network
115 | - **`delete_network(network_id)`** - Delete a network in Meraki
116 | - **`get_network_details(network_id)`** - Get details for a specific network
117 | - **`update_network(network_id, update_data)`** - Update a network's properties
118 | 
119 | ---
120 | 
121 | ## Network Management Tools
122 | 
123 | ### Network Monitoring
124 | - **`get_network_events(network_id, timespan, per_page)`** - Get network events history
125 | - **`get_network_event_types(network_id)`** - Get available network event types
126 | - **`get_network_alerts_history(network_id, timespan)`** - Get network alerts history
127 | - **`get_network_alerts_settings(network_id)`** - Get network alerts settings
128 | - **`update_network_alerts_settings(network_id, defaultDestinations, alerts)`** - Update network alerts settings
129 | 
130 | ### Client Management
131 | - **`get_clients(network_id, timespan)`** - Get a list of clients from a network
132 | - **`get_client_details(network_id, client_id)`** - Get details for a specific client
133 | - **`get_client_usage(network_id, client_id)`** - Get the usage history for a client
134 | - **`get_client_policy(network_id, client_id)`** - Get the policy for a specific client
135 | - **`update_client_policy(network_id, client_id, device_policy, group_policy_id)`** - Update policy for a client
136 | 
137 | ### Network Traffic & Analysis
138 | - **`get_network_traffic(network_id, timespan)`** - Get traffic analysis data for a network
139 | 
140 | ---
141 | 
142 | ## Device Management Tools
143 | 
144 | ### Device Information
145 | - **`get_devices(org_id)`** - Get a list of devices from Meraki
146 | - **`get_network_devices(network_id)`** - Get a list of devices in a specific network
147 | - **`get_device_details(serial)`** - Get details for a specific device by serial number
148 | - **`get_device_status(serial)`** - Get the current status of a device
149 | - **`get_device_uplink(serial)`** - Get the uplink status of a device
150 | 
151 | ### Device Operations
152 | - **`update_device(serial, device_settings)`** - Update a device in the Meraki organization
153 | - **`claim_devices(network_id, serials)`** - Claim one or more devices into a Meraki network
154 | - **`remove_device(serial)`** - Remove a device from its network
155 | - **`reboot_device(serial)`** - Reboot a device
156 | 
157 | ### Device Monitoring
158 | - **`get_device_clients(serial, timespan)`** - Get clients connected to a specific device
159 | 
160 | ---
161 | 
162 | ## Live Device Tools
163 | 
164 | ### Network Diagnostics
165 | - **`ping_device(serial, target_ip, count)`** - Ping a device from another device
166 | - **`get_device_ping_results(serial, ping_id)`** - Get results from a device ping test
167 | - **`cable_test_device(serial, ports)`** - Run cable test on device ports
168 | - **`get_device_cable_test_results(serial, cable_test_id)`** - Get results from a device cable test
169 | 
170 | ### Device Control
171 | - **`blink_device_leds(serial, duration)`** - Blink device LEDs for identification
172 | - **`wake_on_lan_device(serial, mac)`** - Send wake-on-LAN packet to a device
173 | 
174 | ---
175 | 
176 | ## Wireless Management Tools
177 | 
178 | ### Basic Wireless Operations
179 | - **`get_wireless_ssids(network_id)`** - Get wireless SSIDs for a network
180 | - **`update_wireless_ssid(network_id, ssid_number, ssid_settings)`** - Update a wireless SSID
181 | - **`get_wireless_settings(network_id)`** - Get wireless settings for a network
182 | 
183 | ### Advanced Wireless Management
184 | - **`get_wireless_rf_profiles(network_id)`** - Get wireless RF profiles for a network
185 | - **`create_wireless_rf_profile(network_id, name, band_selection_type, **kwargs)`** - Create a wireless RF profile
186 | - **`get_wireless_channel_utilization(network_id, timespan)`** - Get wireless channel utilization history
187 | - **`get_wireless_signal_quality(network_id, timespan)`** - Get wireless signal quality history
188 | - **`get_wireless_connection_stats(network_id, timespan)`** - Get wireless connection statistics
189 | - **`get_wireless_client_connectivity_events(network_id, client_id, timespan)`** - Get wireless client connectivity events
190 | 
191 | ---
192 | 
193 | ## Switch Management Tools
194 | 
195 | ### Basic Switch Operations
196 | - **`get_switch_ports(serial)`** - Get ports for a switch
197 | - **`update_switch_port(serial, port_id, name, tags, enabled, vlan)`** - Update a switch port
198 | - **`get_switch_vlans(network_id)`** - Get VLANs for a network
199 | - **`create_switch_vlan(network_id, vlan_id, name, subnet, appliance_ip)`** - Create a switch VLAN
200 | 
201 | ### Advanced Switch Management
202 | - **`get_switch_port_statuses(serial)`** - Get switch port statuses
203 | - **`cycle_switch_ports(serial, ports)`** - Cycle (restart) switch ports
204 | - **`get_switch_access_control_lists(network_id)`** - Get switch access control lists
205 | - **`update_switch_access_control_lists(network_id, rules)`** - Update switch access control lists
206 | - **`get_switch_qos_rules(network_id)`** - Get switch QoS rules
207 | - **`create_switch_qos_rule(network_id, vlan, protocol, src_port, **kwargs)`** - Create a switch QoS rule
208 | 
209 | ---
210 | 
211 | ## Appliance Management Tools
212 | 
213 | ### Basic Appliance Operations
214 | - **`get_security_center(network_id)`** - Get security information for a network
215 | - **`get_vpn_status(network_id)`** - Get VPN status for a network
216 | - **`get_firewall_rules(network_id)`** - Get firewall rules for a network
217 | - **`update_firewall_rules(network_id, rules)`** - Update firewall rules for a network
218 | 
219 | ### Advanced Appliance Management
220 | - **`get_appliance_vpn_site_to_site(network_id)`** - Get appliance VPN site-to-site configuration
221 | - **`update_appliance_vpn_site_to_site(network_id, mode, hubs, subnets)`** - Update appliance VPN site-to-site configuration
222 | - **`get_appliance_content_filtering(network_id)`** - Get appliance content filtering settings
223 | - **`update_appliance_content_filtering(network_id, **kwargs)`** - Update appliance content filtering settings
224 | - **`get_appliance_security_events(network_id, timespan)`** - Get appliance security events
225 | - **`get_appliance_traffic_shaping(network_id)`** - Get appliance traffic shaping settings
226 | - **`update_appliance_traffic_shaping(network_id, global_bandwidth_limits)`** - Update appliance traffic shaping settings
227 | 
228 | ---
229 | 
230 | ## Camera Management Tools
231 | 
232 | ### Basic Camera Operations
233 | - **`get_camera_video_settings(network_id, serial)`** - Get video settings for a camera
234 | - **`get_camera_quality_settings(network_id)`** - Get quality and retention settings for cameras
235 | 
236 | ### Advanced Camera Management
237 | - **`get_camera_analytics_live(serial)`** - Get live camera analytics
238 | - **`get_camera_analytics_overview(serial, timespan)`** - Get camera analytics overview
239 | - **`get_camera_analytics_zones(serial)`** - Get camera analytics zones
240 | - **`generate_camera_snapshot(serial, timestamp)`** - Generate a camera snapshot
241 | - **`get_camera_sense(serial)`** - Get camera sense configuration
242 | - **`update_camera_sense(serial, sense_enabled, mqtt_broker_id, audio_detection)`** - Update camera sense configuration
243 | 
244 | ---
245 | 
246 | ## Network Automation Tools
247 | 
248 | ### Action Batches
249 | - **`create_action_batch(org_id, actions, confirmed, synchronous)`** - Create an action batch for bulk operations
250 | - **`get_action_batch_status(org_id, batch_id)`** - Get action batch status
251 | - **`get_action_batches(org_id)`** - Get all action batches for an organization
252 | 
253 | ---
254 | 
255 | ## Advanced Monitoring Tools
256 | 
257 | ### Network Events & Alerts
258 | - **`get_network_events(network_id, timespan, per_page)`** - Get network events history
259 | - **`get_network_event_types(network_id)`** - Get available network event types
260 | - **`get_network_alerts_history(network_id, timespan)`** - Get network alerts history
261 | - **`get_network_alerts_settings(network_id)`** - Get network alerts settings
262 | - **`update_network_alerts_settings(network_id, defaultDestinations, alerts)`** - Update network alerts settings
263 | 
264 | ### Organization Monitoring
265 | - **`get_organization_api_requests(org_id, timespan)`** - Get organization API request history
266 | - **`get_organization_webhook_logs(org_id, timespan)`** - Get organization webhook logs
267 | 
268 | ---
269 | 
270 | ## Schema Definitions
271 | 
272 | The MCP includes comprehensive Pydantic schemas for data validation:
273 | 
274 | - `SsidUpdateSchema` - Wireless SSID configuration
275 | - `FirewallRule` - Firewall rule configuration
276 | - `DeviceUpdateSchema` - Device update parameters
277 | - `NetworkUpdateSchema` - Network update parameters
278 | - `AdminCreationSchema` - Admin creation parameters
279 | - `ActionBatchSchema` - Action batch configuration
280 | - `VpnSiteToSiteSchema` - VPN site-to-site configuration
281 | - `ContentFilteringSchema` - Content filtering settings
282 | - `TrafficShapingSchema` - Traffic shaping configuration
283 | - `CameraSenseSchema` - Camera sense settings
284 | - `SwitchQosRuleSchema` - Switch QoS rule configuration
285 | 
286 | ---
287 | 
288 | ## Best Practices
289 | 
290 | 1. **Error Handling**: Always check API responses for errors
291 | 2. **Rate Limiting**: The Meraki API has rate limits; use appropriate delays
292 | 3. **Batch Operations**: Use action batches for bulk operations
293 | 4. **Validation**: Use the provided schemas for data validation
294 | 5. **Monitoring**: Regularly check network events and alerts
295 | 6. **Security**: Keep API keys secure and rotate them regularly
296 | 
297 | ---
298 | 
299 | ## Troubleshooting
300 | 
301 | ### Common Issues
302 | 
303 | 1. **Authentication Errors**: Verify your API key is correct and has appropriate permissions
304 | 2. **Rate Limiting**: If you encounter rate limiting, implement delays between requests
305 | 3. **Network Not Found**: Ensure the network ID is correct and accessible
306 | 4. **Device Not Found**: Verify the device serial number is correct and the device is online
307 | 
308 | ### Debug Information
309 | 
310 | Enable debug logging by setting the appropriate log level in your environment.
311 | 
312 | ---
313 | 
314 | ## Additional Resources
315 | 
316 | - [Meraki API Documentation](https://developer.cisco.com/meraki/api-v1/)
317 | - [MCP Protocol Documentation](https://modelcontextprotocol.io/)
318 | - [FastMCP Documentation](https://github.com/jlowin/fastmcp)
319 | 
320 | For more detailed information about additional tools and future enhancements, see the [Additional Tools Roadmap](ADDITIONAL_TOOLS_ROADMAP.md).
321 | 
322 | ---
323 | 
324 | ## ⚠️ Disclaimer
325 | 
326 | **IMPORTANT: PRODUCTION USE DISCLAIMER**
327 | 
328 | This software is provided "AS IS" without warranty of any kind, either express or implied. The authors and contributors make no representations or warranties regarding the suitability, reliability, availability, accuracy, or completeness of this software for any purpose.
329 | 
330 | **USE AT YOUR OWN RISK**: This MCP server is designed for development, testing, and educational purposes. Running this software in production environments is done entirely at your own risk. The authors and contributors are not responsible for any damages, data loss, service interruptions, or other issues that may arise from the use of this software in production environments.
331 | 
332 | **SECURITY CONSIDERATIONS**: This software requires access to your Meraki API credentials. Ensure that:
333 | - API keys are stored securely and not committed to version control
334 | - API keys have appropriate permissions and are rotated regularly
335 | - Network access is properly secured
336 | - Regular security audits are performed
337 | 
338 | **NO WARRANTY**: The authors disclaim all warranties, including but not limited to warranties of merchantability, fitness for a particular purpose, and non-infringement. In no event shall the authors be liable for any claim, damages, or other liability arising from the use of this software.
339 | 
340 | **SUPPORT**: This is an open-source project. For production use, consider implementing additional testing, monitoring, and support mechanisms appropriate for your environment.
```

--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------

```toml
 1 | [project]
 2 | name = "meraki-magic-mcp"
 3 | version = "0.1.0"
 4 | description = "Add your description here"
 5 | readme = "README.md"
 6 | requires-python = ">=3.13"
 7 | dependencies = [
 8 |     "mcp[cli]>=1.8.0",
 9 |     "meraki>=2.0.2",
10 | ]
11 | 
```

--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------

```
 1 | aiohappyeyeballs==2.6.1
 2 | aiohttp==3.11.18
 3 | aiosignal==1.3.2
 4 | annotated-types==0.7.0
 5 | anyio==4.9.0
 6 | attrs==25.3.0
 7 | build==1.2.2.post1
 8 | certifi==2025.4.26
 9 | charset-normalizer==3.4.2
10 | click==8.1.8
11 | exceptiongroup==1.2.2
12 | fastmcp==2.2.10
13 | frozenlist==1.6.0
14 | h11==0.16.0
15 | httpcore==1.0.9
16 | httpx==0.28.1
17 | httpx-sse==0.4.0
18 | idna==3.10
19 | iniconfig==2.1.0
20 | Jinja2==3.1.6
21 | markdown-it-py==3.0.0
22 | MarkupSafe==3.0.2
23 | mcp==1.7.1
24 | mdurl==0.1.2
25 | meraki==2.0.2
26 | multidict==6.4.3
27 | openapi-pydantic==0.5.1
28 | packaging==25.0
29 | pluggy==1.5.0
30 | propcache==0.3.1
31 | pydantic==2.11.4
32 | pydantic-settings==2.9.1
33 | pydantic_core==2.33.2
34 | Pygments==2.19.1
35 | pyproject_hooks==1.2.0
36 | pytest==7.4.4
37 | python-dotenv==1.1.0
38 | python-multipart==0.0.20
39 | requests==2.32.3
40 | rich==14.0.0
41 | setuptools==70.3.0
42 | shellingham==1.5.4
43 | sniffio==1.3.1
44 | sse-starlette==2.3.4
45 | starlette==0.46.2
46 | typer==0.15.3
47 | typing-inspection==0.4.0
48 | typing_extensions==4.13.2
49 | urllib3==2.4.0
50 | uvicorn==0.34.2
51 | websockets==15.0.1
52 | wheel==0.45.1
53 | yarl==1.20.0
54 | 
```

--------------------------------------------------------------------------------
/meraki-mcp.py:
--------------------------------------------------------------------------------

```python
   1 | import os
   2 | import json
   3 | import meraki
   4 | import asyncio
   5 | import functools
   6 | from typing import Dict, List, Optional, Any, TypedDict, Union, Callable
   7 | from pydantic import BaseModel, Field
   8 | from mcp.server.fastmcp import FastMCP
   9 | from dotenv import load_dotenv
  10 | 
  11 | # Load environment variables from .env file
  12 | load_dotenv()
  13 | 
  14 | # Create an MCP server
  15 | mcp = FastMCP("Meraki Magic MCP")
  16 | 
  17 | # Configuration
  18 | MERAKI_API_KEY = os.getenv("MERAKI_API_KEY")
  19 | MERAKI_ORG_ID = os.getenv("MERAKI_ORG_ID")
  20 | 
  21 | # Initialize Meraki API client using Meraki SDK
  22 | dashboard = meraki.DashboardAPI(api_key=MERAKI_API_KEY, suppress_logging=True)
  23 | 
  24 | ###################
  25 | # ASYNC UTILITIES
  26 | ###################
  27 | 
  28 | def to_async(func: Callable) -> Callable:
  29 |     """
  30 |     Convert a synchronous function to an asynchronous function
  31 | 
  32 |     Args:
  33 |         func: The synchronous function to convert
  34 | 
  35 |     Returns:
  36 |         An asynchronous version of the function
  37 |     """
  38 |     @functools.wraps(func)
  39 |     async def wrapper(*args, **kwargs):
  40 |         loop = asyncio.get_event_loop()
  41 |         return await loop.run_in_executor(
  42 |             None,
  43 |             lambda: func(*args, **kwargs)
  44 |         )
  45 |     return wrapper
  46 | 
  47 | # Create async versions of commonly used Meraki API methods
  48 | async_get_organizations = to_async(dashboard.organizations.getOrganizations)
  49 | async_get_organization = to_async(dashboard.organizations.getOrganization)
  50 | async_get_organization_networks = to_async(dashboard.organizations.getOrganizationNetworks)
  51 | async_get_organization_devices = to_async(dashboard.organizations.getOrganizationDevices)
  52 | async_get_network = to_async(dashboard.networks.getNetwork)
  53 | async_get_network_devices = to_async(dashboard.networks.getNetworkDevices)
  54 | async_get_network_clients = to_async(dashboard.networks.getNetworkClients)
  55 | async_get_device = to_async(dashboard.devices.getDevice)
  56 | async_update_device = to_async(dashboard.devices.updateDevice)
  57 | async_get_wireless_ssids = to_async(dashboard.wireless.getNetworkWirelessSsids)
  58 | async_update_wireless_ssid = to_async(dashboard.wireless.updateNetworkWirelessSsid)
  59 | 
  60 | ###################
  61 | # SCHEMA DEFINITIONS
  62 | ###################
  63 | 
  64 | # Wireless SSID Schema
  65 | class Dot11wSettings(BaseModel):
  66 |     enabled: bool = Field(False, description="Whether 802.11w is enabled or not")
  67 |     required: bool = Field(False, description="Whether 802.11w is required or not")
  68 | 
  69 | class Dot11rSettings(BaseModel):
  70 |     enabled: bool = Field(False, description="Whether 802.11r is enabled or not")
  71 |     adaptive: bool = Field(False, description="Whether 802.11r is adaptive or not")
  72 | 
  73 | class RadiusServer(BaseModel):
  74 |     host: str = Field(..., description="IP address of the RADIUS server")
  75 |     port: int = Field(..., description="Port of the RADIUS server")
  76 |     secret: str = Field(..., description="Secret for the RADIUS server")
  77 |     radsecEnabled: Optional[bool] = Field(None, description="Whether RADSEC is enabled or not")
  78 |     openRoamingCertificateId: Optional[int] = Field(None, description="OpenRoaming certificate ID")
  79 |     caCertificate: Optional[str] = Field(None, description="CA certificate for RADSEC")
  80 | 
  81 | class SsidUpdateSchema(BaseModel):
  82 |     name: Optional[str] = Field(None, description="The name of the SSID")
  83 |     enabled: Optional[bool] = Field(None, description="Whether the SSID is enabled or not")
  84 |     authMode: Optional[str] = Field(None, description="The auth mode for the SSID (e.g., 'open', 'psk', '8021x-radius')")
  85 |     enterpriseAdminAccess: Optional[str] = Field(None, description="Enterprise admin access setting")
  86 |     encryptionMode: Optional[str] = Field(None, description="The encryption mode for the SSID")
  87 |     psk: Optional[str] = Field(None, description="The pre-shared key for the SSID when using PSK auth mode")
  88 |     wpaEncryptionMode: Optional[str] = Field(None, description="WPA encryption mode (e.g., 'WPA1 and WPA2', 'WPA2 only')")
  89 |     dot11w: Optional[Dot11wSettings] = Field(None, description="802.11w settings")
  90 |     dot11r: Optional[Dot11rSettings] = Field(None, description="802.11r settings")
  91 |     splashPage: Optional[str] = Field(None, description="The type of splash page for the SSID")
  92 |     radiusServers: Optional[List[RadiusServer]] = Field(None, description="List of RADIUS servers")
  93 |     visible: Optional[bool] = Field(None, description="Whether the SSID is visible or not")
  94 |     availableOnAllAps: Optional[bool] = Field(None, description="Whether the SSID is available on all APs")
  95 |     bandSelection: Optional[str] = Field(None, description="Band selection for SSID (e.g., '5 GHz band only', 'Dual band operation')")
  96 | 
  97 | # Firewall Rule Schema
  98 | class FirewallRule(BaseModel):
  99 |     comment: str = Field(..., description="Description of the firewall rule")
 100 |     policy: str = Field(..., description="'allow' or 'deny'")
 101 |     protocol: str = Field(..., description="The protocol (e.g., 'tcp', 'udp', 'any')")
 102 |     srcPort: Optional[str] = Field("Any", description="Source port (e.g., '80', '443-8080', 'Any')")
 103 |     srcCidr: str = Field("Any", description="Source CIDR (e.g., '192.168.1.0/24', 'Any')")
 104 |     destPort: Optional[str] = Field("Any", description="Destination port (e.g., '80', '443-8080', 'Any')")
 105 |     destCidr: str = Field("Any", description="Destination CIDR (e.g., '192.168.1.0/24', 'Any')")
 106 |     syslogEnabled: Optional[bool] = Field(False, description="Whether syslog is enabled for this rule")
 107 | 
 108 | # Device Update Schema
 109 | class DeviceUpdateSchema(BaseModel):
 110 |     name: Optional[str] = Field(None, description="The name of the device")
 111 |     tags: Optional[List[str]] = Field(None, description="List of tags for the device")
 112 |     lat: Optional[float] = Field(None, description="Latitude of the device")
 113 |     lng: Optional[float] = Field(None, description="Longitude of the device")
 114 |     address: Optional[str] = Field(None, description="Physical address of the device")
 115 |     notes: Optional[str] = Field(None, description="Notes for the device")
 116 |     moveMapMarker: Optional[bool] = Field(None, description="Whether to move the map marker or not")
 117 |     switchProfileId: Optional[str] = Field(None, description="Switch profile ID")
 118 |     floorPlanId: Optional[str] = Field(None, description="Floor plan ID")
 119 | 
 120 | # Network Update Schema
 121 | class NetworkUpdateSchema(BaseModel):
 122 |     name: Optional[str] = Field(None, description="The name of the network")
 123 |     timeZone: Optional[str] = Field(None, description="The timezone of the network")
 124 |     tags: Optional[List[str]] = Field(None, description="List of tags for the network")
 125 |     enrollmentString: Optional[str] = Field(None, description="Enrollment string for the network")
 126 |     notes: Optional[str] = Field(None, description="Notes for the network")
 127 | 
 128 | # Admin Creation Schema
 129 | class AdminCreationSchema(BaseModel):
 130 |     email: str = Field(..., description="Email address of the admin")
 131 |     name: str = Field(..., description="Name of the admin")
 132 |     orgAccess: str = Field(..., description="Access level for the organization")
 133 |     tags: Optional[List[str]] = Field(None, description="List of tags for the admin")
 134 |     networks: Optional[List[dict]] = Field(None, description="Network access for the admin")
 135 | 
 136 | # Action Batch Schema
 137 | class ActionBatchSchema(BaseModel):
 138 |     actions: List[dict] = Field(..., description="List of actions to perform")
 139 |     confirmed: bool = Field(True, description="Whether the batch is confirmed")
 140 |     synchronous: bool = Field(False, description="Whether the batch is synchronous")
 141 | 
 142 | # VPN Configuration Schema
 143 | class VpnSiteToSiteSchema(BaseModel):
 144 |     mode: str = Field(..., description="VPN mode (none, full, or hub-and-spoke)")
 145 |     hubs: Optional[List[dict]] = Field(None, description="List of hub configurations")
 146 |     subnets: Optional[List[dict]] = Field(None, description="List of subnet configurations")
 147 | 
 148 | # Content Filtering Schema
 149 | class ContentFilteringSchema(BaseModel):
 150 |     allowedUrls: Optional[List[str]] = Field(None, description="List of allowed URLs")
 151 |     blockedUrls: Optional[List[str]] = Field(None, description="List of blocked URLs")
 152 |     blockedUrlPatterns: Optional[List[str]] = Field(None, description="List of blocked URL patterns")
 153 |     youtubeRestrictedForTeenagers: Optional[bool] = Field(None, description="Restrict YouTube for teenagers")
 154 |     youtubeRestrictedForMature: Optional[bool] = Field(None, description="Restrict YouTube for mature content")
 155 | 
 156 | # Traffic Shaping Schema
 157 | class TrafficShapingSchema(BaseModel):
 158 |     globalBandwidthLimits: Optional[dict] = Field(None, description="Global bandwidth limits")
 159 |     rules: Optional[List[dict]] = Field(None, description="Traffic shaping rules")
 160 | 
 161 | # Camera Sense Schema
 162 | class CameraSenseSchema(BaseModel):
 163 |     senseEnabled: Optional[bool] = Field(None, description="Whether camera sense is enabled")
 164 |     mqttBrokerId: Optional[str] = Field(None, description="MQTT broker ID")
 165 |     audioDetection: Optional[dict] = Field(None, description="Audio detection settings")
 166 | 
 167 | # Switch QoS Rule Schema
 168 | class SwitchQosRuleSchema(BaseModel):
 169 |     vlan: int = Field(..., description="VLAN ID")
 170 |     protocol: str = Field(..., description="Protocol (tcp, udp, any)")
 171 |     srcPort: int = Field(..., description="Source port")
 172 |     srcPortRange: Optional[str] = Field(None, description="Source port range")
 173 |     dstPort: Optional[int] = Field(None, description="Destination port")
 174 |     dstPortRange: Optional[str] = Field(None, description="Destination port range")
 175 |     dscp: Optional[int] = Field(None, description="DSCP value")
 176 | 
 177 | #######################
 178 | # ORGANIZATION TOOLS  #
 179 | #######################
 180 | 
 181 | # Get organizations
 182 | @mcp.tool()
 183 | async def get_organizations() -> str:
 184 |     """Get a list of organizations the user has access to"""
 185 |     organizations = await async_get_organizations()
 186 |     return json.dumps(organizations, indent=2)
 187 | 
 188 | # Get organization details
 189 | @mcp.tool()
 190 | async def get_organization_details(org_id: str = None) -> str:
 191 |     """Get details for a specific organization, defaults to the configured organization"""
 192 |     organization_id = org_id or MERAKI_ORG_ID
 193 |     org_details = await async_get_organization(organization_id)
 194 |     return json.dumps(org_details, indent=2)
 195 | 
 196 | # Get networks from Meraki
 197 | @mcp.tool()
 198 | async def get_networks(org_id: str = None) -> str:
 199 |     """Get a list of networks from Meraki"""
 200 |     organization_id = org_id or MERAKI_ORG_ID
 201 |     networks = await async_get_organization_networks(organization_id)
 202 |     return json.dumps(networks, indent=2)
 203 | 
 204 | # Get devices from Meraki
 205 | @mcp.tool()
 206 | async def get_devices(org_id: str = None) -> str:
 207 |     """Get a list of devices from Meraki"""
 208 |     organization_id = org_id or MERAKI_ORG_ID
 209 |     devices = await async_get_organization_devices(organization_id)
 210 |     return json.dumps(devices, indent=2)
 211 | 
 212 | # Create network in Meraki
 213 | @mcp.tool()
 214 | def create_network(name: str, tags: list[str], productTypes: list[str], org_id: str = None, copyFromNetworkId: str = None) -> str:
 215 |     """Create a new network in Meraki, optionally copying from another network."""
 216 |     organization_id = org_id or MERAKI_ORG_ID
 217 |     kwargs = {}
 218 |     if copyFromNetworkId:
 219 |         kwargs['copyFromNetworkId'] = copyFromNetworkId
 220 |     network = dashboard.organizations.createOrganizationNetwork(organization_id, name, productTypes, tags=tags, **kwargs)
 221 |     return json.dumps(network, indent=2)
 222 | 
 223 | # Delete network in Meraki
 224 | @mcp.tool()
 225 | def delete_network(network_id: str) -> str:
 226 |     """Delete a network in Meraki"""
 227 |     dashboard.networks.deleteNetwork(network_id)
 228 |     return f"Network {network_id} deleted"
 229 | 
 230 | # Get organization status
 231 | @mcp.tool()
 232 | def get_organization_status(org_id: str = None) -> str:
 233 |     """Get the status and health of an organization"""
 234 |     organization_id = org_id or MERAKI_ORG_ID
 235 |     status = dashboard.organizations.getOrganizationStatus(organization_id)
 236 |     return json.dumps(status, indent=2)
 237 | 
 238 | # Get organization inventory
 239 | @mcp.tool()
 240 | def get_organization_inventory(org_id: str = None) -> str:
 241 |     """Get the inventory for an organization"""
 242 |     organization_id = org_id or MERAKI_ORG_ID
 243 |     inventory = dashboard.organizations.getOrganizationInventoryDevices(organization_id)
 244 |     return json.dumps(inventory, indent=2)
 245 | 
 246 | # Get organization license state
 247 | @mcp.tool()
 248 | def get_organization_license(org_id: str = None) -> str:
 249 |     """Get the license state for an organization"""
 250 |     organization_id = org_id or MERAKI_ORG_ID
 251 |     license_state = dashboard.organizations.getOrganizationLicensesOverview(organization_id)
 252 |     return json.dumps(license_state, indent=2)
 253 | 
 254 | # Get organization configuration changes
 255 | @mcp.tool()
 256 | def get_organization_conf_change(org_id: str = None) -> str:
 257 |     """Get the org change state for an organization"""
 258 |     organization_id = org_id or MERAKI_ORG_ID
 259 |     org_config_changes = dashboard.organizations.getOrganizationConfigurationChanges(organization_id)
 260 |     return json.dumps(org_config_changes, indent=2)
 261 | 
 262 | #######################
 263 | # NETWORK TOOLS       #
 264 | #######################
 265 | 
 266 | # Get network details
 267 | @mcp.tool()
 268 | def get_network_details(network_id: str) -> str:
 269 |     """Get details for a specific network"""
 270 |     network = dashboard.networks.getNetwork(network_id)
 271 |     return json.dumps(network, indent=2)
 272 | 
 273 | # Get network devices
 274 | @mcp.tool()
 275 | def get_network_devices(network_id: str) -> str:
 276 |     """Get a list of devices in a specific network"""
 277 |     devices = dashboard.networks.getNetworkDevices(network_id)
 278 |     return json.dumps(devices, indent=2)
 279 | 
 280 | # Update network
 281 | @mcp.tool()
 282 | def update_network(network_id: str, update_data: NetworkUpdateSchema) -> str:
 283 |     """
 284 |     Update a network's properties using a schema-validated model
 285 | 
 286 |     Args:
 287 |         network_id: The ID of the network to update
 288 |         update_data: Network properties to update (name, timeZone, tags, enrollmentString, notes)
 289 |     """
 290 |     # Convert the Pydantic model to a dictionary and filter out None values
 291 |     update_dict = {k: v for k, v in update_data.dict().items() if v is not None}
 292 | 
 293 |     result = dashboard.networks.updateNetwork(network_id, **update_dict)
 294 |     return json.dumps(result, indent=2)
 295 | 
 296 | # Get clients from Meraki
 297 | @mcp.tool()
 298 | def get_clients(network_id: str, timespan: int = 86400) -> str:
 299 |     """
 300 |     Get a list of clients from a specific Meraki network.
 301 | 
 302 |     Args:
 303 |         network_id (str): The ID of the Meraki network.
 304 |         timespan (int): The timespan in seconds to get clients (default: 24 hours)
 305 | 
 306 |     Returns:
 307 |         str: JSON-formatted list of clients.
 308 |     """
 309 |     clients = dashboard.networks.getNetworkClients(network_id, timespan=timespan)
 310 |     return json.dumps(clients, indent=2)
 311 | 
 312 | # Get client details
 313 | @mcp.tool()
 314 | def get_client_details(network_id: str, client_id: str) -> str:
 315 |     """Get details for a specific client in a network"""
 316 |     client = dashboard.networks.getNetworkClient(network_id, client_id)
 317 |     return json.dumps(client, indent=2)
 318 | 
 319 | # Get client usage history
 320 | @mcp.tool()
 321 | def get_client_usage(network_id: str, client_id: str) -> str:
 322 |     """Get the usage history for a client"""
 323 |     usage = dashboard.networks.getNetworkClientUsageHistory(network_id, client_id)
 324 |     return json.dumps(usage, indent=2)
 325 | 
 326 | # Get client policy from Meraki
 327 | @mcp.tool()
 328 | async def get_client_policy(network_id: str, client_id: str) -> str:
 329 |     """
 330 |     Get the policy for a specific client in a specific Meraki network.
 331 | 
 332 |     Args:
 333 |         network_id (str): The ID of the Meraki network.
 334 |         client_id (str): The ID (MAC address or client ID) of the client.
 335 | 
 336 |     Returns:
 337 |         str: JSON-formatted client policy.
 338 |     """
 339 |     loop = asyncio.get_event_loop()
 340 |     policy = await loop.run_in_executor(
 341 |         None,
 342 |         lambda: dashboard.networks.getNetworkClientPolicy(network_id, client_id)
 343 |     )
 344 |     return json.dumps(policy, indent=2)
 345 | 
 346 | # Update client policy
 347 | @mcp.tool()
 348 | def update_client_policy(network_id: str, client_id: str, device_policy: str, group_policy_id: str = None) -> str:
 349 |     """Update policy for a client"""
 350 |     kwargs = {'devicePolicy': device_policy}
 351 |     if group_policy_id:
 352 |         kwargs['groupPolicyId'] = group_policy_id
 353 | 
 354 |     result = dashboard.networks.updateNetworkClientPolicy(network_id, client_id, **kwargs)
 355 |     return json.dumps(result, indent=2)
 356 | 
 357 | # Get network traffic analysis
 358 | @mcp.tool()
 359 | def get_network_traffic(network_id: str, timespan: int = 86400) -> str:
 360 |     """Get traffic analysis data for a network"""
 361 |     traffic = dashboard.networks.getNetworkTraffic(network_id, timespan=timespan)
 362 |     return json.dumps(traffic, indent=2)
 363 | 
 364 | #######################
 365 | # DEVICE TOOLS        #
 366 | #######################
 367 | 
 368 | # Get device details
 369 | @mcp.tool()
 370 | async def get_device_details(serial: str) -> str:
 371 |     """Get details for a specific device by serial number"""
 372 |     device = await async_get_device(serial)
 373 |     return json.dumps(device, indent=2)
 374 | 
 375 | # Update device
 376 | @mcp.tool()
 377 | async def update_device(serial: str, device_settings: DeviceUpdateSchema) -> str:
 378 |     """
 379 |     Update a device in the Meraki organization using a schema-validated model
 380 | 
 381 |     Args:
 382 |         serial: The serial number of the device to update
 383 |         device_settings: Device properties to update (name, tags, lat, lng, address, notes, etc.)
 384 | 
 385 |     Returns:
 386 |         Confirmation of the update with the new settings
 387 |     """
 388 |     # Convert the Pydantic model to a dictionary and filter out None values
 389 |     update_dict = {k: v for k, v in device_settings.dict().items() if v is not None}
 390 | 
 391 |     await async_update_device(serial, **update_dict)
 392 | 
 393 |     # Get the updated device details to return
 394 |     updated_device = await async_get_device(serial)
 395 | 
 396 |     return json.dumps({
 397 |         "status": "success",
 398 |         "message": f"Device {serial} updated",
 399 |         "updated_settings": update_dict,
 400 |         "current_device": updated_device
 401 |     }, indent=2)
 402 | 
 403 | # Claim devices into the Meraki organization
 404 | @mcp.tool()
 405 | def claim_devices(network_id: str, serials: list[str]) -> str:
 406 |     """Claim one or more devices into a Meraki network"""
 407 |     dashboard.networks.claimNetworkDevices(network_id, serials)
 408 |     return f"Devices {serials} claimed into network {network_id}"
 409 | 
 410 | # Remove device from network
 411 | @mcp.tool()
 412 | def remove_device(serial: str) -> str:
 413 |     """Remove a device from its network"""
 414 |     dashboard.networks.removeNetworkDevices(serial)
 415 |     return f"Device {serial} removed from network"
 416 | 
 417 | # Reboot device
 418 | @mcp.tool()
 419 | def reboot_device(serial: str) -> str:
 420 |     """Reboot a device"""
 421 |     result = dashboard.devices.rebootDevice(serial)
 422 |     return json.dumps(result, indent=2)
 423 | 
 424 | # Get device clients
 425 | @mcp.tool()
 426 | def get_device_clients(serial: str, timespan: int = 86400) -> str:
 427 |     """Get clients connected to a specific device"""
 428 |     clients = dashboard.devices.getDeviceClients(serial, timespan=timespan)
 429 |     return json.dumps(clients, indent=2)
 430 | 
 431 | # Get device status
 432 | @mcp.tool()
 433 | def get_device_status(serial: str) -> str:
 434 |     """Get the current status of a device"""
 435 |     status = dashboard.devices.getDeviceStatuses(serial)
 436 |     return json.dumps(status, indent=2)
 437 | 
 438 | # Get device uplink status
 439 | @mcp.tool()
 440 | def get_device_uplink(serial: str) -> str:
 441 |     """Get the uplink status of a device"""
 442 |     uplink = dashboard.devices.getDeviceUplink(serial)
 443 |     return json.dumps(uplink, indent=2)
 444 | 
 445 | #######################
 446 | # WIRELESS TOOLS      #
 447 | #######################
 448 | 
 449 | # Get wireless SSIDs
 450 | @mcp.tool()
 451 | async def get_wireless_ssids(network_id: str) -> str:
 452 |     """Get wireless SSIDs for a network"""
 453 |     ssids = await async_get_wireless_ssids(network_id)
 454 |     return json.dumps(ssids, indent=2)
 455 | 
 456 | # Update wireless SSID
 457 | @mcp.tool()
 458 | async def update_wireless_ssid(network_id: str, ssid_number: str, ssid_settings: SsidUpdateSchema) -> str:
 459 |     """
 460 |     Update a wireless SSID with comprehensive schema validation
 461 | 
 462 |     Args:
 463 |         network_id: The ID of the network containing the SSID
 464 |         ssid_number: The number of the SSID to update
 465 |         ssid_settings: Comprehensive SSID settings following the Meraki schema
 466 | 
 467 |     Returns:
 468 |         The updated SSID configuration
 469 |     """
 470 |     # Convert the Pydantic model to a dictionary and filter out None values
 471 |     update_dict = {k: v for k, v in ssid_settings.dict().items() if v is not None}
 472 | 
 473 |     result = await async_update_wireless_ssid(network_id, ssid_number, **update_dict)
 474 |     return json.dumps(result, indent=2)
 475 | 
 476 | # Get wireless settings
 477 | @mcp.tool()
 478 | def get_wireless_settings(network_id: str) -> str:
 479 |     """Get wireless settings for a network"""
 480 |     settings = dashboard.wireless.getNetworkWirelessSettings(network_id)
 481 |     return json.dumps(settings, indent=2)
 482 | 
 483 | 
 484 | 
 485 | #######################
 486 | # SWITCH TOOLS        #
 487 | #######################
 488 | 
 489 | # Get switch ports
 490 | @mcp.tool()
 491 | def get_switch_ports(serial: str) -> str:
 492 |     """Get ports for a switch"""
 493 |     ports = dashboard.switch.getDeviceSwitchPorts(serial)
 494 |     return json.dumps(ports, indent=2)
 495 | 
 496 | # Update switch port
 497 | @mcp.tool()
 498 | def update_switch_port(serial: str, port_id: str, name: str = None, tags: list[str] = None, enabled: bool = None, vlan: int = None) -> str:
 499 |     """Update a switch port"""
 500 |     kwargs = {}
 501 |     if name:
 502 |         kwargs['name'] = name
 503 |     if tags:
 504 |         kwargs['tags'] = tags
 505 |     if enabled is not None:
 506 |         kwargs['enabled'] = enabled
 507 |     if vlan:
 508 |         kwargs['vlan'] = vlan
 509 | 
 510 |     result = dashboard.switch.updateDeviceSwitchPort(serial, port_id, **kwargs)
 511 |     return json.dumps(result, indent=2)
 512 | 
 513 | # Get switch VLAN settings
 514 | @mcp.tool()
 515 | def get_switch_vlans(network_id: str) -> str:
 516 |     """Get VLANs for a network"""
 517 |     vlans = dashboard.switch.getNetworkSwitchVlans(network_id)
 518 |     return json.dumps(vlans, indent=2)
 519 | 
 520 | # Create switch VLAN
 521 | @mcp.tool()
 522 | def create_switch_vlan(network_id: str, vlan_id: int, name: str, subnet: str = None, appliance_ip: str = None) -> str:
 523 |     """Create a switch VLAN"""
 524 |     kwargs = {}
 525 |     if subnet:
 526 |         kwargs['subnet'] = subnet
 527 |     if appliance_ip:
 528 |         kwargs['applianceIp'] = appliance_ip
 529 | 
 530 |     result = dashboard.switch.createNetworkSwitchVlan(network_id, vlan_id, name, **kwargs)
 531 |     return json.dumps(result, indent=2)
 532 | 
 533 | #######################
 534 | # APPLIANCE TOOLS     #
 535 | #######################
 536 | 
 537 | # Get security center
 538 | @mcp.tool()
 539 | def get_security_center(network_id: str) -> str:
 540 |     """Get security information for a network"""
 541 |     security = dashboard.appliance.getNetworkApplianceSecurityCenter(network_id)
 542 |     return json.dumps(security, indent=2)
 543 | 
 544 | # Get VPN status
 545 | @mcp.tool()
 546 | def get_vpn_status(network_id: str) -> str:
 547 |     """Get VPN status for a network"""
 548 |     vpn_status = dashboard.appliance.getNetworkApplianceVpnSiteToSiteVpn(network_id)
 549 |     return json.dumps(vpn_status, indent=2)
 550 | 
 551 | # Get firewall rules
 552 | @mcp.tool()
 553 | def get_firewall_rules(network_id: str) -> str:
 554 |     """Get firewall rules for a network"""
 555 |     rules = dashboard.appliance.getNetworkApplianceFirewallL3FirewallRules(network_id)
 556 |     return json.dumps(rules, indent=2)
 557 | 
 558 | # Update firewall rules
 559 | @mcp.tool()
 560 | def update_firewall_rules(network_id: str, rules: List[FirewallRule]) -> str:
 561 |     """
 562 |     Update firewall rules for a network using schema-validated models
 563 | 
 564 |     Args:
 565 |         network_id: The ID of the network
 566 |         rules: List of firewall rules following the Meraki schema
 567 | 
 568 |     Returns:
 569 |         The updated firewall rules configuration
 570 |     """
 571 |     # Convert the list of Pydantic models to a list of dictionaries
 572 |     rules_dict = [rule.dict(exclude_none=True) for rule in rules]
 573 | 
 574 |     result = dashboard.appliance.updateNetworkApplianceFirewallL3FirewallRules(network_id, rules=rules_dict)
 575 |     return json.dumps(result, indent=2)
 576 | 
 577 | #######################
 578 | # CAMERA TOOLS        #
 579 | #######################
 580 | 
 581 | # Get camera video settings
 582 | @mcp.tool()
 583 | def get_camera_video_settings(network_id: str, serial: str) -> str:
 584 |     """Get video settings for a camera"""
 585 |     settings = dashboard.camera.getDeviceCameraVideoSettings(serial)
 586 |     return json.dumps(settings, indent=2)
 587 | 
 588 | # Get camera quality and retention settings
 589 | @mcp.tool()
 590 | def get_camera_quality_settings(network_id: str) -> str:
 591 |     """Get quality and retention settings for cameras in a network"""
 592 |     settings = dashboard.camera.getNetworkCameraQualityRetentionProfiles(network_id)
 593 |     return json.dumps(settings, indent=2)
 594 | 
 595 | #######################
 596 | # ADVANCED ORGANIZATION TOOLS
 597 | #######################
 598 | 
 599 | # Get organization admins
 600 | @mcp.tool()
 601 | def get_organization_admins(org_id: str = None) -> str:
 602 |     """Get a list of organization admins"""
 603 |     organization_id = org_id or MERAKI_ORG_ID
 604 |     admins = dashboard.organizations.getOrganizationAdmins(organization_id)
 605 |     return json.dumps(admins, indent=2)
 606 | 
 607 | # Create organization admin
 608 | @mcp.tool()
 609 | def create_organization_admin(org_id: str, email: str, name: str, org_access: str, tags: list[str] = None, networks: list[dict] = None) -> str:
 610 |     """Create a new organization admin"""
 611 |     organization_id = org_id or MERAKI_ORG_ID
 612 |     kwargs = {
 613 |         'email': email,
 614 |         'name': name,
 615 |         'orgAccess': org_access
 616 |     }
 617 |     if tags:
 618 |         kwargs['tags'] = tags
 619 |     if networks:
 620 |         kwargs['networks'] = networks
 621 |     
 622 |     result = dashboard.organizations.createOrganizationAdmin(organization_id, **kwargs)
 623 |     return json.dumps(result, indent=2)
 624 | 
 625 | # Get organization API requests
 626 | @mcp.tool()
 627 | def get_organization_api_requests(org_id: str = None, timespan: int = 86400) -> str:
 628 |     """Get organization API request history"""
 629 |     organization_id = org_id or MERAKI_ORG_ID
 630 |     requests = dashboard.organizations.getOrganizationApiRequests(organization_id, timespan=timespan)
 631 |     return json.dumps(requests, indent=2)
 632 | 
 633 | # Get organization webhook logs
 634 | @mcp.tool()
 635 | def get_organization_webhook_logs(org_id: str = None, timespan: int = 86400) -> str:
 636 |     """Get organization webhook logs"""
 637 |     organization_id = org_id or MERAKI_ORG_ID
 638 |     logs = dashboard.organizations.getOrganizationWebhooksLogs(organization_id, timespan=timespan)
 639 |     return json.dumps(logs, indent=2)
 640 | 
 641 | #######################
 642 | # ENHANCED NETWORK MONITORING
 643 | #######################
 644 | 
 645 | # Get network events
 646 | @mcp.tool()
 647 | def get_network_events(network_id: str, timespan: int = 86400, per_page: int = 100) -> str:
 648 |     """Get network events history"""
 649 |     events = dashboard.networks.getNetworkEvents(network_id, timespan=timespan, perPage=per_page)
 650 |     return json.dumps(events, indent=2)
 651 | 
 652 | # Get network event types
 653 | @mcp.tool()
 654 | def get_network_event_types(network_id: str) -> str:
 655 |     """Get available network event types"""
 656 |     event_types = dashboard.networks.getNetworkEventsEventTypes(network_id)
 657 |     return json.dumps(event_types, indent=2)
 658 | 
 659 | # Get network alerts history
 660 | @mcp.tool()
 661 | def get_network_alerts_history(network_id: str, timespan: int = 86400) -> str:
 662 |     """Get network alerts history"""
 663 |     alerts = dashboard.networks.getNetworkAlertsHistory(network_id, timespan=timespan)
 664 |     return json.dumps(alerts, indent=2)
 665 | 
 666 | # Get network alerts settings
 667 | @mcp.tool()
 668 | def get_network_alerts_settings(network_id: str) -> str:
 669 |     """Get network alerts settings"""
 670 |     settings = dashboard.networks.getNetworkAlertsSettings(network_id)
 671 |     return json.dumps(settings, indent=2)
 672 | 
 673 | # Update network alerts settings
 674 | @mcp.tool()
 675 | def update_network_alerts_settings(network_id: str, defaultDestinations: dict = None, alerts: list[dict] = None) -> str:
 676 |     """Update network alerts settings"""
 677 |     kwargs = {}
 678 |     if defaultDestinations:
 679 |         kwargs['defaultDestinations'] = defaultDestinations
 680 |     if alerts:
 681 |         kwargs['alerts'] = alerts
 682 |     
 683 |     result = dashboard.networks.updateNetworkAlertsSettings(network_id, **kwargs)
 684 |     return json.dumps(result, indent=2)
 685 | 
 686 | #######################
 687 | # LIVE DEVICE TOOLS
 688 | #######################
 689 | 
 690 | # Ping device
 691 | @mcp.tool()
 692 | def ping_device(serial: str, target_ip: str, count: int = 5) -> str:
 693 |     """Ping a device from another device"""
 694 |     result = dashboard.devices.createDeviceLiveToolsPing(serial, target_ip, count=count)
 695 |     return json.dumps(result, indent=2)
 696 | 
 697 | # Get ping results
 698 | @mcp.tool()
 699 | def get_device_ping_results(serial: str, ping_id: str) -> str:
 700 |     """Get results from a device ping test"""
 701 |     result = dashboard.devices.getDeviceLiveToolsPing(serial, ping_id)
 702 |     return json.dumps(result, indent=2)
 703 | 
 704 | # Cable test device
 705 | @mcp.tool()
 706 | def cable_test_device(serial: str, ports: list[str]) -> str:
 707 |     """Run cable test on device ports"""
 708 |     result = dashboard.devices.createDeviceLiveToolsCableTest(serial, ports)
 709 |     return json.dumps(result, indent=2)
 710 | 
 711 | # Get cable test results
 712 | @mcp.tool()
 713 | def get_device_cable_test_results(serial: str, cable_test_id: str) -> str:
 714 |     """Get results from a device cable test"""
 715 |     result = dashboard.devices.getDeviceLiveToolsCableTest(serial, cable_test_id)
 716 |     return json.dumps(result, indent=2)
 717 | 
 718 | # Blink device LEDs
 719 | @mcp.tool()
 720 | def blink_device_leds(serial: str, duration: int = 5) -> str:
 721 |     """Blink device LEDs for identification"""
 722 |     result = dashboard.devices.blinkDeviceLeds(serial, duration=duration)
 723 |     return json.dumps(result, indent=2)
 724 | 
 725 | # Wake on LAN
 726 | @mcp.tool()
 727 | def wake_on_lan_device(serial: str, mac: str) -> str:
 728 |     """Send wake-on-LAN packet to a device"""
 729 |     result = dashboard.devices.createDeviceLiveToolsWakeOnLan(serial, mac)
 730 |     return json.dumps(result, indent=2)
 731 | 
 732 | #######################
 733 | # ADVANCED WIRELESS TOOLS
 734 | #######################
 735 | 
 736 | # Get wireless RF profiles
 737 | @mcp.tool()
 738 | def get_wireless_rf_profiles(network_id: str) -> str:
 739 |     """Get wireless RF profiles for a network"""
 740 |     profiles = dashboard.wireless.getNetworkWirelessRfProfiles(network_id)
 741 |     return json.dumps(profiles, indent=2)
 742 | 
 743 | # Create wireless RF profile
 744 | @mcp.tool()
 745 | def create_wireless_rf_profile(network_id: str, name: str, band_selection_type: str, **kwargs) -> str:
 746 |     """Create a wireless RF profile"""
 747 |     result = dashboard.wireless.createNetworkWirelessRfProfile(network_id, name, bandSelectionType=band_selection_type, **kwargs)
 748 |     return json.dumps(result, indent=2)
 749 | 
 750 | # Get wireless channel utilization
 751 | @mcp.tool()
 752 | def get_wireless_channel_utilization(network_id: str, timespan: int = 86400) -> str:
 753 |     """Get wireless channel utilization history"""
 754 |     utilization = dashboard.wireless.getNetworkWirelessChannelUtilizationHistory(network_id, timespan=timespan)
 755 |     return json.dumps(utilization, indent=2)
 756 | 
 757 | # Get wireless signal quality
 758 | @mcp.tool()
 759 | def get_wireless_signal_quality(network_id: str, timespan: int = 86400) -> str:
 760 |     """Get wireless signal quality history"""
 761 |     quality = dashboard.wireless.getNetworkWirelessSignalQualityHistory(network_id, timespan=timespan)
 762 |     return json.dumps(quality, indent=2)
 763 | 
 764 | # Get wireless connection stats
 765 | @mcp.tool()
 766 | def get_wireless_connection_stats(network_id: str, timespan: int = 86400) -> str:
 767 |     """Get wireless connection statistics"""
 768 |     stats = dashboard.wireless.getNetworkWirelessConnectionStats(network_id, timespan=timespan)
 769 |     return json.dumps(stats, indent=2)
 770 | 
 771 | # Get wireless client connectivity events
 772 | @mcp.tool()
 773 | def get_wireless_client_connectivity_events(network_id: str, client_id: str, timespan: int = 86400) -> str:
 774 |     """Get wireless client connectivity events"""
 775 |     events = dashboard.wireless.getNetworkWirelessClientConnectivityEvents(network_id, client_id, timespan=timespan)
 776 |     return json.dumps(events, indent=2)
 777 | 
 778 | #######################
 779 | # ADVANCED SWITCH TOOLS
 780 | #######################
 781 | 
 782 | # Get switch port statuses
 783 | @mcp.tool()
 784 | def get_switch_port_statuses(serial: str) -> str:
 785 |     """Get switch port statuses"""
 786 |     statuses = dashboard.switch.getDeviceSwitchPortsStatuses(serial)
 787 |     return json.dumps(statuses, indent=2)
 788 | 
 789 | # Cycle switch ports
 790 | @mcp.tool()
 791 | def cycle_switch_ports(serial: str, ports: list[str]) -> str:
 792 |     """Cycle (restart) switch ports"""
 793 |     result = dashboard.switch.cycleDeviceSwitchPorts(serial, ports)
 794 |     return json.dumps(result, indent=2)
 795 | 
 796 | # Get switch access control lists
 797 | @mcp.tool()
 798 | def get_switch_access_control_lists(network_id: str) -> str:
 799 |     """Get switch access control lists"""
 800 |     acls = dashboard.switch.getNetworkSwitchAccessControlLists(network_id)
 801 |     return json.dumps(acls, indent=2)
 802 | 
 803 | # Update switch access control lists
 804 | @mcp.tool()
 805 | def update_switch_access_control_lists(network_id: str, rules: list[dict]) -> str:
 806 |     """Update switch access control lists"""
 807 |     result = dashboard.switch.updateNetworkSwitchAccessControlLists(network_id, rules)
 808 |     return json.dumps(result, indent=2)
 809 | 
 810 | # Get switch QoS rules
 811 | @mcp.tool()
 812 | def get_switch_qos_rules(network_id: str) -> str:
 813 |     """Get switch QoS rules"""
 814 |     rules = dashboard.switch.getNetworkSwitchQosRules(network_id)
 815 |     return json.dumps(rules, indent=2)
 816 | 
 817 | # Create switch QoS rule
 818 | @mcp.tool()
 819 | def create_switch_qos_rule(network_id: str, vlan: int, protocol: str, src_port: int, src_port_range: str = None, dst_port: int = None, dst_port_range: str = None, dscp: int = None) -> str:
 820 |     """Create a switch QoS rule"""
 821 |     kwargs = {
 822 |         'vlan': vlan,
 823 |         'protocol': protocol,
 824 |         'srcPort': src_port
 825 |     }
 826 |     if src_port_range:
 827 |         kwargs['srcPortRange'] = src_port_range
 828 |     if dst_port:
 829 |         kwargs['dstPort'] = dst_port
 830 |     if dst_port_range:
 831 |         kwargs['dstPortRange'] = dst_port_range
 832 |     if dscp:
 833 |         kwargs['dscp'] = dscp
 834 |     
 835 |     result = dashboard.switch.createNetworkSwitchQosRule(network_id, **kwargs)
 836 |     return json.dumps(result, indent=2)
 837 | 
 838 | #######################
 839 | # ADVANCED APPLIANCE TOOLS
 840 | #######################
 841 | 
 842 | # Get appliance VPN site-to-site status
 843 | @mcp.tool()
 844 | def get_appliance_vpn_site_to_site(network_id: str) -> str:
 845 |     """Get appliance VPN site-to-site configuration"""
 846 |     vpn = dashboard.appliance.getNetworkApplianceVpnSiteToSiteVpn(network_id)
 847 |     return json.dumps(vpn, indent=2)
 848 | 
 849 | # Update appliance VPN site-to-site
 850 | @mcp.tool()
 851 | def update_appliance_vpn_site_to_site(network_id: str, mode: str, hubs: list[dict] = None, subnets: list[dict] = None) -> str:
 852 |     """Update appliance VPN site-to-site configuration"""
 853 |     kwargs = {'mode': mode}
 854 |     if hubs:
 855 |         kwargs['hubs'] = hubs
 856 |     if subnets:
 857 |         kwargs['subnets'] = subnets
 858 |     
 859 |     result = dashboard.appliance.updateNetworkApplianceVpnSiteToSiteVpn(network_id, **kwargs)
 860 |     return json.dumps(result, indent=2)
 861 | 
 862 | # Get appliance content filtering
 863 | @mcp.tool()
 864 | def get_appliance_content_filtering(network_id: str) -> str:
 865 |     """Get appliance content filtering settings"""
 866 |     filtering = dashboard.appliance.getNetworkApplianceContentFiltering(network_id)
 867 |     return json.dumps(filtering, indent=2)
 868 | 
 869 | # Update appliance content filtering
 870 | @mcp.tool()
 871 | def update_appliance_content_filtering(network_id: str, allowed_urls: list[str] = None, blocked_urls: list[str] = None, blocked_url_patterns: list[str] = None, youtube_restricted_for_teenagers: bool = None, youtube_restricted_for_mature: bool = None) -> str:
 872 |     """Update appliance content filtering settings"""
 873 |     kwargs = {}
 874 |     if allowed_urls:
 875 |         kwargs['allowedUrls'] = allowed_urls
 876 |     if blocked_urls:
 877 |         kwargs['blockedUrls'] = blocked_urls
 878 |     if blocked_url_patterns:
 879 |         kwargs['blockedUrlPatterns'] = blocked_url_patterns
 880 |     if youtube_restricted_for_teenagers is not None:
 881 |         kwargs['youtubeRestrictedForTeenagers'] = youtube_restricted_for_teenagers
 882 |     if youtube_restricted_for_mature is not None:
 883 |         kwargs['youtubeRestrictedForMature'] = youtube_restricted_for_mature
 884 |     
 885 |     result = dashboard.appliance.updateNetworkApplianceContentFiltering(network_id, **kwargs)
 886 |     return json.dumps(result, indent=2)
 887 | 
 888 | # Get appliance security events
 889 | @mcp.tool()
 890 | def get_appliance_security_events(network_id: str, timespan: int = 86400) -> str:
 891 |     """Get appliance security events"""
 892 |     events = dashboard.appliance.getNetworkApplianceSecurityEvents(network_id, timespan=timespan)
 893 |     return json.dumps(events, indent=2)
 894 | 
 895 | # Get appliance traffic shaping
 896 | @mcp.tool()
 897 | def get_appliance_traffic_shaping(network_id: str) -> str:
 898 |     """Get appliance traffic shaping settings"""
 899 |     shaping = dashboard.appliance.getNetworkApplianceTrafficShaping(network_id)
 900 |     return json.dumps(shaping, indent=2)
 901 | 
 902 | # Update appliance traffic shaping
 903 | @mcp.tool()
 904 | def update_appliance_traffic_shaping(network_id: str, global_bandwidth_limits: dict = None) -> str:
 905 |     """Update appliance traffic shaping settings"""
 906 |     kwargs = {}
 907 |     if global_bandwidth_limits:
 908 |         kwargs['globalBandwidthLimits'] = global_bandwidth_limits
 909 |     
 910 |     result = dashboard.appliance.updateNetworkApplianceTrafficShaping(network_id, **kwargs)
 911 |     return json.dumps(result, indent=2)
 912 | 
 913 | #######################
 914 | # CAMERA TOOLS
 915 | #######################
 916 | 
 917 | # Get camera analytics live
 918 | @mcp.tool()
 919 | def get_camera_analytics_live(serial: str) -> str:
 920 |     """Get live camera analytics"""
 921 |     analytics = dashboard.camera.getDeviceCameraAnalyticsLive(serial)
 922 |     return json.dumps(analytics, indent=2)
 923 | 
 924 | # Get camera analytics overview
 925 | @mcp.tool()
 926 | def get_camera_analytics_overview(serial: str, timespan: int = 86400) -> str:
 927 |     """Get camera analytics overview"""
 928 |     overview = dashboard.camera.getDeviceCameraAnalyticsOverview(serial, timespan=timespan)
 929 |     return json.dumps(overview, indent=2)
 930 | 
 931 | # Get camera analytics zones
 932 | @mcp.tool()
 933 | def get_camera_analytics_zones(serial: str) -> str:
 934 |     """Get camera analytics zones"""
 935 |     zones = dashboard.camera.getDeviceCameraAnalyticsZones(serial)
 936 |     return json.dumps(zones, indent=2)
 937 | 
 938 | # Generate camera snapshot
 939 | @mcp.tool()
 940 | def generate_camera_snapshot(serial: str, timestamp: str = None) -> str:
 941 |     """Generate a camera snapshot"""
 942 |     kwargs = {}
 943 |     if timestamp:
 944 |         kwargs['timestamp'] = timestamp
 945 |     
 946 |     result = dashboard.camera.generateDeviceCameraSnapshot(serial, **kwargs)
 947 |     return json.dumps(result, indent=2)
 948 | 
 949 | # Get camera sense
 950 | @mcp.tool()
 951 | def get_camera_sense(serial: str) -> str:
 952 |     """Get camera sense configuration"""
 953 |     sense = dashboard.camera.getDeviceCameraSense(serial)
 954 |     return json.dumps(sense, indent=2)
 955 | 
 956 | # Update camera sense
 957 | @mcp.tool()
 958 | def update_camera_sense(serial: str, sense_enabled: bool = None, mqtt_broker_id: str = None, audio_detection: dict = None) -> str:
 959 |     """Update camera sense configuration"""
 960 |     kwargs = {}
 961 |     if sense_enabled is not None:
 962 |         kwargs['senseEnabled'] = sense_enabled
 963 |     if mqtt_broker_id:
 964 |         kwargs['mqttBrokerId'] = mqtt_broker_id
 965 |     if audio_detection:
 966 |         kwargs['audioDetection'] = audio_detection
 967 |     
 968 |     result = dashboard.camera.updateDeviceCameraSense(serial, **kwargs)
 969 |     return json.dumps(result, indent=2)
 970 | 
 971 | #######################
 972 | # NETWORK AUTOMATION TOOLS
 973 | #######################
 974 | 
 975 | # Create action batch
 976 | @mcp.tool()
 977 | def create_action_batch(org_id: str, actions: list[dict], confirmed: bool = True, synchronous: bool = False) -> str:
 978 |     """Create an action batch for bulk operations"""
 979 |     organization_id = org_id or MERAKI_ORG_ID
 980 |     result = dashboard.organizations.createOrganizationActionBatch(organization_id, actions, confirmed=confirmed, synchronous=synchronous)
 981 |     return json.dumps(result, indent=2)
 982 | 
 983 | # Get action batch status
 984 | @mcp.tool()
 985 | def get_action_batch_status(org_id: str, batch_id: str) -> str:
 986 |     """Get action batch status"""
 987 |     organization_id = org_id or MERAKI_ORG_ID
 988 |     status = dashboard.organizations.getOrganizationActionBatch(organization_id, batch_id)
 989 |     return json.dumps(status, indent=2)
 990 | 
 991 | # Get action batches
 992 | @mcp.tool()
 993 | def get_action_batches(org_id: str = None) -> str:
 994 |     """Get all action batches for an organization"""
 995 |     organization_id = org_id or MERAKI_ORG_ID
 996 |     batches = dashboard.organizations.getOrganizationActionBatches(organization_id)
 997 |     return json.dumps(batches, indent=2)
 998 | 
 999 | # Define resources
1000 | #Add a dynamic greeting resource
1001 | @mcp.resource("greeting: //{name}")
1002 | def greeting(name: str) -> str:
1003 |     """Greet a user by name"""
1004 |     return f"Hello {name}!"
1005 | 
1006 | #execute and return the stdio output
1007 | if __name__ == "__main__":
1008 |     mcp.run()
1009 | 
```