Skip to content

Commit db806c8

Browse files
authored
added ncm_mcp_servers (#71)
1 parent f189e45 commit db806c8

40 files changed

Lines changed: 8604 additions & 0 deletions

‎ncm_mcp_servers/Dockerfile‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
FROM python:3.11-slim
2+
3+
WORKDIR /app
4+
5+
COPY pyproject.toml .
6+
COPY __init__.py ./ncm_mcp_servers/
7+
COPY shared/ ./ncm_mcp_servers/shared/
8+
COPY ncm_fleet/ ./ncm_mcp_servers/ncm_fleet/
9+
COPY ncm_monitoring/ ./ncm_mcp_servers/ncm_monitoring/
10+
COPY ncm_cloud_services/ ./ncm_mcp_servers/ncm_cloud_services/
11+
COPY entrypoint.sh /app/entrypoint.sh
12+
13+
RUN pip install --no-cache-dir -e . && chmod +x /app/entrypoint.sh
14+
15+
ENV MCP_TRANSPORT=streamable-http
16+
ENV NCM_FLEET_PORT=3001
17+
ENV NCM_MONITORING_PORT=3002
18+
ENV NCM_CLOUD_SERVICES_PORT=3003
19+
20+
EXPOSE 3001 3002 3003
21+
22+
ENTRYPOINT ["/app/entrypoint.sh"]

‎ncm_mcp_servers/README.md‎

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
# NCM MCP Servers
2+
3+
![NCM MCP Servers](assets/screenshot.png)
4+
5+
A suite of three focused MCP (Model Context Protocol) servers for the Ericsson Enterprise Wireless NCM API, split by domain responsibility for optimal LLM tool selection.
6+
7+
## Architecture
8+
9+
| Server | Port | Domain | Tools |
10+
|--------|------|--------|-------|
11+
| `ncm-fleet` | 3001 | Routers, groups, accounts, locations, configurations, firmware, products | 17 |
12+
| `ncm-monitoring` | 3002 | Net devices, alerts/logs, speed tests | 6 |
13+
| `ncm-cloud-services` | 3003 | Users, subscriptions, private cellular, exchange | 13 |
14+
15+
All servers default to **Streamable HTTP transport** and run together in a single container.
16+
17+
## Quick Start
18+
19+
### 1. Install
20+
21+
```bash
22+
cd ncm_mcp_servers
23+
pip install -e .
24+
```
25+
26+
### 2. Configure Credentials
27+
28+
Copy the example and fill in your values:
29+
30+
```bash
31+
cp credentials.example.json credentials.json
32+
```
33+
34+
```json
35+
{
36+
"X_CP_API_ID": "your-cp-api-id",
37+
"X_CP_API_KEY": "your-cp-api-key",
38+
"X_ECM_API_ID": "your-ecm-api-id",
39+
"X_ECM_API_KEY": "your-ecm-api-key",
40+
"NCM_API_TOKEN": "your-v3-bearer-token"
41+
}
42+
```
43+
44+
Alternatively, set environment variables with the same names.
45+
46+
### 3. Run (Docker — recommended)
47+
48+
```bash
49+
docker compose up --build
50+
```
51+
52+
Or build and run manually:
53+
54+
```bash
55+
docker build -t ncm-mcp-servers .
56+
docker run -p 3001:3001 -p 3002:3002 -p 3003:3003 \
57+
-v ./credentials.json:/app/credentials.json:ro \
58+
ncm-mcp-servers
59+
```
60+
61+
This starts a single container running all 3 servers:
62+
- `http://localhost:3001/mcp` — ncm-fleet
63+
- `http://localhost:3002/mcp` — ncm-monitoring
64+
- `http://localhost:3003/mcp` — ncm-cloud-services
65+
66+
### 4. Run (Local, without Docker)
67+
68+
```bash
69+
# Run all 3 servers in a single process (recommended for local dev)
70+
ncm-mcp-servers
71+
72+
# Or via python module
73+
python -m ncm_mcp_servers
74+
```
75+
76+
Run servers individually if you only need a subset:
77+
78+
```bash
79+
ncm-fleet # port 3001
80+
ncm-monitoring # port 3002
81+
ncm-cloud-services # port 3003
82+
```
83+
84+
Override ports via environment variables:
85+
```bash
86+
NCM_FLEET_PORT=4001 ncm-fleet
87+
NCM_MONITORING_PORT=4002 ncm-monitoring
88+
NCM_CLOUD_SERVICES_PORT=4003 ncm-cloud-services
89+
```
90+
91+
### 5. Transport Options
92+
93+
The default transport is **Streamable HTTP**. Supported transports:
94+
95+
| Transport | Value | Use case |
96+
|-----------|-------|----------|
97+
| Streamable HTTP | `streamable-http` (default) | Recommended for all MCP clients |
98+
| SSE | `sse` | Legacy MCP clients using Server-Sent Events |
99+
| Stdio | `stdio` | Piped MCP clients (single server only) |
100+
101+
```bash
102+
# Use SSE transport (legacy)
103+
MCP_TRANSPORT=sse ncm-mcp-servers
104+
105+
# Use stdio (only works with individual servers, not the unified runner)
106+
MCP_TRANSPORT=stdio ncm-fleet
107+
```
108+
109+
## MCP Client Configuration
110+
111+
Add to your MCP settings (e.g. `.kiro/settings/mcp.json`):
112+
113+
```json
114+
{
115+
"mcpServers": {
116+
"ncm-fleet": {
117+
"url": "http://localhost:3001/mcp"
118+
},
119+
"ncm-monitoring": {
120+
"url": "http://localhost:3002/mcp"
121+
},
122+
"ncm-cloud-services": {
123+
"url": "http://localhost:3003/mcp"
124+
}
125+
}
126+
}
127+
```
128+
129+
## Tool Consolidation Strategy
130+
131+
CRUD operations are consolidated into `manage_*` tools with an `action` parameter:
132+
133+
```python
134+
manage_router(action="rename", router_id=123, new_name="foo")
135+
manage_router(action="delete", router_id=123)
136+
manage_router(action="update", router_id=123, description="bar")
137+
```
138+
139+
Read-only metrics are consolidated by type:
140+
141+
```python
142+
get_net_device_metrics(metric_type="signal", net_device=1)
143+
get_net_device_metrics(metric_type="usage", net_device=1)
144+
get_net_device_metrics(metric_type="wan", net_device=1)
145+
get_net_device_metrics(metric_type="modem", net_device=1)
146+
```
147+
148+
## Project Structure
149+
150+
```
151+
ncm_mcp_servers/
152+
├── pyproject.toml
153+
├── Dockerfile
154+
├── docker-compose.yml
155+
├── entrypoint.sh
156+
├── credentials.example.json
157+
├── credentials.json # Your credentials (gitignored)
158+
├── ncm_mcp_servers/
159+
│ ├── shared/ # Common utilities
160+
│ │ ├── ncm.py # NCM API client library
161+
│ │ ├── credentials.py # Credential loading
162+
│ │ ├── error_handler.py # Response/error formatting
163+
│ │ └── client.py # NCM client factory
164+
│ ├── ncm_fleet/ # Fleet management server (port 3001)
165+
│ │ ├── server.py
166+
│ │ └── tools/
167+
│ │ ├── routers.py
168+
│ │ ├── groups.py
169+
│ │ ├── accounts.py
170+
│ │ ├── locations.py
171+
│ │ ├── configurations.py
172+
│ │ └── firmware.py
173+
│ ├── ncm_monitoring/ # Monitoring server (port 3002)
174+
│ │ ├── server.py
175+
│ │ └── tools/
176+
│ │ ├── net_devices.py
177+
│ │ ├── alerts.py
178+
│ │ └── speed_tests.py
179+
│ └── ncm_cloud_services/ # Cloud services server (port 3003)
180+
│ ├── server.py
181+
│ └── tools/
182+
│ ├── users.py
183+
│ ├── subscriptions.py
184+
│ ├── private_cellular.py
185+
│ └── exchange.py
186+
└── README.md
187+
```
188+
189+
## Tool Reference
190+
191+
### ncm-fleet (17 tools)
192+
193+
| Tool | Description |
194+
|------|-------------|
195+
| `get_routers` | Query routers by ID, name, account, or group |
196+
| `manage_router` | Rename, delete, update fields, assign to group/account, remove from group |
197+
| `reboot_router` | Reboot a single router |
198+
| `reboot_group` | Reboot all routers in a group |
199+
| `get_groups` | Query groups by ID, name, or account |
200+
| `manage_group` | Create, rename, delete, update groups |
201+
| `get_accounts` | Query accounts by ID or name |
202+
| `manage_subaccount` | Create, rename, delete subaccounts |
203+
| `get_locations` | Query current or historical locations |
204+
| `manage_location` | Create or delete router locations |
205+
| `get_configuration_managers` | Query config sync status |
206+
| `patch_config` | Partial config update (router or group) |
207+
| `put_router_config` | Full config replacement |
208+
| `copy_router_config` | Copy config between routers |
209+
| `resume_updates` | Resume suspended config sync |
210+
| `get_firmware` | Query firmware versions by product |
211+
| `get_products` | Query product catalog |
212+
213+
### ncm-monitoring (6 tools)
214+
215+
| Tool | Description |
216+
|------|-------------|
217+
| `get_net_devices` | Query net devices by router, mode, connection state |
218+
| `get_net_device_health` | Get cellular health scores |
219+
| `get_net_device_metrics` | Signal, usage, WAN, or modem metrics |
220+
| `get_logs` | Alerts, recent alerts, router logs, or activity logs |
221+
| `create_speed_test` | Run speed test on devices or all modems on a router |
222+
| `get_speed_test` | Get speed test status and results |
223+
224+
### ncm-cloud-services (13 tools)
225+
226+
| Tool | Description |
227+
|------|-------------|
228+
| `get_users` | Query NCM users |
229+
| `manage_user` | Create, update, delete users, change role |
230+
| `get_subscriptions` | Query subscriptions |
231+
| `manage_subscription` | Regrade or unlicense devices |
232+
| `get_networks` | Query private cellular networks |
233+
| `manage_network` | Create, update, delete networks |
234+
| `get_radios` | Query radios and operational status |
235+
| `manage_radio` | Update radio settings |
236+
| `manage_sim` | Query or update SIMs |
237+
| `get_exchange_sites` | Query exchange sites |
238+
| `manage_exchange_site` | Create, update, delete sites |
239+
| `get_exchange_resources` | Query exchange resources |
240+
| `manage_exchange_resource` | Create, update, delete resources |

‎ncm_mcp_servers/__init__.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""NCM MCP Servers — a suite of focused MCP servers for Cradlepoint NCM.
2+
3+
This package contains three specialized MCP servers:
4+
- ncm_fleet: Router, group, account, location, configuration, and firmware management
5+
- ncm_monitoring: Net device monitoring, alerts/logs, and speed tests
6+
- ncm_cloud_services: Users, subscriptions, private cellular, and NetCloud Exchange
7+
"""

‎ncm_mcp_servers/__main__.py‎

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
"""Unified runner — starts all three NCM MCP servers concurrently.
2+
3+
Usage:
4+
python -m ncm_mcp_servers
5+
6+
All servers run in a single process using asyncio. Each binds to its
7+
own port (default: 3001, 3002, 3003). Transport defaults to Streamable HTTP.
8+
9+
Environment variables:
10+
MCP_TRANSPORT: "streamable-http" (default) or "sse"
11+
NCM_FLEET_PORT: Fleet server port (default 3001)
12+
NCM_MONITORING_PORT: Monitoring server port (default 3002)
13+
NCM_CLOUD_SERVICES_PORT: Cloud services server port (default 3003)
14+
LOG_LEVEL: Logging level (default INFO)
15+
"""
16+
17+
import asyncio
18+
import os
19+
import signal
20+
import sys
21+
from typing import cast
22+
23+
from ncm_mcp_servers.shared.credentials import load_credentials
24+
from ncm_mcp_servers.shared.logging import get_logger
25+
26+
logger = get_logger("runner")
27+
28+
29+
async def run_all() -> None:
30+
"""Start all three MCP servers concurrently."""
31+
credentials = load_credentials()
32+
transport = os.environ.get("MCP_TRANSPORT", "streamable-http").lower()
33+
34+
logger.info(
35+
"Starting all NCM MCP servers",
36+
extra={"transport": transport},
37+
)
38+
39+
# Import here to avoid circular import issues at module level
40+
from ncm_mcp_servers.ncm_fleet.server import create_server as create_fleet
41+
from ncm_mcp_servers.ncm_monitoring.server import create_server as create_monitoring
42+
from ncm_mcp_servers.ncm_cloud_services.server import create_server as create_cloud
43+
44+
fleet = create_fleet(credentials)
45+
monitoring = create_monitoring(credentials)
46+
cloud = create_cloud(credentials)
47+
48+
# Select the async runner based on transport
49+
if transport == "sse":
50+
tasks = [
51+
asyncio.create_task(fleet.run_sse_async()),
52+
asyncio.create_task(monitoring.run_sse_async()),
53+
asyncio.create_task(cloud.run_sse_async()),
54+
]
55+
else:
56+
# Default: Streamable HTTP
57+
tasks = [
58+
asyncio.create_task(fleet.run_streamable_http_async()),
59+
asyncio.create_task(monitoring.run_streamable_http_async()),
60+
asyncio.create_task(cloud.run_streamable_http_async()),
61+
]
62+
63+
fleet_port = os.environ.get("NCM_FLEET_PORT", "3001")
64+
monitoring_port = os.environ.get("NCM_MONITORING_PORT", "3002")
65+
cloud_port = os.environ.get("NCM_CLOUD_SERVICES_PORT", "3003")
66+
67+
logger.info(f" ncm-fleet -> port {fleet_port}")
68+
logger.info(f" ncm-monitoring -> port {monitoring_port}")
69+
logger.info(f" ncm-cloud-services -> port {cloud_port}")
70+
71+
# Wait for all tasks; if one fails, cancel the rest
72+
done, pending = await asyncio.wait(tasks, return_when=asyncio.FIRST_EXCEPTION)
73+
74+
for task in pending:
75+
task.cancel()
76+
77+
# Re-raise any exceptions from completed tasks
78+
for task in done:
79+
if task.exception():
80+
raise task.exception()
81+
82+
83+
def main() -> None:
84+
"""Entry point for unified server runner."""
85+
# Handle Ctrl+C gracefully
86+
def handle_signal(sig, frame):
87+
logger.info("Shutting down...")
88+
sys.exit(0)
89+
90+
signal.signal(signal.SIGINT, handle_signal)
91+
signal.signal(signal.SIGTERM, handle_signal)
92+
93+
try:
94+
asyncio.run(run_all())
95+
except ValueError as exc:
96+
logger.error("Failed to start servers", extra={"error": str(exc)})
97+
sys.exit(1)
98+
except KeyboardInterrupt:
99+
logger.info("Shutting down...")
100+
101+
102+
if __name__ == "__main__":
103+
main()
565 KB
Loading
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"X_CP_API_ID": "your-cp-api-id",
3+
"X_CP_API_KEY": "your-cp-api-key",
4+
"X_ECM_API_ID": "your-ecm-api-id",
5+
"X_ECM_API_KEY": "your-ecm-api-key",
6+
"NCM_API_TOKEN": "your-v3-bearer-token"
7+
}

‎ncm_mcp_servers/docker-compose.yml‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
services:
2+
ncm-mcp:
3+
build: .
4+
ports:
5+
- "3001:3001"
6+
- "3002:3002"
7+
- "3003:3003"
8+
environment:
9+
- MCP_TRANSPORT=streamable-http
10+
- NCM_FLEET_PORT=3001
11+
- NCM_MONITORING_PORT=3002
12+
- NCM_CLOUD_SERVICES_PORT=3003
13+
volumes:
14+
- ./credentials.json:/app/credentials.json:ro

0 commit comments

Comments
 (0)