From f7d3762eec83a27e5958c5f85f83b6bcd9790e55 Mon Sep 17 00:00:00 2001 From: Louis King Date: Mon, 1 Dec 2025 23:40:48 +0000 Subject: [PATCH 1/3] Updates --- .vscode/mcp.json | 8 + AGENTS.md | 33 + PLAN.md | 745 ------------------- docker-compose.yml | 15 + pyproject.toml | 1 + src/meshcore_api/cli.py | 88 +++ src/meshcore_api/mcp/__init__.py | 3 + src/meshcore_api/mcp/client.py | 144 ++++ src/meshcore_api/mcp/config.py | 76 ++ src/meshcore_api/mcp/server.py | 93 +++ src/meshcore_api/mcp/state.py | 37 + src/meshcore_api/mcp/tools/__init__.py | 6 + src/meshcore_api/mcp/tools/advertisements.py | 109 +++ src/meshcore_api/mcp/tools/messages.py | 188 +++++ test_webhooks.py | 97 --- tests/unit/test_config.py | 2 +- 16 files changed, 802 insertions(+), 843 deletions(-) create mode 100644 .vscode/mcp.json delete mode 100644 PLAN.md create mode 100644 src/meshcore_api/mcp/__init__.py create mode 100644 src/meshcore_api/mcp/client.py create mode 100644 src/meshcore_api/mcp/config.py create mode 100644 src/meshcore_api/mcp/server.py create mode 100644 src/meshcore_api/mcp/state.py create mode 100644 src/meshcore_api/mcp/tools/__init__.py create mode 100644 src/meshcore_api/mcp/tools/advertisements.py create mode 100644 src/meshcore_api/mcp/tools/messages.py delete mode 100644 test_webhooks.py diff --git a/.vscode/mcp.json b/.vscode/mcp.json new file mode 100644 index 0000000..abcdf76 --- /dev/null +++ b/.vscode/mcp.json @@ -0,0 +1,8 @@ +{ + "servers": { + "MeshCoreDocker": { + "type": "http", + "url": "http://localhost:8081/mcp/" + } + } +} diff --git a/AGENTS.md b/AGENTS.md index 8740b55..3273e1e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,6 +19,7 @@ The application provides a Click-based CLI with the following commands: - Start server: `meshcore_api server [OPTIONS]` or `python -m meshcore_api server` - Query database: `meshcore_api query [OPTIONS]` or `python -m meshcore_api query` - Import tags: `meshcore_api tag JSON_FILE [OPTIONS]` or `python -m meshcore_api tag JSON_FILE` +- Start MCP server: `meshcore_api mcp [OPTIONS]` or `python -m meshcore_api mcp` - Show help: `meshcore_api --help` ### Server Command @@ -93,6 +94,38 @@ Supported value types: - `boolean`: True/false values - `coordinate`: Geographic coordinates with latitude and longitude +### MCP Command + +Start the MeshCore MCP (Model Context Protocol) server: +```bash +meshcore_api mcp --api-url http://localhost:8080 +meshcore_api mcp --api-url http://localhost:8080 --api-token "secret" +meshcore_api mcp --api-url http://localhost:8080 --port 9000 +``` + +The MCP server provides AI/LLM integration tools for interacting with the MeshCore API. It runs as an HTTP server (default port 8081) or in stdio mode. + +Common options: +- `--host`: MCP server host (default: 0.0.0.0) +- `--port`: MCP server port (default: 8081) +- `--api-url`: MeshCore API URL (required, e.g., http://localhost:8080) +- `--api-token`: Bearer token for API authentication (if API requires auth) +- `--log-level`: Set logging level (DEBUG, INFO, WARNING, ERROR) +- `--stdio`: Run in stdio mode instead of HTTP server + +Environment variables: +- `MCP_HOST`: MCP server host +- `MCP_PORT`: MCP server port +- `MESHCORE_API_URL`: MeshCore API URL +- `MESHCORE_API_TOKEN`: Bearer token for API authentication + +**Available MCP Tools:** +- `meshcore_get_messages` - Query messages from the mesh network +- `meshcore_send_direct_message` - Send a direct message to a specific node +- `meshcore_send_channel_message` - Send a broadcast message to all nodes +- `meshcore_get_advertisements` - Query advertisements from the mesh network +- `meshcore_send_advertisement` - Send an advertisement to announce this device + ## Database Schema The application stores MeshCore event data in SQLite with the following key tables: diff --git a/PLAN.md b/PLAN.md deleted file mode 100644 index 43b497e..0000000 --- a/PLAN.md +++ /dev/null @@ -1,745 +0,0 @@ -# MeshCore API - Implementation Plan - -## Project Overview - -MeshCore companion application that: -- Subscribes to all MeshCore events via Serial/BLE -- Persists events in SQLite database with configurable retention -- Provides REST API for querying data and sending commands -- Includes mock MeshCore for development without hardware -- Exposes Prometheus metrics for monitoring -- Generates OpenAPI/Swagger documentation - -## Technology Stack - -- **Language**: Python 3.11+ -- **Database**: SQLite with SQLAlchemy ORM -- **API Framework**: FastAPI -- **MeshCore Library**: meshcore_py (v2.2.1+) -- **Metrics**: Prometheus -- **Configuration**: CLI arguments > Environment variables > Defaults - ---- - -## Phase 1: Foundation ✅ COMPLETE - -**Goal**: Working application with database persistence and mock support - -### Completed Components - -#### 1.1 Project Setup ✅ -- [x] Python package structure with pyproject.toml -- [x] Dependencies: meshcore, FastAPI, SQLAlchemy, Prometheus, etc. -- [x] README with quick start guide -- [x] .gitignore configuration - -#### 1.2 Database Layer ✅ -- [x] SQLAlchemy models (core tables): - - `nodes` / `node_tags` - Node tracking with prefix indexing and metadata - - `messages` - Direct and channel messages - - `advertisements` - Node advertisements - - `trace_paths` - Trace path results with SNR - - `telemetry` - Sensor data from nodes - - `events_log` - Raw event log -- [x] Database engine with connection pooling -- [x] Session management with context managers -- [x] Data cleanup for retention policy -- [x] Indexes for fast prefix queries - -#### 1.3 MeshCore Interface ✅ -- [x] Abstract `MeshCoreInterface` base class -- [x] `RealMeshCore` - meshcore_py wrapper -- [x] `MockMeshCore` - Two operation modes: - - Random event generation (configurable intervals) - - Scenario playback (5 built-in scenarios) - -#### 1.4 Built-in Mock Scenarios ✅ -- [x] `simple_chat` - Two nodes exchanging messages -- [x] `trace_path_test` - Multi-hop network tracing -- [x] `telemetry_collection` - Periodic sensor data -- [x] `network_stress` - High-traffic simulation -- [x] `battery_drain` - Battery degradation over time - -#### 1.5 Event Subscriber ✅ -- [x] Event handler for all MeshCore event types -- [x] Database persistence logic -- [x] Node upsert (create/update) logic -- [x] Error handling and logging -- [x] Prometheus metrics collection points - -#### 1.6 Configuration Management ✅ -- [x] CLI argument parsing with argparse -- [x] Environment variable support -- [x] Priority: CLI > Env > Defaults -- [x] 20+ configuration options -- [x] Connection settings (serial/mock) -- [x] Database settings (path, retention) -- [x] API settings (host, port) -- [x] Logging settings (level, format) - -#### 1.7 Utilities ✅ -- [x] Public key address utilities: - - Normalization (lowercase) - - Validation (hex check) - - Prefix extraction - - Prefix matching -- [x] Logging setup: - - JSON formatter for structured logs - - Text formatter with colors - - Configurable log levels -- [x] Prometheus metrics collectors (defined, not yet wired) - -#### 1.8 Main Application ✅ -- [x] Application lifecycle management -- [x] MeshCore connection handling -- [x] Event subscription setup -- [x] Background cleanup task -- [x] Signal handlers (SIGINT, SIGTERM) -- [x] Graceful shutdown - -### Test Results ✅ -- Database initialized with core tables -- Events captured and persisted -- Mock scenarios working (simple_chat verified) -- Node tracking functional -- Message/advertisement storage working -- Configuration system operational - ---- - -## Phase 2: REST API ✅ COMPLETE - -**Goal**: Full REST API with OpenAPI docs - -### 2.1 FastAPI Application Setup -- [x] Create FastAPI app with metadata -- [x] Configure CORS middleware -- [x] Add exception handlers -- [x] Setup startup/shutdown events -- [x] Configure OpenAPI customization: - - Title, version, description - - Contact and license information - - API grouping with tags - - Example values for all models - -### 2.2 Pydantic Models -- [x] Request models for all command endpoints -- [x] Response models for all endpoints -- [x] Validation rules and constraints -- [x] Field descriptions and examples -- [x] Nested models for complex data - -### 2.3 Command Endpoints (POST) -- [x] `POST /api/v1/commands/send_message` - - Send direct message to node - - Input: destination, text, text_type - - Output: message_id, estimated_delivery_ms -- [x] `POST /api/v1/commands/send_channel_message` - - Send channel broadcast - - Input: text, flood -- [x] `POST /api/v1/commands/send_advert` - - Send self-advertisement - - Input: flood -- [x] `POST /api/v1/commands/send_trace_path` - - Initiate trace path - - Input: destination - - Output: trace_id, initiator_tag -- [x] `POST /api/v1/commands/ping` - - Ping a node - - Input: destination -- [x] `POST /api/v1/commands/send_telemetry_request` - - Request telemetry - - Input: destination - -### 2.4 Query Endpoints (GET) -- [x] `GET /api/v1/messages` - - List messages with filters: - - from/to (public key prefix) - - type (contact/channel) - - start_date/end_date - - limit/offset (pagination) -- [x] `GET /api/v1/advertisements` - - List advertisements with filters: - - node (public key prefix) - - adv_type - - date range, pagination -- [x] `GET /api/v1/telemetry` - - List telemetry data - - Filters: node, date range, pagination -- [x] `GET /api/v1/trace-paths` - - List trace path results - - Filters: destination, date range, pagination -- [x] `GET /api/v1/statistics` - - Get latest statistics - - Query param: stat_type (core/radio/packets) -- [x] `GET /api/v1/device_info` - - Get companion device information - -### 2.5 Node Endpoints (GET) -- [x] `GET /api/v1/nodes` - - List all nodes - - Filters: sort, order, pagination -- [x] `GET /api/v1/nodes/{prefix}` - - Search by prefix (2-64 chars) - - Returns all matching nodes -- [x] `GET /api/v1/nodes/{public_key}/messages` - - Get messages for specific node - - Filters: date range, pagination -- [x] `GET /api/v1/nodes/{public_key}/telemetry` - - Get telemetry for node - - Filters: date range, pagination - -### 2.6 Health Endpoints (GET) -- [x] `GET /api/v1/health` - - Overall health status - - MeshCore connection status - - Database connection status - - Uptime, events processed -- [x] `GET /api/v1/health/db` - - Database connectivity check - - Database size - - Table row counts -- [x] `GET /api/v1/health/meshcore` - - MeshCore connection status - - Device info (if connected) - -### 2.7 Dependencies and Middleware -- [x] Database session dependency -- [x] MeshCore instance dependency -- [x] Request logging middleware -- [x] Error response formatting -- [x] CORS configuration - -### 2.8 Integration -- [x] Integrate FastAPI with main application -- [x] Run API server in background task -- [x] Share MeshCore instance with API routes -- [x] Add API configuration options - -### Test Results ✅ -- API server starts successfully on port 8000 -- Health endpoints working (`/api/v1/health`, `/api/v1/health/db`, `/api/v1/health/meshcore`) -- Node endpoints working (list nodes, search by prefix) -- Message endpoints working (query with filters) -- All query endpoints functional with pagination -- Command endpoints implemented and ready for testing -- OpenAPI documentation available at `/docs` and `/redoc` -- CORS middleware configured -- Exception handling working correctly - ---- - -## Phase 3: Observability 📋 PLANNED - -**Goal**: Production-ready observability - -### 3.1 Prometheus Integration -- [ ] Wire up metrics collectors in event handler -- [ ] Add `/metrics` endpoint -- [ ] Implement all metric types: - - Event counters by type - - Message latency histograms - - Node connectivity gauges - - Signal quality histograms (SNR/RSSI) - - Battery/storage gauges - - Radio statistics - - Database metrics - - Error counters -- [ ] Add FastAPI metrics middleware -- [ ] Document Prometheus queries - -### 3.2 Enhanced Logging -- [ ] Add contextual logging throughout -- [ ] Log request/response for API calls -- [ ] Log event processing errors -- [ ] Add correlation IDs for tracing -- [ ] Performance logging for slow queries - -### 3.3 Database Monitoring -- [ ] Periodic database size updates -- [ ] Table row count metrics -- [ ] Query performance tracking -- [ ] Cleanup operation metrics - -### 3.4 Health Monitoring -- [ ] Connection status tracking -- [ ] Auto-reconnect attempts -- [ ] Event processing lag monitoring -- [ ] Alert on connection failures - ---- - -## Phase 4: Docker Deployment 📋 PLANNED - -**Goal**: Production-ready Docker deployment - -### 4.1 Dockerfile -- [ ] Multi-stage build for smaller image -- [ ] Python 3.11+ base image -- [ ] Install dependencies -- [ ] Non-root user -- [ ] Health check -- [ ] Expose ports (API: 8000) - -### 4.2 Docker Compose (Development) -- [ ] meshcore-api service (mock mode) -- [ ] Volume mounts for development -- [ ] Environment variable configuration -- [ ] Port mappings -- [ ] Optional: Prometheus service -- [ ] Optional: Grafana service - -### 4.3 Docker Compose (Production) -- [ ] meshcore-api service (real hardware) -- [ ] Serial device mapping -- [ ] Persistent volume for database -- [ ] Restart policy -- [ ] Logging configuration -- [ ] Health checks - -### 4.4 Prometheus Configuration -- [ ] prometheus.yml scrape config -- [ ] Target: meshcore-api:8000/metrics -- [ ] Scrape interval: 15s - -### 4.5 Grafana Dashboard -- [ ] Dashboard JSON configuration -- [ ] Panels: - - Message rate over time - - Active nodes gauge - - Round-trip latency histogram - - Battery voltage gauge - - Signal quality graphs (SNR/RSSI) - - Event type distribution - - Database size gauge - -### 4.6 Documentation -- [ ] Docker build instructions -- [ ] Docker run examples -- [ ] docker-compose usage -- [ ] Environment variable reference -- [ ] Volume mounting guide -- [ ] Serial device access setup - ---- - -## Phase 5: Testing & Documentation 📋 PLANNED - -**Goal**: Comprehensive testing and documentation - -### 5.1 Unit Tests -- [ ] Database model tests -- [ ] Address utility tests -- [ ] Configuration tests -- [ ] Mock MeshCore tests -- [ ] Event handler tests - -### 5.2 Integration Tests -- [ ] API endpoint tests -- [ ] Database persistence tests -- [ ] Mock scenario tests -- [ ] Configuration priority tests - -### 5.3 API Documentation -- [ ] Complete OpenAPI schema -- [ ] Request/response examples -- [ ] Authentication documentation (future) -- [ ] Error code reference -- [ ] Rate limiting info (future) - -### 5.4 User Documentation -- [ ] Installation guide -- [ ] Configuration guide -- [ ] CLI reference -- [ ] Environment variable reference -- [ ] API usage examples -- [ ] Mock scenario guide -- [ ] Troubleshooting guide - -### 5.5 Developer Documentation -- [ ] Architecture overview -- [ ] Database schema documentation -- [ ] Adding new scenarios -- [ ] Contributing guide -- [ ] Code style guide - ---- - -## Phase 6: Advanced Features 📋 FUTURE - -**Goal**: Additional functionality and integrations - -### 6.1 MCP Server Integration -- [ ] Define MCP protocol schemas -- [ ] Implement MCP tool endpoints -- [ ] Read operations: - - Query battery status - - Query messages - - Query node list - - Query telemetry -- [ ] Write operations: - - Send message - - Send advertisement - - Ping node - - Send telemetry request -- [ ] MCP server documentation -- [ ] Example MCP client usage - -### 6.2 Web UI (Optional) -- [ ] React/Vue frontend -- [ ] Dashboard with real-time updates -- [ ] Node map visualization -- [ ] Message history viewer -- [ ] Network topology graph -- [ ] Configuration interface - -### 6.3 Real-time Features -- [ ] WebSocket endpoint for live events -- [ ] Server-Sent Events (SSE) support -- [ ] Real-time node status updates -- [ ] Live message notifications - -### 6.4 Advanced Querying -- [ ] Full-text search on messages -- [ ] Geographic queries (nodes within radius) -- [ ] Network topology queries -- [ ] Path analysis tools -- [ ] Message threading/conversations - -### 6.5 Alert System -- [ ] Alert rules engine -- [ ] Node offline alerts -- [ ] Low battery alerts -- [ ] Message delivery failures -- [ ] Network congestion alerts -- [ ] Alert delivery (webhook, email) - -### 6.6 Data Export -- [ ] CSV export for all tables -- [ ] JSON export -- [ ] GPX export for node locations -- [ ] Message archive export -- [ ] Statistics reports - -### 6.7 Authentication & Authorization -- [ ] API key authentication -- [ ] JWT token support -- [ ] Role-based access control -- [ ] Rate limiting per API key -- [ ] Usage tracking - -### 6.8 Performance Enhancements -- [ ] PostgreSQL backend option -- [ ] Redis caching layer -- [ ] Message queue for event processing -- [ ] Horizontal scaling support -- [ ] Read replicas - ---- - -## Configuration Reference - -### CLI Arguments - -```bash -Connection: - --serial-port TEXT Serial port device - --serial-baud INTEGER Serial baud rate - --use-mock Use mock MeshCore - --mock-scenario TEXT Scenario name for playback - --mock-loop Loop scenario indefinitely - --mock-nodes INTEGER Number of simulated nodes - --mock-min-interval FLOAT Min event interval (seconds) - --mock-max-interval FLOAT Max event interval (seconds) - --mock-center-lat FLOAT Center latitude - --mock-center-lon FLOAT Center longitude - -Database: - --db-path TEXT Database file path - --retention-days INTEGER Data retention days - --cleanup-interval-hours INTEGER Cleanup interval hours - -API: - --api-host TEXT API host - --api-port INTEGER API port - --api-title TEXT API title - --api-version TEXT API version - -Metrics: - --no-metrics Disable Prometheus metrics - -Logging: - --log-level LEVEL Log level (DEBUG/INFO/WARNING/ERROR/CRITICAL) - --log-format TYPE Log format (json/text) -``` - -### Environment Variables - -```bash -# Connection -MESHCORE_SERIAL_PORT=/dev/ttyUSB0 -MESHCORE_SERIAL_BAUD=115200 -MESHCORE_USE_MOCK=true -MESHCORE_MOCK_SCENARIO=simple_chat -MESHCORE_MOCK_LOOP=true -MESHCORE_MOCK_NODES=10 -MESHCORE_MOCK_MIN_INTERVAL=1.0 -MESHCORE_MOCK_MAX_INTERVAL=10.0 -MESHCORE_MOCK_CENTER_LAT=45.5231 -MESHCORE_MOCK_CENTER_LON=-122.6765 - -# Database -MESHCORE_DB_PATH=/data/meshcore.db -MESHCORE_RETENTION_DAYS=30 -MESHCORE_CLEANUP_INTERVAL_HOURS=1 - -# API -MESHCORE_API_HOST=0.0.0.0 -MESHCORE_API_PORT=8000 -MESHCORE_API_TITLE="MeshCore API" -MESHCORE_API_VERSION="1.0.0" - -# Metrics -MESHCORE_METRICS_ENABLED=true - -# Logging -MESHCORE_LOG_LEVEL=INFO -MESHCORE_LOG_FORMAT=json -``` - ---- - -## Database Schema - -### Tables - -1. **nodes** - Node tracking with prefix indexing -2. **messages** - Direct and channel messages -3. **advertisements** - Node advertisements with GPS -4. **trace_paths** - Trace results with SNR data -6. **telemetry** - Sensor telemetry data -7. **acknowledgments** - Message confirmations with timing -8. **status_responses** - Node status data -9. **statistics** - Device statistics (core/radio/packets) -10. **binary_responses** - Binary protocol responses -11. **control_data** - Control packet data -12. **raw_data** - Raw packet data -13. **device_info** - Companion device information -14. **events_log** - Raw event log for all events - -### Key Indexes - -- `nodes.public_key` (unique) -- `nodes.public_key_prefix_2` (for fast 2-char prefix queries) -- `nodes.public_key_prefix_8` (for fast 8-char prefix queries) -- `messages.from_public_key` -- `messages.to_public_key` -- `messages.timestamp` -- `advertisements.public_key` -- `events_log.event_type` -- `events_log.created_at` (for cleanup) - ---- - -## Mock Scenarios - -### simple_chat -Two nodes (Alice & Bob) exchanging messages -- Duration: 10 seconds -- Events: 2 advertisements, 2 messages, 1 ACK - -### trace_path_test -Trace path through multi-hop network -- Duration: 5 seconds -- Events: 3 advertisements, 1 trace result - -### telemetry_collection -Periodic telemetry from sensor node -- Duration: 15 seconds -- Events: 1 advertisement, 3 telemetry responses - -### network_stress -High-traffic scenario with many nodes -- Duration: 30 seconds -- Events: 10 advertisements, 20 channel messages - -### battery_drain -Simulated battery drain over time -- Duration: 200 seconds -- Events: 20 battery status updates - ---- - -## Metrics Reference - -### Event Counters -- `meshcore_events_total{event_type}` - Total events by type -- `meshcore_messages_total{direction,message_type}` - Messages by direction/type -- `meshcore_advertisements_total{adv_type}` - Advertisements by type - -### Latency -- `meshcore_message_roundtrip_seconds` - Message round-trip time -- `meshcore_ack_latency_seconds` - ACK latency - -### Connectivity -- `meshcore_nodes_total` - Total unique nodes -- `meshcore_nodes_active{node_type}` - Active nodes (last hour) -- `meshcore_path_hop_count` - Path hop distribution - -### Signal Quality -- `meshcore_snr_db` - SNR histogram -- `meshcore_rssi_dbm` - RSSI histogram - -### Device -- `meshcore_battery_voltage` - Battery voltage -- `meshcore_battery_percentage` - Battery percentage -- `meshcore_storage_used_bytes` - Storage used -- `meshcore_storage_total_bytes` - Storage total - -### Radio -- `meshcore_radio_noise_floor_dbm` - Noise floor -- `meshcore_radio_airtime_percent` - Airtime utilization -- `meshcore_packets_total{direction,status}` - Packet counts - -### Database -- `meshcore_db_table_rows{table}` - Rows per table -- `meshcore_db_size_bytes` - Database size -- `meshcore_db_cleanup_rows_deleted{table}` - Cleanup counts - -### Application -- `meshcore_connection_status` - Connection status (1=connected) -- `meshcore_errors_total{component,error_type}` - Error counts - ---- - -## Development Workflow - -### Setup -```bash -# Clone repository -git clone https://github.com/ipnet-mesh/meshcore-api.git -cd meshcore-api - -# Install dependencies -pip install -r requirements.txt - -# Or with Poetry -poetry install -``` - -### Running -```bash -# Development with mock -python -m meshcore_api --use-mock --log-level DEBUG - -# With scenario -python -m meshcore_api --use-mock --mock-scenario simple_chat - -# Production with hardware -python -m meshcore_api --serial-port /dev/ttyUSB0 -``` - -### Testing -```bash -# Run tests -pytest - -# With coverage -pytest --cov=meshcore_api - -# Specific test -pytest tests/test_database.py -``` - -### Code Quality -```bash -# Format -black src/ tests/ - -# Lint -ruff check src/ tests/ - -# Type check -mypy src/ -``` - ---- - -## Deployment - -### Local Development -```bash -python -m meshcore_api --use-mock -``` - -### Docker (Mock) -```bash -docker-compose up --build -``` - -### Docker (Production) -```bash -docker-compose -f docker-compose.prod.yml up -d -``` - -### Systemd Service -```ini -[Unit] -Description=MeshCore API -After=network.target - -[Service] -Type=simple -User=meshcore -WorkingDirectory=/opt/meshcore-api -Environment="MESHCORE_SERIAL_PORT=/dev/ttyUSB0" -Environment="MESHCORE_DB_PATH=/var/lib/meshcore/data.db" -ExecStart=/usr/bin/python3 -m meshcore_api -Restart=always - -[Install] -WantedBy=multi-user.target -``` - ---- - -## Future Considerations - -### Scalability -- Separate API server from event collector -- Use message queue (Redis/RabbitMQ) for events -- PostgreSQL for multi-instance deployments -- Read replicas for query performance - -### Security -- API authentication (API keys, JWT) -- Rate limiting -- Input validation -- SQL injection prevention -- Encrypted storage option - -### Data Privacy -- Optional message content exclusion -- GDPR compliance features -- Data export tools -- User data deletion - -### Performance -- Connection pooling optimization -- Query optimization -- Caching frequently accessed data -- Batch inserts for events - ---- - -## Contributing - -1. Create feature branch from `main` -2. Implement changes with tests -3. Follow code style (black, ruff) -4. Update documentation -5. Submit pull request - -## License - -See LICENSE file for details. diff --git a/docker-compose.yml b/docker-compose.yml index 3429a7c..11d2264 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,6 +11,7 @@ services: MESHCORE_RETENTION_DAYS: "30" MESHCORE_SERIAL_PORT: "/dev/ttyUSB0" # uncomment and set when using real hardware ENABLE_METRICS: "true" + # MESHCORE_API_BEARER_TOKEN: "your-secret-token" # uncomment to enable auth # WEBHOOK_MESSAGE_DIRECT: http://does-not-exist:5000/webhook ports: - "8080:8080" @@ -20,5 +21,19 @@ services: - "/dev/ttyUSB0:/dev/ttyUSB0" # needed only when using real hardware restart: unless-stopped + meshcore-mcp: + image: ghcr.io/ipnet-mesh/meshcore-api:latest + command: ["mcp"] + environment: + MESHCORE_API_URL: "http://meshcore-api:8080" + # MESHCORE_API_TOKEN: "${MESHCORE_API_BEARER_TOKEN:-}" # uncomment if API auth is enabled + MCP_PORT: "8081" + MESHCORE_LOG_LEVEL: "INFO" + ports: + - "8081:8081" + depends_on: + - meshcore-api + restart: unless-stopped + volumes: data: diff --git a/pyproject.toml b/pyproject.toml index 62e020d..b736851 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,6 +18,7 @@ dependencies = [ "click>=8.1.0", "httpx>=0.27.0", "jsonpath-ng>=1.6.0", + "mcp>=1.9.0", ] [project.optional-dependencies] diff --git a/src/meshcore_api/cli.py b/src/meshcore_api/cli.py index 6b1ce16..bcaca4a 100644 --- a/src/meshcore_api/cli.py +++ b/src/meshcore_api/cli.py @@ -439,5 +439,93 @@ def tag(json_file, db_path, dry_run, verbose, continue_on_error, validate_only): sys.exit(1) +@cli.command() +@click.option( + "--host", + type=str, + help="MCP server host (default: 0.0.0.0)", +) +@click.option( + "--port", + type=int, + help="MCP server port (default: 8081)", +) +@click.option( + "--api-url", + type=str, + help="MeshCore API URL (e.g., http://localhost:8080)", +) +@click.option( + "--api-token", + type=str, + help="MeshCore API bearer token for authentication", +) +@click.option( + "--log-level", + type=click.Choice(["DEBUG", "INFO", "WARNING", "ERROR"], case_sensitive=False), + help="Logging level (default: INFO)", +) +@click.option( + "--stdio", + is_flag=True, + help="Run in stdio mode instead of HTTP server", +) +def mcp(host, port, api_url, api_token, log_level, stdio): + """Start the MeshCore MCP server. + + The MCP server provides Model Context Protocol tools for interacting + with the MeshCore API. It can run as an HTTP server (default) or in + stdio mode for direct integration. + + Examples: + + \b + # Start MCP server pointing to local API + meshcore_api mcp --api-url http://localhost:8080 + + # With authentication + meshcore_api mcp --api-url http://localhost:8080 --api-token "secret" + + # Custom port + meshcore_api mcp --api-url http://localhost:8080 --port 9000 + + # Debug logging + meshcore_api mcp --api-url http://localhost:8080 --log-level DEBUG + + # Stdio mode for direct MCP integration + meshcore_api mcp --api-url http://localhost:8080 --stdio + """ + from .mcp.config import MCPConfig + from .mcp.server import run_server, run_stdio + + # Build CLI args dict + cli_args = {} + if host is not None: + cli_args["host"] = host + if port is not None: + cli_args["port"] = port + if api_url is not None: + cli_args["api_url"] = api_url + if api_token is not None: + cli_args["api_token"] = api_token + if log_level is not None: + cli_args["log_level"] = log_level + + # Load configuration + config = MCPConfig.from_args_and_env(cli_args) + + # Setup logging + setup_logging(level=config.log_level) + + # Log configuration + logger.info(config.display()) + + # Run server + if stdio: + run_stdio(config) + else: + run_server(config) + + if __name__ == "__main__": cli() diff --git a/src/meshcore_api/mcp/__init__.py b/src/meshcore_api/mcp/__init__.py new file mode 100644 index 0000000..a9c3c4b --- /dev/null +++ b/src/meshcore_api/mcp/__init__.py @@ -0,0 +1,3 @@ +"""MeshCore MCP Server - Model Context Protocol integration.""" + +__version__ = "0.1.0" diff --git a/src/meshcore_api/mcp/client.py b/src/meshcore_api/mcp/client.py new file mode 100644 index 0000000..a723649 --- /dev/null +++ b/src/meshcore_api/mcp/client.py @@ -0,0 +1,144 @@ +"""HTTP client for MeshCore API.""" + +import logging +from typing import Any, Optional + +import httpx + +from .state import state + +logger = logging.getLogger(__name__) + +# Default timeout for API requests (in seconds) +DEFAULT_TIMEOUT = 30.0 + + +class APIError(Exception): + """Exception raised for API errors.""" + + def __init__(self, message: str, status_code: Optional[int] = None, detail: Any = None): + self.message = message + self.status_code = status_code + self.detail = detail + super().__init__(message) + + +def _check_configured() -> Optional[str]: + """Check if the API is configured and return error message if not.""" + if not state.is_configured: + return ( + "Error: API not configured. " + "Set MESHCORE_API_URL environment variable or use --api-url argument." + ) + return None + + +def _build_url(path: str) -> str: + """Build full URL from base URL and path.""" + base = state.api_url.rstrip("/") + path = path.lstrip("/") + return f"{base}/{path}" + + +async def api_get( + path: str, params: Optional[dict] = None, timeout: float = DEFAULT_TIMEOUT +) -> dict: + """ + Make a GET request to the MeshCore API. + + Args: + path: API path (e.g., "/api/v1/messages") + params: Query parameters + timeout: Request timeout in seconds + + Returns: + JSON response as dict + + Raises: + APIError: If the request fails + """ + error = _check_configured() + if error: + logger.error(f"API not configured: {error}") + raise APIError(error) + + url = _build_url(path) + headers = state.get_auth_headers() + + # Filter out None values from params + if params: + params = {k: v for k, v in params.items() if v is not None} + + logger.debug(f"API GET: {url}") + + try: + async with httpx.AsyncClient(timeout=timeout) as client: + response = await client.get(url, params=params, headers=headers) + + if response.status_code == 422: + detail = response.json().get("detail", []) + logger.error(f"Validation error: {detail}") + raise APIError(f"Validation error: {detail}", status_code=422, detail=detail) + + response.raise_for_status() + return response.json() + + except httpx.HTTPStatusError as e: + logger.error(f"HTTP error {e.response.status_code}: {e.response.text}") + raise APIError( + f"HTTP error {e.response.status_code}: {e.response.text}", + status_code=e.response.status_code, + ) + except httpx.RequestError as e: + logger.error(f"Request error: {e}") + raise APIError(f"Request failed: {e}") + + +async def api_post( + path: str, json_data: Optional[dict] = None, timeout: float = DEFAULT_TIMEOUT +) -> dict: + """ + Make a POST request to the MeshCore API. + + Args: + path: API path (e.g., "/api/v1/commands/send_message") + json_data: JSON body data + timeout: Request timeout in seconds + + Returns: + JSON response as dict + + Raises: + APIError: If the request fails + """ + error = _check_configured() + if error: + logger.error(f"API not configured: {error}") + raise APIError(error) + + url = _build_url(path) + headers = state.get_auth_headers() + + logger.debug(f"API POST: {url}") + + try: + async with httpx.AsyncClient(timeout=timeout) as client: + response = await client.post(url, json=json_data or {}, headers=headers) + + if response.status_code == 422: + detail = response.json().get("detail", []) + logger.error(f"Validation error: {detail}") + raise APIError(f"Validation error: {detail}", status_code=422, detail=detail) + + response.raise_for_status() + return response.json() + + except httpx.HTTPStatusError as e: + logger.error(f"HTTP error {e.response.status_code}: {e.response.text}") + raise APIError( + f"HTTP error {e.response.status_code}: {e.response.text}", + status_code=e.response.status_code, + ) + except httpx.RequestError as e: + logger.error(f"Request error: {e}") + raise APIError(f"Request failed: {e}") diff --git a/src/meshcore_api/mcp/config.py b/src/meshcore_api/mcp/config.py new file mode 100644 index 0000000..eb55384 --- /dev/null +++ b/src/meshcore_api/mcp/config.py @@ -0,0 +1,76 @@ +"""Configuration management for MCP server.""" + +import os +from dataclasses import dataclass +from typing import Optional + + +@dataclass +class MCPConfig: + """MCP server configuration.""" + + # === Server === + host: str = "0.0.0.0" + port: int = 8081 + + # === API Connection === + api_url: Optional[str] = None + api_token: Optional[str] = None + + # === Logging === + log_level: str = "INFO" + + @classmethod + def from_args_and_env(cls, cli_args: Optional[dict] = None) -> "MCPConfig": + """ + Load configuration from CLI arguments, environment variables, and defaults. + + Priority: CLI args > Environment variables > Defaults + + Args: + cli_args: Optional dictionary of CLI arguments + + Returns: + MCPConfig instance + """ + if cli_args is None: + cli_args = {} + + def get_value(cli_key: str, env_var: str, default, type_converter=str): + """Get value with priority: CLI > Env > Default.""" + cli_value = cli_args.get(cli_key) + if cli_value is not None: + return cli_value + env_value = os.getenv(env_var) + if env_value is not None: + if type_converter == bool: + return env_value.lower() in ("true", "1", "yes", "on") + return type_converter(env_value) + return default + + config = cls() + + config.host = get_value("host", "MCP_HOST", config.host) + config.port = get_value("port", "MCP_PORT", config.port, int) + config.api_url = get_value("api_url", "MESHCORE_API_URL", config.api_url) + config.api_token = get_value("api_token", "MESHCORE_API_TOKEN", config.api_token) + config.log_level = get_value("log_level", "MESHCORE_LOG_LEVEL", config.log_level) + + return config + + @property + def is_configured(self) -> bool: + """Check if the API URL is configured.""" + return self.api_url is not None + + def display(self) -> str: + """Display configuration in human-readable format.""" + lines = [ + "MCP Server Configuration:", + f" Host: {self.host}", + f" Port: {self.port}", + f" API URL: {self.api_url or '(not configured)'}", + f" API Token: {'configured' if self.api_token else 'not configured'}", + f" Log Level: {self.log_level}", + ] + return "\n".join(lines) diff --git a/src/meshcore_api/mcp/server.py b/src/meshcore_api/mcp/server.py new file mode 100644 index 0000000..3e9c3fa --- /dev/null +++ b/src/meshcore_api/mcp/server.py @@ -0,0 +1,93 @@ +""" +MeshCore MCP Server - HTTP Implementation + +Provides MCP tools for interacting with MeshCore API. +Supports message and advertisement operations via HTTP/Streamable transport. +""" + +import logging +import sys + +from mcp.server.fastmcp import FastMCP + +from .config import MCPConfig +from .state import state +from .tools import advertisements, messages + +logger = logging.getLogger(__name__) + +# Initialize MCP server with FastMCP +mcp = FastMCP("meshcore-mcp") + +# Register all tools +messages.register_tools(mcp) +advertisements.register_tools(mcp) + + +def create_app(config: MCPConfig): + """ + Create and configure the MCP server application. + + Args: + config: MCP server configuration + + Returns: + Starlette application for streamable HTTP transport + """ + # Configure API connection via global state + state.configure(api_url=config.api_url, api_token=config.api_token) + + if not state.is_configured: + logger.warning( + "No API URL configured. Set --api-url or MESHCORE_API_URL environment variable." + ) + logger.warning("Tools will return errors until API is configured.") + + # Get the Starlette app for streamable HTTP transport + return mcp.streamable_http_app() + + +def run_server(config: MCPConfig): + """ + Run the MCP server with the given configuration. + + Args: + config: MCP server configuration + """ + import uvicorn + + logger.info(f"Starting MeshCore MCP Server on {config.host}:{config.port}") + logger.info(f"Server URL: http://{config.host}:{config.port}") + + if config.api_url: + logger.info(f"API URL: {config.api_url}") + else: + logger.warning( + "No API URL configured. Set --api-url or MESHCORE_API_URL environment variable." + ) + + # Create the application + app = create_app(config) + + # Run with uvicorn + uvicorn.run(app, host=config.host, port=config.port) + + +def run_stdio(config: MCPConfig): + """ + Run the MCP server in stdio mode for direct integration. + + Args: + config: MCP server configuration + """ + # Configure API connection via global state + state.configure(api_url=config.api_url, api_token=config.api_token) + + if not state.is_configured: + print( + "Warning: No API URL configured. Set MESHCORE_API_URL environment variable.", + file=sys.stderr, + ) + + # Run in stdio mode + mcp.run() diff --git a/src/meshcore_api/mcp/state.py b/src/meshcore_api/mcp/state.py new file mode 100644 index 0000000..0a5383b --- /dev/null +++ b/src/meshcore_api/mcp/state.py @@ -0,0 +1,37 @@ +"""Server state management for MeshCore MCP Server.""" + +from typing import Optional + + +class ServerState: + """Maintains global server state for API connectivity.""" + + # API configuration + api_url: Optional[str] = None + api_token: Optional[str] = None + + def configure(self, api_url: Optional[str] = None, api_token: Optional[str] = None): + """ + Configure the API connection settings. + + Args: + api_url: Base URL for the MeshCore API (e.g., "http://localhost:8080") + api_token: Bearer token for authentication (optional if API is public) + """ + self.api_url = api_url + self.api_token = api_token + + @property + def is_configured(self) -> bool: + """Check if the API URL is configured.""" + return self.api_url is not None + + def get_auth_headers(self) -> dict: + """Get authentication headers for API requests.""" + if self.api_token: + return {"Authorization": f"Bearer {self.api_token}"} + return {} + + +# Global state instance +state = ServerState() diff --git a/src/meshcore_api/mcp/tools/__init__.py b/src/meshcore_api/mcp/tools/__init__.py new file mode 100644 index 0000000..66828cd --- /dev/null +++ b/src/meshcore_api/mcp/tools/__init__.py @@ -0,0 +1,6 @@ +"""MCP tools for MeshCore API.""" + +# Tools are registered in their respective modules +from . import advertisements, messages + +__all__ = ["messages", "advertisements"] diff --git a/src/meshcore_api/mcp/tools/advertisements.py b/src/meshcore_api/mcp/tools/advertisements.py new file mode 100644 index 0000000..57f8abd --- /dev/null +++ b/src/meshcore_api/mcp/tools/advertisements.py @@ -0,0 +1,109 @@ +"""Advertisement tools for MeshCore API.""" + +import logging +from typing import Optional + +from ..client import APIError, api_get, api_post + +logger = logging.getLogger(__name__) + + +def register_tools(mcp): + """Register advertisement tools with the MCP server.""" + + @mcp.tool() + async def meshcore_get_advertisements( + node_public_key: Optional[str] = None, + adv_type: Optional[str] = None, + start_date: Optional[str] = None, + end_date: Optional[str] = None, + limit: int = 100, + offset: int = 0, + ) -> str: + """ + Query advertisements from the mesh network. + + Args: + node_public_key: Filter by node public key (full 64 hex characters) + adv_type: Filter by advertisement type (none/chat/repeater/room) + start_date: Filter advertisements after this date (ISO 8601 format) + end_date: Filter advertisements before this date (ISO 8601 format) + limit: Maximum number of advertisements to return (1-1000, default: 100) + offset: Number of advertisements to skip (default: 0) + + Returns: + Formatted list of advertisements + """ + try: + params = { + "node_public_key": node_public_key, + "adv_type": adv_type, + "start_date": start_date, + "end_date": end_date, + "limit": limit, + "offset": offset, + } + + result = await api_get("/api/v1/advertisements", params=params) + + advertisements = result.get("advertisements", []) + total = result.get("total", 0) + + if not advertisements: + return "No advertisements found" + + output = f"Advertisements ({len(advertisements)} of {total} total):\n" + output += "=" * 60 + "\n" + + for i, adv in enumerate(advertisements, 1): + output += f"\n[{i}] Advertisement\n" + output += f" ID: {adv.get('id', 'N/A')}\n" + output += f" Public Key: {adv.get('public_key', 'N/A')}\n" + output += f" Type: {adv.get('adv_type', 'N/A')}\n" + output += f" Name: {adv.get('name', 'N/A')}\n" + if adv.get("flags") is not None: + output += f" Flags: {adv.get('flags')}\n" + output += f" Received: {adv.get('received_at', 'N/A')}\n" + output += "-" * 60 + "\n" + + return output + + except APIError as e: + return f"Error querying advertisements: {e.message}" + except Exception as e: + logger.error(f"Unexpected error querying advertisements: {e}") + return f"Error querying advertisements: {str(e)}" + + @mcp.tool() + async def meshcore_send_advertisement(flood: bool = False) -> str: + """ + Send an advertisement to announce this device on the mesh network. + + Args: + flood: Enable flooding to propagate the advertisement further (default: false) + + Returns: + Status message indicating success or failure + """ + try: + result = await api_post("/api/v1/commands/send_advert", json_data={"flood": flood}) + + success = result.get("success", False) + message = result.get("message", "Unknown result") + queue_info = result.get("queue_info") + + output = f"Advertisement send {'succeeded' if success else 'failed'}: {message}" + + if queue_info: + output += f"\n Queue position: {queue_info.get('position', 'N/A')}" + output += f"\n Estimated wait: {queue_info.get('estimated_wait_seconds', 'N/A')}s" + if queue_info.get("debounced"): + output += "\n (Command was debounced)" + + return output + + except APIError as e: + return f"Error sending advertisement: {e.message}" + except Exception as e: + logger.error(f"Unexpected error sending advertisement: {e}") + return f"Error sending advertisement: {str(e)}" diff --git a/src/meshcore_api/mcp/tools/messages.py b/src/meshcore_api/mcp/tools/messages.py new file mode 100644 index 0000000..9fc2a49 --- /dev/null +++ b/src/meshcore_api/mcp/tools/messages.py @@ -0,0 +1,188 @@ +"""Message tools for MeshCore API.""" + +import logging +from typing import Optional + +from ..client import APIError, api_get, api_post + +logger = logging.getLogger(__name__) + + +def register_tools(mcp): + """Register message tools with the MCP server.""" + + @mcp.tool() + async def meshcore_get_messages( + sender_public_key: Optional[str] = None, + channel_idx: Optional[int] = None, + message_type: Optional[str] = None, + start_date: Optional[str] = None, + end_date: Optional[str] = None, + limit: int = 100, + offset: int = 0, + ) -> str: + """ + Query messages from the mesh network. + + Args: + sender_public_key: Filter by sender public key (full 64 hex characters) + channel_idx: Filter by channel index (for channel messages) + message_type: Filter by message type ('contact' or 'channel') + start_date: Filter messages after this sender_timestamp (ISO 8601 format) + end_date: Filter messages before this sender_timestamp (ISO 8601 format) + limit: Maximum number of messages to return (1-1000, default: 100) + offset: Number of messages to skip (default: 0) + + Returns: + Formatted list of messages + """ + try: + params = { + "sender_public_key": sender_public_key, + "channel_idx": channel_idx, + "message_type": message_type, + "start_date": start_date, + "end_date": end_date, + "limit": limit, + "offset": offset, + } + + result = await api_get("/api/v1/messages", params=params) + + messages = result.get("messages", []) + total = result.get("total", 0) + + if not messages: + return "No messages found" + + output = f"Messages ({len(messages)} of {total} total):\n" + output += "=" * 60 + "\n" + + for i, msg in enumerate(messages, 1): + msg_type = msg.get("message_type", "unknown").upper() + direction = msg.get("direction", "unknown") + + output += f"\n[{i}] {msg_type} MESSAGE ({direction})\n" + output += f" ID: {msg.get('id', 'N/A')}\n" + + if msg.get("pubkey_prefix"): + output += f" Sender Key: {msg.get('pubkey_prefix')}\n" + + if msg.get("channel_idx") is not None: + output += f" Channel: {msg.get('channel_idx')}\n" + + output += f" Content: {msg.get('content', '')}\n" + + if msg.get("snr") is not None: + output += f" SNR: {msg.get('snr')} dB\n" + + if msg.get("path_len") is not None: + output += f" Path Length: {msg.get('path_len')} hops\n" + + if msg.get("sender_timestamp"): + output += f" Sender Time: {msg.get('sender_timestamp')}\n" + + output += f" Received: {msg.get('received_at', 'N/A')}\n" + output += "-" * 60 + "\n" + + return output + + except APIError as e: + return f"Error querying messages: {e.message}" + except Exception as e: + logger.error(f"Unexpected error querying messages: {e}") + return f"Error querying messages: {str(e)}" + + @mcp.tool() + async def meshcore_send_direct_message( + destination: str, text: str, text_type: str = "plain" + ) -> str: + """ + Send a direct message to a specific node. + + Args: + destination: Destination node public key (full 64 hex characters) + text: Message text content (1-1000 characters) + text_type: Text type - 'plain', 'cli_data', or 'signed_plain' (default: 'plain') + + Returns: + Status message indicating success or failure + """ + if len(destination) != 64: + return ( + f"Error: destination must be a 64-character public key " + f"(got {len(destination)} characters)" + ) + + if not text or len(text) > 1000: + return "Error: text must be 1-1000 characters" + + try: + result = await api_post( + "/api/v1/commands/send_message", + json_data={"destination": destination, "text": text, "text_type": text_type}, + ) + + success = result.get("success", False) + message = result.get("message", "Unknown result") + queue_info = result.get("queue_info") + estimated_delivery = result.get("estimated_delivery_ms") + + output = f"Direct message send {'succeeded' if success else 'failed'}: {message}" + + if estimated_delivery: + output += f"\n Estimated delivery: {estimated_delivery}ms" + + if queue_info: + output += f"\n Queue position: {queue_info.get('position', 'N/A')}" + output += f"\n Estimated wait: {queue_info.get('estimated_wait_seconds', 'N/A')}s" + if queue_info.get("debounced"): + output += "\n (Command was debounced)" + + return output + + except APIError as e: + return f"Error sending direct message: {e.message}" + except Exception as e: + logger.error(f"Unexpected error sending direct message: {e}") + return f"Error sending direct message: {str(e)}" + + @mcp.tool() + async def meshcore_send_channel_message(text: str, flood: bool = False) -> str: + """ + Send a broadcast message to all nodes on the channel. + + Args: + text: Message text content (1-1000 characters) + flood: Enable flooding to propagate the message further (default: false) + + Returns: + Status message indicating success or failure + """ + if not text or len(text) > 1000: + return "Error: text must be 1-1000 characters" + + try: + result = await api_post( + "/api/v1/commands/send_channel_message", json_data={"text": text, "flood": flood} + ) + + success = result.get("success", False) + message = result.get("message", "Unknown result") + queue_info = result.get("queue_info") + + output = f"Channel message send {'succeeded' if success else 'failed'}: {message}" + + if queue_info: + output += f"\n Queue position: {queue_info.get('position', 'N/A')}" + output += f"\n Estimated wait: {queue_info.get('estimated_wait_seconds', 'N/A')}s" + if queue_info.get("debounced"): + output += "\n (Command was debounced)" + + return output + + except APIError as e: + return f"Error sending channel message: {e.message}" + except Exception as e: + logger.error(f"Unexpected error sending channel message: {e}") + return f"Error sending channel message: {str(e)}" diff --git a/test_webhooks.py b/test_webhooks.py deleted file mode 100644 index fbe4b45..0000000 --- a/test_webhooks.py +++ /dev/null @@ -1,97 +0,0 @@ -#!/usr/bin/env python3 -"""Simple webhook receiver for testing webhook functionality.""" - -import asyncio -import json -from datetime import datetime - -from aiohttp import web - -# Store received webhooks -received_webhooks = { - "direct": [], - "channel": [], - "advertisement": [], -} - - -async def webhook_direct(request): - """Handle direct message webhooks.""" - data = await request.json() - received_webhooks["direct"].append(data) - print(f"\n[{datetime.now()}] Direct Message Webhook Received:") - print(json.dumps(data, indent=2)) - return web.json_response({"status": "ok"}) - - -async def webhook_channel(request): - """Handle channel message webhooks.""" - data = await request.json() - received_webhooks["channel"].append(data) - print(f"\n[{datetime.now()}] Channel Message Webhook Received:") - print(json.dumps(data, indent=2)) - return web.json_response({"status": "ok"}) - - -async def webhook_advertisement(request): - """Handle advertisement webhooks.""" - data = await request.json() - received_webhooks["advertisement"].append(data) - print(f"\n[{datetime.now()}] Advertisement Webhook Received:") - print(json.dumps(data, indent=2)) - return web.json_response({"status": "ok"}) - - -async def status(request): - """Return webhook statistics.""" - stats = { - "direct_count": len(received_webhooks["direct"]), - "channel_count": len(received_webhooks["channel"]), - "advertisement_count": len(received_webhooks["advertisement"]), - "total": sum(len(v) for v in received_webhooks.values()), - } - print(f"\n[{datetime.now()}] Webhook Statistics:") - print(json.dumps(stats, indent=2)) - return web.json_response(stats) - - -async def main(): - """Run the webhook test server.""" - app = web.Application() - app.router.add_post("/webhooks/direct", webhook_direct) - app.router.add_post("/webhooks/channel", webhook_channel) - app.router.add_post("/webhooks/advertisement", webhook_advertisement) - app.router.add_get("/status", status) - - runner = web.AppRunner(app) - await runner.setup() - site = web.TCPSite(runner, "localhost", 9000) - await site.start() - - print("=" * 80) - print("Webhook Test Server Started") - print("=" * 80) - print(f"Listening on: http://localhost:9000") - print("") - print("Webhook endpoints:") - print(" - Direct Messages: POST http://localhost:9000/webhooks/direct") - print(" - Channel Messages: POST http://localhost:9000/webhooks/channel") - print(" - Advertisements: POST http://localhost:9000/webhooks/advertisement") - print(" - Status: GET http://localhost:9000/status") - print("") - print("Press Ctrl+C to stop") - print("=" * 80) - print("") - - try: - # Keep server running - while True: - await asyncio.sleep(3600) - except KeyboardInterrupt: - print("\n\nShutting down webhook server...") - finally: - await runner.cleanup() - - -if __name__ == "__main__": - asyncio.run(main()) diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 3905d18..a3f2150 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -31,7 +31,7 @@ def test_default_config_values(self): # API defaults assert config.api_host == "0.0.0.0" - assert config.api_port == 8000 + assert config.api_port == 8080 assert config.api_title == "MeshCore API" assert config.api_version == "1.0.0" assert config.api_bearer_token is None From 2471528bb3be3e6ee3b0dc1d9bb8c49bc2229b47 Mon Sep 17 00:00:00 2001 From: Louis King Date: Mon, 1 Dec 2025 23:48:44 +0000 Subject: [PATCH 2/3] Added VSCode MCP server --- .vscode/mcp.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.vscode/mcp.json b/.vscode/mcp.json index abcdf76..3d6af27 100644 --- a/.vscode/mcp.json +++ b/.vscode/mcp.json @@ -1,8 +1,8 @@ { "servers": { - "MeshCoreDocker": { + "MeshCoreIPNet": { "type": "http", - "url": "http://localhost:8081/mcp/" + "url": "https://mcp.ipnt.uk/mcp/" } } } From b84e08c8166895829927cad0d2e8f454c52c50c1 Mon Sep 17 00:00:00 2001 From: Louis King Date: Mon, 1 Dec 2025 23:57:17 +0000 Subject: [PATCH 3/3] Updates --- AGENTS.md | 8 ++++ README.md | 129 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 137 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 3273e1e..7dcdea6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -105,6 +105,14 @@ meshcore_api mcp --api-url http://localhost:8080 --port 9000 The MCP server provides AI/LLM integration tools for interacting with the MeshCore API. It runs as an HTTP server (default port 8081) or in stdio mode. +**Important Architecture Note:** The MCP server is a standalone HTTP client that communicates **exclusively with the MeshCore REST API over HTTP**. It does NOT: +- Connect directly to any database +- Communicate with the MeshCore companion device/hardware +- Require access to the SQLite database file +- Need serial/BLE connectivity + +This design allows the MCP server to run on a completely separate machine from the MeshCore API server, as long as it can reach the API endpoint over the network. + Common options: - `--host`: MCP server host (default: 0.0.0.0) - `--port`: MCP server port (default: 8081) diff --git a/README.md b/README.md index cc5df00..2add39a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ MeshCore companion application for event collection, persistence, and REST API a - Custom node metadata tags with typed values (strings, numbers, booleans, coordinates) - **Webhooks** for real-time event notifications to external URLs - REST API for querying collected data and sending commands +- **MCP Server** for AI/LLM integration via Model Context Protocol - Mock MeshCore implementation for development without hardware - Prometheus metrics for monitoring - OpenAPI/Swagger documentation @@ -442,6 +443,134 @@ curl http://localhost:9000/status The test server displays received webhooks in real-time and provides a status endpoint for monitoring. +## MCP Server (AI/LLM Integration) + +The MeshCore MCP (Model Context Protocol) server enables AI assistants and LLMs to interact with your mesh network through a standardized interface. + +### Architecture + +**Important:** The MCP server is a standalone HTTP client that communicates **exclusively with the MeshCore REST API over HTTP**. It does NOT: + +- Connect directly to any database +- Communicate with the MeshCore companion device/hardware +- Require access to the SQLite database file +- Need serial/BLE connectivity + +This design allows the MCP server to run on a completely separate machine from the MeshCore API server, as long as it can reach the API endpoint over the network. + +``` +┌─────────────┐ HTTP ┌──────────────────┐ Serial/BLE ┌──────────────┐ +│ AI/LLM │ ◄────────────► │ MeshCore API │ ◄────────────────► │ MeshCore │ +│ (Claude, │ │ Server │ │ Device │ +│ etc.) │ │ (port 8080) │ │ │ +└─────────────┘ │ + SQLite DB │ └──────────────┘ + │ └──────────────────┘ + │ MCP ▲ + ▼ │ HTTP +┌─────────────┐ │ +│ MCP Server │ ────────────────────────┘ +│ (port 8081)│ +└─────────────┘ +``` + +### Quick Start + +```bash +# Start the MeshCore API server first +meshcore_api server --use-mock --api-port 8080 + +# In another terminal, start the MCP server +meshcore_api mcp --api-url http://localhost:8080 + +# With authentication (if API requires it) +meshcore_api mcp --api-url http://localhost:8080 --api-token "your-token" + +# Run in stdio mode (for direct LLM integration) +meshcore_api mcp --api-url http://localhost:8080 --stdio +``` + +### Configuration + +**CLI Arguments:** +```bash +meshcore_api mcp \ + --host 0.0.0.0 \ + --port 8081 \ + --api-url http://localhost:8080 \ + --api-token "optional-bearer-token" \ + --log-level INFO +``` + +**Environment Variables:** +```bash +export MCP_HOST=0.0.0.0 +export MCP_PORT=8081 +export MESHCORE_API_URL=http://localhost:8080 +export MESHCORE_API_TOKEN=your-bearer-token +meshcore_api mcp +``` + +### Available MCP Tools + +The MCP server exposes these tools to AI assistants: + +| Tool | Description | +|------|-------------| +| `meshcore_get_messages` | Query messages from the mesh network with filtering by sender, channel, type, and date range | +| `meshcore_send_direct_message` | Send a direct message to a specific node (requires 64-char public key) | +| `meshcore_send_channel_message` | Send a broadcast message to all nodes on the channel | +| `meshcore_get_advertisements` | Query node advertisements with filtering by node, type, and date range | +| `meshcore_send_advertisement` | Send an advertisement to announce this device on the network | + +### Example Tool Usage + +When an AI assistant uses the MCP tools: + +```python +# Query recent messages +meshcore_get_messages(limit=10, message_type="channel") + +# Send a direct message to a node +meshcore_send_direct_message( + destination="abc123...64chars...", + text="Hello from AI!", + text_type="plain" +) + +# Send a channel broadcast +meshcore_send_channel_message(text="Hello mesh network!", flood=False) + +# Query advertisements from a specific node +meshcore_get_advertisements(node_public_key="abc123...64chars...", limit=5) +``` + +### Running Remotely + +Since the MCP server only needs HTTP access to the API, you can run it anywhere: + +```bash +# On a remote machine (API server at 192.168.1.100) +meshcore_api mcp --api-url http://192.168.1.100:8080 + +# With a cloud-hosted API +meshcore_api mcp --api-url https://meshcore.example.com --api-token "secret" +``` + +### VSCode Integration + +Add to your VSCode MCP settings to enable Claude integration: + +```json +{ + "mcpServers": { + "meshcore": { + "command": "meshcore_api", + "args": ["mcp", "--api-url", "http://localhost:8080", "--stdio"] + } + } +} +``` + ## API Documentation Once running, access interactive API docs at: