Metadata-Version: 2.4
Name: eclipse-ditto-mcp-server
Version: 0.1.1
Summary: MCP server for the Eclipse Ditto digital twin HTTP API
Project-URL: Homepage, https://github.com/nagarjunr/iot-mcp-servers/tree/main/eclipse-ditto
Project-URL: Repository, https://github.com/nagarjunr/iot-mcp-servers
Author: Nagarjun Rajendran
License-Expression: MIT
Keywords: digital-twin,eclipse-ditto,iot,mcp,model-context-protocol
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Requires-Python: >=3.12
Requires-Dist: fastmcp>=2.13.0
Requires-Dist: httpx>=0.28.1
Description-Content-Type: text/markdown

# Eclipse Ditto MCP Server

<!-- mcp-name: io.github.nagarjunr/eclipse-ditto -->

A [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) server for [Eclipse Ditto](https://eclipse.dev/ditto/) — the open-source (EPL-2.0) digital twin framework. It exposes Ditto's HTTP API as MCP tools so an AI agent can manage devices ("things"), their attributes/features, policies, connections, and cluster admin — the full Ditto HTTP API surface.

Since [Bosch IoT Things](https://www.bosch-iot-suite.com/things/) and other commercial IoT platforms are built on Ditto's open protocol, this server also works against those deployments — nothing in the code is vendor-specific.

Built with [FastMCP](https://github.com/jlowin/fastmcp).

## Tools

### Things

| Tool | Description |
|------|-------------|
| `list_things(namespaces, filter, fields, limit)` | Search/list things. `filter` is a [Ditto RQL expression](https://eclipse.dev/ditto/basic-rql.html), e.g. `eq(attributes/location,"lab-1")`. |
| `count_things(namespaces, filter)` | Count things matching a filter without fetching them. |
| `get_things(thing_ids, fields)` | Bulk-fetch multiple things by ID in one request. |
| `get_thing(thing_id, fields)` | Fetch the full digital twin for one thing. |
| `put_thing(thing_id, thing, overwrite=False)` | Create a thing, or replace one with `overwrite=True`. |
| `patch_thing(thing_id, patch)` | Partially update a thing via RFC-7396 JSON Merge Patch. |
| `delete_thing(thing_id, confirm=False)` | Delete a thing. **Irreversible.** |
| `get_thing_policy_id` / `set_thing_policy_id` | Read/repoint the policy controlling a thing. |
| `get_thing_definition` / `set_thing_definition` / `delete_thing_definition` | Manage a thing's WoT/model definition. |
| `whoami()` | Return the identity Ditto authenticated this connector as — useful to sanity-check credentials. |

### Attributes & Features

| Tool | Description |
|------|-------------|
| `get_thing_attributes(thing_id, path=None)` | Get all attributes, or one by JSON pointer path. |
| `set_thing_attributes(thing_id, value, path=None, overwrite=False)` | Set the whole attributes object, or one attribute. |
| `delete_thing_attributes(thing_id, path=None, confirm=False)` | Delete attributes (whole object or one path). |
| `get_thing_features(thing_id)` | Get all features (sensor/actuator state). |
| `get_feature` / `put_feature` / `delete_feature` | Read/create-replace/delete one whole feature. |
| `get_feature_properties` / `set_feature_properties` / `delete_feature_properties` | Read/write/delete feature properties or `desiredProperties`, whole or by path. |
| `get_feature_definition` / `set_feature_definition` / `delete_feature_definition` | Manage a feature's definition. |

### Messages

| Tool | Description |
|------|-------------|
| `send_message(thing_id, subject, payload, feature_id=None, direction="inbox", timeout=10)` | Send a live message to/from a thing or feature (e.g. invoke a device operation). |
| `claim_thing(thing_id, payload=None, timeout=10)` | Send a claim message to gain access to a thing. |

### Policies

| Tool | Description |
|------|-------------|
| `get_policy` / `put_policy` / `delete_policy` | Read/create-replace/delete a policy. |
| `get_policy_entries(policy_id, label=None)` | Read all entries, or one by label. |
| `set_policy_entry` / `delete_policy_entry` | Create-replace/delete one policy entry. |

### Connectivity & DevOps *(require `DITTO_DEVOPS_USERNAME`/`DITTO_DEVOPS_PASSWORD`)*

Ditto's Connections API and DevOps API (log levels, cluster config, piggyback commands) sit behind a separate "devops" Basic-auth realm on the gateway, independent of the policy-based auth used for things/policies.

| Tool | Description |
|------|-------------|
| `list_connections` / `get_connection` | List/fetch connection configs. |
| `create_connection(connection, dry_run=False)` | Create a connection (ID generated by Ditto). |
| `update_connection(connection_id, connection)` | Update an existing connection (404 if it doesn't exist). |
| `delete_connection(connection_id, confirm=False)` | Delete a connection. |
| `get_connection_status` / `get_connection_metrics` / `get_connection_logs` | Inspect a connection's live state. |
| `send_connection_command(connection_id, command)` | Open/close/reset-metrics/enable-logs/reset-logs. |
| `get_logging_config` / `set_logging_config` | Read/set cluster log levels. |
| `get_devops_config(path)` | Read cluster-wide runtime config. |
| `send_devops_piggyback(command, service_name=None, instance_index=None)` | Low-level admin command — Ditto's most powerful, least-guarded API. Use with care. |

### Live events

| Tool | Description |
|------|-------------|
| `poll_thing_changes(thing_ids, namespaces, filter, fields, duration_seconds=10, max_events=50)` | Open a short-lived [SSE](https://eclipse.dev/ditto/httpapi-sse.html) connection and collect live thing-change events. A request/response approximation of Ditto's persistent change feed — see caveat below. |

## Safety conventions

- **Create vs. replace:** every `put_*`/`set_*`/`create_*` tool defaults to `overwrite=False`, using Ditto's `If-None-Match: *` header so create-only is race-free (not a check-then-set). Pass `overwrite=True` to replace existing data.
- **Deletes are confirm-gated:** every `delete_*` tool requires `confirm=True`. There is no bulk/cascading delete.
- **Not covered (deliberately):** WoT Web-of-Things descriptor endpoints (`/.well-known/wot`, `/devops/wot/*`), and fine-grained policy sub-resources (`subjects/{id}`, `resources/{path}`, `imports`, token-integration actions) — whole-entry `set_policy_entry`/`get_policy_entries` covers the common case. Add these later if you need them.

## Configuration

| Env var | Required | Description |
|---------|----------|--------------|
| `DITTO_BASE_URL` | Yes | Base URL of the Ditto HTTP API, e.g. `http://localhost:8080` or your gateway's public URL. |
| `DITTO_USERNAME` / `DITTO_PASSWORD` | No | Basic auth for the Things/Policies/Messages/Search API. Typical for self-hosted Ditto (nginx demo setup uses `ditto`/`ditto`). |
| `DITTO_BEARER_TOKEN` | No | Bearer token, used instead of Basic auth for the same API. Typical for OAuth2-fronted deployments (e.g. Bosch IoT Things). |
| `DITTO_DEVOPS_USERNAME` / `DITTO_DEVOPS_PASSWORD` | No | Basic auth for the separate DevOps/Connections realm (self-hosted default: `devops`/`foobar`). Required only for the Connectivity and DevOps tools. |

If both `DITTO_BEARER_TOKEN` and Basic auth credentials are set, the bearer token takes precedence. If neither is set, requests are sent unauthenticated (only works if your Ditto instance allows anonymous/pre-authenticated access).

## Running locally

```bash
uv sync
export DITTO_BASE_URL=http://localhost:8080
export DITTO_USERNAME=ditto
export DITTO_PASSWORD=ditto
export DITTO_DEVOPS_USERNAME=devops    # only needed for connectivity/devops tools
export DITTO_DEVOPS_PASSWORD=foobar
uv run dittomcpserver.py
```

See [`mcp.json`](mcp.json) for ready-to-use client configs (stdio and Docker).

## Testing against a real Ditto instance

This repo's [design principles](../README.md#design-principles) call for testing against a real instance rather than mocks. `docker-compose.yml` (adapted from [Ditto's official example](https://github.com/eclipse-ditto/ditto/tree/master/deployment/docker), trimmed to the services this connector needs) spins up a full local Ditto stack — MongoDB, the Ditto microservices, and an nginx front door with Basic auth.

```bash
docker compose up -d       # start Ditto on http://localhost:8080 (user: ditto / pass: ditto; devops: devops / foobar)
uv run pytest -v           # tests/conftest.py also starts/stops the stack automatically
docker compose down        # stop it manually if you started it yourself
```

The 47 tests cover every tool group (things, attributes/features, messages, policies, connectivity, devops, live events) against this real stack, including delete/confirm gating and create/overwrite gating.

### A note on the live-events approximation

`poll_thing_changes` opens Ditto's SSE change feed for a bounded window instead of holding a persistent subscription (which doesn't map cleanly onto a single MCP tool call). Ditto's SSE stream sends periodic empty `data:` keepalive lines even when nothing has changed — the tool checks its deadline on every line received, including keepalives, so it reliably returns within `duration_seconds` regardless of how chatty or quiet the stream is.

## License notes

`docker-compose.yml` and the files under `docker/` are adapted from the [Eclipse Ditto](https://github.com/eclipse-ditto/ditto) project (EPL-2.0) and are used here only to spin up a local test instance — they are not part of the MCP server itself.
