ExoSense MCP Server
by diodechain
README.md
# ExoSense MCP Server (Python)
A Model Context Protocol (MCP) server for interacting with the ExoSense platform using GraphQL, implemented in Python.
## Overview
Invoked by:
python3 -m exosense_mcp.server
This MCP server provides tools to interact with ExoSense devices, sensors, groups, assets, and data through a standardized GraphQL interface. It features a modular architecture with session-based authentication and supports comprehensive IoT device management operations.
The server is implemented using `aiohttp` and follows the JSON-RPC 2.0 protocol for MCP communication. Tools are loaded dynamically from a configuration file, making it easy to add or modify tools without changing the core server code.
## Features
- **GraphQL API**: All interactions with ExoSense use GraphQL for efficient data fetching
- **Session-based Authentication**: Token/OAuth authentication managed at the session level
- **Modular Tool Architecture**: 18 specialized tools for different ExoSense operations
- **Dynamic Tool Loading**: Tools are loaded from `config.yml` at startup
- **Type-Safe**: Full Python type hints with Pydantic validation
- **Pre-built Queries**: Common GraphQL queries and mutations included
- **Error Handling**: Comprehensive error handling and validation
- **HTTP Streaming**: Built on aiohttp with HTTP streaming support
## Installation
Use a virtual environment (recommended, and required on systems with externally-managed Python such as Debian/Ubuntu):
```bash
python3 -m venv .venv
source .venv/bin/activate # On Windows: .venv\Scripts\activate
pip install -r requirements.txt
```
Or install as a package:
```bash
pip install -e .
```
If you see an "externally-managed-environment" error from pip, create and use a venv as above; do not use `--break-system-packages`.
## Configuration
The server can be configured using environment variables. You can set them in one of two ways:
1. **Using a `.env` file** (recommended for local development):
- Create a `.env` file in the `python/` directory
- Add the following variables:
```
EXOSENSE_API_URL=https://api.exosense.com
EXOSENSE_ORIGIN=https://exosense.com
EXOSENSE_AUTH_TOKEN=your-token-here
PORT=9000
HTTP_STREAMING=Private
```
- The `.env` file is automatically loaded when the server starts
2. **Using system environment variables**:
- Set environment variables directly in your shell or system configuration
### Environment Variables
- `EXOSENSE_API_URL`: GraphQL endpoint for ExoSense (default: `https://api.exosense.com`)
- `EXOSENSE_ORIGIN`: Host name of the solution being referenced (default: `https://exosense.com`)
- `EXOSENSE_AUTH_TOKEN`: Default authentication token (optional, required for private mode)
- `PORT`: Server port (default: `9000`)
- `HTTP_STREAMING`: Set to `"Private"` for backward compatibility (optional, no longer required for auth mode)
- `DIODE_JOIN_ADDRESS`: Diode join address for private networks (optional; omit for public publish). Set in `.env` per deployment to avoid config merge conflicts.
- `DIODE_CLIENT`: Omit or `embedded` (default) — run Diode in this process. Set to **`container`** only when a sidecar publishes Diode (see **Diode** below).
- `LISTEN_HOST`: Bind address for the HTTP MCP server (default: `127.0.0.1`).
- `LOG_LEVEL`: Python logging level (default: `INFO`). Use **`WARNING`** in production to cut log I/O overhead.
**Performance:** The server reuses a single **HTTP connection pool** to ExoSense (no new TCP/TLS per GraphQL call). **`GET /health`**, **`POST /mcp`**, and **`/.well-known/mcp-authentication`** are logged at **DEBUG** only so health probes and MCP traffic do not spam **INFO**. JSON-RPC responses use **compact JSON** (no extra whitespace). **`initialize`** avoids copying all headers into a `dict` when authenticating. **`ToolContext`** and **`inspect`** are not recreated on every tool call. For lowest latency on probes, set **`LOG_LEVEL=WARNING`**.
### Hybrid Authentication Mode
The server uses a **hybrid authentication approach** that supports both single-tenant and multi-tenant scenarios:
1. **Client-Provided Auth (Multi-Tenant)**: If a client provides authentication headers:
- The server will use the client's credentials from headers
- Supports `x-automation-token` + `x-origin` OR `Authorization: Automation <token>` + `origin`
- Each client session can have different credentials
- Perfect for SaaS platforms or multi-organization deployments
2. **Environment Fallback (Single-Tenant Default)**: If no auth headers are provided:
- The server falls back to `EXOSENSE_ORIGIN` and `EXOSENSE_AUTH_TOKEN` from `.env`
- IT can set a default API key without exposing it to clients
- Clients can still override by providing their own headers
- Perfect for internal tools or when you want a default tenant
**Priority**: Headers (client-provided) → `.env` variables (IT-provided default)
This allows IT to configure a default API key in `.env` for convenience, while still allowing pipelines and clients to use their own credentials when needed.
## Usage
### Starting the Server
```bash
python3 -m exosense_mcp.server
.venv/bin/python -m exosense_mcp.server
```
Or using the installed script:
```bash
exosense-mcp-server
```
The server will:
1. Test the connection to ExoSense (if credentials are provided)
2. Load all tools from `config.yml`
3. Start the HTTP server on the configured port (default: 9000; set `PORT` in `.env` to override)
4. Listen for MCP requests at `http://localhost:9000/mcp` (or whatever port you set)
### Running as a systemd service
To run the server under systemd with the project’s virtual environment (good for VMs with many projects). The unit file is set up for projects under `/root/projects/` (e.g. `/root/projects/exosense-pymcp`).
1. **Install the project and create a venv** in `/root/projects/exosense-pymcp`:
```bash
mkdir -p /root/projects/exosense-pymcp
cp -r . /root/projects/exosense-pymcp/ # or clone/git pull
cd /root/projects/exosense-pymcp
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
```
2. **Configure** a `.env` file in that directory (see Configuration above).
3. **Install the unit file and start the service:**
```bash
sudo cp contrib/exosense-pymcp.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now exosense-pymcp
```
4. **Check status and logs:**
```bash
sudo systemctl status exosense-pymcp
journalctl -u exosense-pymcp -f
```
Each project on the VM can have its own unit file; copy and edit the template to point at its path under `/root/projects/`.
### Pipeline Authentication
For pipelines that want to use their own ExoSense credentials (instead of the server's `.env` default), see **[PIPELINE_AUTHENTICATION.md](PIPELINE_AUTHENTICATION.md)** for detailed integration examples including:
- cURL examples
- Python/Node.js code samples
- GitHub Actions and GitLab CI configurations
- Security best practices
### MCP Client Configuration
To connect an MCP client to the ExoSense MCP server, configure your client with:
```json
{
"mcpServers": {
"exosense": {
"type": "http",
"url": "http://localhost:9000/mcp",
"headers": {
"x-automation-token": "<YOUR_EXOSENSE_API_TOKEN>",
"origin": "https://your-exosense-instance.com"
}
}
}
}
```
**Note**: If `HTTP_STREAMING=Private` is set in the server's `.env` file, headers are optional as the server will use `.env` authentication.
### Diode (optional – publish MCP over the internet)
You can run the server’s **embedded Diode client** so the MCP endpoint is reachable over Diode without coordinating an external client, **or** run in **container** mode when another process in the stack publishes Diode for you.
1. **Install the Diode CLI** from [https://diode.io/download#cli](https://diode.io/download#cli) so the `diode` binary is on your PATH (or in the project directory). Not required when **`DIODE_CLIENT=container`** if publish is handled elsewhere.
2. In **`config.yml`**, set **`auto-start-diode: true`** when you want Diode publish enabled (embedded path) or to document intent with container Diode.
3. **`.env`**: By default the server uses the **embedded** Diode client (no variable needed). Set **`DIODE_CLIENT=container`** only when this process must **not** start Diode and a container/sidecar handles publish. Optional **`DIODE_CLIENT=embedded`** is the same as omitting the variable.
4. Start the server as usual: `python3 -m exosense_mcp.server`
With **`auto-start-diode: true`** and embedded mode (default), the server spawns the Diode CLI on startup, creates a local client database under **`diode_client/`**, and prints the public MCP URL (e.g. `https://<client>.diode.link:9000/mcp`). The Diode process is stopped automatically when the server exits.
To use a private Diode network instead of public publish, set **`DIODE_JOIN_ADDRESS`** in your `.env` file (or environment) to your join address. Keeping it in `.env` avoids merge conflicts when deploying to different environments.
### Diode deploy MCP (Cursor)
To deploy packaged releases via Diode’s **`diode_deploy`** tool, add the Diode MCP server with the deploy preset. This repo includes **[`.cursor/mcp.json`](.cursor/mcp.json)**, which passes deploy settings using **Cursor’s env interpolation**: values like **`${env:DIODE_MCP_DEPLOY_TARGET}`** are filled from the **operating-system environment** of the Cursor process—not by reading `.env` automatically.
1. Add **`DIODE_MCP_DEPLOY_TARGET`** and **`DIODE_MCP_DEPLOY_UUID`** to your **`.env`** (see **`.env.example`**) so they stay out of git and match your deployment.
2. Make sure those variables are **exported into the environment Cursor inherits**, for example:
- Launch Cursor from a terminal after: `set -a && source .env && set +a && cursor .` (adjust for your shell), or
- Use **[direnv](https://direnv.net/)** so the project directory exports them when you `cd` into it, or
- Define the same variables in your shell profile / OS user environment.
**Semantics:** **`DIODE_MCP_DEPLOY_TARGET`** is the `diode://…` URL for the deploy files listener. **`DIODE_MCP_DEPLOY_UUID`** is the deploy token; when set, the tool can rename tarballs to `{UUID}.tar.gz` and you can omit `deploy_token` in tool calls (see Diode CLI `docs/mcp-spec.md`).
**Python container:** **`main.py`** at the repo root imports and runs the MCP server (required by the deploy platform). **`deploy.manifest.json`** sets `start_command` to **`python3 main.py`** (avoid `python3 -m …` here — the ingest validator rejects those characters). The server still binds to **`127.0.0.1`** by default. **`requirements.txt`** is installed before start.
**Do not put `.env` in the deploy tarball** — it is too sensitive. Use **`scripts/diode_deploy_bundle.sh`**, which excludes **`.env`**. Anything you rely on locally from **`.env`** (e.g. **`EXOSENSE_API_URL`**, **`EXOSENSE_ORIGIN`**, **`EXOSENSE_AUTH_TOKEN`**, **`PORT`**, **`DIODE_CLIENT`**, **`LISTEN_HOST`**, **`DIODE_JOIN_ADDRESS`**, **`LOG_LEVEL`**, **`HTTP_STREAMING`**) must be **configured on the Diode Deploy project** so the running container receives the same variables as process environment. See **`.env.example`** for the full list of knobs; mirror those into the deploy UI or project env settings.
Adjust **`command`** in **`mcp.json`** if your Diode binary is not at `/Users/hr/opt/diode/diode` (e.g. `"diode"` when it is on `PATH`).
## Development
### Project Structure
```
python/
├── exosense_mcp/
│ ├── __init__.py
│ ├── server.py # Main MCP server (aiohttp-based)
│ ├── auth.py # Authentication handler
│ ├── exosense_client.py # GraphQL client for ExoSense
│ ├── utils.py # Utility functions
│ ├── tools/ # Modular tool architecture
│ │ ├── __init__.py
│ │ ├── types.py # Tool context and types
│ │ ├── _helpers.py # Helper functions for tools
│ │ └── *.py # Individual tool modules (18 tools)
│ ├── graphql/ # GraphQL query builders
│ │ ├── __init__.py
│ │ ├── assets.py
│ │ ├── groups.py
│ │ ├── devices_products.py
│ │ ├── insight_modules.py
│ │ ├── logs.py
│ │ ├── reports.py
│ │ ├── work_instructions.py
│ │ └── condition_policies.py
│ ├── types/ # Type definitions
│ │ ├── __init__.py
│ │ ├── auth.py # Authentication types
│ │ └── graphql.py # GraphQL types
│ ├── resources/ # MCP resources (documentation links)
│ │ └── index.py
│ └── prompts/ # MCP prompts
│ └── *.py
├── config.yml # Tool configuration (lists all tools)
├── pyproject.toml
├── requirements.txt
└── README.md
```
### Tool Architecture
Each tool follows a consistent pattern:
1. **Pydantic Model**: Defines the tool's parameters with validation
2. **Execute Function**: `async def execute(arguments: Dict[str, Any], context: ToolContext) -> Dict[str, Any]`
3. **TOOL_METADATA**: Dictionary containing tool name, description, and JSON schema
Example tool structure:
```python
from typing import Dict, Any
from pydantic import BaseModel, Field, ValidationError
from .types import ToolContext
from ._helpers import pydantic_to_json_schema, format_success_response, format_error_response
class MyToolParams(BaseModel):
param1: str = Field(..., description="Description of param1")
param2: int = Field(10, ge=1, description="Description of param2")
async def execute(arguments: Dict[str, Any], context: ToolContext) -> Dict[str, Any]:
try:
# Validate arguments
try:
args = MyToolParams(**arguments)
except ValidationError as e:
return format_error_response(Exception(f"Invalid arguments: {e}"))
# Get authenticated client
auth = context.session.get("authorization") if context.session else None
import exosense_mcp.server as server_module
client = server_module.get_exosense_client(auth)
# Tool logic here
result = await client.query(...)
return format_success_response(result, "Success message")
except Exception as error:
return format_error_response(error)
# Tool metadata
schema = pydantic_to_json_schema(MyToolParams)
TOOL_METADATA = {
"name": "exosense-my-tool",
"description": "Description of what the tool does",
"inputSchema": schema
}
```
### Adding New Tools
To add a new tool:
1. **Create a new tool file** in `exosense_mcp/tools/` (e.g., `my_new_tool.py`)
2. **Follow the tool architecture pattern** above
3. **Add the tool to `config.yml`**:
```yaml
tools:
- file: exosense_mcp/tools/my_new_tool.py
name: exosense-my-new-tool
```
4. **Restart the server** - tools are loaded dynamically at startup
## Available Tools
The server provides 18 specialized MCP tools for ExoSense operations:
1. **`exosense-current-user`** - Get current user information
2. **`exosense-get-root-group`** - Get root group information
3. **`exosense-get-groups`** - Query groups with filtering and include options
4. **`exosense-get-products`** - Get all IoT Connectors (products)
5. **`exosense-get-devices`** - Query devices with filtering and include options
6. **`exosense-get-assets`** - Query assets with filtering and include options (returns summary only)
7. **`exosense-get-asset-details`** - Get high-level statistics for a specific asset by ID or name
8. **`exosense-find-asset`** - Find assets by fuzzy name matching (e.g., "my battery" → "Battery Bank")
9. **`exosense-get-asset-statuses`** - Get status information for specific assets
10. **`exosense-get-insight-modules`** - Get all available internal insight modules
11. **`exosense-get-insight-module`** - Get detailed information about a specific insight module
12. **`exosense-get-asset-historical-data`** - Generate and retrieve historical data reports
13. **`exosense-get-work-instructions`** - Get work instructions
14. **`exosense-get-conditions`** - Get conditions
15. **`exosense-get-condition-comments`** - Get condition comments
16. **`exosense-get-event-logs`** - Get event logs
All tools use session-based authentication and provide comprehensive error handling.
## Dependencies
- `pydantic>=2.0.0` - Data validation and settings management
- `httpx>=0.25.0` - Async HTTP client for GraphQL requests
- `python-dotenv>=1.0.0` - Environment variable management
- `aiohttp>=3.9.0` - Async HTTP server for MCP protocol
- `pyyaml>=6.0.0` - YAML configuration file parsing
## Testing
The server includes connection testing on startup. When you start the server, it will:
1. Test the connection to ExoSense (if credentials are provided)
2. Display connection status and configuration
3. Load and register all tools from `config.yml`
4. Start the HTTP server
## Contributing
1. Follow the existing modular tool architecture
2. Create new tools in separate files under `exosense_mcp/tools/`
3. Use Pydantic models for parameter validation
4. Add proper Python type hints for all new functionality
5. Include comprehensive error handling using `format_error_response`
6. Add tools to `config.yml` for dynamic loading
7. Follow the tool pattern: Pydantic model + `execute()` function + `TOOL_METADATA`
## License
MIT
This server cannot be deployed
Maintenance
ActivityInactive
ResponsivenessNo issues